feat(krokier): Nordpfeil + Windrichtung + Export-Dokument (v1.6.0)

Macht aus dem digitalen Kroki ein echtes taktisches Lagebild:

Karte:
- Nordpfeil-Overlay (dreht mit dem Karten-Bearing mit)
- Windrichtung in 8 Richtungen setzbar (Windrose mit Drift-Pfeil + Label
  "Wind aus NW"), pro Einsatz gespeichert (Project.windDirection)

Export (PNG & PDF):
- Nordpfeil, Massstabsbalken und Windanzeige werden in die Karte eingebrannt
- PDF zusätzlich: Symbol-Legende (Namen aus /api/icons) + bestehende Kopfzeile
  (Einsatz/Ort/Zeit/Nr) → fertiges, weitergebbares Krokier-Dokument

Technik:
- windDirection: Schema + idempotente Migration + Zod-Validierung + Typ
- Neue Komponente MapCompass (Nordpfeil/Windrose/Wind-Setzer)
- MapView verfolgt Bearing über 'rotate'-Event

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pepe Ziberi
2026-07-22 20:56:11 +02:00
parent 226a15e882
commit 6b6bae9ce1
10 changed files with 280 additions and 2 deletions

View File

@@ -8,6 +8,7 @@ import Moveable from 'react-moveable'
import { getSymbolById, getSymbolDataUri } from '@/lib/fw-symbols'
import type { Project, DrawFeature, DrawMode } from '@/types'
import { MeasurePanel } from './measure-panel'
import { MapCompass } from './map-compass'
import { FALLBACK_HOSE_TYPES, type HoseTypeData, type MeasurementSummary } from '@/lib/hose-calc'
// Haversine distance between two [lng, lat] points in meters
@@ -91,6 +92,9 @@ interface MapViewProps {
/** Vollbild-Umschaltung (mobil als schwebender Button über der Karte) */
isFullscreen?: boolean
onToggleFullscreen?: () => void
/** Windrichtung fürs Lagebild (Grad, „Wind aus") + Setter */
windDirection?: number | null
onWindChange?: (dir: number | null) => void
}
export function MapView({
@@ -110,6 +114,8 @@ export function MapView({
onSymbolPlaced,
isFullscreen,
onToggleFullscreen,
windDirection,
onWindChange,
}: MapViewProps) {
const mapContainer = useRef<HTMLDivElement | null>(null)
const map = useRef<maplibregl.Map | null>(null)
@@ -120,6 +126,7 @@ export function MapView({
const [isMapLoaded, setIsMapLoaded] = useState(false)
const [activeBaseLayer, setActiveBaseLayer] = useState<'osm' | 'satellite' | 'swisstopo'>('osm')
const [layerDropdownOpen, setLayerDropdownOpen] = useState(false)
const [mapBearing, setMapBearing] = useState(0)
const [measurePointCount, setMeasurePointCount] = useState(0)
const [measureFinished, setMeasureFinished] = useState(false)
// Messergebnis für das React-Panel (ersetzt das frühere innerHTML-DOM-Overlay)
@@ -706,6 +713,9 @@ export function MapView({
map.current.addControl(new maplibregl.NavigationControl(), 'bottom-right')
map.current.addControl(new maplibregl.ScaleControl(), 'bottom-left')
// Bearing verfolgen, damit Nordpfeil/Windrose mit der Kartendrehung mitlaufen
map.current.on('rotate', () => setMapBearing(map.current?.getBearing() || 0))
// Geolocation: eigene Position dauerhaft anzeigen (Punkt + Genauigkeit + Blickrichtung)
const geolocate = new maplibregl.GeolocateControl({
positionOptions: { enableHighAccuracy: true },
@@ -2193,6 +2203,14 @@ export function MapView({
</button>
)}
{/* Nordpfeil + Windrose (Lagebild) */}
<MapCompass
bearing={mapBearing}
windDirection={windDirection}
canEdit={canEdit}
onWindChange={(dir) => onWindChange?.(dir)}
/>
{/* Moveable controls for selected symbol */}
{isSymbolSelected && selectedSymbolRef.current && canEdit && (
<>