diff --git a/sigap-website/app/_components/map/layers/cluster-layer.tsx b/sigap-website/app/_components/map/layers/cluster-layer.tsx index 7f632d0..20e401c 100644 --- a/sigap-website/app/_components/map/layers/cluster-layer.tsx +++ b/sigap-website/app/_components/map/layers/cluster-layer.tsx @@ -1,11 +1,11 @@ "use client" import { useEffect, useCallback } from "react" - import mapboxgl from "mapbox-gl" import type { GeoJSON } from "geojson" import type { IClusterLayerProps } from "@/app/_utils/types/map" import { extractCrimeIncidents } from "@/app/_utils/map" +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" interface ExtendedClusterLayerProps extends IClusterLayerProps { clusteringEnabled?: boolean @@ -23,6 +23,9 @@ export default function ClusterLayer({ showClusters = false, sourceType = "cbt", }: ExtendedClusterLayerProps) { + // Define layer IDs for consistent management + const LAYER_IDS = ['clusters', 'cluster-count', 'crime-points', 'crime-count-labels']; + const handleClusterClick = useCallback( (e: any) => { if (!map) return @@ -64,6 +67,13 @@ export default function ClusterLayer({ [map], ) + // Use centralized layer visibility management + useEffect(() => { + if (!map) return; + + return manageLayerVisibility(map, LAYER_IDS, visible && showClusters && !focusedDistrictId); + }, [map, visible, showClusters, focusedDistrictId]); + useEffect(() => { if (!map || !visible) return diff --git a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx index 8acd156..8eb8115 100644 --- a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx @@ -3,6 +3,7 @@ import { getCrimeRateColor, getFillOpacity } from "@/app/_utils/map" import type { IExtrusionLayerProps } from "@/app/_utils/types/map" import { useEffect, useRef } from "react" +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" export default function DistrictExtrusionLayer({ visible = true, @@ -17,6 +18,9 @@ export default function DistrictExtrusionLayer({ const extrusionCreatedRef = useRef(false) const lastFocusedDistrictRef = useRef(null) + // Define layer IDs for consistent management + const LAYER_IDS = ['district-extrusion']; + // Helper to (re)create the extrusion layer const createExtrusionLayer = () => { if (!map) return @@ -51,7 +55,6 @@ export default function DistrictExtrusionLayer({ } } - // Create the extrusion layer map.addLayer( { @@ -339,5 +342,23 @@ export default function DistrictExtrusionLayer({ rotationAnimationRef.current = requestAnimationFrame(animate) } + // Use centralized layer visibility management + useEffect(() => { + if (!map) return; + + // Special case: also cancel animations when hiding the layer + return manageLayerVisibility(map, LAYER_IDS, visible && !!focusedDistrictId, () => { + if (!visible && animationRef.current) { + cancelAnimationFrame(animationRef.current); + animationRef.current = null; + } + + if (!visible && rotationAnimationRef.current) { + cancelAnimationFrame(rotationAnimationRef.current); + rotationAnimationRef.current = null; + } + }); + }, [map, visible, focusedDistrictId]); + return null } diff --git a/sigap-website/app/_components/map/layers/fault-lines.tsx b/sigap-website/app/_components/map/layers/fault-lines.tsx index 5adfbcd..cf6e4e2 100644 --- a/sigap-website/app/_components/map/layers/fault-lines.tsx +++ b/sigap-website/app/_components/map/layers/fault-lines.tsx @@ -2,6 +2,7 @@ import React, { useEffect } from 'react'; import mapboxgl from 'mapbox-gl'; +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" interface FaultLinesLayerProps { map: mapboxgl.Map | null; @@ -9,16 +10,16 @@ interface FaultLinesLayerProps { } export default function FaultLinesLayer({ map, visible = true }: FaultLinesLayerProps) { + // Define layer IDs for consistent management + const LAYER_IDS = ['indo-faults-line-layer']; + useEffect(() => { if (!map) return; // Function to add fault lines layer - function addFaultLines() { - const sourceId = 'indo_faults_lines'; - const layerId = 'indo_faults_line_layer'; - - // Make sure map is defined - if (!map) return; + const setupFaultLines = () => { + const sourceId = 'indo-faults-lines'; + const layerId = 'indo-faults-line-layer'; try { // Check if the source already exists @@ -45,55 +46,22 @@ export default function FaultLinesLayer({ map, visible = true }: FaultLinesLayer 'line-opacity': 0.5 } }); - } else if (map.getLayer(layerId)) { - // If the layer exists, just update its visibility - map.setLayoutProperty( - layerId, - 'visibility', - visible ? 'visible' : 'none' - ); } + + // Use the manageLayerVisibility utility to handle layer visibility + manageLayerVisibility(map, LAYER_IDS, visible); } catch (error) { console.warn("Error adding fault lines:", error); } - } - - // Function to clean up layers - function cleanupFaultLines() { - try { - if (!map || !map.getStyle()) return; - - const layerId = 'indo_faults_line_layer'; - const sourceId = 'indo_faults_lines'; - - if (map.getLayer(layerId)) { - map.removeLayer(layerId); - } - - if (map.getSource(sourceId)) { - map.removeSource(sourceId); - } - } catch (error) { - console.warn("Error cleaning up fault lines:", error); - } - } - - let timeoutId: NodeJS.Timeout | undefined; + }; // Try to add the layers now or set up listeners for when map is ready try { if (map.loaded() && map.isStyleLoaded()) { - addFaultLines(); + setupFaultLines(); } else { // Use multiple events to catch map ready state - map.on('load', addFaultLines); - map.on('style.load', addFaultLines); - map.on('styledata', addFaultLines); - - // Fallback timeout - timeoutId = setTimeout(() => { - addFaultLines(); - }, 2000); + map.once('load', setupFaultLines); } } catch (error) { console.warn("Error setting up fault lines:", error); @@ -101,15 +69,9 @@ export default function FaultLinesLayer({ map, visible = true }: FaultLinesLayer // Single cleanup function return () => { - if (timeoutId) clearTimeout(timeoutId); - if (map) { - map.off('load', addFaultLines); - map.off('style.load', addFaultLines); - map.off('styledata', addFaultLines); + map.off('load', setupFaultLines); } - - cleanupFaultLines(); }; }, [map, visible]); diff --git a/sigap-website/app/_components/map/layers/heatmap-layer.tsx b/sigap-website/app/_components/map/layers/heatmap-layer.tsx index bbf89fb..4a3ab65 100644 --- a/sigap-website/app/_components/map/layers/heatmap-layer.tsx +++ b/sigap-website/app/_components/map/layers/heatmap-layer.tsx @@ -1,6 +1,8 @@ import { Layer, Source } from "react-map-gl/mapbox"; -import { useMemo } from "react"; +import { useMemo, useEffect } from "react"; import { ICrimes } from "@/app/_utils/types/crimes"; +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility"; +import type mapboxgl from "mapbox-gl"; interface HeatmapLayerProps { crimes: ICrimes[]; @@ -9,8 +11,9 @@ interface HeatmapLayerProps { filterCategory: string | "all"; visible?: boolean; useAllData?: boolean; - enableInteractions?: boolean; // Add new prop - setFocusedDistrictId?: (id: string | null, isMarkerClick?: boolean) => void; // Add new prop + enableInteractions?: boolean; + setFocusedDistrictId?: (id: string | null, isMarkerClick?: boolean) => void; + map?: mapboxgl.Map | null; } export default function HeatmapLayer({ @@ -21,8 +24,12 @@ export default function HeatmapLayer({ year, month, enableInteractions = true, - setFocusedDistrictId + setFocusedDistrictId, + map }: HeatmapLayerProps) { + // Define layer IDs for consistent management + const LAYER_IDS = ['crime-heatmap']; + // Convert crime data to GeoJSON format for the heatmap const heatmapData = useMemo(() => { const features = crimes.flatMap(crime => @@ -74,11 +81,14 @@ export default function HeatmapLayer({ }; }, [crimes, filterCategory, useAllData, year, month]); - if (!visible) return null; + // Manage layer visibility + useEffect(() => { + if (!map) return; - // The heatmap layer doesn't generally support direct interactions like clicks, - // but we're including the props to maintain consistency with other layers - // and to support future interaction patterns if needed + return manageLayerVisibility(map, LAYER_IDS, visible); + }, [map, visible]); + + if (!visible) return null; return ( diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index a786ce9..8ac45d4 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -22,13 +22,9 @@ import DistrictFillLineLayer from "./district-layer" import TimezoneLayer from "./timezone" import FaultLinesLayer from "./fault-lines" -import EWSAlertLayer from "./ews-alert-layer" -import PanicButtonDemo from "../controls/panic-button-demo" - -import type { IIncidentLog } from "@/app/_utils/types/ews" -import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data" import RecentIncidentsLayer from "./recent-incidents-layer" import IncidentPopup from "../pop-up/incident-popup" +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" // Interface for crime incident interface ICrimeIncident { @@ -112,33 +108,6 @@ export default function Layers({ const crimeDataByDistrict = processCrimeDataByDistrict(crimes) - const [ewsIncidents, setEwsIncidents] = useState([]) - const [showPanicDemo, setShowPanicDemo] = useState(true) - const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) - - // useEffect(() => { - // setEwsIncidents(getAllIncidents()) - // }, []) - - // const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => { - // const newIncident = addMockIncident({ priority }) - // setEwsIncidents(getAllIncidents()) - // }, []) - - // const handleResolveIncident = useCallback((id: string) => { - // resolveIncident(id) - // setEwsIncidents(getAllIncidents()) - // }, []) - - // const handleResolveAllAlerts = useCallback(() => { - // ewsIncidents.forEach((incident) => { - // if (incident.status === "active") { - // resolveIncident(incident.id) - // } - // }) - // setEwsIncidents(getAllIncidents()) - // }, [ewsIncidents]) - const handlePopupClose = useCallback(() => { selectedDistrictRef.current = null setSelectedDistrict(null) @@ -167,8 +136,6 @@ export default function Layers({ const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any) } - - } }, [map, crimeDataByDistrict]) @@ -248,26 +215,17 @@ export default function Layers({ } const handleCloseDistrictPopup = useCallback(() => { - // console.log("Closing district popup") - animateExtrusionDown() handlePopupClose() }, [handlePopupClose, animateExtrusionDown]) const handleDistrictClick = useCallback( (feature: IDistrictFeature) => { - // console.log("District clicked:", feature) - - // If we're currently interacting with a marker, don't process district click if (isInteractingWithMarker.current) { - // console.log("Ignoring district click because we're interacting with a marker") return } - // Clear any existing incident selection setSelectedIncident(null) - - // Set the district as selected setSelectedDistrict(feature) selectedDistrictRef.current = feature setFocusedDistrictId(feature.id) @@ -282,7 +240,6 @@ export default function Layers({ easing: (t) => t * (2 - t), }) - // Hide clusters when focusing on a district if (map.getLayer("clusters")) { map.getMap().setLayoutProperty("clusters", "visibility", "none") } @@ -403,13 +360,9 @@ export default function Layers({ }, [crimes, filterCategory, year, month, crimeDataByDistrict]) const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => { - // console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick) - - // If this is from a marker click, set the marker interaction flag if (isMarkerClick) { isInteractingWithMarker.current = true - // Reset the flag after a delay setTimeout(() => { isInteractingWithMarker.current = false }, 1000) @@ -431,33 +384,41 @@ export default function Layers({ activeControl === "recents" const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu" - // Ensure showPanicDemo is always defined - // const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) - - // Always render the DistrictExtrusionLayer when a district is focused - // This ensures it's available when needed const shouldShowExtrusion = focusedDistrictId !== null && !isInteractingWithMarker.current useEffect(() => { if (!mapboxMap) return; - const ensureLayerVisibility = () => { - if (activeControl !== "recents") { - const recentLayerIds = [ - "very-recent-incidents-pulse", - "recent-incidents-glow", - "recent-incidents" - ]; + const recentLayerIds = ["very-recent-incidents-pulse", "recent-incidents-glow", "recent-incidents"]; + const timelineLayerIds = ["timeline-markers-bg", "timeline-markers"]; + const heatmapLayerIds = ["heatmap-layer"]; + const unitsLayerIds = ["units-points", "incidents-points", "units-labels", "units-connection-lines"]; + const clusterLayerIds = ["clusters", "cluster-count", "crime-points", "crime-count-labels"]; + const unclusteredLayerIds = ["unclustered-point"]; - recentLayerIds.forEach(layerId => { - if (mapboxMap.getLayer(layerId)) { - mapboxMap.setLayoutProperty(layerId, "visibility", "none"); - } - }); - } - }; + if (activeControl !== "recents") { + manageLayerVisibility(mapboxMap, recentLayerIds, false); + } - ensureLayerVisibility(); + if (activeControl !== "timeline") { + manageLayerVisibility(mapboxMap, timelineLayerIds, false); + } + + if (activeControl !== "heatmap") { + manageLayerVisibility(mapboxMap, heatmapLayerIds, false); + } + + if (activeControl !== "units") { + manageLayerVisibility(mapboxMap, unitsLayerIds, false); + } + + if (activeControl !== "clusters") { + manageLayerVisibility(mapboxMap, clusterLayerIds, false); + } + + if (activeControl !== "incidents" && activeControl !== "recents" && activeControl !== "historical") { + manageLayerVisibility(mapboxMap, unclusteredLayerIds, false); + } }, [activeControl, mapboxMap]); @@ -479,7 +440,6 @@ export default function Layers({ onDistrictClick={handleDistrictClick} /> - {/* Always render the extrusion layer when a district is focused */} {shouldShowExtrusion && ( - - {/* {showEWS && } */} - - {/* {showEWS && displayPanicDemo && ( -
- inc.status === "active")} - /> -
- )} */} - - ) } diff --git a/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx b/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx index aed3fe7..ac1d1a7 100644 --- a/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx +++ b/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx @@ -5,7 +5,8 @@ import type { IIncidentLogs } from "@/app/_utils/types/crimes" import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM, PITCH_3D, ZOOM_3D } from "@/app/_utils/const/map" import IncidentLogsPopup from "../pop-up/incident-logs-popup" import type mapboxgl from "mapbox-gl" -import type { MapMouseEvent, MapGeoJSONFeature } from "react-map-gl/mapbox" +import type { MapGeoJSONFeature, MapMouseEvent } from "react-map-gl/mapbox" +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" interface IRecentIncidentsLayerProps { visible?: boolean @@ -46,8 +47,12 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents = const animationFrameRef = useRef(null) const [selectedIncident, setSelectedIncident] = useState(null) - // Add a ref to track if layers are initialized - const layersInitialized = useRef(false); + // Define layer IDs once to be consistent + const LAYER_IDS = [ + "very-recent-incidents-pulse", + "recent-incidents-glow", + "recent-incidents" + ]; // Filter incidents from the last 24 hours const recentIncidents = incidents.filter((incident) => { @@ -140,40 +145,20 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents = setSelectedIncident(null) }, [map]) - // This effect handles visibility changes and ensures markers are shown/hidden properly + // Effect to manage layer visibility consistently useEffect(() => { - if (!map) return; + const cleanup = manageLayerVisibility(map, LAYER_IDS, visible, () => { + // When layers become invisible, close any open popup + if (!visible) setSelectedIncident(null); - // Function to update layer visibility - const updateLayerVisibility = () => { - if (!map.isStyleLoaded()) return; - - const layers = [ - "very-recent-incidents-pulse", - "recent-incidents-glow", - "recent-incidents" - ]; - - layers.forEach(layerId => { - if (map.getLayer(layerId)) { - map.setLayoutProperty( - layerId, - "visibility", - visible ? "visible" : "none" - ); - } - }); - - // If closing, also close any open popups - if (!visible) { - setSelectedIncident(null); + // Cancel animation frame when hiding the layer + if (!visible && animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; } - }; + }); - // If layers are initialized, update their visibility - if (layersInitialized.current) { - updateLayerVisibility(); - } + return cleanup; }, [visible, map]); useEffect(() => { @@ -403,9 +388,6 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents = // Ensure click handler is properly registered map.off("click", "recent-incidents", handleIncidentClick) map.on("click", "recent-incidents", handleIncidentClick) - - // Mark layers as initialized - layersInitialized.current = true; } catch (error) { console.error("Error setting up recent incidents layer:", error) } @@ -437,16 +419,8 @@ export default function RecentIncidentsLayer({ visible = false, map, incidents = } }, [map, visible, recentIncidents, handleIncidentClick, twoHoursInMs]) - // Close popup when layer becomes invisible - useEffect(() => { - if (!visible) { - setSelectedIncident(null) - } - }, [visible]) - return ( <> - {/* Popup component */} {selectedIncident && ( {time}; } -export default function TimezoneLayer({ map }: TimezoneLayerProps) { +export default function TimezoneLayer({ + map, + visible = true, +}: TimezoneLayerProps) { const hoverTimezone = useRef(null); const markersRef = useRef([]); + // Define layer IDs for consistent management + const LAYER_IDS = ["timezone-layer"]; + useEffect(() => { if (!map) return; - // Function to add timezone data and markers - function addTimezoneLayers() { - // Add timezone data source - const url = "/geojson/timezones_wVVG8.geojson"; // Changed path to public folder - if (map && !map.getSource('timezone')) { - map.addSource('timezone', { - 'type': 'geojson', - 'generateId': true, - 'data': url - }); + // Function to add timezone layer + const setupTimezoneLayer = () => { + const sourceId = "timezone-source"; + const layerId = "timezone-layer"; - // Add timezone boundaries - map.addLayer({ - 'id': 'timezone-line', - 'type': 'line', - 'source': 'timezone', - 'layout': {}, - 'paint': { - 'line-color': 'orange', - 'line-width': 1, - 'line-opacity': 0.5 - } - }); + try { + // Check if the source already exists + if (!map.getSource(sourceId)) { + // Add timezone data source + const url = "/geojson/timezones_wVVG8.geojson"; + map.addSource(sourceId, { + type: "geojson", + generateId: true, + data: url, + }); + + // Add a line layer to visualize the timezones + map.addLayer({ + id: layerId, + type: "line", + source: sourceId, + layout: { + visibility: visible ? "visible" : "none", + }, + paint: { + "line-color": "green", + "line-width": 1, + "line-opacity": 0.5, + "line-dasharray": [2, 2], + }, + }); + } // Create markers for Indonesian time zones - const createTimeMarker = (lngLat: [number, number], timeZone: string, label: string) => { - const markerElement = document.createElement('div'); + const createTimeMarker = ( + lngLat: [number, number], + timeZone: string, + label: string, + ) => { + const markerElement = document.createElement("div"); const root = createRoot(markerElement); root.render( -
-

+

+

- {label} -
+ + {label} + +
, ); const marker = new mapboxgl.Marker(markerElement) @@ -89,59 +115,52 @@ export default function TimezoneLayer({ map }: TimezoneLayerProps) { }; // WIB (GMT+7) - createTimeMarker([107.4999769225339, 3.4359354227361933], "Asia/Jakarta", "WIB / GMT+7"); + createTimeMarker( + [107.4999769225339, 3.4359354227361933], + "Asia/Jakarta", + "WIB / GMT+7", + ); // WITA (GMT+8) - createTimeMarker([119.1174733337183, 3.4359354227361933], "Asia/Makassar", "WITA / GMT+8"); + createTimeMarker( + [119.1174733337183, 3.4359354227361933], + "Asia/Makassar", + "WITA / GMT+8", + ); // WIT (GMT+9) - createTimeMarker([131.58387377752751, 3.4359354227361933], "Asia/Jayapura", "WIT / GMT+9"); + createTimeMarker( + [131.58387377752751, 3.4359354227361933], + "Asia/Jayapura", + "WIT / GMT+9", + ); + + // Use the manageLayerVisibility utility to handle layer visibility + manageLayerVisibility(map, LAYER_IDS, visible); + } catch (error) { + console.warn("Error adding timezone layer:", error); } - } - - // Check if style is loaded, otherwise wait for it - if (map.isStyleLoaded()) { - addTimezoneLayers(); - } else { - // Use both 'load' and 'style.load' events to ensure we catch the style loading - map.on('load', addTimezoneLayers); - map.on('style.load', addTimezoneLayers); - - // Fallback: If style hasn't loaded within 2 seconds, try again - const timeoutId = setTimeout(() => { - if (map.isStyleLoaded()) { - addTimezoneLayers(); - } - }, 2000); - - // Clean up the timeout if component unmounts - return () => { - clearTimeout(timeoutId); - map.off('load', addTimezoneLayers); - map.off('style.load', addTimezoneLayers); - - cleanupLayers(); - }; - } - - function cleanupLayers() { - if (map && map.getLayer('timezone-line')) { - map.removeLayer('timezone-line'); - } - if (map && map.getSource('timezone')) { - map.removeSource('timezone'); - } - - // Remove all markers - markersRef.current.forEach(marker => marker.remove()); - markersRef.current = []; - } - - // Clean up function - return () => { - cleanupLayers(); }; - }, [map]); + + // Try to add the layers now or set up listeners for when map is ready + try { + if (map.loaded() && map.isStyleLoaded()) { + setupTimezoneLayer(); + } else { + // Use event to catch map ready state + map.once("load", setupTimezoneLayer); + } + } catch (error) { + console.warn("Error setting up timezone layer:", error); + } + + // Cleanup function + return () => { + if (map) { + map.off("load", setupTimezoneLayer); + } + }; + }, [map, visible]); return null; } diff --git a/sigap-website/app/_components/map/layers/uncluster-layer.tsx b/sigap-website/app/_components/map/layers/uncluster-layer.tsx index 5cbcfe1..5a113ff 100644 --- a/sigap-website/app/_components/map/layers/uncluster-layer.tsx +++ b/sigap-website/app/_components/map/layers/uncluster-layer.tsx @@ -2,6 +2,7 @@ import type { IUnclusteredPointLayerProps } from "@/app/_utils/types/map" import { useEffect, useCallback, useRef } from "react" +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" export default function UnclusteredPointLayer({ visible = true, @@ -13,6 +14,9 @@ export default function UnclusteredPointLayer({ // Add a ref to track if we're currently interacting with a marker const isInteractingWithMarker = useRef(false); + // Define layer IDs for consistent management + const LAYER_IDS = ['unclustered-point']; + const handleIncidentClick = useCallback( (e: any) => { if (!map) return @@ -73,7 +77,17 @@ export default function UnclusteredPointLayer({ }, 500); }, [map], - ) + ); + + // Use centralized layer visibility management + useEffect(() => { + if (!map) return; + + // Special case for this layer: also consider focusedDistrictId + const isActuallyVisible = visible && !(focusedDistrictId && !isInteractingWithMarker.current); + + return manageLayerVisibility(map, LAYER_IDS, isActuallyVisible); + }, [map, visible, focusedDistrictId]); useEffect(() => { if (!map || !visible) return diff --git a/sigap-website/app/_components/map/layers/units-layer.tsx b/sigap-website/app/_components/map/layers/units-layer.tsx index 03544cb..e610e45 100644 --- a/sigap-website/app/_components/map/layers/units-layer.tsx +++ b/sigap-website/app/_components/map/layers/units-layer.tsx @@ -13,6 +13,7 @@ import { BASE_BEARING, BASE_DURATION, BASE_PITCH, BASE_ZOOM } from "@/app/_utils import { INearestUnits } from "@/app/(pages)/(admin)/dashboard/crime-management/units/action" import { useGetNearestUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries" import IncidentPopup from "../pop-up/incident-popup" +import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility" interface UnitsLayerProps { crimes: ICrimes[] @@ -67,6 +68,14 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible // Add a ref to store pre-processed incidents by district for optimization const districtIncidentsCache = useRef>(new Map()); + // Define layer IDs for consistent management + const LAYER_IDS = [ + 'units-points', + 'units-symbols', + 'incidents-points', + 'units-connection-lines' + ]; + // Use either provided units or loaded units const unitsData = useMemo(() => { return units.length > 0 ? units : loadedUnits || [] @@ -594,6 +603,19 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible } }, [map]) + // Use centralized layer visibility management + useEffect(() => { + if (!map) return; + + const cleanup = manageLayerVisibility(map, LAYER_IDS, visible, () => { + if (!visible) { + handleClosePopup(); + } + }); + + return cleanup; + }, [map, visible, handleClosePopup]); + // Clean up on unmount or when visibility changes useEffect(() => { if (!visible) { @@ -601,15 +623,6 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible } }, [visible, handleClosePopup]) - // Debug untuk komponen render - // useEffect(() => { - // console.log("Render state:", { - // selectedUnit: selectedUnit?.code_unit, - // selectedIncident: selectedIncident?.id, - // visible - // }) - // }, [selectedUnit, selectedIncident, visible]) - if (!visible) return null return ( diff --git a/sigap-website/app/_utils/map/layer-visibility.ts b/sigap-website/app/_utils/map/layer-visibility.ts new file mode 100644 index 0000000..25e733a --- /dev/null +++ b/sigap-website/app/_utils/map/layer-visibility.ts @@ -0,0 +1,58 @@ +import type mapboxgl from "mapbox-gl" + +/** + * Manages visibility for map layers in a consistent way + * + * @param map The mapbox map instance + * @param layerIds Array of layer IDs to manage + * @param isVisible Boolean indicating if layers should be visible + * @param onCleanup Optional callback function to execute during cleanup + * @returns A cleanup function to remove listeners + */ +export function manageLayerVisibility( + map: mapboxgl.Map | null | undefined, + layerIds: string[], + isVisible: boolean, + onCleanup?: () => void +): () => void { + if (!map) return () => { } + + // Check if map is loaded, if not wait for it + if (!map.isStyleLoaded()) { + const setupOnLoad = () => { + updateLayersVisibility(map, layerIds, isVisible) + } + + map.once('load', setupOnLoad) + return () => { + map.off('load', setupOnLoad) + if (onCleanup) onCleanup() + } + } + + // Map is loaded, update visibility directly + updateLayersVisibility(map, layerIds, isVisible) + + return () => { + if (onCleanup) onCleanup() + } +} + +/** + * Updates visibility for specified layers + */ +function updateLayersVisibility( + map: mapboxgl.Map, + layerIds: string[], + isVisible: boolean +): void { + layerIds.forEach(layerId => { + if (map.getLayer(layerId)) { + map.setLayoutProperty( + layerId, + "visibility", + isVisible ? "visible" : "none" + ) + } + }) +}