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

- 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:
Pepe Ziberi
2026-07-19 15:27:42 +02:00
parent 6e3663cf7f
commit 703265f47e
5 changed files with 122 additions and 31 deletions

View File

@@ -13,7 +13,7 @@ import { RightSidebar } from '@/components/layout/right-sidebar'
import { ProjectDialog } from '@/components/dialogs/project-dialog'
import { ExerciseCockpit } from '@/components/exercise/exercise-cockpit'
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 { useAuth } from '@/components/providers/auth-provider'
import { Button } from '@/components/ui/button'
@@ -668,22 +668,38 @@ export default function AppPage() {
broadcastFeatures(newFeatures)
}, [addAudit, broadcastFeatures])
const handleLineLabelConfirm = useCallback((label: string) => {
if (pendingLineFeature && label) {
setFeatures(prev => prev.map(f =>
f.id === pendingLineFeature.id
? { ...f, properties: { ...f.properties, label } }
: f
))
// Wendet den gewählten Linientyp an: setzt properties.lineType und bei Leitung/
// Rettungsachse die konventionelle Farbe/Stärke (Normal behält die Zeichenfarbe).
const applyLineKind = useCallback((f: DrawFeature, kind: LineKind, label: string): DrawFeature => {
const def = LINE_KINDS.find(k => k.value === kind)
const properties: Record<string, any> = { ...f.properties, lineType: kind }
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)
setIsLineLabelDialogOpen(false)
}, [pendingLineFeature])
}, [pendingLineFeature, applyLineKind, broadcastFeatures])
const handleLineLabelSkip = useCallback(() => {
setPendingLineFeature(null)
setIsLineLabelDialogOpen(false)
}, [])
const handleLineLabelConfirm = useCallback((label: string, kind: LineKind) => {
finalizeLineFeature(kind, label)
}, [finalizeLineFeature])
const handleLineLabelSkip = useCallback((kind: LineKind) => {
finalizeLineFeature(kind, '')
}, [finalizeLineFeature])
const handleSymbolDrop = useCallback((iconId: string, coordinates: [number, number], imageUrl?: string) => {
const currentZoom = mapRef.current?.getZoom() || 17

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { cn } from '@/lib/utils'
import {
Dialog,
DialogContent,
@@ -12,28 +13,44 @@ import {
DialogFooter,
} 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 {
open: boolean
onOpenChange: (open: boolean) => void
onConfirm: (label: string) => void
onSkip: () => void
lineType: 'linestring' | 'arrow' | 'polygon'
onConfirm: (label: string, kind: LineKind) => void
onSkip: (kind: LineKind) => void
lineType: 'linestring' | 'arrow' | 'polygon' | 'dangerzone'
}
export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineType }: LineLabelDialogProps) {
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(() => {
if (open) setLabel('')
if (open) {
setLabel('')
setKind('normal')
}
}, [open])
const handleConfirm = () => {
onConfirm(label.trim())
onConfirm(label.trim(), kind)
setLabel('')
}
const handleSkip = () => {
onSkip()
onSkip(kind)
setLabel('')
}
@@ -47,8 +64,10 @@ export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineTyp
}
}
const title = lineType === 'polygon' ? 'Fläche beschriften' : 'Leitung beschriften'
const placeholder = lineType === 'polygon'
const title = lineType === 'polygon' || lineType === 'dangerzone'
? 'Fläche beschriften'
: showKindSelector ? 'Linie festlegen' : 'Leitung beschriften'
const placeholder = lineType === 'polygon' || lineType === 'dangerzone'
? 'z.B. Brandzone, Sperrgebiet...'
: 'z.B. 1, L2, Zuleitung...'
@@ -58,7 +77,41 @@ export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineTyp
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</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">
<Label htmlFor="line-label">Bezeichnung (optional)</Label>
<Input
@@ -67,16 +120,16 @@ export function LineLabelDialog({ open, onOpenChange, onConfirm, onSkip, lineTyp
onChange={(e) => setLabel(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
autoFocus
autoFocus={!showKindSelector}
/>
<p className="text-[11px] text-muted-foreground">
Wird am Mittelpunkt auf der Karte angezeigt. Leer lassen für keine Beschriftung.
</p>
</div>
<p className="text-xs text-muted-foreground">
Wird am Mittelpunkt der Leitung auf der Karte angezeigt. Leer lassen für keine Beschriftung.
</p>
</div>
<DialogFooter className="gap-2">
<Button variant="outline" onClick={handleSkip}>
Ohne Label
{showKindSelector ? 'Übernehmen, ohne Label' : 'Ohne Label'}
</Button>
<Button onClick={handleConfirm} disabled={!label.trim()}>
Beschriften

View File

@@ -810,12 +810,12 @@ export function MapView({
},
})
// Line layer
// Line layer — alle Linien AUSSER Rettungsachse (die bekommt ihren eigenen gestrichelten Layer)
m.addLayer({
id: 'draw-lines',
type: 'line',
source: 'draw-features',
filter: ['==', ['geometry-type'], 'LineString'],
filter: ['all', ['==', ['geometry-type'], 'LineString'], ['!=', ['get', 'lineType'], 'rettungsachse']],
layout: {
'line-cap': '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
m.addLayer({
id: 'draw-points',
@@ -1378,6 +1395,7 @@ export function MapView({
color: (f.properties.color as string) || '#000000',
width: (f.properties.width as number) || 3,
isDangerZone: f.properties.isDangerZone ? 1 : 0,
lineType: (f.properties.lineType as string) || 'normal',
},
}))