362 lines
17 KiB
TypeScript
362 lines
17 KiB
TypeScript
"use client"
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/app/_components/ui/card"
|
|
import { Skeleton } from "@/app/_components/ui/skeleton"
|
|
import MapView from "./map"
|
|
import { Button } from "@/app/_components/ui/button"
|
|
import { AlertCircle } from "lucide-react"
|
|
import { getMonthName } from "@/app/_utils/common"
|
|
import { useRef, useState, useCallback, useMemo, useEffect } from "react"
|
|
import { useFullscreen } from "@/app/_hooks/use-fullscreen"
|
|
import { Overlay } from "./overlay"
|
|
import MapLegend from "./legends/map-legend"
|
|
import UnitsLegend from "./legends/units-legend"
|
|
import TimelineLegend from "./legends/timeline-legend"
|
|
import { useGetAvailableYears, useGetCrimeCategories, useGetCrimes, useGetCrimeTypes, useGetRecentIncidents } from "@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/_queries/queries"
|
|
import MapSelectors from "./controls/map-selector"
|
|
|
|
import { cn } from "@/app/_lib/utils"
|
|
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
|
|
import { CrimeTimelapse } from "./controls/bottom/crime-timelapse"
|
|
import { ITooltips } from "./controls/top/tooltips"
|
|
import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
|
|
import Tooltips from "./controls/top/tooltips"
|
|
import Layers from "./layers/layers"
|
|
import DistrictLayer, { DistrictFeature } from "./layers/district-layer-old"
|
|
import { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries"
|
|
|
|
export default function CrimeMap() {
|
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
|
|
const [selectedDistrict, setSelectedDistrict] = useState<DistrictFeature | null>(null)
|
|
const [showLegend, setShowLegend] = useState<boolean>(true)
|
|
const [activeControl, setActiveControl] = useState<ITooltips>("incidents")
|
|
const [selectedSourceType, setSelectedSourceType] = useState<string>("cbu")
|
|
const [selectedYear, setSelectedYear] = useState<number>(2024)
|
|
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
|
const [selectedCategory, setSelectedCategory] = useState<string | "all">("all")
|
|
const [yearProgress, setYearProgress] = useState(0)
|
|
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
|
|
const [isSearchActive, setIsSearchActive] = useState(false)
|
|
const [showUnitsLayer, setShowUnitsLayer] = useState(false)
|
|
const [showClusters, setShowClusters] = useState(false)
|
|
const [showHeatmap, setShowHeatmap] = useState(false)
|
|
const [showUnclustered, setShowUnclustered] = useState(true)
|
|
const [useAllYears, setUseAllYears] = useState<boolean>(false)
|
|
const [useAllMonths, setUseAllMonths] = useState<boolean>(false)
|
|
const [showEWS, setShowEWS] = useState<boolean>(true)
|
|
|
|
const mapContainerRef = useRef<HTMLDivElement>(null)
|
|
|
|
const { isFullscreen } = useFullscreen(mapContainerRef)
|
|
|
|
const { data: availableSourceTypes, isLoading: isTypeLoading } = useGetCrimeTypes()
|
|
|
|
const {
|
|
data: availableYears,
|
|
isLoading: isYearsLoading,
|
|
error: yearsError
|
|
} = useGetAvailableYears()
|
|
|
|
const { data: categoriesData, isLoading: isCategoryLoading } = useGetCrimeCategories()
|
|
|
|
const categories = useMemo(() =>
|
|
categoriesData ? categoriesData.map(category => category.name) : []
|
|
, [categoriesData])
|
|
|
|
const {
|
|
data: crimes,
|
|
isLoading: isCrimesLoading,
|
|
error: crimesError
|
|
} = useGetCrimes()
|
|
|
|
const { data: fetchedUnits, isLoading } = useGetUnitsQuery()
|
|
|
|
const { data: recentIncidents } = useGetRecentIncidents()
|
|
|
|
useEffect(() => {
|
|
if (activeControl === "heatmap" || activeControl === "timeline") {
|
|
setUseAllYears(true);
|
|
setUseAllMonths(true);
|
|
} else {
|
|
setUseAllYears(false);
|
|
setUseAllMonths(false);
|
|
}
|
|
}, [activeControl]);
|
|
|
|
const crimesBySourceType = useMemo(() => {
|
|
if (!crimes) return [];
|
|
return crimes.filter(crime => crime.source_type === selectedSourceType);
|
|
}, [crimes, selectedSourceType]);
|
|
|
|
const filteredByYearAndMonth = useMemo(() => {
|
|
if (!crimesBySourceType) return [];
|
|
|
|
if (useAllYears) {
|
|
if (useAllMonths) {
|
|
return crimesBySourceType;
|
|
} else {
|
|
return crimesBySourceType.filter((crime) => {
|
|
return selectedMonth === "all" ? true : crime.month === selectedMonth;
|
|
});
|
|
}
|
|
}
|
|
|
|
return crimesBySourceType.filter((crime) => {
|
|
const yearMatch = crime.year === selectedYear;
|
|
|
|
if (selectedMonth === "all" || useAllMonths) {
|
|
return yearMatch;
|
|
} else {
|
|
return yearMatch && crime.month === selectedMonth;
|
|
}
|
|
});
|
|
}, [crimesBySourceType, selectedYear, selectedMonth, useAllYears, useAllMonths]);
|
|
|
|
const filteredCrimes = useMemo(() => {
|
|
if (!filteredByYearAndMonth) return []
|
|
if (selectedCategory === "all") return filteredByYearAndMonth
|
|
|
|
return filteredByYearAndMonth.map((crime) => {
|
|
const filteredIncidents = crime.crime_incidents.filter(
|
|
incident => incident.crime_categories.name === selectedCategory
|
|
)
|
|
|
|
return {
|
|
...crime,
|
|
crime_incidents: filteredIncidents,
|
|
number_of_crime: filteredIncidents.length
|
|
}
|
|
})
|
|
}, [filteredByYearAndMonth, selectedCategory])
|
|
|
|
useEffect(() => {
|
|
if (selectedSourceType === "cbu") {
|
|
if (activeControl !== "clusters" && activeControl !== "reports" &&
|
|
activeControl !== "layers" && activeControl !== "search" &&
|
|
activeControl !== "alerts") {
|
|
setActiveControl("clusters");
|
|
setShowClusters(true);
|
|
setShowUnclustered(false);
|
|
}
|
|
}
|
|
}, [selectedSourceType, activeControl]);
|
|
|
|
const handleSourceTypeChange = useCallback((sourceType: string) => {
|
|
setSelectedSourceType(sourceType);
|
|
|
|
if (sourceType === "cbu") {
|
|
setActiveControl("clusters");
|
|
setShowClusters(true);
|
|
setShowUnclustered(false);
|
|
} else {
|
|
setActiveControl("incidents");
|
|
setShowUnclustered(true);
|
|
setShowClusters(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleTimelineChange = useCallback((year: number, month: number, progress: number) => {
|
|
setSelectedYear(year)
|
|
setSelectedMonth(month)
|
|
setYearProgress(progress)
|
|
}, [])
|
|
|
|
const handleTimelinePlayingChange = useCallback((playing: boolean) => {
|
|
setisTimelapsePlaying(playing)
|
|
|
|
if (playing) {
|
|
setSelectedDistrict(null)
|
|
}
|
|
}, [])
|
|
|
|
const resetFilters = useCallback(() => {
|
|
setSelectedYear(2024)
|
|
setSelectedMonth("all")
|
|
setSelectedCategory("all")
|
|
}, [])
|
|
|
|
const getMapTitle = () => {
|
|
if (useAllYears) {
|
|
return `All Years Data ${selectedCategory !== "all" ? `- ${selectedCategory}` : ''}`;
|
|
}
|
|
|
|
let title = `${selectedYear}`;
|
|
if (selectedMonth !== "all" && !useAllMonths) {
|
|
title += ` - ${getMonthName(Number(selectedMonth))}`;
|
|
}
|
|
if (selectedCategory !== "all") {
|
|
title += ` - ${selectedCategory}`;
|
|
}
|
|
return title;
|
|
}
|
|
|
|
const handleControlChange = (controlId: ITooltips) => {
|
|
if (selectedSourceType === "cbu" &&
|
|
!["clusters", "reports", "layers", "search", "alerts"].includes(controlId as string)) {
|
|
return;
|
|
}
|
|
|
|
setActiveControl(controlId);
|
|
|
|
if (controlId === "clusters") {
|
|
setShowClusters(true)
|
|
} else {
|
|
setShowClusters(false)
|
|
}
|
|
|
|
if (controlId === "incidents") {
|
|
setShowUnclustered(true)
|
|
} else {
|
|
setShowUnclustered(false)
|
|
}
|
|
|
|
if (controlId === "search") {
|
|
setIsSearchActive(prev => !prev);
|
|
}
|
|
|
|
if (controlId === "units") {
|
|
setShowUnitsLayer(true);
|
|
} else if (showUnitsLayer) {
|
|
setShowUnitsLayer(false);
|
|
}
|
|
|
|
if (controlId === "heatmap" || controlId === "timeline") {
|
|
setUseAllYears(true);
|
|
setUseAllMonths(true);
|
|
} else {
|
|
setUseAllYears(false);
|
|
setUseAllMonths(false);
|
|
}
|
|
|
|
setShowEWS(true);
|
|
}
|
|
|
|
const showTimelineLayer = activeControl === "timeline";
|
|
|
|
return (
|
|
<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">
|
|
<CardTitle>Crime Map {getMapTitle()}</CardTitle>
|
|
<MapSelectors
|
|
availableYears={availableYears || []}
|
|
selectedYear={selectedYear}
|
|
setSelectedYear={setSelectedYear}
|
|
selectedMonth={selectedMonth}
|
|
setSelectedMonth={setSelectedMonth}
|
|
selectedCategory={selectedCategory}
|
|
setSelectedCategory={setSelectedCategory}
|
|
categories={categories}
|
|
isYearsLoading={isYearsLoading}
|
|
isCategoryLoading={isCategoryLoading}
|
|
/>
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
{isCrimesLoading ? (
|
|
<div className="flex items-center justify-center h-96">
|
|
<Skeleton className="h-full w-full rounded-md" />
|
|
</div>
|
|
) : crimesError ? (
|
|
<div className="flex flex-col items-center justify-center h-96 gap-4">
|
|
<AlertCircle className="h-10 w-10 text-destructive" />
|
|
<p className="text-center">Failed to load crime data. Please try again later.</p>
|
|
<Button onClick={() => window.location.reload()}>Retry</Button>
|
|
</div>
|
|
) : (
|
|
<div className="mapbox-container overlay-bg relative h-[600px]" ref={mapContainerRef}>
|
|
<div className={cn(
|
|
"transition-all duration-300 ease-in-out",
|
|
!sidebarCollapsed && isFullscreen && "ml-[400px]"
|
|
)}>
|
|
<div className="">
|
|
<MapView mapStyle="mapbox://styles/mapbox/dark-v11" className="h-[600px] w-full rounded-md">
|
|
<Layers
|
|
crimes={filteredCrimes || []}
|
|
units={fetchedUnits || []}
|
|
year={selectedYear.toString()}
|
|
month={selectedMonth.toString()}
|
|
filterCategory={selectedCategory}
|
|
activeControl={activeControl}
|
|
useAllData={useAllYears}
|
|
showEWS={showEWS}
|
|
recentIncidents={recentIncidents || []}
|
|
sourceType={selectedSourceType}
|
|
/>
|
|
|
|
{isFullscreen && (
|
|
<>
|
|
<div className="absolute flex w-full p-2">
|
|
<Tooltips
|
|
activeControl={activeControl}
|
|
onControlChange={handleControlChange}
|
|
selectedSourceType={selectedSourceType}
|
|
setSelectedSourceType={handleSourceTypeChange}
|
|
availableSourceTypes={availableSourceTypes || []}
|
|
selectedYear={selectedYear}
|
|
setSelectedYear={setSelectedYear}
|
|
selectedMonth={selectedMonth}
|
|
setSelectedMonth={setSelectedMonth}
|
|
selectedCategory={selectedCategory}
|
|
setSelectedCategory={setSelectedCategory}
|
|
availableYears={availableYears || []}
|
|
categories={categories}
|
|
crimes={filteredCrimes}
|
|
/>
|
|
</div>
|
|
|
|
<CrimeSidebar
|
|
crimes={filteredCrimes || []}
|
|
defaultCollapsed={sidebarCollapsed}
|
|
selectedCategory={selectedCategory}
|
|
selectedYear={selectedYear}
|
|
selectedMonth={selectedMonth}
|
|
sourceType={selectedSourceType} // Pass the sourceType
|
|
/>
|
|
{isFullscreen && (
|
|
<div className="absolute bottom-20 right-0 z-20 p-2">
|
|
{showClusters && (
|
|
<MapLegend position="bottom-right" />
|
|
)}
|
|
{showUnclustered && !showClusters && (
|
|
<MapLegend position="bottom-right" />
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{isFullscreen && showUnitsLayer && (
|
|
<div className="absolute bottom-20 right-0 z-10 p-2">
|
|
<UnitsLegend
|
|
categories={categories}
|
|
position="bottom-right"
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{isFullscreen && showTimelineLayer && (
|
|
<div className="absolute flex bottom-20 right-0 z-10 p-2">
|
|
<TimelineLegend position="bottom-right" />
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
{isFullscreen && (
|
|
<div className="absolute flex w-full bottom-0">
|
|
<CrimeTimelapse
|
|
startYear={2020}
|
|
endYear={2024}
|
|
autoPlay={false}
|
|
onChange={handleTimelineChange}
|
|
onPlayingChange={handleTimelinePlayingChange}
|
|
/>
|
|
</div>
|
|
)}
|
|
</MapView>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|