import { useEffect, useRef, useState } from "react"; import maplibregl from "maplibre-gl"; import { LAST_VIEW_KEY } from "./mapConfig"; import { getViewFromMap } from "./mapGeo"; import { clearAllMarkers, applyOcclusionForExpanded } from "./markerManager"; // MapLibre base style by theme function getBaseStyle(theme) { const t = theme || "dark"; let tiles; switch (t) { case "light": tiles = ["https://a.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"]; break; case "blue": tiles = ["https://a.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png"]; break; case "dark": default: tiles = ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"]; break; } return { version: 8, sources: { "carto-base": { type: "raster", tiles, tileSize: 256, attribution: "© OpenStreetMap contributors © CARTO", }, }, layers: [ { id: "carto-base-layer", type: "raster", source: "carto-base", }, ], }; } export function useMapCore(theme) { const containerRef = useRef(null); const mapRef = useRef(null); const markersRef = useRef([]); const expandedElRef = useRef(null); // default large radius const [viewParams, setViewParams] = useState({ center: null, radiusKm: 750, }); const [hasLastView, setHasLastView] = useState(false); function closeExpandedIfAny() { const el = expandedElRef.current; if (el && el.__renderCompact) el.__renderCompact(); expandedElRef.current = null; } function safeUpdateViewParams(map) { try { const vp = getViewFromMap(map); if (vp && Array.isArray(vp.center) && vp.center.length === 2) { setViewParams(vp); } } catch { // ignore } } useEffect(() => { if (!containerRef.current) return; const map = new maplibregl.Map({ container: containerRef.current, style: getBaseStyle(theme || "dark"), center: [-95, 40], zoom: 3.5, pitch: 0, bearing: 0, antialias: true, }); // Restore last view if available let hadLastView = false; try { const raw = localStorage.getItem(LAST_VIEW_KEY); if (raw) { const v = JSON.parse(raw); if ( typeof v.lat === "number" && typeof v.lon === "number" && typeof v.zoom === "number" ) { map.setCenter([v.lon, v.lat]); map.setZoom(v.zoom); hadLastView = true; } } } catch {} setHasLastView(hadLastView); map.addControl(new maplibregl.NavigationControl(), "top-right"); mapRef.current = map; // Re-occlude markers when post expanded const reOcclude = () => { const el = expandedElRef.current; if (el) applyOcclusionForExpanded(markersRef, el); }; map.on("move", reOcclude); map.on("zoom", reOcclude); // Close expanded when clicking/dragging map map.on("click", () => closeExpandedIfAny()); map.on("dragstart", () => closeExpandedIfAny()); // ✅ CRITICAL FIX: // Set view params ASAP and also on load/idle/moveend. // On some devices/prod, relying on 'load' only can miss initial fetch triggers. const update = () => safeUpdateViewParams(map); map.on("load", update); map.on("idle", update); map.on("moveend", () => { update(); try { const vp = getViewFromMap(map); const center = vp.center; localStorage.setItem( LAST_VIEW_KEY, JSON.stringify({ lat: center[1], lon: center[0], zoom: map.getZoom(), }) ); } catch {} }); // Also do an early update next frame (helps first load) requestAnimationFrame(update); return () => { clearAllMarkers(markersRef, expandedElRef); map.remove(); mapRef.current = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Theme style switch useEffect(() => { const map = mapRef.current; if (!map) return; // setStyle triggers style reload; we still want viewParams updated after map.setStyle(getBaseStyle(theme || "dark")); // update view params after style swap (next frame) requestAnimationFrame(() => { try { safeUpdateViewParams(map); } catch {} }); }, [theme]); return { containerRef, mapRef, markersRef, expandedElRef, viewParams, hasLastView, }; }