feat(draw): Linien-Typ-Abfrage – Normal / Leitung / Rettungsachse (v1.4.13)
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m14s
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 31m14s
- Beim Zeichnen von Linie/Pfeil fragt der Dialog jetzt den Linientyp ab: Normal (gewählte Farbe), Leitung (blau, dicker), Rettungsachse (grün, gestrichelt) - Typ in properties.lineType gespeichert; Leitung/Rettungsachse setzen konventionelle Farbe + Stärke, Normal behält die Zeichenfarbe - Eigener MapLibre-Layer mit Dash-Muster für Rettungsachse (klar als freizuhaltende Achse erkennbar) - Typ-Auswahl auch ohne Label übernehmbar; Änderung wird live gesynct - ROADMAP: 5.1 abgehakt Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,7 +76,11 @@ Legende Aufwand: 🟢 klein · 🟡 mittel · 🔴 gross
|
|||||||
|
|
||||||
## Phase 5 – Kür
|
## Phase 5 – Kür
|
||||||
|
|
||||||
- [ ] **5.1 – Linien-Typ-Abfrage** 🟡 (Rettungsachse / Leitung / normal)
|
- [x] **5.1 – Linien-Typ-Abfrage** 🟡 ✅
|
||||||
|
- Beim Zeichnen einer Linie/eines Pfeils fragt der Dialog nun den Typ ab:
|
||||||
|
**Normal** (gewählte Farbe), **Leitung** (blau, dicker), **Rettungsachse** (grün,
|
||||||
|
gestrichelt – als freizuhaltende Achse klar erkennbar).
|
||||||
|
- Typ in `properties.lineType`; eigener MapLibre-Layer mit Dash-Muster für Rettungsachse.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lageplan",
|
"name": "lageplan",
|
||||||
"version": "1.4.12",
|
"version": "1.4.13",
|
||||||
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
"description": "Feuerwehr Lageplan - Krokier-App für Einsatzdokumentation",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { RightSidebar } from '@/components/layout/right-sidebar'
|
|||||||
import { ProjectDialog } from '@/components/dialogs/project-dialog'
|
import { ProjectDialog } from '@/components/dialogs/project-dialog'
|
||||||
import { ExerciseCockpit } from '@/components/exercise/exercise-cockpit'
|
import { ExerciseCockpit } from '@/components/exercise/exercise-cockpit'
|
||||||
import { TextDialog } from '@/components/dialogs/text-dialog'
|
import { TextDialog } from '@/components/dialogs/text-dialog'
|
||||||
import { LineLabelDialog } from '@/components/dialogs/line-label-dialog'
|
import { LineLabelDialog, LINE_KINDS, type LineKind } from '@/components/dialogs/line-label-dialog'
|
||||||
import { useToast } from '@/components/ui/use-toast'
|
import { useToast } from '@/components/ui/use-toast'
|
||||||
import { useAuth } from '@/components/providers/auth-provider'
|
import { useAuth } from '@/components/providers/auth-provider'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -668,22 +668,38 @@ export default function AppPage() {
|
|||||||
broadcastFeatures(newFeatures)
|
broadcastFeatures(newFeatures)
|
||||||
}, [addAudit, broadcastFeatures])
|
}, [addAudit, broadcastFeatures])
|
||||||
|
|
||||||
const handleLineLabelConfirm = useCallback((label: string) => {
|
// Wendet den gewählten Linientyp an: setzt properties.lineType und – bei Leitung/
|
||||||
if (pendingLineFeature && label) {
|
// Rettungsachse – die konventionelle Farbe/Stärke (Normal behält die Zeichenfarbe).
|
||||||
setFeatures(prev => prev.map(f =>
|
const applyLineKind = useCallback((f: DrawFeature, kind: LineKind, label: string): DrawFeature => {
|
||||||
f.id === pendingLineFeature.id
|
const def = LINE_KINDS.find(k => k.value === kind)
|
||||||
? { ...f, properties: { ...f.properties, label } }
|
const properties: Record<string, any> = { ...f.properties, lineType: kind }
|
||||||
: f
|
if (def && kind !== 'normal') {
|
||||||
))
|
properties.color = def.color
|
||||||
|
properties.width = def.width
|
||||||
|
}
|
||||||
|
if (label) properties.label = label
|
||||||
|
return { ...f, properties }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const finalizeLineFeature = useCallback((kind: LineKind, label: string) => {
|
||||||
|
if (pendingLineFeature) {
|
||||||
|
const next = featuresRef.current.map(f =>
|
||||||
|
f.id === pendingLineFeature.id ? applyLineKind(f, kind, label) : f
|
||||||
|
)
|
||||||
|
setFeatures(next)
|
||||||
|
broadcastFeatures(next)
|
||||||
}
|
}
|
||||||
setPendingLineFeature(null)
|
setPendingLineFeature(null)
|
||||||
setIsLineLabelDialogOpen(false)
|
setIsLineLabelDialogOpen(false)
|
||||||
}, [pendingLineFeature])
|
}, [pendingLineFeature, applyLineKind, broadcastFeatures])
|
||||||
|
|
||||||
const handleLineLabelSkip = useCallback(() => {
|
const handleLineLabelConfirm = useCallback((label: string, kind: LineKind) => {
|
||||||
setPendingLineFeature(null)
|
finalizeLineFeature(kind, label)
|
||||||
setIsLineLabelDialogOpen(false)
|
}, [finalizeLineFeature])
|
||||||
}, [])
|
|
||||||
|
const handleLineLabelSkip = useCallback((kind: LineKind) => {
|
||||||
|
finalizeLineFeature(kind, '')
|
||||||
|
}, [finalizeLineFeature])
|
||||||
|
|
||||||
const handleSymbolDrop = useCallback((iconId: string, coordinates: [number, number], imageUrl?: string) => {
|
const handleSymbolDrop = useCallback((iconId: string, coordinates: [number, number], imageUrl?: string) => {
|
||||||
const currentZoom = mapRef.current?.getZoom() || 17
|
const currentZoom = mapRef.current?.getZoom() || 17
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -12,28 +13,44 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
} from '@/components/ui/dialog'
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
export type LineKind = 'normal' | 'leitung' | 'rettungsachse'
|
||||||
|
|
||||||
|
// Zentrale Definition der Linientypen — Farbe/Stärke werden beim Zeichnen übernommen.
|
||||||
|
export const LINE_KINDS: { value: LineKind; label: string; color: string; width: number; dashed: boolean; hint: string }[] = [
|
||||||
|
{ value: 'normal', label: 'Normal', color: '', width: 0, dashed: false, hint: 'Freie Linie in gewählter Farbe' },
|
||||||
|
{ value: 'leitung', label: 'Leitung', color: '#2563eb', width: 4, dashed: false, hint: 'Schlauchleitung (blau)' },
|
||||||
|
{ value: 'rettungsachse', label: 'Rettungsachse', color: '#16a34a', width: 6, dashed: true, hint: 'Freihalten — Rettungs-/Zufahrtsachse (grün, gestrichelt)' },
|
||||||
|
]
|
||||||
|
|
||||||
interface LineLabelDialogProps {
|
interface LineLabelDialogProps {
|
||||||
open: boolean
|
open: boolean
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void
|
||||||
onConfirm: (label: string) => void
|
onConfirm: (label: string, kind: LineKind) => void
|
||||||
onSkip: () => void
|
onSkip: (kind: LineKind) => void
|
||||||
lineType: 'linestring' | 'arrow' | 'polygon'
|
lineType: 'linestring' | 'arrow' | 'polygon' | 'dangerzone'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineType }: LineLabelDialogProps) {
|
export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineType }: LineLabelDialogProps) {
|
||||||
const [label, setLabel] = useState('')
|
const [label, setLabel] = useState('')
|
||||||
|
const [kind, setKind] = useState<LineKind>('normal')
|
||||||
|
|
||||||
|
// Linientyp-Auswahl nur für echte Linien/Pfeile (nicht für Flächen/Gefahrenzonen)
|
||||||
|
const showKindSelector = lineType === 'linestring' || lineType === 'arrow'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (open) setLabel('')
|
if (open) {
|
||||||
|
setLabel('')
|
||||||
|
setKind('normal')
|
||||||
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
onConfirm(label.trim())
|
onConfirm(label.trim(), kind)
|
||||||
setLabel('')
|
setLabel('')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSkip = () => {
|
const handleSkip = () => {
|
||||||
onSkip()
|
onSkip(kind)
|
||||||
setLabel('')
|
setLabel('')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +64,10 @@ export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineTyp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = lineType === 'polygon' ? 'Fläche beschriften' : 'Leitung beschriften'
|
const title = lineType === 'polygon' || lineType === 'dangerzone'
|
||||||
const placeholder = lineType === 'polygon'
|
? 'Fläche beschriften'
|
||||||
|
: showKindSelector ? 'Linie festlegen' : 'Leitung beschriften'
|
||||||
|
const placeholder = lineType === 'polygon' || lineType === 'dangerzone'
|
||||||
? 'z.B. Brandzone, Sperrgebiet...'
|
? 'z.B. Brandzone, Sperrgebiet...'
|
||||||
: 'z.B. 1, L2, Zuleitung...'
|
: 'z.B. 1, L2, Zuleitung...'
|
||||||
|
|
||||||
@@ -58,7 +77,41 @@ export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineTyp
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{title}</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3 py-2">
|
<div className="space-y-4 py-2">
|
||||||
|
{showKindSelector && (
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<Label>Linientyp</Label>
|
||||||
|
<div className="grid grid-cols-3 gap-1.5">
|
||||||
|
{LINE_KINDS.map((k) => (
|
||||||
|
<button
|
||||||
|
key={k.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setKind(k.value)}
|
||||||
|
className={cn(
|
||||||
|
'flex flex-col items-center gap-1 rounded-lg border-2 px-1.5 py-2 text-xs font-medium transition-all',
|
||||||
|
kind === k.value
|
||||||
|
? 'border-primary bg-primary/5'
|
||||||
|
: 'border-border hover:border-muted-foreground'
|
||||||
|
)}
|
||||||
|
title={k.hint}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="h-1 w-8 rounded-full"
|
||||||
|
style={{
|
||||||
|
backgroundColor: k.color || '#64748b',
|
||||||
|
...(k.dashed
|
||||||
|
? { background: `repeating-linear-gradient(90deg, ${k.color} 0 5px, transparent 5px 9px)` }
|
||||||
|
: {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{k.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-[11px] text-muted-foreground">{LINE_KINDS.find(k => k.value === kind)?.hint}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<Label htmlFor="line-label">Bezeichnung (optional)</Label>
|
<Label htmlFor="line-label">Bezeichnung (optional)</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -67,16 +120,16 @@ export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineTyp
|
|||||||
onChange={(e) => setLabel(e.target.value)}
|
onChange={(e) => setLabel(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
autoFocus
|
autoFocus={!showKindSelector}
|
||||||
/>
|
/>
|
||||||
</div>
|
<p className="text-[11px] text-muted-foreground">
|
||||||
<p className="text-xs text-muted-foreground">
|
Wird am Mittelpunkt auf der Karte angezeigt. Leer lassen für keine Beschriftung.
|
||||||
Wird am Mittelpunkt der Leitung auf der Karte angezeigt. Leer lassen für keine Beschriftung.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<DialogFooter className="gap-2">
|
<DialogFooter className="gap-2">
|
||||||
<Button variant="outline" onClick={handleSkip}>
|
<Button variant="outline" onClick={handleSkip}>
|
||||||
Ohne Label
|
{showKindSelector ? 'Übernehmen, ohne Label' : 'Ohne Label'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleConfirm} disabled={!label.trim()}>
|
<Button onClick={handleConfirm} disabled={!label.trim()}>
|
||||||
Beschriften
|
Beschriften
|
||||||
|
|||||||
@@ -810,12 +810,12 @@ export function MapView({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
// Line layer
|
// Line layer — alle Linien AUSSER Rettungsachse (die bekommt ihren eigenen gestrichelten Layer)
|
||||||
m.addLayer({
|
m.addLayer({
|
||||||
id: 'draw-lines',
|
id: 'draw-lines',
|
||||||
type: 'line',
|
type: 'line',
|
||||||
source: 'draw-features',
|
source: 'draw-features',
|
||||||
filter: ['==', ['geometry-type'], 'LineString'],
|
filter: ['all', ['==', ['geometry-type'], 'LineString'], ['!=', ['get', 'lineType'], 'rettungsachse']],
|
||||||
layout: {
|
layout: {
|
||||||
'line-cap': 'round',
|
'line-cap': 'round',
|
||||||
'line-join': 'round',
|
'line-join': 'round',
|
||||||
@@ -826,6 +826,23 @@ export function MapView({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Rettungsachse — gestrichelt, damit sie als freizuhaltende Achse klar erkennbar ist
|
||||||
|
m.addLayer({
|
||||||
|
id: 'draw-lines-rettungsachse',
|
||||||
|
type: 'line',
|
||||||
|
source: 'draw-features',
|
||||||
|
filter: ['all', ['==', ['geometry-type'], 'LineString'], ['==', ['get', 'lineType'], 'rettungsachse']],
|
||||||
|
layout: {
|
||||||
|
'line-cap': 'butt',
|
||||||
|
'line-join': 'round',
|
||||||
|
},
|
||||||
|
paint: {
|
||||||
|
'line-color': ['get', 'color'],
|
||||||
|
'line-width': ['coalesce', ['get', 'width'], 6],
|
||||||
|
'line-dasharray': [2, 1.5],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
// Point layer
|
// Point layer
|
||||||
m.addLayer({
|
m.addLayer({
|
||||||
id: 'draw-points',
|
id: 'draw-points',
|
||||||
@@ -1378,6 +1395,7 @@ export function MapView({
|
|||||||
color: (f.properties.color as string) || '#000000',
|
color: (f.properties.color as string) || '#000000',
|
||||||
width: (f.properties.width as number) || 3,
|
width: (f.properties.width as number) || 3,
|
||||||
isDangerZone: f.properties.isDangerZone ? 1 : 0,
|
isDangerZone: f.properties.isDangerZone ? 1 : 0,
|
||||||
|
lineType: (f.properties.lineType as string) || 'normal',
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user