/* Quick City IFM — Berlin coverage map: real Bezirke geodata (same pattern as the LNS Germany map) */ let _berlinDataCache = null; async function loadBerlinData() { if (_berlinDataCache) return _berlinDataCache; try { const resp = await fetch('https://cdn.jsdelivr.net/gh/funkeinteraktiv/Berlin-Geodaten@master/berlin_bezirke.geojson'); const geo = await resp.json(); _berlinDataCache = geo; } catch (e) { _berlinDataCache = null; } return _berlinDataCache; } const QC_IFM_BERLIN_VIEWBOX = { w: 100, h: 76 }; // fallback until real data loads // Real approximate coordinates [lng, lat] per named cluster area. const QC_IFM_BERLIN_CLUSTERS = [ { id: 'hsl', name: 'Hohenschönhausen / Lichtenberg / Weißensee', count: 45, lng: 13.49, lat: 52.545 }, { id: 'ppb', name: 'Pankow / Prenzlauer Berg / südl. Weißensee', count: 37.5, lng: 13.41, lat: 52.555 }, { id: 'cwg', name: 'Charlottenburg / Wilmersdorf / Grunewald', count: 37.5, lng: 13.30, lat: 52.50 }, { id: 'fkm', name: 'Friedrichshain / Kreuzberg / östl. Mitte', count: 32.5, lng: 13.42, lat: 52.51 }, { id: 'wmm', name: 'Wedding / Moabit / nördl. Mitte', count: 32.5, lng: 13.35, lat: 52.535 }, { id: 'trw', name: 'Tegel / Reinickendorf / Wittenau', count: 32.5, lng: 13.30, lat: 52.58 }, { id: 'mha', name: 'Marzahn / Hellersdorf / Ahrensfelde', count: 32.5, lng: 13.585,lat: 52.55 }, { id: 'slz', name: 'Steglitz / Lichterfelde / Zehlendorf', count: 32.5, lng: 13.30, lat: 52.44 }, { id: 'ssf', name: 'Spandau / Staaken / Falkenhagener Feld', count: 27.5, lng: 13.18, lat: 52.545 }, { id: 'nbt', name: 'Neukölln / Britz / nördl. Treptow', count: 27.5, lng: 13.45, lat: 52.475 }, { id: 'tsf', name: 'Tiergarten / Schöneberg / Friedenau', count: 27.5, lng: 13.34, lat: 52.487 }, { id: 'bkp', name: 'Buch / Karow / Panketal', count: 22.5, lng: 13.50, lat: 52.62 }, { id: 'fhh', name: 'Frohnau / Hermsdorf / Hohen Neuendorf', count: 22.5, lng: 13.29, lat: 52.64 }, { id: 'tmm', name: 'Tempelhof / Mariendorf / Marienfelde', count: 22.5, lng: 13.38, lat: 52.44 }, { id: 'kas', name: 'Köpenick / Adlershof / südöstl. Berlin', count: 20, lng: 13.57, lat: 52.44 }, { id: 'lbs', name: 'Lichtenrade / Buckow / Schönefeld', count: 17.5, lng: 13.45, lat: 52.40 }, { id: 'wkg', name: 'Wannsee / Kladow / Gatow', count: 12.5, lng: 13.15, lat: 52.44 }, { id: 'fal', name: 'Falkensee (Brandenburg)', count: 5, lng: 13.08, lat: 52.56, outside: true } ]; const QC_IFM_HQ = { name: 'Zentrale — Blankenburger Str. 18, 13089 Berlin', lng: 13.459, lat: 52.586 }; function IfmBerlinMap({ rightShift } = {}) { const { t } = window.useT ? window.useT() : { t: (k, f) => f }; const [data, setData] = React.useState(_berlinDataCache); const [hoverId, setHoverId] = React.useState(null); const [hoverCellIdx, setHoverCellIdx] = React.useState(null); const [hoverHQ, setHoverHQ] = React.useState(false); const clusters = QC_IFM_BERLIN_CLUSTERS; const hovered = hoverId ? clusters.find(c => c.id === hoverId) : null; const totalSites = Math.round(clusters.reduce((s, c) => s + c.count, 0)); React.useEffect(() => { if (!data) { let cancelled = false; loadBerlinData().then(d => { if (!cancelled) setData(d); }); return () => { cancelled = true; }; } }, []); const projected = React.useMemo(() => { if (typeof window === 'undefined' || !window.d3 || !data) return null; const padTop = 4, padRight = rightShift ? 12 : 18, padBottom = 6, padLeft = rightShift ? 24 : 18, w = 100; // Aspect ratio comes ONLY from Berlin's own bounds (stable, known-good data) — the live // Falkensee fetch is drawn on top afterward and never allowed to affect the box shape/scale. const [[minLng, minLat], [maxLng, maxLat]] = window.d3.geoBounds(data); const midLat = (minLat + maxLat) / 2; const dx = (maxLng - minLng) * Math.cos(midLat * Math.PI / 180); const dy = (maxLat - minLat); const innerW = w - padLeft - padRight; const innerH = innerW * (dy / dx); const h = Math.round(innerH + padTop + padBottom); const proj = window.d3.geoMercator().fitExtent([[padLeft, padTop], [w - padRight, innerH + padTop]], data); const path = window.d3.geoPath(proj); const districtPaths = data.features.map(f => ({ name: f.properties && f.properties.name, d: path(f) })); const points = clusters.map(c => { const r = proj([c.lng, c.lat]) || [0, 0]; return { ...c, x: r[0], y: r[1] }; }); const hq = proj([QC_IFM_HQ.lng, QC_IFM_HQ.lat]) || [0, 0]; // Falkensee has no reliable live geodata source (fetch has twice produced degenerate/huge // geometry and broken the layout) — draw a small fixed decorative blob at its marker instead. const falMarker = clusters.find(c => c.id === 'fal'); const markerProj = proj([falMarker.lng, falMarker.lat]); const falX = Math.max(padLeft * 0.5, 6); const falY = markerProj[1]; const falkenseeBlob = { x: falX, y: falY, r: 5 }; return { districtPaths, points, hq: { x: hq[0], y: hq[1] }, falkenseeBlob, w, h, padLeft }; }, [data]); const hoveredBezirk = (!hovered && hoverCellIdx !== null && projected) ? projected.districtPaths[hoverCellIdx] : null; const falMarkerFixed = projected && projected.falkenseeBlob ? { x: projected.falkenseeBlob.x, y: projected.falkenseeBlob.y } : null; return (