feat: Enhance heatmap and timeline layers with new data handling and filtering options

This commit is contained in:
vergiLgood1 2025-05-05 11:11:11 +07:00
parent a19e8ec32d
commit fa7651619b
4 changed files with 136 additions and 96 deletions

View File

@ -27,7 +27,6 @@ import Layers from "./layers/layers"
import DistrictLayer, { DistrictFeature } from "./layers/district-layer-old" import DistrictLayer, { DistrictFeature } from "./layers/district-layer-old"
import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries" import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries"
// Updated CrimeIncident type to match the structure in crime_incidents
interface ICrimeIncident { interface ICrimeIncident {
id: string id: string
district?: string district?: string
@ -42,7 +41,6 @@ interface ICrimeIncident {
} }
export default function CrimeMap() { export default function CrimeMap() {
// State for sidebar
const [sidebarCollapsed, setSidebarCollapsed] = useState(true) const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null) const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null) const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
@ -55,28 +53,25 @@ export default function CrimeMap() {
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false) const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
const [isSearchActive, setIsSearchActive] = useState(false) const [isSearchActive, setIsSearchActive] = useState(false)
const [showUnitsLayer, setShowUnitsLayer] = useState(false) const [showUnitsLayer, setShowUnitsLayer] = useState(false)
const [useAllYears, setUseAllYears] = useState<boolean>(false)
const [useAllMonths, setUseAllMonths] = useState<boolean>(false)
const mapContainerRef = useRef<HTMLDivElement>(null) const mapContainerRef = useRef<HTMLDivElement>(null)
// Use the custom fullscreen hook
const { isFullscreen } = useFullscreen(mapContainerRef) const { isFullscreen } = useFullscreen(mapContainerRef)
// Get available years
const { const {
data: availableYears, data: availableYears,
isLoading: isYearsLoading, isLoading: isYearsLoading,
error: yearsError error: yearsError
} = useGetAvailableYears() } = useGetAvailableYears()
// Extract all unique categories
const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories() const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories()
// Transform categories data to string array
const categories = useMemo(() => const categories = useMemo(() =>
categoriesData ? categoriesData.map(category => category.name) : [] categoriesData ? categoriesData.map(category => category.name) : []
, [categoriesData]) , [categoriesData])
// Get all crime data in a single request
const { const {
data: crimes, data: crimes,
isLoading: isCrimesLoading, isLoading: isCrimesLoading,
@ -85,22 +80,40 @@ export default function CrimeMap() {
const { data: fetchedUnits, isLoading } = useGetUnitsQuery() const { data: fetchedUnits, isLoading } = useGetUnitsQuery()
// Filter crimes based on selected year and month useEffect(() => {
if (activeControl === "heatmap" || activeControl === "timeline") {
setUseAllYears(true);
setUseAllMonths(true);
} else {
setUseAllYears(false);
setUseAllMonths(false);
}
}, [activeControl]);
const filteredByYearAndMonth = useMemo(() => { const filteredByYearAndMonth = useMemo(() => {
if (!crimes) return [] if (!crimes) return [];
if (useAllYears) {
if (useAllMonths) {
return crimes;
} else {
return crimes.filter((crime) => {
return selectedMonth === "all" ? true : crime.month === selectedMonth;
});
}
}
return crimes.filter((crime) => { return crimes.filter((crime) => {
const yearMatch = crime.year === selectedYear const yearMatch = crime.year === selectedYear;
if (selectedMonth === "all") { if (selectedMonth === "all" || useAllMonths) {
return yearMatch return yearMatch;
} else { } else {
return yearMatch && crime.month === selectedMonth return yearMatch && crime.month === selectedMonth;
} }
}) });
}, [crimes, selectedYear, selectedMonth]) }, [crimes, selectedYear, selectedMonth, useAllYears, useAllMonths]);
// Filter incidents based on selected category
const filteredCrimes = useMemo(() => { const filteredCrimes = useMemo(() => {
if (!filteredByYearAndMonth) return [] if (!filteredByYearAndMonth) return []
if (selectedCategory === "all") return filteredByYearAndMonth if (selectedCategory === "all") return filteredByYearAndMonth
@ -118,7 +131,6 @@ export default function CrimeMap() {
}) })
}, [filteredByYearAndMonth, selectedCategory]) }, [filteredByYearAndMonth, selectedCategory])
// Set up event listener for incident clicks from the district layer
useEffect(() => { useEffect(() => {
const handleIncidentClickEvent = (e: CustomEvent) => { const handleIncidentClickEvent = (e: CustomEvent) => {
console.log("Received incident_click event:", e.detail); console.log("Received incident_click event:", e.detail);
@ -127,14 +139,11 @@ export default function CrimeMap() {
return; return;
} }
// Find the incident in filtered crimes data using the id from the event
let foundIncident: ICrimeIncident | undefined; let foundIncident: ICrimeIncident | undefined;
// Search through all crimes and their incidents to find matching incident
filteredCrimes.forEach(crime => { filteredCrimes.forEach(crime => {
crime.crime_incidents.forEach(incident => { crime.crime_incidents.forEach(incident => {
if (incident.id === e.detail.id) { if (incident.id === e.detail.id) {
// Map the found incident to ICrimeIncident type
foundIncident = { foundIncident = {
id: incident.id, id: incident.id,
district: crime.districts.name, district: crime.districts.name,
@ -156,29 +165,23 @@ export default function CrimeMap() {
return; return;
} }
// Validate the coordinates
if (!foundIncident.latitude || !foundIncident.longitude) { if (!foundIncident.latitude || !foundIncident.longitude) {
console.error("Invalid incident coordinates:", foundIncident); console.error("Invalid incident coordinates:", foundIncident);
return; return;
} }
// When an incident is clicked, clear any selected district
setSelectedDistrict(null); setSelectedDistrict(null);
// Set the selected incident
setSelectedIncident(foundIncident); setSelectedIncident(foundIncident);
}; };
// Add event listener to the map container and document
const mapContainer = mapContainerRef.current; const mapContainer = mapContainerRef.current;
// Clean up previous listeners to prevent duplicates
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
if (mapContainer) { if (mapContainer) {
mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener); mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
} }
// Listen on both the container and document to ensure we catch the event
document.addEventListener('incident_click', handleIncidentClickEvent as EventListener); document.addEventListener('incident_click', handleIncidentClickEvent as EventListener);
if (mapContainer) { if (mapContainer) {
@ -193,7 +196,6 @@ export default function CrimeMap() {
}; };
}, [filteredCrimes]); }, [filteredCrimes]);
// Set up event listener for fly-to map control
useEffect(() => { useEffect(() => {
const handleMapFlyTo = (e: CustomEvent) => { const handleMapFlyTo = (e: CustomEvent) => {
if (!e.detail) { if (!e.detail) {
@ -203,14 +205,12 @@ export default function CrimeMap() {
const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail; const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail;
// Find the map instance
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
if (!mapInstance) { if (!mapInstance) {
console.error("Map instance not found"); console.error("Map instance not found");
return; return;
} }
// Create and dispatch a custom event that MapView component will listen for
const mapboxEvent = new CustomEvent('mapbox_fly', { const mapboxEvent = new CustomEvent('mapbox_fly', {
detail: { detail: {
center: [longitude, latitude], center: [longitude, latitude],
@ -225,7 +225,6 @@ export default function CrimeMap() {
mapInstance.dispatchEvent(mapboxEvent); mapInstance.dispatchEvent(mapboxEvent);
}; };
// Add event listener
document.addEventListener('mapbox_fly_to', handleMapFlyTo as EventListener); document.addEventListener('mapbox_fly_to', handleMapFlyTo as EventListener);
return () => { return () => {
@ -233,19 +232,16 @@ export default function CrimeMap() {
}; };
}, []); }, []);
// Add event listener for map reset
useEffect(() => { useEffect(() => {
const handleMapReset = (e: CustomEvent) => { const handleMapReset = (e: CustomEvent) => {
const { duration } = e.detail || { duration: 1500 }; const { duration } = e.detail || { duration: 1500 };
// Find the map instance
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
if (!mapInstance) { if (!mapInstance) {
console.error("Map instance not found"); console.error("Map instance not found");
return; return;
} }
// Create and dispatch the reset event that MapView will listen for
const mapboxEvent = new CustomEvent('mapbox_fly', { const mapboxEvent = new CustomEvent('mapbox_fly', {
detail: { detail: {
duration: duration, duration: duration,
@ -257,7 +253,6 @@ export default function CrimeMap() {
mapInstance.dispatchEvent(mapboxEvent); mapInstance.dispatchEvent(mapboxEvent);
}; };
// Add event listener
document.addEventListener('mapbox_reset', handleMapReset as EventListener); document.addEventListener('mapbox_reset', handleMapReset as EventListener);
return () => { return () => {
@ -265,11 +260,9 @@ export default function CrimeMap() {
}; };
}, []); }, []);
// Update the popup close handler to reset the map view
const handlePopupClose = () => { const handlePopupClose = () => {
setSelectedIncident(null); setSelectedIncident(null);
// Dispatch map reset event to reset zoom, pitch, and bearing
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map'); const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
if (mapInstance) { if (mapInstance) {
@ -284,57 +277,61 @@ export default function CrimeMap() {
} }
} }
// Handle year-month timeline change
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => { const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
setSelectedYear(year) setSelectedYear(year)
setSelectedMonth(month) setSelectedMonth(month)
setYearProgress(progress) setYearProgress(progress)
}, []) }, [])
// Handle timeline playing state change
const handleTimelinePlayingChange = useCallback((playing: boolean) => { const handleTimelinePlayingChange = useCallback((playing: boolean) => {
setisTimelapsePlaying(playing) setisTimelapsePlaying(playing)
// When timelapse starts, close any open popups/details
if (playing) { if (playing) {
setSelectedIncident(null) setSelectedIncident(null)
setSelectedDistrict(null) setSelectedDistrict(null)
} }
}, []) }, [])
// Reset filters
const resetFilters = useCallback(() => { const resetFilters = useCallback(() => {
setSelectedYear(2024) setSelectedYear(2024)
setSelectedMonth("all") setSelectedMonth("all")
setSelectedCategory("all") setSelectedCategory("all")
}, []) }, [])
// Determine the title based on filters
const getMapTitle = () => { const getMapTitle = () => {
let title = `${selectedYear}` if (useAllYears) {
if (selectedMonth !== "all") { return `All Years Data ${selectedCategory !== "all" ? `- ${selectedCategory}` : ''}`;
title += ` - ${getMonthName(Number(selectedMonth))}` }
let title = `${selectedYear}`;
if (selectedMonth !== "all" && !useAllMonths) {
title += ` - ${getMonthName(Number(selectedMonth))}`;
} }
if (selectedCategory !== "all") { if (selectedCategory !== "all") {
title += ` - ${selectedCategory}` title += ` - ${selectedCategory}`;
} }
return title return title;
} }
// Handle control changes from the top controls component
const handleControlChange = (controlId: ITooltips) => { const handleControlChange = (controlId: ITooltips) => {
setActiveControl(controlId) setActiveControl(controlId);
// Toggle search state when search control is clicked
if (controlId === "search") { if (controlId === "search") {
setIsSearchActive(prev => !prev) setIsSearchActive(prev => !prev);
} }
// Toggle units layer visibility when units control is clicked
if (controlId === "units") { if (controlId === "units") {
setShowUnitsLayer(true) setShowUnitsLayer(true);
} else if (showUnitsLayer) { } else if (showUnitsLayer) {
setShowUnitsLayer(false) setShowUnitsLayer(false);
}
if (controlId === "heatmap" || controlId === "timeline") {
setUseAllYears(true);
setUseAllMonths(true);
} else {
setUseAllYears(false);
setUseAllMonths(false);
} }
} }
@ -375,7 +372,6 @@ export default function CrimeMap() {
!sidebarCollapsed && isFullscreen && "ml-[400px]" !sidebarCollapsed && isFullscreen && "ml-[400px]"
)}> )}>
<MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md"> <MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md">
{/* Replace the DistrictLayer with the new Layers component */}
<Layers <Layers
crimes={filteredCrimes || []} crimes={filteredCrimes || []}
units={fetchedUnits || []} units={fetchedUnits || []}
@ -383,9 +379,9 @@ export default function CrimeMap() {
month={selectedMonth.toString()} month={selectedMonth.toString()}
filterCategory={selectedCategory} filterCategory={selectedCategory}
activeControl={activeControl} activeControl={activeControl}
useAllData={useAllYears}
/> />
{/* Popup for selected incident */}
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
<> <>
<CrimePopup <CrimePopup
@ -397,7 +393,6 @@ export default function CrimeMap() {
</> </>
)} )}
{/* Components that are only visible in fullscreen mode */}
{isFullscreen && ( {isFullscreen && (
<> <>
<div className="absolute flex w-full p-2"> <div className="absolute flex w-full p-2">
@ -416,7 +411,6 @@ export default function CrimeMap() {
/> />
</div> </div>
{/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */}
<CrimeSidebar <CrimeSidebar
crimes={filteredCrimes || []} crimes={filteredCrimes || []}
defaultCollapsed={sidebarCollapsed} defaultCollapsed={sidebarCollapsed}

View File

@ -8,20 +8,40 @@ interface HeatmapLayerProps {
month: string; month: string;
filterCategory: string | "all"; filterCategory: string | "all";
visible?: boolean; visible?: boolean;
useAllData?: boolean; // Add prop to indicate if we should use all data
} }
export default function HeatmapLayer({ crimes, visible = true }: HeatmapLayerProps) { export default function HeatmapLayer({
crimes,
visible = true,
useAllData = false,
filterCategory
}: HeatmapLayerProps) {
// Convert crime data to GeoJSON format for the heatmap // Convert crime data to GeoJSON format for the heatmap
const heatmapData = useMemo(() => { const heatmapData = useMemo(() => {
const features = crimes.flatMap(crime => const features = crimes.flatMap(crime =>
crime.crime_incidents crime.crime_incidents
.filter(incident => incident.locations?.latitude && incident.locations?.longitude) .filter(incident => {
// Enhanced filtering logic
if (!incident.locations?.latitude || !incident.locations?.longitude) {
return false;
}
// Filter by category if specified
if (filterCategory !== "all" &&
incident.crime_categories?.name !== filterCategory) {
return false;
}
return true;
})
.map(incident => ({ .map(incident => ({
type: "Feature" as const, type: "Feature" as const,
properties: { properties: {
id: incident.id, id: incident.id,
category: incident.crime_categories?.name || "Unknown", category: incident.crime_categories?.name || "Unknown",
intensity: 1, // Base intensity value intensity: 1, // Base intensity value
timestamp: incident.timestamp ? new Date(incident.timestamp).getTime() : null,
}, },
geometry: { geometry: {
type: "Point" as const, type: "Point" as const,
@ -34,7 +54,7 @@ export default function HeatmapLayer({ crimes, visible = true }: HeatmapLayerPro
type: "FeatureCollection" as const, type: "FeatureCollection" as const,
features, features,
}; };
}, [crimes]); }, [crimes, filterCategory]);
if (!visible) return null; if (!visible) return null;
@ -44,36 +64,36 @@ export default function HeatmapLayer({ crimes, visible = true }: HeatmapLayerPro
id="crime-heatmap" id="crime-heatmap"
type="heatmap" type="heatmap"
paint={{ paint={{
// Heatmap radius // Enhanced heatmap configuration
'heatmap-radius': [ 'heatmap-radius': [
'interpolate', 'interpolate',
['linear'], ['linear'],
['zoom'], ['zoom'],
8, 10, // At zoom level 8, radius will be 10px 8, 12, // At zoom level 8, radius will be 12px
13, 25 // At zoom level 13, radius will be 25px 13, 30 // At zoom level 13, radius will be 30px
], ],
// Heatmap intensity
'heatmap-intensity': [ 'heatmap-intensity': [
'interpolate', 'interpolate',
['linear'], ['linear'],
['zoom'], ['zoom'],
8, 0.5, // Less intense at zoom level 8 8, 0.7, // More intense at zoom level 8
13, 1.5 // More intense at zoom level 13 13, 2.0 // Even more intense at zoom level 13
], ],
// Color gradient from low to high density // Improved color gradient for better visualization
'heatmap-color': [ 'heatmap-color': [
'interpolate', 'interpolate',
['linear'], ['linear'],
['heatmap-density'], ['heatmap-density'],
0, 'rgba(33,102,172,0)', 0, 'rgba(33,102,172,0)',
0.2, 'rgb(103,169,207)', 0.1, 'rgb(36,104,180)',
0.4, 'rgb(209,229,240)', 0.3, 'rgb(103,169,207)',
0.6, 'rgb(253,219,199)', 0.5, 'rgb(209,229,240)',
0.8, 'rgb(239,138,98)', 0.7, 'rgb(253,219,199)',
0.9, 'rgb(239,138,98)',
1, 'rgb(178,24,43)' 1, 'rgb(178,24,43)'
], ],
// Heatmap opacity // Higher opacity when showing all data for better visibility
'heatmap-opacity': 0.8, 'heatmap-opacity': useAllData ? 0.9 : 0.8,
// Heatmap weight based on properties // Heatmap weight based on properties
'heatmap-weight': [ 'heatmap-weight': [
'interpolate', 'interpolate',

View File

@ -42,6 +42,7 @@ interface LayersProps {
filterCategory: string | "all"; filterCategory: string | "all";
activeControl: ITooltips; activeControl: ITooltips;
tilesetId?: string; tilesetId?: string;
useAllData?: boolean; // New prop to indicate if we're showing all data
} }
export default function Layers({ export default function Layers({
@ -53,6 +54,7 @@ export default function Layers({
filterCategory, filterCategory,
activeControl, activeControl,
tilesetId = MAPBOX_TILESET_ID, tilesetId = MAPBOX_TILESET_ID,
useAllData = false,
}: LayersProps) { }: LayersProps) {
const { current: map } = useMap() const { current: map } = useMap()
@ -226,9 +228,12 @@ export default function Layers({
// District fill should only be visible for incidents and clusters // District fill should only be visible for incidents and clusters
const showDistrictFill = activeControl === "incidents" || activeControl === "clusters"; const showDistrictFill = activeControl === "incidents" || activeControl === "clusters";
// Only show incident markers for incidents and clusters views - exclude timeline mode
const showIncidentMarkers = (activeControl === "incidents" || activeControl === "clusters")
return ( return (
<> <>
{/* Ensure we pass the proper defined handler */} {/* Standard District Layer with incident points */}
<DistrictFillLineLayer <DistrictFillLineLayer
visible={true} visible={true}
map={mapboxMap} map={mapboxMap}
@ -244,17 +249,6 @@ export default function Layers({
activeControl={activeControl} activeControl={activeControl}
/> />
{/* Standard District Layer with incident points */}
{/* <DistrictLayer
crimes={crimes}
year={year}
month={month}
filterCategory={filterCategory}
visible={true} // Keep the layer but control fill opacity
showFill={showDistrictFill}
activeControl={activeControl}
/> */}
{/* Heatmap Layer */} {/* Heatmap Layer */}
<HeatmapLayer <HeatmapLayer
crimes={crimes} crimes={crimes}
@ -262,9 +256,10 @@ export default function Layers({
month={month} month={month}
filterCategory={filterCategory} filterCategory={filterCategory}
visible={showHeatmapLayer} visible={showHeatmapLayer}
useAllData={useAllData}
/> />
{/* Timeline Layer - show average incident time per district */} {/* Timeline Layer - make sure this is the only visible layer in timeline mode */}
<TimelineLayer <TimelineLayer
crimes={crimes} crimes={crimes}
year={year} year={year}
@ -272,9 +267,10 @@ export default function Layers({
filterCategory={filterCategory} filterCategory={filterCategory}
visible={showTimelineLayer} visible={showTimelineLayer}
map={mapboxMap} map={mapboxMap}
useAllData={useAllData}
/> />
{/* Units Layer - show police stations and connection lines */} {/* Units Layer */}
<UnitsLayer <UnitsLayer
crimes={crimes} crimes={crimes}
units={units} units={units}
@ -283,7 +279,7 @@ export default function Layers({
map={mapboxMap} map={mapboxMap}
/> />
{/* District base layer is always needed */} {/* District base layer */}
<DistrictExtrusionLayer <DistrictExtrusionLayer
visible={visible} visible={visible}
map={mapboxMap} map={mapboxMap}
@ -292,9 +288,9 @@ export default function Layers({
crimeDataByDistrict={crimeDataByDistrict} crimeDataByDistrict={crimeDataByDistrict}
/> />
{/* Cluster Layer - only enable clustering and make visible when the clusters control is active */} {/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */}
<ClusterLayer <ClusterLayer
visible={visible} visible={visible && !showTimelineLayer}
map={mapboxMap} map={mapboxMap}
crimes={crimes} crimes={crimes}
filterCategory={filterCategory} filterCategory={filterCategory}
@ -303,9 +299,9 @@ export default function Layers({
showClusters={showClustersLayer} showClusters={showClustersLayer}
/> />
{/* Unclustered Points Layer - hide when in cluster mode or units mode */} {/* Unclustered Points Layer - explicitly hide in timeline mode */}
<UnclusteredPointLayer <UnclusteredPointLayer
visible={visible && !showClustersLayer && showDistrictLayer && !showUnitsLayer} visible={visible && showIncidentMarkers && !focusedDistrictId && !showTimelineLayer}
map={mapboxMap} map={mapboxMap}
crimes={crimes} crimes={crimes}
filterCategory={filterCategory} filterCategory={filterCategory}

View File

@ -7,7 +7,6 @@ import mapboxgl from 'mapbox-gl'
import { format } from 'date-fns' import { format } from 'date-fns'
import { calculateAverageTimeOfDay } from '@/app/_utils/time' import { calculateAverageTimeOfDay } from '@/app/_utils/time'
interface TimelineLayerProps { interface TimelineLayerProps {
crimes: ICrimes[] crimes: ICrimes[]
year: string year: string
@ -15,6 +14,7 @@ interface TimelineLayerProps {
filterCategory: string | "all" filterCategory: string | "all"
visible?: boolean visible?: boolean
map?: mapboxgl.Map | null map?: mapboxgl.Map | null
useAllData?: boolean // New prop to use all data
} }
export default function TimelineLayer({ export default function TimelineLayer({
@ -23,7 +23,8 @@ export default function TimelineLayer({
month, month,
filterCategory, filterCategory,
visible = false, visible = false,
map map,
useAllData = false // Default to false
}: TimelineLayerProps) { }: TimelineLayerProps) {
// State to hold the currently selected district for popup display // State to hold the currently selected district for popup display
const [selectedDistrict, setSelectedDistrict] = useState<string | null>(null) const [selectedDistrict, setSelectedDistrict] = useState<string | null>(null)
@ -61,11 +62,13 @@ export default function TimelineLayer({
}) })
} }
// Add valid incidents to the district group // Filter incidents appropriately before adding
crime.crime_incidents.forEach(incident => { crime.crime_incidents.forEach(incident => {
// Skip invalid incidents
if (!incident.timestamp) return if (!incident.timestamp) return
if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return
// Add to appropriate district group
const group = districtGroups.get(crime.district_id) const group = districtGroups.get(crime.district_id)
if (group) { if (group) {
group.incidents.push({ group.incidents.push({
@ -105,8 +108,11 @@ export default function TimelineLayer({
} }
}) })
// Add title to indicate all years data
const title = useAllData ? "All Years Data" : `Year: ${year}${month !== "all" ? `, Month: ${month}` : ""}`;
return result return result
}, [crimes, filterCategory]) }, [crimes, filterCategory, useAllData, year, month])
// Convert processed data to GeoJSON for display // Convert processed data to GeoJSON for display
const timelineGeoJSON = useMemo(() => { const timelineGeoJSON = useMemo(() => {
@ -141,6 +147,30 @@ export default function TimelineLayer({
} }
} }
// Add an effect to hide all other incident markers and clusters when timeline is active
useEffect(() => {
if (!map || !visible) return;
// Hide incident markers when timeline mode is activated
if (map.getLayer("unclustered-point")) {
map.setLayoutProperty("unclustered-point", "visibility", "none");
}
// Hide clusters when timeline mode is activated
if (map.getLayer("clusters")) {
map.setLayoutProperty("clusters", "visibility", "none");
}
if (map.getLayer("cluster-count")) {
map.setLayoutProperty("cluster-count", "visibility", "none");
}
return () => {
// This cleanup won't restore visibility since that's handled by the parent component
// based on the activeControl value
};
}, [map, visible]);
// Event handlers // Event handlers
useEffect(() => { useEffect(() => {
if (!map || !visible) return if (!map || !visible) return