feat: Enhance heatmap and timeline layers with new data handling and filtering options
This commit is contained in:
parent
a19e8ec32d
commit
fa7651619b
|
@ -27,7 +27,6 @@ import Layers from "./layers/layers"
|
|||
import DistrictLayer, { DistrictFeature } from "./layers/district-layer-old"
|
||||
import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries"
|
||||
|
||||
// Updated CrimeIncident type to match the structure in crime_incidents
|
||||
interface ICrimeIncident {
|
||||
id: string
|
||||
district?: string
|
||||
|
@ -42,7 +41,6 @@ interface ICrimeIncident {
|
|||
}
|
||||
|
||||
export default function CrimeMap() {
|
||||
// State for sidebar
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
||||
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
||||
const [selectedIncident, setSelectedIncident] = useState<ICrimeIncident | null>(null)
|
||||
|
@ -55,28 +53,25 @@ export default function CrimeMap() {
|
|||
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
|
||||
const [isSearchActive, setIsSearchActive] = useState(false)
|
||||
const [showUnitsLayer, setShowUnitsLayer] = useState(false)
|
||||
const [useAllYears, setUseAllYears] = useState<boolean>(false)
|
||||
const [useAllMonths, setUseAllMonths] = useState<boolean>(false)
|
||||
|
||||
const mapContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Use the custom fullscreen hook
|
||||
const { isFullscreen } = useFullscreen(mapContainerRef)
|
||||
|
||||
// Get available years
|
||||
const {
|
||||
data: availableYears,
|
||||
isLoading: isYearsLoading,
|
||||
error: yearsError
|
||||
} = useGetAvailableYears()
|
||||
|
||||
// Extract all unique categories
|
||||
const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories()
|
||||
|
||||
// Transform categories data to string array
|
||||
const categories = useMemo(() =>
|
||||
categoriesData ? categoriesData.map(category => category.name) : []
|
||||
, [categoriesData])
|
||||
|
||||
// Get all crime data in a single request
|
||||
const {
|
||||
data: crimes,
|
||||
isLoading: isCrimesLoading,
|
||||
|
@ -85,22 +80,40 @@ export default function CrimeMap() {
|
|||
|
||||
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(() => {
|
||||
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) => {
|
||||
const yearMatch = crime.year === selectedYear
|
||||
const yearMatch = crime.year === selectedYear;
|
||||
|
||||
if (selectedMonth === "all") {
|
||||
return yearMatch
|
||||
if (selectedMonth === "all" || useAllMonths) {
|
||||
return yearMatch;
|
||||
} 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(() => {
|
||||
if (!filteredByYearAndMonth) return []
|
||||
if (selectedCategory === "all") return filteredByYearAndMonth
|
||||
|
@ -118,7 +131,6 @@ export default function CrimeMap() {
|
|||
})
|
||||
}, [filteredByYearAndMonth, selectedCategory])
|
||||
|
||||
// Set up event listener for incident clicks from the district layer
|
||||
useEffect(() => {
|
||||
const handleIncidentClickEvent = (e: CustomEvent) => {
|
||||
console.log("Received incident_click event:", e.detail);
|
||||
|
@ -127,14 +139,11 @@ export default function CrimeMap() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Find the incident in filtered crimes data using the id from the event
|
||||
let foundIncident: ICrimeIncident | undefined;
|
||||
|
||||
// Search through all crimes and their incidents to find matching incident
|
||||
filteredCrimes.forEach(crime => {
|
||||
crime.crime_incidents.forEach(incident => {
|
||||
if (incident.id === e.detail.id) {
|
||||
// Map the found incident to ICrimeIncident type
|
||||
foundIncident = {
|
||||
id: incident.id,
|
||||
district: crime.districts.name,
|
||||
|
@ -156,29 +165,23 @@ export default function CrimeMap() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Validate the coordinates
|
||||
if (!foundIncident.latitude || !foundIncident.longitude) {
|
||||
console.error("Invalid incident coordinates:", foundIncident);
|
||||
return;
|
||||
}
|
||||
|
||||
// When an incident is clicked, clear any selected district
|
||||
setSelectedDistrict(null);
|
||||
|
||||
// Set the selected incident
|
||||
setSelectedIncident(foundIncident);
|
||||
};
|
||||
|
||||
// Add event listener to the map container and document
|
||||
const mapContainer = mapContainerRef.current;
|
||||
|
||||
// Clean up previous listeners to prevent duplicates
|
||||
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||
if (mapContainer) {
|
||||
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);
|
||||
|
||||
if (mapContainer) {
|
||||
|
@ -193,7 +196,6 @@ export default function CrimeMap() {
|
|||
};
|
||||
}, [filteredCrimes]);
|
||||
|
||||
// Set up event listener for fly-to map control
|
||||
useEffect(() => {
|
||||
const handleMapFlyTo = (e: CustomEvent) => {
|
||||
if (!e.detail) {
|
||||
|
@ -203,14 +205,12 @@ export default function CrimeMap() {
|
|||
|
||||
const { longitude, latitude, zoom, bearing, pitch, duration } = e.detail;
|
||||
|
||||
// Find the map instance
|
||||
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
|
||||
if (!mapInstance) {
|
||||
console.error("Map instance not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and dispatch a custom event that MapView component will listen for
|
||||
const mapboxEvent = new CustomEvent('mapbox_fly', {
|
||||
detail: {
|
||||
center: [longitude, latitude],
|
||||
|
@ -225,7 +225,6 @@ export default function CrimeMap() {
|
|||
mapInstance.dispatchEvent(mapboxEvent);
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
document.addEventListener('mapbox_fly_to', handleMapFlyTo as EventListener);
|
||||
|
||||
return () => {
|
||||
|
@ -233,19 +232,16 @@ export default function CrimeMap() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Add event listener for map reset
|
||||
useEffect(() => {
|
||||
const handleMapReset = (e: CustomEvent) => {
|
||||
const { duration } = e.detail || { duration: 1500 };
|
||||
|
||||
// Find the map instance
|
||||
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
|
||||
if (!mapInstance) {
|
||||
console.error("Map instance not found");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create and dispatch the reset event that MapView will listen for
|
||||
const mapboxEvent = new CustomEvent('mapbox_fly', {
|
||||
detail: {
|
||||
duration: duration,
|
||||
|
@ -257,7 +253,6 @@ export default function CrimeMap() {
|
|||
mapInstance.dispatchEvent(mapboxEvent);
|
||||
};
|
||||
|
||||
// Add event listener
|
||||
document.addEventListener('mapbox_reset', handleMapReset as EventListener);
|
||||
|
||||
return () => {
|
||||
|
@ -265,11 +260,9 @@ export default function CrimeMap() {
|
|||
};
|
||||
}, []);
|
||||
|
||||
// Update the popup close handler to reset the map view
|
||||
const handlePopupClose = () => {
|
||||
setSelectedIncident(null);
|
||||
|
||||
// Dispatch map reset event to reset zoom, pitch, and bearing
|
||||
const mapInstance = mapContainerRef.current?.querySelector('.mapboxgl-map');
|
||||
|
||||
if (mapInstance) {
|
||||
|
@ -284,57 +277,61 @@ export default function CrimeMap() {
|
|||
}
|
||||
}
|
||||
|
||||
// Handle year-month timeline change
|
||||
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
|
||||
setSelectedYear(year)
|
||||
setSelectedMonth(month)
|
||||
setYearProgress(progress)
|
||||
}, [])
|
||||
|
||||
// Handle timeline playing state change
|
||||
const handleTimelinePlayingChange = useCallback((playing: boolean) => {
|
||||
setisTimelapsePlaying(playing)
|
||||
|
||||
// When timelapse starts, close any open popups/details
|
||||
if (playing) {
|
||||
setSelectedIncident(null)
|
||||
setSelectedDistrict(null)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Reset filters
|
||||
const resetFilters = useCallback(() => {
|
||||
setSelectedYear(2024)
|
||||
setSelectedMonth("all")
|
||||
setSelectedCategory("all")
|
||||
}, [])
|
||||
|
||||
// Determine the title based on filters
|
||||
const getMapTitle = () => {
|
||||
let title = `${selectedYear}`
|
||||
if (selectedMonth !== "all") {
|
||||
title += ` - ${getMonthName(Number(selectedMonth))}`
|
||||
if (useAllYears) {
|
||||
return `All Years Data ${selectedCategory !== "all" ? `- ${selectedCategory}` : ''}`;
|
||||
}
|
||||
|
||||
let title = `${selectedYear}`;
|
||||
if (selectedMonth !== "all" && !useAllMonths) {
|
||||
title += ` - ${getMonthName(Number(selectedMonth))}`;
|
||||
}
|
||||
if (selectedCategory !== "all") {
|
||||
title += ` - ${selectedCategory}`
|
||||
title += ` - ${selectedCategory}`;
|
||||
}
|
||||
return title
|
||||
return title;
|
||||
}
|
||||
|
||||
// Handle control changes from the top controls component
|
||||
const handleControlChange = (controlId: ITooltips) => {
|
||||
setActiveControl(controlId)
|
||||
setActiveControl(controlId);
|
||||
|
||||
// Toggle search state when search control is clicked
|
||||
if (controlId === "search") {
|
||||
setIsSearchActive(prev => !prev)
|
||||
setIsSearchActive(prev => !prev);
|
||||
}
|
||||
|
||||
// Toggle units layer visibility when units control is clicked
|
||||
if (controlId === "units") {
|
||||
setShowUnitsLayer(true)
|
||||
setShowUnitsLayer(true);
|
||||
} 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]"
|
||||
)}>
|
||||
<MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md">
|
||||
{/* Replace the DistrictLayer with the new Layers component */}
|
||||
<Layers
|
||||
crimes={filteredCrimes || []}
|
||||
units={fetchedUnits || []}
|
||||
|
@ -383,9 +379,9 @@ export default function CrimeMap() {
|
|||
month={selectedMonth.toString()}
|
||||
filterCategory={selectedCategory}
|
||||
activeControl={activeControl}
|
||||
useAllData={useAllYears}
|
||||
/>
|
||||
|
||||
{/* Popup for selected incident */}
|
||||
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
||||
<>
|
||||
<CrimePopup
|
||||
|
@ -397,7 +393,6 @@ export default function CrimeMap() {
|
|||
</>
|
||||
)}
|
||||
|
||||
{/* Components that are only visible in fullscreen mode */}
|
||||
{isFullscreen && (
|
||||
<>
|
||||
<div className="absolute flex w-full p-2">
|
||||
|
@ -416,7 +411,6 @@ export default function CrimeMap() {
|
|||
/>
|
||||
</div>
|
||||
|
||||
{/* Pass selectedCategory, selectedYear, and selectedMonth to the sidebar */}
|
||||
<CrimeSidebar
|
||||
crimes={filteredCrimes || []}
|
||||
defaultCollapsed={sidebarCollapsed}
|
||||
|
|
|
@ -8,20 +8,40 @@ interface HeatmapLayerProps {
|
|||
month: string;
|
||||
filterCategory: string | "all";
|
||||
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
|
||||
const heatmapData = useMemo(() => {
|
||||
const features = crimes.flatMap(crime =>
|
||||
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 => ({
|
||||
type: "Feature" as const,
|
||||
properties: {
|
||||
id: incident.id,
|
||||
category: incident.crime_categories?.name || "Unknown",
|
||||
intensity: 1, // Base intensity value
|
||||
timestamp: incident.timestamp ? new Date(incident.timestamp).getTime() : null,
|
||||
},
|
||||
geometry: {
|
||||
type: "Point" as const,
|
||||
|
@ -34,7 +54,7 @@ export default function HeatmapLayer({ crimes, visible = true }: HeatmapLayerPro
|
|||
type: "FeatureCollection" as const,
|
||||
features,
|
||||
};
|
||||
}, [crimes]);
|
||||
}, [crimes, filterCategory]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
|
@ -44,36 +64,36 @@ export default function HeatmapLayer({ crimes, visible = true }: HeatmapLayerPro
|
|||
id="crime-heatmap"
|
||||
type="heatmap"
|
||||
paint={{
|
||||
// Heatmap radius
|
||||
// Enhanced heatmap configuration
|
||||
'heatmap-radius': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8, 10, // At zoom level 8, radius will be 10px
|
||||
13, 25 // At zoom level 13, radius will be 25px
|
||||
8, 12, // At zoom level 8, radius will be 12px
|
||||
13, 30 // At zoom level 13, radius will be 30px
|
||||
],
|
||||
// Heatmap intensity
|
||||
'heatmap-intensity': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['zoom'],
|
||||
8, 0.5, // Less intense at zoom level 8
|
||||
13, 1.5 // More intense at zoom level 13
|
||||
8, 0.7, // More intense at zoom level 8
|
||||
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': [
|
||||
'interpolate',
|
||||
['linear'],
|
||||
['heatmap-density'],
|
||||
0, 'rgba(33,102,172,0)',
|
||||
0.2, 'rgb(103,169,207)',
|
||||
0.4, 'rgb(209,229,240)',
|
||||
0.6, 'rgb(253,219,199)',
|
||||
0.8, 'rgb(239,138,98)',
|
||||
0.1, 'rgb(36,104,180)',
|
||||
0.3, 'rgb(103,169,207)',
|
||||
0.5, 'rgb(209,229,240)',
|
||||
0.7, 'rgb(253,219,199)',
|
||||
0.9, 'rgb(239,138,98)',
|
||||
1, 'rgb(178,24,43)'
|
||||
],
|
||||
// Heatmap opacity
|
||||
'heatmap-opacity': 0.8,
|
||||
// Higher opacity when showing all data for better visibility
|
||||
'heatmap-opacity': useAllData ? 0.9 : 0.8,
|
||||
// Heatmap weight based on properties
|
||||
'heatmap-weight': [
|
||||
'interpolate',
|
||||
|
|
|
@ -42,6 +42,7 @@ interface LayersProps {
|
|||
filterCategory: string | "all";
|
||||
activeControl: ITooltips;
|
||||
tilesetId?: string;
|
||||
useAllData?: boolean; // New prop to indicate if we're showing all data
|
||||
}
|
||||
|
||||
export default function Layers({
|
||||
|
@ -53,6 +54,7 @@ export default function Layers({
|
|||
filterCategory,
|
||||
activeControl,
|
||||
tilesetId = MAPBOX_TILESET_ID,
|
||||
useAllData = false,
|
||||
}: LayersProps) {
|
||||
const { current: map } = useMap()
|
||||
|
||||
|
@ -226,9 +228,12 @@ export default function Layers({
|
|||
// District fill should only be visible for incidents and 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 (
|
||||
<>
|
||||
{/* Ensure we pass the proper defined handler */}
|
||||
{/* Standard District Layer with incident points */}
|
||||
<DistrictFillLineLayer
|
||||
visible={true}
|
||||
map={mapboxMap}
|
||||
|
@ -244,17 +249,6 @@ export default function Layers({
|
|||
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 */}
|
||||
<HeatmapLayer
|
||||
crimes={crimes}
|
||||
|
@ -262,9 +256,10 @@ export default function Layers({
|
|||
month={month}
|
||||
filterCategory={filterCategory}
|
||||
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
|
||||
crimes={crimes}
|
||||
year={year}
|
||||
|
@ -272,9 +267,10 @@ export default function Layers({
|
|||
filterCategory={filterCategory}
|
||||
visible={showTimelineLayer}
|
||||
map={mapboxMap}
|
||||
useAllData={useAllData}
|
||||
/>
|
||||
|
||||
{/* Units Layer - show police stations and connection lines */}
|
||||
{/* Units Layer */}
|
||||
<UnitsLayer
|
||||
crimes={crimes}
|
||||
units={units}
|
||||
|
@ -283,7 +279,7 @@ export default function Layers({
|
|||
map={mapboxMap}
|
||||
/>
|
||||
|
||||
{/* District base layer is always needed */}
|
||||
{/* District base layer */}
|
||||
<DistrictExtrusionLayer
|
||||
visible={visible}
|
||||
map={mapboxMap}
|
||||
|
@ -292,9 +288,9 @@ export default function Layers({
|
|||
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
|
||||
visible={visible}
|
||||
visible={visible && !showTimelineLayer}
|
||||
map={mapboxMap}
|
||||
crimes={crimes}
|
||||
filterCategory={filterCategory}
|
||||
|
@ -303,9 +299,9 @@ export default function Layers({
|
|||
showClusters={showClustersLayer}
|
||||
/>
|
||||
|
||||
{/* Unclustered Points Layer - hide when in cluster mode or units mode */}
|
||||
{/* Unclustered Points Layer - explicitly hide in timeline mode */}
|
||||
<UnclusteredPointLayer
|
||||
visible={visible && !showClustersLayer && showDistrictLayer && !showUnitsLayer}
|
||||
visible={visible && showIncidentMarkers && !focusedDistrictId && !showTimelineLayer}
|
||||
map={mapboxMap}
|
||||
crimes={crimes}
|
||||
filterCategory={filterCategory}
|
||||
|
|
|
@ -7,7 +7,6 @@ import mapboxgl from 'mapbox-gl'
|
|||
import { format } from 'date-fns'
|
||||
import { calculateAverageTimeOfDay } from '@/app/_utils/time'
|
||||
|
||||
|
||||
interface TimelineLayerProps {
|
||||
crimes: ICrimes[]
|
||||
year: string
|
||||
|
@ -15,6 +14,7 @@ interface TimelineLayerProps {
|
|||
filterCategory: string | "all"
|
||||
visible?: boolean
|
||||
map?: mapboxgl.Map | null
|
||||
useAllData?: boolean // New prop to use all data
|
||||
}
|
||||
|
||||
export default function TimelineLayer({
|
||||
|
@ -23,7 +23,8 @@ export default function TimelineLayer({
|
|||
month,
|
||||
filterCategory,
|
||||
visible = false,
|
||||
map
|
||||
map,
|
||||
useAllData = false // Default to false
|
||||
}: TimelineLayerProps) {
|
||||
// State to hold the currently selected district for popup display
|
||||
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 => {
|
||||
// Skip invalid incidents
|
||||
if (!incident.timestamp) return
|
||||
if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return
|
||||
|
||||
// Add to appropriate district group
|
||||
const group = districtGroups.get(crime.district_id)
|
||||
if (group) {
|
||||
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
|
||||
}, [crimes, filterCategory])
|
||||
}, [crimes, filterCategory, useAllData, year, month])
|
||||
|
||||
// Convert processed data to GeoJSON for display
|
||||
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
|
||||
useEffect(() => {
|
||||
if (!map || !visible) return
|
||||
|
|
Loading…
Reference in New Issue