"use client" import { useEffect, useMemo, useState, useCallback } from "react" import { Layer, Source } from "react-map-gl/mapbox" import type { ICrimes } from "@/app/_utils/types/crimes" import type mapboxgl from "mapbox-gl" import { format } from "date-fns" import { calculateAverageTimeOfDay } from "@/app/_utils/time" import TimelinePopup from "../pop-up/timeline-popup" import TimeZonesDisplay from "./timezone" interface TimelineLayerProps { crimes: ICrimes[] year: string month: string filterCategory: string | "all" visible?: boolean map?: mapboxgl.Map | null useAllData?: boolean } export default function TimelineLayer({ crimes, year, month, filterCategory, visible = false, map, useAllData = false, }: TimelineLayerProps) { // State for selected district and popup const [selectedDistrict, setSelectedDistrict] = useState(null) const [showTimeZones, setShowTimeZones] = useState(true) // Process district data to extract average incident times const districtTimeData = useMemo(() => { // Group incidents by district const districtGroups = new Map< string, { districtId: string districtName: string incidents: Array<{ timestamp: Date; category: string }> center: [number, number] } >() crimes.forEach((crime) => { if (!crime.districts || !crime.district_id) return // Initialize district group if not exists if (!districtGroups.has(crime.district_id)) { // Find a central location for the district from any incident const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude) const center: [number, number] = centerIncident ? [centerIncident.locations.longitude, centerIncident.locations.latitude] : [0, 0] districtGroups.set(crime.district_id, { districtId: crime.district_id, districtName: crime.districts.name, incidents: [], center, }) } // 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({ timestamp: new Date(incident.timestamp), category: incident.crime_categories.name, }) } }) }) // Calculate average time for each district const result = Array.from(districtGroups.values()) .filter((group) => group.incidents.length > 0 && group.center[0] !== 0) .map((group) => { const avgTimeInfo = calculateAverageTimeOfDay(group.incidents.map((inc) => inc.timestamp)) return { id: group.districtId, name: group.districtName, center: group.center, avgHour: avgTimeInfo.hour, avgMinute: avgTimeInfo.minute, formattedTime: avgTimeInfo.formattedTime, timeDescription: avgTimeInfo.description, totalIncidents: group.incidents.length, timeOfDay: avgTimeInfo.timeOfDay, earliestTime: format(avgTimeInfo.earliest, "p"), latestTime: format(avgTimeInfo.latest, "p"), mostFrequentHour: avgTimeInfo.mostFrequentHour, categoryCounts: group.incidents.reduce( (acc, inc) => { acc[inc.category] = (acc[inc.category] || 0) + 1 return acc }, {} as Record, ), } }) return result }, [crimes, filterCategory, year, month]) // Convert processed data to GeoJSON for display const timelineGeoJSON = useMemo(() => { return { type: "FeatureCollection" as const, features: districtTimeData.map((district) => ({ type: "Feature" as const, properties: { id: district.id, name: district.name, avgTime: district.formattedTime, timeDescription: district.timeDescription, totalIncidents: district.totalIncidents, timeOfDay: district.timeOfDay, hour: district.avgHour, minute: district.avgMinute, }, geometry: { type: "Point" as const, coordinates: district.center, }, })), } }, [districtTimeData]) // Handle marker click const handleMarkerClick = useCallback( (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return const feature = e.features[0] const props = feature.properties if (!props) return // Get the corresponding district data for detailed info const districtData = districtTimeData.find((d) => d.id === props.id) if (!districtData) return // Fly to the location if (map) { map.flyTo({ center: districtData.center, zoom: 12, duration: 1000, pitch: 45, bearing: 0, }) } // Set the selected district for popup setSelectedDistrict(districtData) }, [map, districtTimeData], ) // Handle popup close const handleClosePopup = useCallback(() => { setSelectedDistrict(null) }, []) // Add an effect to hide other layers 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") } // Set up event handlers const handleMouseEnter = () => { if (map) map.getCanvas().style.cursor = "pointer" } const handleMouseLeave = () => { if (map) map.getCanvas().style.cursor = "" } // Add event listeners if (map.getLayer("timeline-markers")) { map.on("click", "timeline-markers", handleMarkerClick) map.on("mouseenter", "timeline-markers", handleMouseEnter) map.on("mouseleave", "timeline-markers", handleMouseLeave) } return () => { // Clean up event listeners if (map) { map.off("click", "timeline-markers", handleMarkerClick) map.off("mouseenter", "timeline-markers", handleMouseEnter) map.off("mouseleave", "timeline-markers", handleMouseLeave) } } }, [map, visible, handleMarkerClick]) // Clean up on unmount or when visibility changes useEffect(() => { if (!visible) { setSelectedDistrict(null) } }, [visible]) if (!visible) return null return ( <> {/* Digital clock background */} {/* Digital clock display */} {/* Custom Popup Component */} {selectedDistrict && ( )} ) }