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

544 lines
22 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 { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useFullscreen } from "@/app/_hooks/use-fullscreen";
import { Overlay } from "./overlay";
import clusterLegend 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 { 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 { useGetUnitsQuery } from "@/app/(pages)/(admin)/dashboard/crime-management/units/_queries/queries";
import { IDistrictFeature } from "@/app/_utils/types/map";
import EWSAlertLayer from "./layers/ews-alert-layer";
import { IIncidentLog } from "@/app/_utils/types/ews";
import {
addMockIncident,
getAllIncidents,
resolveIncident,
} from "@/app/_utils/mock/ews-data";
import { useMap } from "react-map-gl/mapbox";
import PanicButtonDemo from "./controls/panic-button-demo";
import ClusterLegend from "./legends/map-legend";
export default function CrimeMap() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
const [activeControl, setActiveControl] = useState<ITooltipsControl>(
"clusters",
);
const [selectedDistrict, setSelectedDistrict] = useState<
IDistrictFeature | null
>(null);
const [selectedSourceType, setSelectedSourceType] = useState<string>("cbu");
const [selectedYear, setSelectedYear] = useState<number | "all">();
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all");
const [selectedCategory, setSelectedCategory] = useState<string | "all">(
"all",
);
const [ewsIncidents, setEwsIncidents] = useState<IIncidentLog[]>([]);
const [useAllYears, setUseAllYears] = useState<boolean>(false);
const [useAllMonths, setUseAllMonths] = useState<boolean>(false);
const [showAllIncidents, setShowAllIncidents] = useState(false);
const [showUnitsLayer, setShowUnitsLayer] = useState(false);
const [showClusters, setShowClusters] = useState(false);
const [showHeatmap, setShowHeatmap] = useState(false);
const [showTimelineLayer, setShowTimelineLayer] = useState(false);
const [showEWS, setShowEWS] = useState<boolean>(true);
const [showPanicDemo, setShowPanicDemo] = useState(true);
const [displayPanicDemo, setDisplayPanicDemo] = useState(
showEWS && showPanicDemo,
);
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false);
const [yearProgress, setYearProgress] = useState(0);
const [isSearchActive, setIsSearchActive] = useState(false);
const mapContainerRef = useRef<HTMLDivElement>(null);
const { current: mapInstance } = useMap();
const mapboxMap = mapInstance?.getMap() || 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(() => {
const currentYear = new Date().getFullYear();
const defaultYear = selectedSourceType === "cbu" ? 2024 : currentYear;
setSelectedYear(defaultYear);
}, [selectedSourceType]);
useEffect(() => {
if (
activeControl === "heatmap" || activeControl === "timeline" ||
activeControl === "incidents"
) {
setSelectedYear("all");
setUseAllYears(true);
setUseAllMonths(true);
} else if (selectedYear === "all") {
const currentYear = new Date().getFullYear();
setSelectedYear(selectedSourceType === "cbu" ? 2024 : currentYear);
setUseAllYears(false);
setUseAllMonths(false);
}
}, [activeControl, selectedSourceType, selectedYear]);
const crimesBySourceType = useMemo(() => {
if (!crimes) return [];
return crimes.filter((crime) =>
crime.source_type === selectedSourceType
);
}, [crimes, selectedSourceType]);
const filteredByYearAndMonth = useMemo(() => {
if (!crimesBySourceType || crimesBySourceType.length === 0) 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 || filteredByYearAndMonth.length === 0)
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);
}
}
}, [selectedSourceType, activeControl]);
useEffect(() => {
setEwsIncidents(getAllIncidents());
}, []);
useEffect(() => {
const handleSetYear = (e: CustomEvent) => {
if (typeof e.detail === 'number') {
setSelectedYear(e.detail);
}
};
window.addEventListener('set-year', handleSetYear as EventListener);
return () => {
window.removeEventListener('set-year', handleSetYear as EventListener);
};
}, []);
const handleTriggerAlert = useCallback(
(priority: "high" | "medium" | "low") => {
const newIncident = addMockIncident({ priority });
setEwsIncidents(getAllIncidents());
},
[],
);
const handleResolveIncident = useCallback((id: string) => {
resolveIncident(id);
setEwsIncidents(getAllIncidents());
}, []);
const handleResolveAllAlerts = useCallback(() => {
ewsIncidents.forEach((incident) => {
if (incident.status === "active") {
resolveIncident(incident.id);
}
});
setEwsIncidents(getAllIncidents());
}, [ewsIncidents]);
const handleSourceTypeChange = useCallback((sourceType: string) => {
setSelectedSourceType(sourceType);
const currentYear = new Date().getFullYear();
const defaultYear = sourceType === "cbu" ? 2024 : currentYear;
setSelectedYear(defaultYear);
if (sourceType === "cbu") {
setActiveControl("clusters");
setShowClusters(true);
} else {
setActiveControl("clusters");
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(() => {
const currentYear = new Date().getFullYear();
const defaultYear = selectedSourceType === "cbu" ? 2024 : currentYear;
setSelectedYear(defaultYear);
setSelectedMonth("all");
setSelectedCategory("all");
}, [selectedSourceType]);
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") {
setShowAllIncidents(true);
} else {
setShowAllIncidents(false);
}
if (controlId === "search") {
setIsSearchActive((prev) => !prev);
}
if (controlId === "units") {
setShowUnitsLayer(true);
} else {
setShowUnitsLayer(false);
}
if (controlId === "timeline") {
setShowTimelineLayer(true);
} else {
setShowTimelineLayer(false);
}
if (
controlId === "heatmap" || controlId === "timeline" ||
controlId === "incidents"
) {
setUseAllYears(true);
setUseAllMonths(true);
} else {
setUseAllYears(false);
setUseAllMonths(false);
}
setShowEWS(true);
};
useEffect(() => {
console.log(`Current source type: ${selectedSourceType}`);
console.log(`Total crimes before filtering: ${crimes?.length || 0}`);
console.log(
`Total crimes after source type filtering: ${crimesBySourceType.length}`,
);
console.log(
`Total crimes after all filtering: ${filteredCrimes.length}`,
);
}, [crimes, crimesBySourceType, filteredCrimes, selectedSourceType]);
const disableYearMonth = activeControl === "incidents" || activeControl === "heatmap" || activeControl === "timeline"
const activeIncidents = useMemo(() => {
return ewsIncidents.filter((incident) => incident.status === "active");
}, [ewsIncidents])
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 ?? "all"}
setSelectedYear={setSelectedYear}
selectedMonth={selectedMonth}
setSelectedMonth={setSelectedMonth}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
categories={categories}
isYearsLoading={isYearsLoading}
isCategoryLoading={isCategoryLoading}
disableYearMonth={activeControl === "incidents" ||
activeControl === "heatmap" ||
activeControl === "timeline"}
/>
</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() ?? "all"}
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 ?? "all"}
setSelectedYear={setSelectedYear}
selectedMonth={selectedMonth}
setSelectedMonth={setSelectedMonth}
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
availableYears={availableYears || []}
categories={categories}
crimes={filteredCrimes}
disableYearMonth={disableYearMonth}
/>
</div>
{mapboxMap && (
<EWSAlertLayer
map={mapboxMap}
incidents={ewsIncidents}
onIncidentResolved={handleResolveIncident}
/>
)}
{displayPanicDemo && (
<div className="absolute top-0 right-20 z-50 p-2">
<PanicButtonDemo
onTriggerAlert={handleTriggerAlert}
onResolveAllAlerts={handleResolveAllAlerts}
activeIncidents={activeIncidents}
/>
</div>
)}
<CrimeSidebar
crimes={filteredCrimes || []}
recentIncidents={recentIncidents || []}
defaultCollapsed={sidebarCollapsed}
selectedCategory={selectedCategory}
selectedYear={selectedYear ?? "all"}
selectedMonth={selectedMonth}
sourceType={selectedSourceType}
/>
<div className="absolute bottom-20 right-0 z-20 p-2">
{showClusters && (
<ClusterLegend position="bottom-right" />
)}
</div>
{showUnitsLayer && (
<div className="absolute bottom-20 right-0 z-10 p-2">
<UnitsLegend
categories={categories}
position="bottom-right"
/>
</div>
)}
{showTimelineLayer && (
<div className="absolute flex bottom-20 right-0 z-10 p-2">
<TimelineLegend position="bottom-right" />
</div>
)}
</>
)}
<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>
);
}