MIF_E31221222/sigap-website/app/_components/map/crime-map.tsx

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 { ITooltipsControl } 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<ITooltipsControl>("clusters")
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("clusters");
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: ITooltipsControl) => {
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>
)
}