"use client"; import { type CSSProperties, useEffect, useRef } from "react"; import "maplibre-gl/dist/maplibre-gl.css"; import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState"; import { BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers"; import type { EditorMode } from "@/uhm/lib/editor/session/sessionTypes"; import { useMapInstance } from "./map/useMapInstance"; import { setupMapLayers } from "./map/useMapLayers"; import { useMapInteraction } from "./map/useMapInteraction"; import { useMapSync } from "./map/useMapSync"; export type MapHoverPayload = { featureId: string | number; feature: Feature | null; point: { x: number; y: number }; lngLat: { lng: number; lat: number }; }; type MapProps = { mode: EditorMode; draft: FeatureCollection; backgroundVisibility: BackgroundLayerVisibility; geometryVisibility?: Record; selectedFeatureIds: (string | number)[]; onSelectFeatureIds: (ids: (string | number)[]) => void; labelContextDraft?: FeatureCollection; onCreateFeature?: (feature: FeatureCollection["features"][number]) => void; onDeleteFeature?: (id: string | number) => void; onUpdateFeature?: (id: string | number, geometry: Geometry) => void; allowGeometryEditing?: boolean; respectBindingFilter?: boolean; height?: CSSProperties["height"]; fitToDraftBounds?: boolean; fitBoundsKey?: string | number | null; onHoverFeatureChange?: ((payload: MapHoverPayload | null) => void) | undefined; highlightFeatures?: FeatureCollection | null; focusFeatureCollection?: FeatureCollection | null; focusRequestKey?: string | number | null; focusPadding?: number | import("maplibre-gl").PaddingOptions; }; export default function Map({ mode, draft, backgroundVisibility, geometryVisibility, selectedFeatureIds, onSelectFeatureIds, labelContextDraft, onCreateFeature, onDeleteFeature, onUpdateFeature, allowGeometryEditing = true, respectBindingFilter = true, height = "100vh", fitToDraftBounds = false, fitBoundsKey = null, onHoverFeatureChange, highlightFeatures = null, focusFeatureCollection = null, focusRequestKey = null, focusPadding, }: MapProps) { const modeRef = useRef(mode); const draftRef = useRef(draft); const onSelectFeatureIdsRef = useRef(onSelectFeatureIds); const onHoverFeatureChangeRef = useRef(onHoverFeatureChange); const onCreateRef = useRef(onCreateFeature); const onDeleteRef = useRef(onDeleteFeature); const onUpdateRef = useRef(onUpdateFeature); useEffect(() => { modeRef.current = mode; }, [mode]); useEffect(() => { draftRef.current = draft; }, [draft]); useEffect(() => { onSelectFeatureIdsRef.current = onSelectFeatureIds; }, [onSelectFeatureIds]); useEffect(() => { onHoverFeatureChangeRef.current = onHoverFeatureChange; }, [onHoverFeatureChange]); useEffect(() => { onCreateRef.current = onCreateFeature; }, [onCreateFeature]); useEffect(() => { onDeleteRef.current = onDeleteFeature; }, [onDeleteFeature]); useEffect(() => { onUpdateRef.current = onUpdateFeature; }, [onUpdateFeature]); const { mapRef, containerRef, fatalInitError, zoomLevel, zoomBounds, isGlobeProjection, setIsGlobeProjection, isMapLoaded, geolocationCenteredRef, handleZoomByStep, handleZoomSliderChange, } = useMapInstance(); const { editingEngineRef, setupMapInteractions, cleanupMapInteractions, } = useMapInteraction({ mapRef, mode, modeRef, draftRef, allowGeometryEditing, selectedFeatureIds, onSelectFeatureIdsRef, onCreateRef, onDeleteRef, onUpdateRef, onHoverFeatureChangeRef, }); const { applyDraftToMap, applyHighlightToMap, tryCenterToUserLocation, } = useMapSync({ mapRef, draft, labelContextDraft, backgroundVisibility, geometryVisibility, selectedFeatureIds, respectBindingFilter, fitToDraftBounds, fitBoundsKey, highlightFeatures, focusFeatureCollection, focusRequestKey, focusPadding, allowGeometryEditing, editingEngineRef, geolocationCenteredRef, }); useEffect(() => { const map = mapRef.current; if (!map || !isMapLoaded) return; setupMapLayers(map, backgroundVisibility, highlightFeatures, applyHighlightToMap); setupMapInteractions(map); applyDraftToMap(draftRef.current); tryCenterToUserLocation(); return () => { cleanupMapInteractions(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMapLoaded]); return (
{fatalInitError ? (
Map khong khoi tao duoc
{fatalInitError}
) : null}
handleZoomSliderChange(Number(event.target.value))} style={{ flex: 1, accentColor: "#38bdf8", cursor: "pointer", }} aria-label="Map zoom" />
{zoomLevel.toFixed(1)}x
); } const zoomButtonStyle: React.CSSProperties = { width: "28px", height: "28px", borderRadius: "999px", border: "1px solid #334155", background: "#1e293b", color: "#f8fafc", fontSize: "18px", lineHeight: "1", cursor: "pointer", display: "grid", placeItems: "center", };