"use client"; import maplibregl from "maplibre-gl"; import "maplibre-gl/dist/maplibre-gl.css"; import { createContext, forwardRef, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState, type ReactNode, } from "react"; import { cn } from "@/lib/utils"; import { DEFAULT_STYLE, type MapStyleSpec } from "@/lib/map-styles"; type MapContextValue = { map: maplibregl.Map | null; isLoaded: boolean; }; const MapContext = createContext(null); export function useMap() { const context = useContext(MapContext); if (!context) throw new Error("useMap must be used within a provider"); return context; } type MapProps = { children?: ReactNode; className?: string; mapStyle?: MapStyleSpec; center?: [number, number]; zoom?: number; pitch?: number; bearing?: number; minZoom?: number; maxZoom?: number; }; export type MapRef = maplibregl.Map; export const Map = forwardRef(function Map( { children, className, mapStyle = DEFAULT_STYLE.style, center = [0, 20], zoom = 2.5, pitch = 49, bearing = -20, minZoom = 2, maxZoom = 16, }, ref, ) { const containerRef = useRef(null); const [mapInstance, setMapInstance] = useState(null); const [isLoaded, setIsLoaded] = useState(false); useImperativeHandle(ref, () => mapInstance as maplibregl.Map, [mapInstance]); useEffect(() => { if (!containerRef.current) return; const map = new maplibregl.Map({ container: containerRef.current, style: DEFAULT_STYLE.style as maplibregl.StyleSpecification | string, center, zoom, pitch, bearing, minZoom, maxZoom, maxPitch: 85, attributionControl: false, renderWorldCopies: false, }); map.on("load", () => setIsLoaded(true)); setMapInstance(map); return () => { map.remove(); setIsLoaded(false); setMapInstance(null); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (!mapInstance || !isLoaded) return; mapInstance.setStyle(mapStyle as maplibregl.StyleSpecification | string); const applyTerrain = () => { if (typeof mapStyle === "object" && "terrain" in mapStyle) { const spec = mapStyle as Record; try { mapInstance.setTerrain( spec.terrain as maplibregl.TerrainSpecification, ); } catch { /* terrain source not yet loaded */ } } else { try { mapInstance.setTerrain(null); } catch { /* no terrain to remove */ } } }; mapInstance.once("style.load", applyTerrain); return () => { mapInstance.off("style.load", applyTerrain); }; }, [mapInstance, isLoaded, mapStyle]); const ctx = useMemo( () => ({ map: mapInstance, isLoaded }), [mapInstance, isLoaded], ); return (
{mapInstance && children}
); }); Map.displayName = "Map";