// Mapbox-backed Lagos map with real admin boundaries (GeoJSON overlays). const MAPBOX_TOKEN = "pk.eyJ1IjoiamFkczYxMSIsImEiOiJjbW9rajl0bWQwNmpuMnFzYWZ1cHg3ZG5nIn0.h9r38ftbVfrAaz0lAmlEBg"; function MapCanvas({ focus, layers = {}, subject, mapStyle, pitch = 0, bearing = 0 }) { const mapEl = React.useRef(null); const mapRef = React.useRef(null); const [tick, setTick] = React.useState(0); React.useEffect(() => { let map, ro, rzTimer, cancelled = false; const addBoundaries = async (m) => { try { const [sen, lines, pts] = await Promise.all([ fetch("data/lagos_senatorial.geojson").then(r => r.json()), fetch("data/lagos_adminlines.geojson").then(r => r.json()), fetch("data/lagos_adminpoints.geojson").then(r => r.json()), ]); // 1) Senatorial district polygons (coloured by district) m.addSource("lagos-senatorial", { type: "geojson", data: sen }); m.addLayer({ id: "sen-fill", type: "fill", source: "lagos-senatorial", paint: { "fill-color": [ "match", ["get", "sendist_en"], "Lagos West", "#c49a3c", "Lagos Central", "#1aa088", "Lagos East", "#56647a", "#aab3c2" ], "fill-opacity": 0.18, }, layout: { visibility: layers.senatorial ? "visible" : "none" }, }); m.addLayer({ id: "sen-line", type: "line", source: "lagos-senatorial", paint: { "line-color": "#1a2332", "line-width": 1.5, "line-opacity": 0.55, "line-dasharray": [2, 2] }, layout: { visibility: layers.senatorial ? "visible" : "none" }, }); // 2) LGA admin lines (adm_level == 2 is LGA, 1 is state) m.addSource("lagos-adminlines", { type: "geojson", data: lines }); m.addLayer({ id: "adm-line-lga", type: "line", source: "lagos-adminlines", filter: ["==", ["get", "adm_level"], 2], paint: { "line-color": "#0a0f15", "line-width": 1.1, "line-opacity": 0.45 }, layout: { visibility: layers.lgaLines !== false ? "visible" : "none" }, }); m.addLayer({ id: "adm-line-state", type: "line", source: "lagos-adminlines", filter: ["==", ["get", "adm_level"], 1], paint: { "line-color": "#0a0f15", "line-width": 2, "line-opacity": 0.8 }, layout: { visibility: layers.lgaLines !== false ? "visible" : "none" }, }); // 3) LGA name labels (admin_level == 2) m.addSource("lagos-points", { type: "geojson", data: pts }); m.addLayer({ id: "lga-labels", type: "symbol", source: "lagos-points", filter: ["==", ["get", "admin_level"], 2], layout: { "text-field": ["get", "name"], "text-font": ["Open Sans Semibold", "Arial Unicode MS Bold"], "text-size": 11, "text-anchor": "center", "text-letter-spacing": 0.04, "text-allow-overlap": false, "visibility": layers.labels !== false ? "visible" : "none", }, paint: { "text-color": "#1a2332", "text-halo-color": "rgba(255,255,255,0.9)", "text-halo-width": 1.6, }, }); } catch (e) { console.warn("boundaries load failed", e); } }; const tryInit = () => { if (cancelled) return; if (!window.mapboxgl) { requestAnimationFrame(tryInit); return; } const el = mapEl.current; if (!el) { requestAnimationFrame(tryInit); return; } const w = el.clientWidth, h = el.clientHeight; if (w < 10 || h < 10) { requestAnimationFrame(tryInit); return; } const isSat = mapStyle === "satellite"; window.mapboxgl.accessToken = MAPBOX_TOKEN; map = new window.mapboxgl.Map({ container: el, style: isSat ? "mapbox://styles/mapbox/satellite-streets-v12" : "mapbox://styles/mapbox/light-v11", center: [3.46, 6.50], zoom: isSat ? 12.5 : 10.3, pitch: pitch || (isSat ? 52 : 0), bearing: bearing || (isSat ? -20 : 0), attributionControl: false, pitchWithRotate: isSat, dragRotate: isSat, }); mapRef.current = map; const bump = () => setTick(t => t + 1); map.on("load", () => { if (!isSat) { try { map.setPaintProperty("water", "fill-color", "#a9c7c0"); } catch(e) {} } if (isSat) { try { map.addSource("mapbox-dem", { type: "raster-dem", url: "mapbox://mapbox.mapbox-terrain-dem-v1", tileSize: 512, maxzoom: 14, }); map.setTerrain({ source: "mapbox-dem", exaggeration: 1.5 }); map.addLayer({ id: "3d-buildings", source: "composite", "source-layer": "building", filter: ["==", "extrude", "true"], type: "fill-extrusion", minzoom: 12, paint: { "fill-extrusion-color": "#e8722a", "fill-extrusion-height": ["interpolate", ["linear"], ["zoom"], 14, 0, 16, ["get", "height"]], "fill-extrusion-base": ["interpolate", ["linear"], ["zoom"], 14, 0, 16, ["get", "min_height"]], "fill-extrusion-opacity": 0.55, }, }); } catch(e) { console.warn("3D buildings failed", e); } } map.resize(); map.triggerRepaint(); if (!isSat) addBoundaries(map); bump(); }); map.on("move", bump); map.on("moveend", bump); let lastW = w, lastH = h; ro = new ResizeObserver(entries => { const r = entries[0].contentRect; if (Math.abs(r.width - lastW) < 2 && Math.abs(r.height - lastH) < 2) return; lastW = r.width; lastH = r.height; clearTimeout(rzTimer); rzTimer = setTimeout(() => { try { map.resize(); map.triggerRepaint(); bump(); } catch(e) {} }, 80); }); ro.observe(el); }; requestAnimationFrame(tryInit); return () => { cancelled = true; if (ro) ro.disconnect(); clearTimeout(rzTimer); if (map) map.remove(); mapRef.current = null; }; }, []); // React to layer toggle changes React.useEffect(() => { const m = mapRef.current; if (!m || !m.isStyleLoaded || !m.isStyleLoaded()) return; const set = (id, v) => { if (m.getLayer(id)) m.setLayoutProperty(id, "visibility", v ? "visible" : "none"); }; set("sen-fill", !!layers.senatorial); set("sen-line", !!layers.senatorial); set("adm-line-lga", layers.lgaLines !== false); set("adm-line-state", layers.lgaLines !== false); set("lga-labels", layers.labels !== false); }, [layers.senatorial, layers.lgaLines, layers.labels, tick]); const map = mapRef.current; const w = mapEl.current?.clientWidth || 0; const h = mapEl.current?.clientHeight || 0; const ready = map && w > 0 && h > 0; const proj = ready ? (lng, lat) => { const p = map.project([lng, lat]); return [p.x, p.y]; } : null; const color = { building:"#c49a3c", completed:"#1aa088", planned:"#56647a", stalled:"#b4413a" }; return (
{proj && ( {layers.pipeline !== false && window.PIPELINE.map(p => { const [x,y] = proj(p.lng, p.lat); return ( ); })} {subject && (() => { const [x,y] = proj(subject.lng, subject.lat); return ( ); })()} )}
); } window.MapCanvas = MapCanvas;