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 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}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue