/* ================================================ Map components — Leaflet wrappers ================================================ */ const TILE_URLS = { satellite: { url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", attribution: "Tiles © Esri", maxZoom: 18, }, illustrated: { // Watercolor-like / artistic style via Stamen url: "https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.jpg", attribution: "© Stamen · Stadia", maxZoom: 16, }, }; window.TILE_URLS = TILE_URLS; /* A static Leaflet map showing all parcels as polygons */ function ParcelsMap({ parcels, mapStyle = "satellite", activeId, onSelect, center = [48.085, 7.65], zoom = 13 }) { const containerRef = React.useRef(null); const mapRef = React.useRef(null); const layersRef = React.useRef({ tile: null, polys: {} }); // init map once React.useEffect(() => { if (!containerRef.current || mapRef.current) return; const m = L.map(containerRef.current, { center, zoom, zoomControl: true, attributionControl: true, }); mapRef.current = m; return () => { m.remove(); mapRef.current = null; }; }, []); // switch tile layer React.useEffect(() => { const m = mapRef.current; if (!m) return; if (layersRef.current.tile) m.removeLayer(layersRef.current.tile); const cfg = TILE_URLS[mapStyle] || TILE_URLS.satellite; const tile = L.tileLayer(cfg.url, { maxZoom: cfg.maxZoom, attribution: cfg.attribution }); tile.addTo(m); layersRef.current.tile = tile; }, [mapStyle]); // draw polygons React.useEffect(() => { const m = mapRef.current; if (!m) return; // clear existing Object.values(layersRef.current.polys).forEach((p) => m.removeLayer(p)); layersRef.current.polys = {}; parcels.forEach((p) => { const isActive = p.id === activeId; const poly = L.polygon(p.koordinaten, { color: p.farbe || "#6B1F2E", weight: isActive ? 3 : 2, fillColor: p.farbe || "#6B1F2E", fillOpacity: isActive ? 0.55 : 0.35, opacity: 0.95, }); poly.bindTooltip(p.lagebuch, { permanent: true, direction: "center", className: "parcel-label", }); poly.on("click", () => onSelect && onSelect(p)); poly.addTo(m); layersRef.current.polys[p.id] = poly; }); // fit bounds first render if (parcels.length && !layersRef.current.didFit) { const group = L.featureGroup(Object.values(layersRef.current.polys)); m.fitBounds(group.getBounds().pad(0.2)); layersRef.current.didFit = true; } }, [parcels, activeId]); // focus active parcel React.useEffect(() => { const m = mapRef.current; if (!m || !activeId) return; const poly = layersRef.current.polys[activeId]; if (poly) m.fitBounds(poly.getBounds().pad(0.8), { animate: true }); }, [activeId]); return
; } window.ParcelsMap = ParcelsMap; /* Static single-parcel map (for detail / summary) */ function SingleParcelMap({ parcel, mapStyle = "satellite" }) { const containerRef = React.useRef(null); React.useEffect(() => { if (!containerRef.current) return; const cfg = TILE_URLS[mapStyle] || TILE_URLS.satellite; const m = L.map(containerRef.current, { zoomControl: false, attributionControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false, tap: false, }); L.tileLayer(cfg.url, { maxZoom: cfg.maxZoom, attribution: cfg.attribution }).addTo(m); const poly = L.polygon(parcel.koordinaten, { color: parcel.farbe || "#6B1F2E", weight: 3, fillColor: parcel.farbe || "#6B1F2E", fillOpacity: 0.5, }).addTo(m); m.fitBounds(poly.getBounds().pad(0.4)); return () => m.remove(); }, [parcel, mapStyle]); return ; } window.SingleParcelMap = SingleParcelMap; /* Non-interactive thumbnail map for list cards. Uses a dedicated SVG renderer and forces an initial fitBounds AFTER the container has real size — otherwise Leaflet's overlay SVG can be 0×0 and polygons won't paint. */ function ThumbnailMap({ parcel, mapStyle = "satellite" }) { const containerRef = React.useRef(null); React.useEffect(() => { if (!containerRef.current) return; const el = containerRef.current; const cfg = TILE_URLS[mapStyle] || TILE_URLS.satellite; const color = parcel.farbe || "#6B1F2E"; // compute bounds once so we can pass them at init const bounds = L.latLngBounds(parcel.koordinaten).pad(0.5); const renderer = L.canvas({ padding: 2 }); const m = L.map(el, { zoomControl: false, attributionControl: false, dragging: false, scrollWheelZoom: false, doubleClickZoom: false, boxZoom: false, keyboard: false, touchZoom: false, tap: false, renderer, }); L.tileLayer(cfg.url, { maxZoom: cfg.maxZoom, attribution: cfg.attribution }).addTo(m); // halo (cream outline) + filled polygon + vertex dots L.polygon(parcel.koordinaten, { renderer, color: "#F5EFE3", weight: 4, opacity: 0.9, fill: false, lineJoin: "round", }).addTo(m); const poly = L.polygon(parcel.koordinaten, { renderer, color, weight: 2.5, fillColor: color, fillOpacity: 0.65, lineJoin: "round", }).addTo(m); // (no vertex dots — only polygon outline + fill) const fit = () => { if (!m._container || !m._container.isConnected) return; const r = el.getBoundingClientRect(); if (r.width < 2 || r.height < 2) return; m.invalidateSize(false); m.fitBounds(bounds, { animate: false }); }; // Initial fit on next frame (after layout) requestAnimationFrame(() => requestAnimationFrame(fit)); // Resize observer: fires when container first gets real size too const ro = new ResizeObserver(fit); ro.observe(el); return () => { ro.disconnect(); m.remove(); }; }, [parcel.id, mapStyle]); return ; } window.ThumbnailMap = ThumbnailMap; /* Interactive polygon drawer for the wizard */ function PolygonDrawer({ points, onPointsChange, mapStyle = "satellite", center = [48.085, 7.65] }) { const containerRef = React.useRef(null); const mapRef = React.useRef(null); const layersRef = React.useRef({ tile: null, poly: null, line: null, markers: [] }); const pointsRef = React.useRef(points); const prevCountRef = React.useRef(points.length); pointsRef.current = points; React.useEffect(() => { const m = mapRef.current; if (!m) return; if (points.length >= 3 && points.length - prevCountRef.current > 1) { m.fitBounds(L.polygon(points).getBounds().pad(0.2), { animate: true }); } prevCountRef.current = points.length; }, [points]); React.useEffect(() => { if (!containerRef.current || mapRef.current) return; const m = L.map(containerRef.current, { center, zoom: 16, zoomControl: true, attributionControl: true, }); mapRef.current = m; m.on("click", (e) => { const next = [...pointsRef.current, [e.latlng.lat, e.latlng.lng]]; onPointsChange(next); }); return () => { m.remove(); mapRef.current = null; }; }, []); // tile layer React.useEffect(() => { const m = mapRef.current; if (!m) return; if (layersRef.current.tile) m.removeLayer(layersRef.current.tile); const cfg = TILE_URLS[mapStyle] || TILE_URLS.satellite; const tile = L.tileLayer(cfg.url, { maxZoom: cfg.maxZoom, attribution: cfg.attribution }); tile.addTo(m); layersRef.current.tile = tile; }, [mapStyle]); // redraw polygon / markers React.useEffect(() => { const m = mapRef.current; if (!m) return; const L_ = layersRef.current; if (L_.poly) { m.removeLayer(L_.poly); L_.poly = null; } if (L_.line) { m.removeLayer(L_.line); L_.line = null; } L_.markers.forEach((mk) => m.removeLayer(mk)); L_.markers = []; if (points.length >= 3) { L_.poly = L.polygon(points, { color: "#6B1F2E", weight: 2.5, fillColor: "#6B1F2E", fillOpacity: 0.3, dashArray: "4 3", }).addTo(m); } else if (points.length === 2) { L_.line = L.polyline(points, { color: "#6B1F2E", weight: 2.5, dashArray: "4 3" }).addTo(m); } points.forEach((pt, idx) => { const icon = L.divIcon({ className: "", html: `