refactor: enhance unclustered point layer to include GeoJSON conversion and improve layer setup; add optional prop for incident marker visibility

This commit is contained in:
vergiLgood1 2025-05-07 03:42:14 +07:00
parent 6bfc148821
commit ae6eb40a13
3 changed files with 94 additions and 35 deletions

View File

@ -64,8 +64,11 @@ export default function FlyToHandler({ map }: Pick<IBaseLayerProps, "map">) {
},
})
// Add a pulsing effect using animations
// Add a slower pulsing effect
let size = 10
let frameCount = 0
const animationSpeed = 3; // Higher value = slower animation (skip frames)
const animatePulse = () => {
if (!map || !map.getLayer("target-incident-highlight")) {
if (animationRef.current) {
@ -75,7 +78,11 @@ export default function FlyToHandler({ map }: Pick<IBaseLayerProps, "map">) {
return
}
size = (size % 20) + 1
frameCount++;
// Only update size every few frames to slow down the animation
if (frameCount % animationSpeed === 0) {
size = (size % 20) + 0.5; // Smaller increment for smoother, slower animation
}
map.setPaintProperty("target-incident-highlight", "circle-radius", [
"interpolate",

View File

@ -3,7 +3,6 @@
import { IUnclusteredPointLayerProps } from "@/app/_utils/types/map"
import { useEffect, useCallback } from "react"
export default function UnclusteredPointLayer({
visible = true,
map,
@ -11,6 +10,37 @@ export default function UnclusteredPointLayer({
filterCategory = "all",
focusedDistrictId,
}: IUnclusteredPointLayerProps) {
// Konversi crimes ke GeoJSON FeatureCollection
const geojsonData = {
type: "FeatureCollection" as const,
features: crimes.flatMap((crime) =>
crime.crime_incidents
.filter(
(incident) =>
(filterCategory === "all" || incident.crime_categories.name === filterCategory) &&
incident.locations &&
typeof incident.locations.longitude === "number" &&
typeof incident.locations.latitude === "number"
)
.map((incident) => ({
type: "Feature" as const,
geometry: {
type: "Point" as const,
coordinates: [incident.locations.longitude, incident.locations.latitude],
},
properties: {
id: incident.id,
district: crime.districts.name,
category: incident.crime_categories.name,
incidentType: incident.crime_categories.type || "",
description: incident.description,
status: incident.status || "",
timestamp: incident.timestamp ? incident.timestamp.toString() : "",
},
}))
),
}
const handleIncidentClick = useCallback(
(e: any) => {
if (!map) return
@ -75,20 +105,31 @@ export default function UnclusteredPointLayer({
useEffect(() => {
if (!map || !visible) return
const onStyleLoad = () => {
if (!map) return
const setupLayerAndSource = () => {
try {
const layers = map.getStyle().layers
let firstSymbolId: string | undefined
// First check if source exists and update it
if (map.getSource("crime-incidents")) {
(map.getSource("crime-incidents") as any).setData(geojsonData);
} else {
// If not, add source
map.addSource("crime-incidents", {
type: "geojson",
data: geojsonData,
});
}
// Get layers to find first symbol layer
const layers = map.getStyle().layers;
let firstSymbolId: string | undefined;
for (const layer of layers) {
if (layer.type === "symbol") {
firstSymbolId = layer.id
break
firstSymbolId = layer.id;
break;
}
}
if (map.getSource("crime-incidents") && !map.getLayer("unclustered-point")) {
// Check if layer exists
if (!map.getLayer("unclustered-point")) {
map.addLayer(
{
id: "unclustered-point",
@ -106,43 +147,53 @@ export default function UnclusteredPointLayer({
},
},
firstSymbolId,
)
);
map.on("mouseenter", "unclustered-point", () => {
map.getCanvas().style.cursor = "pointer"
})
map.getCanvas().style.cursor = "pointer";
});
map.on("mouseleave", "unclustered-point", () => {
map.getCanvas().style.cursor = ""
})
map.off("click", "unclustered-point", handleIncidentClick)
map.on("click", "unclustered-point", handleIncidentClick)
} else if (map.getLayer("unclustered-point")) {
map.getCanvas().style.cursor = "";
});
} else {
// Update visibility based on focused district
map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible")
// Ensure click handler is registered
map.off("click", "unclustered-point", handleIncidentClick)
map.on("click", "unclustered-point", handleIncidentClick)
map.setLayoutProperty("unclustered-point", "visibility", focusedDistrictId ? "none" : "visible");
}
} catch (error) {
console.error("Error adding unclustered point layer:", error)
}
}
// Always ensure click handler is properly registered
map.off("click", "unclustered-point", handleIncidentClick);
map.on("click", "unclustered-point", handleIncidentClick);
} catch (error) {
console.error("Error setting up unclustered point layer:", error);
}
};
// Check if style is loaded and set up layer accordingly
if (map.isStyleLoaded()) {
onStyleLoad()
setupLayerAndSource();
} else {
map.once("style.load", onStyleLoad)
// Add event listener for style loading completion
const onStyleLoad = () => {
setupLayerAndSource();
};
map.once("style.load", onStyleLoad);
// Also wait a bit and try again as a fallback
setTimeout(() => {
if (map.isStyleLoaded()) {
setupLayerAndSource();
}
}, 500);
}
return () => {
if (map) {
map.off("click", "unclustered-point", handleIncidentClick)
map.off("click", "unclustered-point", handleIncidentClick);
}
}
}, [map, visible, focusedDistrictId, handleIncidentClick])
};
}, [map, visible, focusedDistrictId, handleIncidentClick, crimes, filterCategory, geojsonData]);
return null
return null;
}

View File

@ -104,4 +104,5 @@ export interface IUnclusteredPointLayerProps extends IBaseLayerProps {
crimes: ICrimes[];
filterCategory: string | 'all';
focusedDistrictId: string | null;
showIncidentMarkers?: boolean; // Add this prop to control marker visibility
}