feat: refactor crime data fetching and filtering logic in crime management components
This commit is contained in:
parent
91f9574285
commit
0d6f9acf66
|
@ -1,10 +1,7 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from "react"
|
import { useState, useMemo } from "react"
|
||||||
|
import { useGetAvailableYears, useGetCrimes } from "../_queries/queries"
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
|
||||||
import { useGetAvailableYears } from "../_queries/queries"
|
|
||||||
import { getCrimeByYearAndMonth } from "../action"
|
|
||||||
|
|
||||||
type CrimeData = any // Replace with your actual crime data type
|
type CrimeData = any // Replace with your actual crime data type
|
||||||
|
|
||||||
|
@ -24,87 +21,43 @@ interface PrefetchedCrimeDataResult {
|
||||||
export function usePrefetchedCrimeData(initialYear: number = 2024, initialMonth: number | "all" = "all"): PrefetchedCrimeDataResult {
|
export function usePrefetchedCrimeData(initialYear: number = 2024, initialMonth: number | "all" = "all"): PrefetchedCrimeDataResult {
|
||||||
const [selectedYear, setSelectedYear] = useState<number>(initialYear)
|
const [selectedYear, setSelectedYear] = useState<number>(initialYear)
|
||||||
const [selectedMonth, setSelectedMonth] = useState<number | "all">(initialMonth)
|
const [selectedMonth, setSelectedMonth] = useState<number | "all">(initialMonth)
|
||||||
const [prefetchedData, setPrefetchedData] = useState<Record<string, CrimeData>>({})
|
|
||||||
const [isPrefetching, setIsPrefetching] = useState<boolean>(true)
|
|
||||||
const [prefetchError, setPrefetchError] = useState<Error | null>(null)
|
|
||||||
|
|
||||||
const queryClient = useQueryClient()
|
|
||||||
|
|
||||||
// Get available years
|
// Get available years
|
||||||
const { data: availableYears, isLoading: isYearsLoading, error: yearsError } = useGetAvailableYears()
|
const {
|
||||||
|
data: availableYears,
|
||||||
|
isLoading: isYearsLoading,
|
||||||
|
error: yearsError
|
||||||
|
} = useGetAvailableYears()
|
||||||
|
|
||||||
// Track if we've prefetched data
|
// Get all crime data in a single request
|
||||||
const hasPrefetched = useRef<boolean>(false)
|
const {
|
||||||
|
data: allCrimes,
|
||||||
|
isLoading: isCrimesLoading,
|
||||||
|
error: crimesError
|
||||||
|
} = useGetCrimes()
|
||||||
|
|
||||||
// Prefetch all data combinations
|
// Filter crimes based on selected year and month
|
||||||
useEffect(() => {
|
const filteredCrimes = useMemo(() => {
|
||||||
const prefetchAllData = async () => {
|
if (!allCrimes) return []
|
||||||
if (!availableYears || hasPrefetched.current) return
|
|
||||||
|
|
||||||
setIsPrefetching(true)
|
return allCrimes.filter((crime: any) => {
|
||||||
const dataCache: Record<string, CrimeData> = {}
|
const yearMatch = crime.year === selectedYear
|
||||||
|
|
||||||
try {
|
if (selectedMonth === "all") {
|
||||||
// Prefetch data for all years with "all" months
|
return yearMatch
|
||||||
for (const year of availableYears) {
|
} else {
|
||||||
if (year === null) continue
|
return yearMatch && crime.month === selectedMonth
|
||||||
|
|
||||||
// Prefetch "all" months for this year
|
|
||||||
const allMonthsKey = `${year}-all`
|
|
||||||
const allMonthsData = await getCrimeByYearAndMonth(year, "all")
|
|
||||||
dataCache[allMonthsKey] = allMonthsData
|
|
||||||
|
|
||||||
// Also prefetch each individual month for this year
|
|
||||||
for (let month = 1; month <= 12; month++) {
|
|
||||||
const monthKey = `${year}-${month}`
|
|
||||||
const monthData = await getCrimeByYearAndMonth(year, month)
|
|
||||||
dataCache[monthKey] = monthData
|
|
||||||
|
|
||||||
// Pre-populate the React Query cache
|
|
||||||
queryClient.setQueryData(["crimes", year, month], monthData)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pre-populate the React Query cache for "all" months
|
|
||||||
queryClient.setQueryData(["crimes", year, "all"], allMonthsData)
|
|
||||||
}
|
|
||||||
|
|
||||||
setPrefetchedData(dataCache)
|
|
||||||
hasPrefetched.current = true
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error prefetching crime data:", error)
|
|
||||||
setPrefetchError(error instanceof Error ? error : new Error("Failed to prefetch data"))
|
|
||||||
} finally {
|
|
||||||
setIsPrefetching(false)
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
}, [allCrimes, selectedYear, selectedMonth])
|
||||||
|
|
||||||
prefetchAllData()
|
|
||||||
}, [availableYears, queryClient])
|
|
||||||
|
|
||||||
// Get the current data based on selected filters
|
|
||||||
const currentKey = `${selectedYear}-${selectedMonth}`
|
|
||||||
const currentData = prefetchedData[currentKey]
|
|
||||||
|
|
||||||
// Add better debugging and handle potential undefined values
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (isPrefetching) {
|
|
||||||
// console.log("Currently prefetching data...")
|
|
||||||
// } else {
|
|
||||||
// console.log(`Current data for ${currentKey}:`, currentData);
|
|
||||||
// if (!currentData) {
|
|
||||||
// console.log("Available prefetched data keys:", Object.keys(prefetchedData));
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }, [isPrefetching, currentKey, currentData, prefetchedData]);
|
|
||||||
|
|
||||||
// Ensure we always return a valid crimes array, even if empty
|
|
||||||
return {
|
return {
|
||||||
availableYears,
|
availableYears,
|
||||||
isYearsLoading,
|
isYearsLoading,
|
||||||
yearsError,
|
yearsError,
|
||||||
crimes: Array.isArray(currentData) ? currentData : [],
|
crimes: filteredCrimes,
|
||||||
isCrimesLoading: isPrefetching || !currentData,
|
isCrimesLoading,
|
||||||
crimesError: prefetchError,
|
crimesError,
|
||||||
setSelectedYear,
|
setSelectedYear,
|
||||||
setSelectedMonth,
|
setSelectedMonth,
|
||||||
selectedYear,
|
selectedYear,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
|
import { ICrimes, ICrimesByYearAndMonth } from '@/app/_utils/types/crimes';
|
||||||
import { getInjection } from '@/di/container';
|
import { getInjection } from '@/di/container';
|
||||||
import db from '@/prisma/db';
|
import db from '@/prisma/db';
|
||||||
import {
|
import {
|
||||||
|
@ -108,7 +109,7 @@ export async function getCrimeCategories() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCrimes() {
|
export async function getCrimes(): Promise<ICrimes[]> {
|
||||||
const instrumentationService = getInjection('IInstrumentationService');
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
return await instrumentationService.instrumentServerAction(
|
return await instrumentationService.instrumentServerAction(
|
||||||
'District Crime Data',
|
'District Crime Data',
|
||||||
|
@ -120,10 +121,28 @@ export async function getCrimes() {
|
||||||
districts: {
|
districts: {
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
name: true,
|
||||||
|
geographics: {
|
||||||
|
select: {
|
||||||
|
address: true,
|
||||||
|
land_area: true,
|
||||||
|
year: true,
|
||||||
|
latitude: true,
|
||||||
|
longitude: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
demographics: {
|
||||||
|
select: {
|
||||||
|
number_of_unemployed: true,
|
||||||
|
population: true,
|
||||||
|
population_density: true,
|
||||||
|
year: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
crime_incidents: {
|
crime_incidents: {
|
||||||
select: {
|
select: {
|
||||||
|
id: true,
|
||||||
timestamp: true,
|
timestamp: true,
|
||||||
description: true,
|
description: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
@ -182,7 +201,7 @@ export async function getCrimes() {
|
||||||
export async function getCrimeByYearAndMonth(
|
export async function getCrimeByYearAndMonth(
|
||||||
year: number,
|
year: number,
|
||||||
month: number | 'all'
|
month: number | 'all'
|
||||||
) {
|
): Promise<ICrimesByYearAndMonth[]> {
|
||||||
const instrumentationService = getInjection('IInstrumentationService');
|
const instrumentationService = getInjection('IInstrumentationService');
|
||||||
return await instrumentationService.instrumentServerAction(
|
return await instrumentationService.instrumentServerAction(
|
||||||
'District Crime Data',
|
'District Crime Data',
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
|
||||||
import { Skeleton } from "@/app/_components/ui/skeleton"
|
import { Skeleton } from "@/app/_components/ui/skeleton"
|
||||||
import DistrictLayer, { type DistrictFeature, ICrimeData } from "./layers/district-layer"
|
import DistrictLayer, { type DistrictFeature } from "./layers/district-layer"
|
||||||
import MapView from "./map"
|
import MapView from "./map"
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { AlertCircle } from "lucide-react"
|
import { AlertCircle } from "lucide-react"
|
||||||
|
@ -11,8 +11,7 @@ import { useRef, useState, useCallback, useMemo, useEffect } from "react"
|
||||||
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
||||||
import { Overlay } from "./overlay"
|
import { Overlay } from "./overlay"
|
||||||
import MapLegend from "./controls/map-legend"
|
import MapLegend from "./controls/map-legend"
|
||||||
import { usePrefetchedCrimeData } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_hooks/use-prefetch-crimes"
|
import { useGetAvailableYears, useGetCrimeCategories, useGetCrimes } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
|
||||||
import { useGetCrimeCategories } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
|
|
||||||
import { ITopTooltipsMapId } from "./controls/map-tooltips"
|
import { ITopTooltipsMapId } from "./controls/map-tooltips"
|
||||||
import MapSelectors from "./controls/map-selector"
|
import MapSelectors from "./controls/map-selector"
|
||||||
import TopNavigation from "./controls/map-navigations"
|
import TopNavigation from "./controls/map-navigations"
|
||||||
|
@ -20,10 +19,10 @@ import CrimeSidebar from "./sidebar/map-sidebar"
|
||||||
import SidebarToggle from "./sidebar/sidebar-toggle"
|
import SidebarToggle from "./sidebar/sidebar-toggle"
|
||||||
import { cn } from "@/app/_lib/utils"
|
import { cn } from "@/app/_lib/utils"
|
||||||
import CrimePopup from "./pop-up/crime-popup"
|
import CrimePopup from "./pop-up/crime-popup"
|
||||||
|
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
|
||||||
|
|
||||||
// Updated CrimeIncident type to match the structure in crime_incidents
|
// Updated CrimeIncident type to match the structure in crime_incidents
|
||||||
export interface CrimeIncident {
|
interface CrimeIncident {
|
||||||
id: string
|
id: string
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
description: string
|
description: string
|
||||||
|
@ -42,6 +41,8 @@ export default function CrimeMap() {
|
||||||
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null)
|
const [selectedIncident, setSelectedIncident] = useState<CrimeIncident | null>(null)
|
||||||
const [showLegend, setShowLegend] = useState<boolean>(true)
|
const [showLegend, setShowLegend] = useState<boolean>(true)
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
||||||
|
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
||||||
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
|
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
|
||||||
|
|
||||||
const mapContainerRef = useRef<HTMLDivElement>(null)
|
const mapContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
@ -49,23 +50,12 @@ export default function CrimeMap() {
|
||||||
// Use the custom fullscreen hook
|
// Use the custom fullscreen hook
|
||||||
const { isFullscreen } = useFullscreen(mapContainerRef)
|
const { isFullscreen } = useFullscreen(mapContainerRef)
|
||||||
|
|
||||||
// Toggle sidebar function
|
// Get available years
|
||||||
const toggleSidebar = useCallback(() => {
|
|
||||||
setSidebarCollapsed(!sidebarCollapsed)
|
|
||||||
}, [sidebarCollapsed])
|
|
||||||
|
|
||||||
// Use our new prefetched data hook
|
|
||||||
const {
|
const {
|
||||||
availableYears,
|
data: availableYears,
|
||||||
isYearsLoading,
|
isLoading: isYearsLoading,
|
||||||
crimes,
|
error: yearsError
|
||||||
isCrimesLoading,
|
} = useGetAvailableYears()
|
||||||
crimesError,
|
|
||||||
setSelectedYear,
|
|
||||||
setSelectedMonth,
|
|
||||||
selectedYear,
|
|
||||||
selectedMonth,
|
|
||||||
} = usePrefetchedCrimeData()
|
|
||||||
|
|
||||||
// Extract all unique categories
|
// Extract all unique categories
|
||||||
const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories()
|
const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories()
|
||||||
|
@ -75,12 +65,34 @@ export default function CrimeMap() {
|
||||||
categoriesData ? categoriesData.map(category => category.name) : []
|
categoriesData ? categoriesData.map(category => category.name) : []
|
||||||
, [categoriesData])
|
, [categoriesData])
|
||||||
|
|
||||||
|
// Get all crime data in a single request
|
||||||
|
const {
|
||||||
|
data: crimes,
|
||||||
|
isLoading: isCrimesLoading,
|
||||||
|
error: crimesError
|
||||||
|
} = useGetCrimes()
|
||||||
|
|
||||||
|
// Filter crimes based on selected year and month
|
||||||
|
const filteredByYearAndMonth = useMemo(() => {
|
||||||
|
if (!crimes) return []
|
||||||
|
|
||||||
|
return crimes.filter((crime) => {
|
||||||
|
const yearMatch = crime.year === selectedYear
|
||||||
|
|
||||||
|
if (selectedMonth === "all") {
|
||||||
|
return yearMatch
|
||||||
|
} else {
|
||||||
|
return yearMatch && crime.month === selectedMonth
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [crimes, selectedYear, selectedMonth])
|
||||||
|
|
||||||
// Filter incidents based on selected category
|
// Filter incidents based on selected category
|
||||||
const filteredCrimes = useMemo(() => {
|
const filteredCrimes = useMemo(() => {
|
||||||
if (!crimes) return []
|
if (!filteredByYearAndMonth) return []
|
||||||
if (selectedCategory === "all") return crimes
|
if (selectedCategory === "all") return filteredByYearAndMonth
|
||||||
|
|
||||||
return crimes.map((crime: ICrimeData) => {
|
return filteredByYearAndMonth.map((crime) => {
|
||||||
const filteredIncidents = crime.crime_incidents.filter(
|
const filteredIncidents = crime.crime_incidents.filter(
|
||||||
incident => incident.crime_categories.name === selectedCategory
|
incident => incident.crime_categories.name === selectedCategory
|
||||||
)
|
)
|
||||||
|
@ -91,75 +103,100 @@ export default function CrimeMap() {
|
||||||
number_of_crime: filteredIncidents.length
|
number_of_crime: filteredIncidents.length
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [crimes, selectedCategory])
|
}, [filteredByYearAndMonth, selectedCategory])
|
||||||
|
|
||||||
// Extract all incidents from all districts for marker display
|
// Extract all incidents from all districts for marker display
|
||||||
const allIncidents = useMemo(() => {
|
// const allIncidents = useMemo(() => {
|
||||||
if (!filteredCrimes) return []
|
// if (!filteredCrimes) return []
|
||||||
|
|
||||||
return filteredCrimes.flatMap((crime: ICrimeData) =>
|
// return filteredCrimes.flatMap((crime) =>
|
||||||
crime.crime_incidents.map((incident) => ({
|
// crime.crime_incidents.map((incident) => ({
|
||||||
id: incident.id,
|
// id: incident.id,
|
||||||
timestamp: incident.timestamp,
|
// timestamp: incident.timestamp,
|
||||||
description: incident.description,
|
// description: incident.description,
|
||||||
status: incident.status,
|
// status: incident.status,
|
||||||
category: incident.crime_categories.name,
|
// category: incident.crime_categories.name,
|
||||||
type: incident.crime_categories.type,
|
// type: incident.crime_categories.type,
|
||||||
address: incident.locations.address,
|
// address: incident.locations.address,
|
||||||
latitude: incident.locations.latitude,
|
// latitude: incident.locations.latitude,
|
||||||
longitude: incident.locations.longitude,
|
// longitude: incident.locations.longitude,
|
||||||
}))
|
// }))
|
||||||
)
|
// )
|
||||||
}, [filteredCrimes])
|
// }, [filteredCrimes])
|
||||||
|
|
||||||
// Handle district click
|
|
||||||
const handleDistrictClick = (feature: DistrictFeature) => {
|
|
||||||
setSelectedDistrict(feature)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle incident marker click
|
// Handle incident marker click
|
||||||
const handleIncidentClick = (incident: CrimeIncident) => {
|
const handleIncidentClick = (incident: CrimeIncident) => {
|
||||||
|
console.log("Incident clicked directly:", incident);
|
||||||
if (!incident.longitude || !incident.latitude) {
|
if (!incident.longitude || !incident.latitude) {
|
||||||
console.error("Invalid incident coordinates:", incident);
|
console.error("Invalid incident coordinates:", incident);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When an incident is clicked, clear any selected district
|
||||||
|
setSelectedDistrict(null);
|
||||||
|
|
||||||
|
// Set the selected incident
|
||||||
setSelectedIncident(incident);
|
setSelectedIncident(incident);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up event listener for incident clicks from the district layer
|
// Set up event listener for incident clicks from the district layer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleIncidentClick = (e: CustomEvent) => {
|
const handleIncidentClickEvent = (e: CustomEvent) => {
|
||||||
// console.log("Received incident_click event:", e.detail)
|
console.log("Received incident_click event:", e.detail);
|
||||||
if (e.detail) {
|
if (e.detail) {
|
||||||
setSelectedIncident(e.detail)
|
if (!e.detail.longitude || !e.detail.latitude) {
|
||||||
|
console.error("Invalid incident coordinates in event:", e.detail);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// When an incident is clicked, clear any selected district
|
||||||
|
setSelectedDistrict(null);
|
||||||
|
|
||||||
|
// Set the selected incident
|
||||||
|
setSelectedIncident(e.detail);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add event listener to the map container and document
|
// 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);
|
||||||
|
if (mapContainer) {
|
||||||
|
mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
|
}
|
||||||
|
|
||||||
// Listen on both the container and document to ensure we catch the event
|
// Listen on both the container and document to ensure we catch the event
|
||||||
document.addEventListener('incident_click', handleIncidentClick as EventListener)
|
document.addEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
|
|
||||||
if (mapContainer) {
|
if (mapContainer) {
|
||||||
mapContainer.addEventListener('incident_click', handleIncidentClick as EventListener)
|
mapContainer.addEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('incident_click', handleIncidentClick as EventListener)
|
document.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
|
|
||||||
if (mapContainer) {
|
if (mapContainer) {
|
||||||
mapContainer.removeEventListener('incident_click', handleIncidentClick as EventListener)
|
mapContainer.removeEventListener('incident_click', handleIncidentClickEvent as EventListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
// Handle district click
|
||||||
|
const handleDistrictClick = (feature: DistrictFeature) => {
|
||||||
|
// When a district is clicked, clear any selected incident
|
||||||
|
setSelectedIncident(null);
|
||||||
|
|
||||||
|
// Set the selected district
|
||||||
|
setSelectedDistrict(feature);
|
||||||
|
}
|
||||||
|
|
||||||
// Reset filters
|
// Reset filters
|
||||||
const resetFilters = useCallback(() => {
|
const resetFilters = useCallback(() => {
|
||||||
setSelectedYear(2024)
|
setSelectedYear(2024)
|
||||||
setSelectedMonth("all")
|
setSelectedMonth("all")
|
||||||
setSelectedCategory("all")
|
setSelectedCategory("all")
|
||||||
}, [setSelectedYear, setSelectedMonth])
|
}, [])
|
||||||
|
|
||||||
// Determine the title based on filters
|
// Determine the title based on filters
|
||||||
const getMapTitle = () => {
|
const getMapTitle = () => {
|
||||||
|
@ -173,6 +210,11 @@ export default function CrimeMap() {
|
||||||
return title
|
return title
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Toggle sidebar function
|
||||||
|
const toggleSidebar = useCallback(() => {
|
||||||
|
setSidebarCollapsed(!sidebarCollapsed)
|
||||||
|
}, [sidebarCollapsed])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full p-0 border-none shadow-none h-96">
|
<Card className="w-full p-0 border-none shadow-none h-96">
|
||||||
<CardHeader className="flex flex-row pb-2 pt-0 px-0 items-center justify-between">
|
<CardHeader className="flex flex-row pb-2 pt-0 px-0 items-center justify-between">
|
||||||
|
@ -218,19 +260,16 @@ export default function CrimeMap() {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Popup for selected incident */}
|
{/* Popup for selected incident */}
|
||||||
{selectedIncident && selectedIncident.longitude !== undefined && selectedIncident.latitude !== undefined && (
|
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
||||||
<>
|
<>
|
||||||
{console.log("Rendering CrimePopup with:", selectedIncident)}
|
{/* {console.log("About to render CrimePopup with:", selectedIncident)} */}
|
||||||
<CrimePopup
|
<CrimePopup
|
||||||
longitude={selectedIncident.longitude}
|
longitude={selectedIncident.longitude}
|
||||||
latitude={selectedIncident.latitude}
|
latitude={selectedIncident.latitude}
|
||||||
onClose={() => setSelectedIncident(null)}
|
onClose={() => setSelectedIncident(null)}
|
||||||
crime={{
|
crime={selectedIncident}
|
||||||
...selectedIncident,
|
|
||||||
latitude: selectedIncident.latitude,
|
|
||||||
longitude: selectedIncident.longitude
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { CRIME_RATE_COLORS, MAPBOX_TILESET_ID } from "@/app/_utils/const/map"
|
||||||
|
|
||||||
import { $Enums } from "@prisma/client"
|
import { $Enums } from "@prisma/client"
|
||||||
import DistrictPopup from "../pop-up/district-popup"
|
import DistrictPopup from "../pop-up/district-popup"
|
||||||
|
import { ICrimes } from "@/app/_utils/types/crimes"
|
||||||
|
|
||||||
// Types for district properties
|
// Types for district properties
|
||||||
export interface DistrictFeature {
|
export interface DistrictFeature {
|
||||||
|
@ -44,47 +45,47 @@ export interface DistrictFeature {
|
||||||
selectedMonth?: string
|
selectedMonth?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updated interface to match the structure returned by getCrimeByYearAndMonth
|
// Updated interface to match the structure in crimes.ts
|
||||||
export interface ICrimeData {
|
// export interface ICrimeData {
|
||||||
id: string
|
// id: string
|
||||||
district_id: string
|
// district_id: string
|
||||||
districts: {
|
// districts: {
|
||||||
name: string
|
// name: string
|
||||||
geographics: {
|
// geographics: {
|
||||||
address: string
|
// address: string | null
|
||||||
land_area: number
|
// land_area: number | null
|
||||||
year: number
|
// year: number | null
|
||||||
latitude: number
|
// latitude: number
|
||||||
longitude: number
|
// longitude: number
|
||||||
}
|
// }[]
|
||||||
demographics: {
|
// demographics: {
|
||||||
number_of_unemployed: number
|
// number_of_unemployed: number
|
||||||
population: number
|
// population: number
|
||||||
population_density: number
|
// population_density: number
|
||||||
year: number
|
// year: number
|
||||||
}
|
// }[]
|
||||||
}
|
// }
|
||||||
number_of_crime: number
|
// number_of_crime: number
|
||||||
level: $Enums.crime_rates
|
// level: $Enums.crime_rates
|
||||||
score: number
|
// score: number
|
||||||
month: number
|
// month: number
|
||||||
year: number
|
// year: number
|
||||||
crime_incidents: Array<{
|
// crime_incidents: Array<{
|
||||||
id: string
|
// id: string
|
||||||
timestamp: Date
|
// timestamp: Date
|
||||||
description: string
|
// description: string
|
||||||
status: string
|
// status: string
|
||||||
crime_categories: {
|
// crime_categories: {
|
||||||
name: string
|
// name: string
|
||||||
type: string
|
// type: string | null
|
||||||
}
|
// }
|
||||||
locations: {
|
// locations: {
|
||||||
address: string
|
// address: string | null
|
||||||
latitude: number
|
// latitude: number
|
||||||
longitude: number
|
// longitude: number
|
||||||
}
|
// }
|
||||||
}>
|
// }>
|
||||||
}
|
// }
|
||||||
|
|
||||||
// District layer props
|
// District layer props
|
||||||
export interface DistrictLayerProps {
|
export interface DistrictLayerProps {
|
||||||
|
@ -93,7 +94,7 @@ export interface DistrictLayerProps {
|
||||||
year?: string
|
year?: string
|
||||||
month?: string
|
month?: string
|
||||||
filterCategory?: string | "all"
|
filterCategory?: string | "all"
|
||||||
crimes?: ICrimeData[]
|
crimes?: ICrimes[]
|
||||||
tilesetId?: string
|
tilesetId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -114,18 +115,13 @@ export default function DistrictLayer({
|
||||||
feature: any
|
feature: any
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
// Menggunakan useRef untuk menyimpan informasi distrik yang dipilih
|
|
||||||
// sehingga nilainya tidak hilang saat komponen di-render ulang
|
|
||||||
const selectedDistrictRef = useRef<DistrictFeature | null>(null)
|
const selectedDistrictRef = useRef<DistrictFeature | null>(null)
|
||||||
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
||||||
|
|
||||||
// Use a ref to track whether layers have been added
|
|
||||||
const layersAdded = useRef(false)
|
const layersAdded = useRef(false)
|
||||||
|
|
||||||
// Process crime data to map to districts by district_id (kode_kec)
|
|
||||||
const crimeDataByDistrict = crimes.reduce(
|
const crimeDataByDistrict = crimes.reduce(
|
||||||
(acc, crime) => {
|
(acc, crime) => {
|
||||||
// Use district_id (which corresponds to kode_kec in the tileset) as the key
|
|
||||||
const districtId = crime.district_id
|
const districtId = crime.district_id
|
||||||
|
|
||||||
acc[districtId] = {
|
acc[districtId] = {
|
||||||
|
@ -137,15 +133,19 @@ export default function DistrictLayer({
|
||||||
{} as Record<string, { number_of_crime?: number; level?: $Enums.crime_rates }>,
|
{} as Record<string, { number_of_crime?: number; level?: $Enums.crime_rates }>,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handle click on district
|
const handleDistrictClick = (e: any) => {
|
||||||
const handleClick = (e: any) => {
|
const incidentFeatures = map?.queryRenderedFeatures(e.point, { layers: ["unclustered-point", "clusters"] });
|
||||||
|
|
||||||
|
if (incidentFeatures && incidentFeatures.length > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!map || !e.features || e.features.length === 0) return
|
if (!map || !e.features || e.features.length === 0) return
|
||||||
|
|
||||||
const feature = e.features[0]
|
const feature = e.features[0]
|
||||||
const districtId = feature.properties.kode_kec // Using kode_kec as the unique identifier
|
const districtId = feature.properties.kode_kec
|
||||||
const crimeData = crimeDataByDistrict[districtId] || {}
|
const crimeData = crimeDataByDistrict[districtId] || {}
|
||||||
|
|
||||||
// Get ALL crime_incidents for this district by aggregating from all matching crime records
|
|
||||||
let crime_incidents: Array<{
|
let crime_incidents: Array<{
|
||||||
id: string
|
id: string
|
||||||
timestamp: Date
|
timestamp: Date
|
||||||
|
@ -158,12 +158,8 @@ export default function DistrictLayer({
|
||||||
longitude: number
|
longitude: number
|
||||||
}> = []
|
}> = []
|
||||||
|
|
||||||
// Find all crime data records for this district (across all months)
|
|
||||||
const districtCrimes = crimes.filter(crime => crime.district_id === districtId)
|
const districtCrimes = crimes.filter(crime => crime.district_id === districtId)
|
||||||
|
|
||||||
console.log(`Found ${districtCrimes.length} crime data records for district ID ${districtId}`)
|
|
||||||
|
|
||||||
// Collect all crime incidents from all month records for this district
|
|
||||||
districtCrimes.forEach(crimeRecord => {
|
districtCrimes.forEach(crimeRecord => {
|
||||||
if (crimeRecord && crimeRecord.crime_incidents) {
|
if (crimeRecord && crimeRecord.crime_incidents) {
|
||||||
const incidents = crimeRecord.crime_incidents.map(incident => ({
|
const incidents = crimeRecord.crime_incidents.map(incident => ({
|
||||||
|
@ -182,14 +178,10 @@ export default function DistrictLayer({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log(`Aggregated ${crime_incidents.length} total crime incidents for district`)
|
|
||||||
|
|
||||||
// Get demographics and geographics from the first record (should be the same across all records)
|
|
||||||
const firstDistrictCrime = districtCrimes.length > 0 ? districtCrimes[0] : null
|
const firstDistrictCrime = districtCrimes.length > 0 ? districtCrimes[0] : null
|
||||||
const demographics = firstDistrictCrime?.districts.demographics
|
const demographics = firstDistrictCrime?.districts.demographics?.[0]
|
||||||
const geographics = firstDistrictCrime?.districts.geographics
|
const geographics = firstDistrictCrime?.districts.geographics?.[0]
|
||||||
|
|
||||||
// Make sure we have valid coordinates from the click event
|
|
||||||
const clickLng = e.lngLat ? e.lngLat.lng : null
|
const clickLng = e.lngLat ? e.lngLat.lng : null
|
||||||
const clickLat = e.lngLat ? e.lngLat.lat : null
|
const clickLat = e.lngLat ? e.lngLat.lat : null
|
||||||
|
|
||||||
|
@ -203,62 +195,115 @@ export default function DistrictLayer({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a complete district object ensuring all required properties are present
|
|
||||||
const district: DistrictFeature = {
|
const district: DistrictFeature = {
|
||||||
id: districtId,
|
id: districtId,
|
||||||
name: feature.properties.nama || feature.properties.kecamatan || "Unknown District",
|
name: feature.properties.nama || feature.properties.kecamatan || "Unknown District",
|
||||||
properties: feature.properties,
|
properties: feature.properties,
|
||||||
longitude: geographics.longitude || clickLng || 0,
|
longitude: geographics.longitude || clickLng || 0,
|
||||||
latitude: geographics.latitude || clickLat || 0,
|
latitude: geographics.latitude || clickLat || 0,
|
||||||
// Use the total crime count across all months
|
|
||||||
number_of_crime: crimeData.number_of_crime || 0,
|
number_of_crime: crimeData.number_of_crime || 0,
|
||||||
// Use the level from the currently selected month/year (or default to low)
|
|
||||||
level: crimeData.level || $Enums.crime_rates.low,
|
level: crimeData.level || $Enums.crime_rates.low,
|
||||||
demographics,
|
demographics: {
|
||||||
geographics,
|
number_of_unemployed: demographics.number_of_unemployed,
|
||||||
// Include all aggregated crime incidents
|
population: demographics.population,
|
||||||
|
population_density: demographics.population_density,
|
||||||
|
year: demographics.year,
|
||||||
|
},
|
||||||
|
geographics: {
|
||||||
|
address: geographics.address || "",
|
||||||
|
land_area: geographics.land_area || 0,
|
||||||
|
year: geographics.year || 0,
|
||||||
|
latitude: geographics.latitude,
|
||||||
|
longitude: geographics.longitude,
|
||||||
|
},
|
||||||
crime_incidents: crime_incidents || [],
|
crime_incidents: crime_incidents || [],
|
||||||
selectedYear: year,
|
selectedYear: year,
|
||||||
selectedMonth: month
|
selectedMonth: month
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!district.longitude || !district.latitude) {
|
if (!district.longitude || !district.latitude) {
|
||||||
console.error("Invalid district coordinates:", district);
|
console.error("Invalid district coordinates:", district);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Selected district:", district);
|
|
||||||
console.log(`Selected district has ${district.crime_incidents?.length || 0} crime_incidents out of ${district.number_of_crime} total crimes`);
|
|
||||||
|
|
||||||
// Set the reference BEFORE handling the onClick or setState
|
|
||||||
selectedDistrictRef.current = district;
|
selectedDistrictRef.current = district;
|
||||||
|
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(district);
|
onClick(district);
|
||||||
} else {
|
} else {
|
||||||
setSelectedDistrict(district);
|
setSelectedDistrict(district);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pastikan event handler klik selalu diperbarui
|
const handleIncidentClick = useCallback((e: any) => {
|
||||||
// dan re-attach setiap kali ada perubahan data
|
if (!map) return;
|
||||||
useEffect(() => {
|
|
||||||
if (!map || !visible || !map.getMap().getLayer("district-fill")) return;
|
|
||||||
|
|
||||||
// Re-attach click handler
|
const features = map.queryRenderedFeatures(e.point, { layers: ["unclustered-point"] })
|
||||||
map.off("click", "district-fill", handleClick);
|
if (!features || features.length === 0) return
|
||||||
map.on("click", "district-fill", handleClick);
|
|
||||||
|
|
||||||
console.log("Re-attached click handler, current district:", selectedDistrict?.name || "None");
|
const incident = features[0]
|
||||||
|
if (!incident.properties) return
|
||||||
|
|
||||||
return () => {
|
e.originalEvent.stopPropagation()
|
||||||
if (map) {
|
e.preventDefault()
|
||||||
map.off("click", "district-fill", handleClick);
|
|
||||||
}
|
const incidentDetails = {
|
||||||
};
|
id: incident.properties.id,
|
||||||
}, [map, visible, crimes, filterCategory, year, month]);
|
district: incident.properties.district,
|
||||||
|
category: incident.properties.category,
|
||||||
|
type: incident.properties.incidentType,
|
||||||
|
description: incident.properties.description,
|
||||||
|
status: incident.properties?.status || "Unknown",
|
||||||
|
longitude: (incident.geometry as any).coordinates[0],
|
||||||
|
latitude: (incident.geometry as any).coordinates[1],
|
||||||
|
timestamp: new Date(),
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Incident clicked:", incidentDetails);
|
||||||
|
|
||||||
|
const customEvent = new CustomEvent('incident_click', {
|
||||||
|
detail: incidentDetails,
|
||||||
|
bubbles: true
|
||||||
|
})
|
||||||
|
|
||||||
|
if (map.getMap().getCanvas()) {
|
||||||
|
map.getMap().getCanvas().dispatchEvent(customEvent)
|
||||||
|
} else {
|
||||||
|
document.dispatchEvent(customEvent)
|
||||||
|
}
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
|
||||||
|
const handleClusterClick = useCallback((e: any) => {
|
||||||
|
if (!map) return;
|
||||||
|
|
||||||
|
e.originalEvent.stopPropagation()
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const features = map.queryRenderedFeatures(e.point, { layers: ["clusters"] })
|
||||||
|
|
||||||
|
if (!features || features.length === 0) return
|
||||||
|
|
||||||
|
const clusterId: number = features[0].properties?.cluster_id as number
|
||||||
|
|
||||||
|
(map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(
|
||||||
|
clusterId,
|
||||||
|
(err, zoom) => {
|
||||||
|
if (err) return
|
||||||
|
|
||||||
|
map.easeTo({
|
||||||
|
center: (features[0].geometry as any).coordinates,
|
||||||
|
zoom: zoom ?? undefined,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}, [map]);
|
||||||
|
|
||||||
|
const handleCloseDistrictPopup = useCallback(() => {
|
||||||
|
selectedDistrictRef.current = null;
|
||||||
|
setSelectedDistrict(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Add district layer to the map when it's loaded
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !visible) return;
|
if (!map || !visible) return;
|
||||||
|
|
||||||
|
@ -404,6 +449,9 @@ export default function DistrictLayer({
|
||||||
],
|
],
|
||||||
"circle-opacity": 0.75,
|
"circle-opacity": 0.75,
|
||||||
},
|
},
|
||||||
|
layout: {
|
||||||
|
"visibility": "visible",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
firstSymbolId,
|
firstSymbolId,
|
||||||
)
|
)
|
||||||
|
@ -439,31 +487,15 @@ export default function DistrictLayer({
|
||||||
"circle-stroke-width": 1,
|
"circle-stroke-width": 1,
|
||||||
"circle-stroke-color": "#fff",
|
"circle-stroke-color": "#fff",
|
||||||
},
|
},
|
||||||
|
layout: {
|
||||||
|
"visibility": "visible",
|
||||||
|
}
|
||||||
},
|
},
|
||||||
firstSymbolId,
|
firstSymbolId,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
map.on("click", "clusters", (e) => {
|
// Add improved mouse interaction for clusters and points
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: ["clusters"] })
|
|
||||||
|
|
||||||
if (!features || features.length === 0) return
|
|
||||||
|
|
||||||
const clusterId = features[0].properties?.cluster_id
|
|
||||||
|
|
||||||
; (map.getSource("crime-incidents") as mapboxgl.GeoJSONSource).getClusterExpansionZoom(
|
|
||||||
clusterId,
|
|
||||||
(err, zoom) => {
|
|
||||||
if (err) return
|
|
||||||
|
|
||||||
map.easeTo({
|
|
||||||
center: (features[0].geometry as any).coordinates,
|
|
||||||
zoom: zoom ?? undefined,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
map.on("mouseenter", "clusters", () => {
|
map.on("mouseenter", "clusters", () => {
|
||||||
map.getCanvas().style.cursor = "pointer"
|
map.getCanvas().style.cursor = "pointer"
|
||||||
})
|
})
|
||||||
|
@ -479,15 +511,31 @@ export default function DistrictLayer({
|
||||||
map.on("mouseleave", "unclustered-point", () => {
|
map.on("mouseleave", "unclustered-point", () => {
|
||||||
map.getCanvas().style.cursor = ""
|
map.getCanvas().style.cursor = ""
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Also add hover effect for district fill
|
||||||
|
map.on("mouseenter", "district-fill", () => {
|
||||||
|
map.getCanvas().style.cursor = "pointer"
|
||||||
|
})
|
||||||
|
|
||||||
|
map.on("mouseleave", "district-fill", () => {
|
||||||
|
map.getCanvas().style.cursor = ""
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove old event listeners to avoid duplicates
|
||||||
|
map.off("click", "clusters", handleClusterClick);
|
||||||
|
map.off("click", "unclustered-point", handleIncidentClick);
|
||||||
|
|
||||||
|
// Add event listeners
|
||||||
|
map.on("click", "clusters", handleClusterClick);
|
||||||
|
map.on("click", "unclustered-point", handleIncidentClick);
|
||||||
|
|
||||||
|
map.off("click", "district-fill", handleDistrictClick);
|
||||||
|
map.on("click", "district-fill", handleDistrictClick);
|
||||||
|
|
||||||
|
map.on("mouseleave", "district-fill", () => setHoverInfo(null));
|
||||||
|
|
||||||
|
layersAdded.current = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebind the click event listener for "district-fill"
|
|
||||||
map.on("click", "district-fill", handleClick);
|
|
||||||
|
|
||||||
// Ensure hover info is cleared when leaving the layer
|
|
||||||
map.on("mouseleave", "district-fill", () => setHoverInfo(null));
|
|
||||||
|
|
||||||
layersAdded.current = true;
|
|
||||||
} else {
|
} else {
|
||||||
if (map.getMap().getLayer("district-fill")) {
|
if (map.getMap().getLayer("district-fill")) {
|
||||||
map.getMap().setPaintProperty("district-fill", "fill-color", [
|
map.getMap().setPaintProperty("district-fill", "fill-color", [
|
||||||
|
@ -527,19 +575,16 @@ export default function DistrictLayer({
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (map) {
|
if (map) {
|
||||||
// Remove the click event listener when the component unmounts or dependencies change
|
map.off("click", "district-fill", handleDistrictClick);
|
||||||
map.off("click", "district-fill", handleClick);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [map, visible, tilesetId, crimes, filterCategory, year, month]);
|
}, [map, visible, tilesetId, crimes, filterCategory, year, month, handleIncidentClick, handleClusterClick]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!map || !layersAdded.current) return
|
if (!map || !layersAdded.current) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (map.getMap().getLayer("district-fill")) {
|
if (map.getMap().getLayer("district-fill")) {
|
||||||
// Create a safety check for empty or invalid data
|
|
||||||
const colorEntries = Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => {
|
const colorEntries = Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => {
|
||||||
if (!data || !data.level) {
|
if (!data || !data.level) {
|
||||||
return [
|
return [
|
||||||
|
@ -584,10 +629,8 @@ export default function DistrictLayer({
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allIncidents = crimes.flatMap((crime) => {
|
const allIncidents = crimes.flatMap((crime) => {
|
||||||
// Make sure we handle cases where crime_incidents might be undefined
|
|
||||||
if (!crime.crime_incidents) return []
|
if (!crime.crime_incidents) return []
|
||||||
|
|
||||||
// Apply category filter if specified
|
|
||||||
let filteredIncidents = crime.crime_incidents
|
let filteredIncidents = crime.crime_incidents
|
||||||
|
|
||||||
if (filterCategory !== "all") {
|
if (filterCategory !== "all") {
|
||||||
|
@ -598,7 +641,6 @@ export default function DistrictLayer({
|
||||||
}
|
}
|
||||||
|
|
||||||
return filteredIncidents.map((incident) => {
|
return filteredIncidents.map((incident) => {
|
||||||
// Handle possible null/undefined values
|
|
||||||
if (!incident.locations) {
|
if (!incident.locations) {
|
||||||
console.warn("Missing location for incident:", incident.id)
|
console.warn("Missing location for incident:", incident.id)
|
||||||
return null
|
return null
|
||||||
|
@ -622,7 +664,7 @@ export default function DistrictLayer({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}).filter(Boolean) // Remove null values
|
}).filter(Boolean)
|
||||||
});
|
});
|
||||||
|
|
||||||
(map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({
|
(map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({
|
||||||
|
@ -635,25 +677,22 @@ export default function DistrictLayer({
|
||||||
}
|
}
|
||||||
}, [map, crimes, filterCategory])
|
}, [map, crimes, filterCategory])
|
||||||
|
|
||||||
// Effect khusus untuk memastikan state selectedDistrict dipertahankan
|
|
||||||
// ketika data berubah (tahun/bulan diubah)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Jika kita memiliki district yang dipilih dalam ref, pastikan
|
|
||||||
// state juga diperbarui dengan data terbaru
|
|
||||||
if (selectedDistrictRef.current) {
|
if (selectedDistrictRef.current) {
|
||||||
// Cari crime data terbaru untuk district yang dipilih
|
|
||||||
const districtId = selectedDistrictRef.current.id;
|
const districtId = selectedDistrictRef.current.id;
|
||||||
const crimeData = crimeDataByDistrict[districtId] || {};
|
const crimeData = crimeDataByDistrict[districtId] || {};
|
||||||
|
|
||||||
// Cari data district terkini
|
|
||||||
const districtCrime = crimes.find(crime => crime.district_id === districtId);
|
const districtCrime = crimes.find(crime => crime.district_id === districtId);
|
||||||
|
|
||||||
// Perbarui data district dengan informasi terkini
|
|
||||||
if (districtCrime) {
|
if (districtCrime) {
|
||||||
const demographics = districtCrime.districts.demographics;
|
const demographics = districtCrime.districts.demographics?.[0];
|
||||||
const geographics = districtCrime.districts.geographics;
|
const geographics = districtCrime.districts.geographics?.[0];
|
||||||
|
|
||||||
|
if (!demographics || !geographics) {
|
||||||
|
console.error("Missing district data:", { demographics, geographics });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Filter crime_incidents
|
|
||||||
const crime_incidents = districtCrime.crime_incidents
|
const crime_incidents = districtCrime.crime_incidents
|
||||||
.filter(incident =>
|
.filter(incident =>
|
||||||
filterCategory === "all" || incident.crime_categories.name === filterCategory
|
filterCategory === "all" || incident.crime_categories.name === filterCategory
|
||||||
|
@ -662,32 +701,39 @@ export default function DistrictLayer({
|
||||||
id: incident.id,
|
id: incident.id,
|
||||||
timestamp: incident.timestamp,
|
timestamp: incident.timestamp,
|
||||||
description: incident.description,
|
description: incident.description,
|
||||||
status: incident.status,
|
status: incident.status || "",
|
||||||
category: incident.crime_categories.name,
|
category: incident.crime_categories.name,
|
||||||
type: incident.crime_categories.type,
|
type: incident.crime_categories.type || "",
|
||||||
address: incident.locations.address,
|
address: incident.locations.address || "",
|
||||||
latitude: incident.locations.latitude,
|
latitude: incident.locations.latitude,
|
||||||
longitude: incident.locations.longitude
|
longitude: incident.locations.longitude
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Buat district object baru dengan data terkini
|
|
||||||
const updatedDistrict: DistrictFeature = {
|
const updatedDistrict: DistrictFeature = {
|
||||||
...selectedDistrictRef.current,
|
...selectedDistrictRef.current,
|
||||||
...crimeData,
|
number_of_crime: crimeData.number_of_crime || 0,
|
||||||
demographics,
|
level: crimeData.level || selectedDistrictRef.current.level,
|
||||||
geographics,
|
demographics: {
|
||||||
|
number_of_unemployed: demographics.number_of_unemployed,
|
||||||
|
population: demographics.population,
|
||||||
|
population_density: demographics.population_density,
|
||||||
|
year: demographics.year,
|
||||||
|
},
|
||||||
|
geographics: {
|
||||||
|
address: geographics.address || "",
|
||||||
|
land_area: geographics.land_area || 0,
|
||||||
|
year: geographics.year || 0,
|
||||||
|
latitude: geographics.latitude,
|
||||||
|
longitude: geographics.longitude,
|
||||||
|
},
|
||||||
crime_incidents,
|
crime_incidents,
|
||||||
selectedYear: year,
|
selectedYear: year,
|
||||||
selectedMonth: month
|
selectedMonth: month
|
||||||
};
|
};
|
||||||
|
|
||||||
// Perbarui ref tetapi BUKAN state di sini
|
|
||||||
selectedDistrictRef.current = updatedDistrict;
|
selectedDistrictRef.current = updatedDistrict;
|
||||||
|
|
||||||
// Gunakan functional update untuk menghindari loop
|
|
||||||
// Hanya update jika berbeda dari state sebelumnya
|
|
||||||
setSelectedDistrict(prevDistrict => {
|
setSelectedDistrict(prevDistrict => {
|
||||||
// Jika sudah sama, tidak perlu update
|
|
||||||
if (prevDistrict?.id === updatedDistrict.id &&
|
if (prevDistrict?.id === updatedDistrict.id &&
|
||||||
prevDistrict?.selectedYear === updatedDistrict.selectedYear &&
|
prevDistrict?.selectedYear === updatedDistrict.selectedYear &&
|
||||||
prevDistrict?.selectedMonth === updatedDistrict.selectedMonth) {
|
prevDistrict?.selectedMonth === updatedDistrict.selectedMonth) {
|
||||||
|
@ -695,100 +741,19 @@ export default function DistrictLayer({
|
||||||
}
|
}
|
||||||
return updatedDistrict;
|
return updatedDistrict;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Updated selected district with new data:", updatedDistrict.name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [crimes, filterCategory, year, month]); // hapus crimeDataByDistrict dari dependencies
|
}, [crimes, filterCategory, year, month]);
|
||||||
|
|
||||||
const handleIncidentClick = useCallback((e: any) => {
|
|
||||||
if (!map) return;
|
|
||||||
|
|
||||||
// Try to query for crime_incidents at the click location
|
|
||||||
const features = map.queryRenderedFeatures(e.point, { layers: ["unclustered-point"] })
|
|
||||||
if (!features || features.length === 0) return
|
|
||||||
|
|
||||||
const incident = features[0]
|
|
||||||
if (!incident.properties) return
|
|
||||||
|
|
||||||
// Prevent the click from propagating to other layers
|
|
||||||
e.originalEvent.stopPropagation()
|
|
||||||
|
|
||||||
// Extract the incident details from the feature properties
|
|
||||||
const incidentDetails = {
|
|
||||||
id: incident.properties.id,
|
|
||||||
district: incident.properties.district,
|
|
||||||
category: incident.properties.category,
|
|
||||||
type: incident.properties.incidentType,
|
|
||||||
description: incident.properties.description,
|
|
||||||
status: incident.properties?.status || "Unknown",
|
|
||||||
longitude: (incident.geometry as any).coordinates[0],
|
|
||||||
latitude: (incident.geometry as any).coordinates[1],
|
|
||||||
timestamp: new Date(),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch the custom event to the map container element directly
|
|
||||||
const customEvent = new CustomEvent('incident_click', {
|
|
||||||
detail: incidentDetails,
|
|
||||||
bubbles: true // Make sure the event bubbles up
|
|
||||||
})
|
|
||||||
|
|
||||||
if (map.getMap().getCanvas()) {
|
|
||||||
map.getMap().getCanvas().dispatchEvent(customEvent)
|
|
||||||
} else {
|
|
||||||
document.dispatchEvent(customEvent) // Fallback
|
|
||||||
}
|
|
||||||
}, [map]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!map || !visible) return;
|
|
||||||
|
|
||||||
// Add click handler for individual incident points
|
|
||||||
if (map.getMap().getLayer("unclustered-point")) {
|
|
||||||
map.on("click", "unclustered-point", handleIncidentClick)
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (map && map.getMap().getLayer("unclustered-point")) {
|
|
||||||
map.off("click", "unclustered-point", handleIncidentClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [map, visible, handleIncidentClick]);
|
|
||||||
|
|
||||||
if (!visible) return null
|
if (!visible) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* {hoverInfo && (
|
|
||||||
<div
|
|
||||||
className="absolute z-10 bg-white rounded-md shadow-md px-3 py-2 pointer-events-none"
|
|
||||||
style={{
|
|
||||||
left: hoverInfo.x + 10,
|
|
||||||
top: hoverInfo.y + 10,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className="text-sm font-medium">
|
|
||||||
{hoverInfo.feature.properties.nama || hoverInfo.feature.properties.kecamatan}
|
|
||||||
</p>
|
|
||||||
{hoverInfo.feature.properties.number_of_crime !== undefined && (
|
|
||||||
<p className="text-xs text-gray-600">
|
|
||||||
{hoverInfo.feature.properties.number_of_crime} crime_incidents
|
|
||||||
{hoverInfo.feature.properties.level && (
|
|
||||||
<span className="ml-2 text-xs font-semibold text-gray-500">({hoverInfo.feature.properties.level})</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)} */}
|
|
||||||
|
|
||||||
{selectedDistrictRef.current ? (
|
{selectedDistrictRef.current ? (
|
||||||
<DistrictPopup
|
<DistrictPopup
|
||||||
longitude={selectedDistrictRef.current.longitude || 0}
|
longitude={selectedDistrictRef.current.longitude || 0}
|
||||||
latitude={selectedDistrictRef.current.latitude || 0}
|
latitude={selectedDistrictRef.current.latitude || 0}
|
||||||
onClose={() => {
|
onClose={handleCloseDistrictPopup}
|
||||||
selectedDistrictRef.current = null;
|
|
||||||
setSelectedDistrict(null);
|
|
||||||
}}
|
|
||||||
district={selectedDistrictRef.current}
|
district={selectedDistrictRef.current}
|
||||||
year={year}
|
year={year}
|
||||||
month={month}
|
month={month}
|
||||||
|
|
|
@ -86,6 +86,7 @@ export default function DistrictPopup({
|
||||||
return year || "All time"
|
return year || "All time"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup
|
<Popup
|
||||||
longitude={longitude}
|
longitude={longitude}
|
||||||
|
@ -117,10 +118,10 @@ export default function DistrictPopup({
|
||||||
</div>
|
</div>
|
||||||
{getCrimeRateBadge(district.level)}
|
{getCrimeRateBadge(district.level)}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-white/80 text-xs flex items-center gap-2">
|
{/* <div className="mt-1 text-white/80 text-xs flex items-center gap-2">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3 w-3" />
|
||||||
<span>{getTimePeriod()}</span>
|
<span>{getTimePeriod()}</span>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-3 gap-1 p-2 bg-background">
|
<div className="grid grid-cols-3 gap-1 p-2 bg-background">
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import {
|
||||||
|
$Enums,
|
||||||
|
crime_categories,
|
||||||
|
crime_incidents,
|
||||||
|
crimes,
|
||||||
|
demographics,
|
||||||
|
districts,
|
||||||
|
geographics,
|
||||||
|
locations,
|
||||||
|
} from '@prisma/client';
|
||||||
|
|
||||||
|
export interface ICrimes extends crimes {
|
||||||
|
districts: {
|
||||||
|
name: string;
|
||||||
|
geographics: {
|
||||||
|
year: number | null;
|
||||||
|
address: string | null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
land_area: number | null;
|
||||||
|
}[];
|
||||||
|
demographics: {
|
||||||
|
year: number;
|
||||||
|
population: number;
|
||||||
|
number_of_unemployed: number;
|
||||||
|
population_density: number;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
crime_incidents: {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
status: $Enums.crime_status | null;
|
||||||
|
timestamp: Date;
|
||||||
|
crime_categories: {
|
||||||
|
name: string;
|
||||||
|
type: string | null;
|
||||||
|
};
|
||||||
|
locations: {
|
||||||
|
address: string | null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICrimesByYearAndMonth extends crimes {
|
||||||
|
districts: {
|
||||||
|
name: string;
|
||||||
|
geographics: {
|
||||||
|
year: number | null;
|
||||||
|
address: string | null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
land_area: number | null;
|
||||||
|
};
|
||||||
|
demographics: {
|
||||||
|
year: number;
|
||||||
|
population: number;
|
||||||
|
number_of_unemployed: number;
|
||||||
|
population_density: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
crime_incidents: {
|
||||||
|
id: string;
|
||||||
|
description: string;
|
||||||
|
status: $Enums.crime_status | null;
|
||||||
|
timestamp: Date;
|
||||||
|
crime_categories: {
|
||||||
|
name: string;
|
||||||
|
type: string | null;
|
||||||
|
};
|
||||||
|
locations: {
|
||||||
|
address: string | null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
}
|
Loading…
Reference in New Issue