feat: replace SmoothYearTimeline with CrimeTimelapse component and enhance timeline functionality
This commit is contained in:
parent
e488bad7c1
commit
4623fac52b
|
@ -1,27 +1,31 @@
|
||||||
import { useState, useEffect, useRef } from "react"
|
import { useState, useEffect, useRef, useCallback } from "react"
|
||||||
import { Pause, Play } from "lucide-react"
|
import { Pause, Play } from "lucide-react"
|
||||||
import { Button } from "@/app/_components/ui/button"
|
import { Button } from "@/app/_components/ui/button"
|
||||||
import { cn } from "@/app/_lib/utils"
|
import { cn } from "@/app/_lib/utils"
|
||||||
import { Slider } from "@/app/_components/ui/slider"
|
import { Slider } from "@/app/_components/ui/slider"
|
||||||
import { getMonthName } from "@/app/_utils/common"
|
import { getMonthName } from "@/app/_utils/common"
|
||||||
|
|
||||||
interface SmoothYearTimelineProps {
|
interface CrimeTimelapseProps {
|
||||||
startYear: number
|
startYear: number
|
||||||
endYear: number
|
endYear: number
|
||||||
onChange?: (year: number, month: number, progress: number) => void
|
onChange?: (year: number, month: number, progress: number) => void
|
||||||
|
onPlayingChange?: (isPlaying: boolean) => void
|
||||||
className?: string
|
className?: string
|
||||||
autoPlay?: boolean
|
autoPlay?: boolean
|
||||||
autoPlaySpeed?: number // Time to progress through one month in ms
|
autoPlaySpeed?: number // Time to progress through one month in ms
|
||||||
|
enablePerformanceMode?: boolean // Flag untuk mode performa
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SmoothYearTimeline({
|
export function CrimeTimelapse({
|
||||||
startYear = 2020,
|
startYear = 2020,
|
||||||
endYear = 2024,
|
endYear = 2024,
|
||||||
onChange,
|
onChange,
|
||||||
|
onPlayingChange,
|
||||||
className,
|
className,
|
||||||
autoPlay = true,
|
autoPlay = true,
|
||||||
autoPlaySpeed = 1000, // Speed of month progress
|
autoPlaySpeed = 1000, // Speed of month progress
|
||||||
}: SmoothYearTimelineProps) {
|
enablePerformanceMode = true, // Default aktifkan mode performa tinggi
|
||||||
|
}: CrimeTimelapseProps) {
|
||||||
const [currentYear, setCurrentYear] = useState<number>(startYear)
|
const [currentYear, setCurrentYear] = useState<number>(startYear)
|
||||||
const [currentMonth, setCurrentMonth] = useState<number>(1) // Start at January (1)
|
const [currentMonth, setCurrentMonth] = useState<number>(1) // Start at January (1)
|
||||||
const [progress, setProgress] = useState<number>(0) // Progress within the current month
|
const [progress, setProgress] = useState<number>(0) // Progress within the current month
|
||||||
|
@ -29,18 +33,23 @@ export function SmoothYearTimeline({
|
||||||
const [isDragging, setIsDragging] = useState<boolean>(false)
|
const [isDragging, setIsDragging] = useState<boolean>(false)
|
||||||
const animationRef = useRef<number | null>(null)
|
const animationRef = useRef<number | null>(null)
|
||||||
const lastUpdateTimeRef = useRef<number>(0)
|
const lastUpdateTimeRef = useRef<number>(0)
|
||||||
|
const frameSkipCountRef = useRef<number>(0) // Untuk pelompatan frame
|
||||||
|
|
||||||
// Calculate total months from start to end year
|
// Jumlah frame yang akan dilewati saat performance mode aktif
|
||||||
const totalMonths = ((endYear - startYear) * 12) + 12 // +12 to include all months of end year
|
const frameSkipThreshold = enablePerformanceMode ? 3 : 0
|
||||||
|
|
||||||
const calculateOverallProgress = (): number => {
|
// Hitung total bulan dari awal hingga akhir tahun (memoisasi)
|
||||||
|
const totalMonths = useRef(((endYear - startYear) * 12) + 12) // +12 untuk memasukkan semua bulan tahun akhir
|
||||||
|
|
||||||
|
// Menggunakan useCallback untuk fungsi yang sering dipanggil
|
||||||
|
const calculateOverallProgress = useCallback((): number => {
|
||||||
const yearDiff = currentYear - startYear
|
const yearDiff = currentYear - startYear
|
||||||
const monthProgress = (yearDiff * 12) + (currentMonth - 1)
|
const monthProgress = (yearDiff * 12) + (currentMonth - 1)
|
||||||
return ((monthProgress + progress) / (totalMonths - 1)) * 100
|
return ((monthProgress + progress) / (totalMonths.current - 1)) * 100
|
||||||
}
|
}, [currentYear, currentMonth, progress, startYear, totalMonths])
|
||||||
|
|
||||||
const calculateTimeFromProgress = (overallProgress: number): { year: number; month: number; progress: number } => {
|
const calculateTimeFromProgress = useCallback((overallProgress: number): { year: number; month: number; progress: number } => {
|
||||||
const totalProgress = (overallProgress * (totalMonths - 1)) / 100
|
const totalProgress = (overallProgress * (totalMonths.current - 1)) / 100
|
||||||
const monthsFromStart = Math.floor(totalProgress)
|
const monthsFromStart = Math.floor(totalProgress)
|
||||||
|
|
||||||
const year = startYear + Math.floor(monthsFromStart / 12)
|
const year = startYear + Math.floor(monthsFromStart / 12)
|
||||||
|
@ -52,20 +61,26 @@ export function SmoothYearTimeline({
|
||||||
month: Math.min(month, 12),
|
month: Math.min(month, 12),
|
||||||
progress: monthProgress
|
progress: monthProgress
|
||||||
}
|
}
|
||||||
}
|
}, [startYear, endYear, totalMonths])
|
||||||
|
|
||||||
// Calculate the current position for the active marker
|
// Calculate the current position for the active marker
|
||||||
const calculateMarkerPosition = (): string => {
|
const calculateMarkerPosition = useCallback((): string => {
|
||||||
const overallProgress = calculateOverallProgress()
|
return `${calculateOverallProgress()}%`
|
||||||
return `${overallProgress}%`
|
}, [calculateOverallProgress])
|
||||||
}
|
|
||||||
|
|
||||||
const animate = (timestamp: number) => {
|
// Optimasi animasi dengan throttling berbasis requestAnimationFrame
|
||||||
|
const animate = useCallback((timestamp: number) => {
|
||||||
if (!lastUpdateTimeRef.current) {
|
if (!lastUpdateTimeRef.current) {
|
||||||
lastUpdateTimeRef.current = timestamp
|
lastUpdateTimeRef.current = timestamp
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDragging) {
|
if (!isDragging) {
|
||||||
|
// Performance optimization: skip frames in performance mode
|
||||||
|
frameSkipCountRef.current++
|
||||||
|
|
||||||
|
if (frameSkipCountRef.current > frameSkipThreshold || !enablePerformanceMode) {
|
||||||
|
frameSkipCountRef.current = 0
|
||||||
|
|
||||||
const elapsed = timestamp - lastUpdateTimeRef.current
|
const elapsed = timestamp - lastUpdateTimeRef.current
|
||||||
const progressIncrement = elapsed / autoPlaySpeed
|
const progressIncrement = elapsed / autoPlaySpeed
|
||||||
|
|
||||||
|
@ -92,21 +107,26 @@ export function SmoothYearTimeline({
|
||||||
}
|
}
|
||||||
|
|
||||||
setProgress(newProgress)
|
setProgress(newProgress)
|
||||||
|
|
||||||
|
// Notify parent component only after calculating all changes
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(newYear, newMonth, newProgress)
|
onChange(newYear, newMonth, newProgress)
|
||||||
}
|
}
|
||||||
|
|
||||||
lastUpdateTimeRef.current = timestamp
|
lastUpdateTimeRef.current = timestamp
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
animationRef.current = requestAnimationFrame(animate)
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
}
|
}
|
||||||
}
|
}, [isPlaying, isDragging, progress, currentMonth, currentYear, onChange,
|
||||||
|
autoPlaySpeed, startYear, endYear, enablePerformanceMode, frameSkipThreshold])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
lastUpdateTimeRef.current = 0
|
lastUpdateTimeRef.current = 0
|
||||||
|
frameSkipCountRef.current = 0
|
||||||
animationRef.current = requestAnimationFrame(animate)
|
animationRef.current = requestAnimationFrame(animate)
|
||||||
} else if (animationRef.current) {
|
} else if (animationRef.current) {
|
||||||
cancelAnimationFrame(animationRef.current)
|
cancelAnimationFrame(animationRef.current)
|
||||||
|
@ -117,13 +137,21 @@ export function SmoothYearTimeline({
|
||||||
cancelAnimationFrame(animationRef.current)
|
cancelAnimationFrame(animationRef.current)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isPlaying, currentYear, currentMonth, progress, isDragging])
|
}, [isPlaying, animate])
|
||||||
|
|
||||||
const handlePlayPause = () => {
|
// Memoized handler for play/pause
|
||||||
setIsPlaying(!isPlaying)
|
const handlePlayPause = useCallback(() => {
|
||||||
|
setIsPlaying(prevState => {
|
||||||
|
const newPlayingState = !prevState
|
||||||
|
if (onPlayingChange) {
|
||||||
|
onPlayingChange(newPlayingState)
|
||||||
}
|
}
|
||||||
|
return newPlayingState
|
||||||
|
})
|
||||||
|
}, [onPlayingChange])
|
||||||
|
|
||||||
const handleSliderChange = (value: number[]) => {
|
// Memoized handler for slider change
|
||||||
|
const handleSliderChange = useCallback((value: number[]) => {
|
||||||
const overallProgress = value[0]
|
const overallProgress = value[0]
|
||||||
const { year, month, progress } = calculateTimeFromProgress(overallProgress)
|
const { year, month, progress } = calculateTimeFromProgress(overallProgress)
|
||||||
|
|
||||||
|
@ -134,31 +162,46 @@ export function SmoothYearTimeline({
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
onChange(year, month, progress)
|
onChange(year, month, progress)
|
||||||
}
|
}
|
||||||
}
|
}, [calculateTimeFromProgress, onChange])
|
||||||
|
|
||||||
const handleSliderDragStart = () => {
|
const handleSliderDragStart = useCallback(() => {
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
|
if (onPlayingChange) {
|
||||||
|
onPlayingChange(true) // Treat dragging as a form of "playing" for performance optimization
|
||||||
}
|
}
|
||||||
|
}, [onPlayingChange])
|
||||||
|
|
||||||
const handleSliderDragEnd = () => {
|
const handleSliderDragEnd = useCallback(() => {
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
|
if (onPlayingChange) {
|
||||||
|
onPlayingChange(isPlaying) // Restore to actual playing state
|
||||||
}
|
}
|
||||||
|
}, [isPlaying, onPlayingChange])
|
||||||
|
|
||||||
// Create year markers
|
// Komputasi tahun marker dilakukan sekali saja dan di-cache
|
||||||
const yearMarkers = []
|
const yearMarkers = useRef<number[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
const markers = []
|
||||||
for (let year = startYear; year <= endYear; year++) {
|
for (let year = startYear; year <= endYear; year++) {
|
||||||
yearMarkers.push(year)
|
markers.push(year)
|
||||||
}
|
}
|
||||||
|
yearMarkers.current = markers
|
||||||
|
}, [startYear, endYear])
|
||||||
|
|
||||||
|
// Gunakan React.memo untuk komponen child jika diperlukan
|
||||||
|
// Contoh: const YearMarker = React.memo(({ year, isActive }) => { ... })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("w-full bg-transparent text-emerald-500", className)}>
|
<div className={cn("w-full bg-transparent text-emerald-500", className)}>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Current month/year marker that moves with the slider */}
|
{/* Current month/year marker that moves with the slider */}
|
||||||
<div
|
<div
|
||||||
className="absolute bottom-full mb-2 transform -translate-x-1/2 bg-emerald-500 text-background px-3 py-1 rounded-full text-xs font-bold z-20"
|
className={cn(
|
||||||
|
"absolute bottom-full mb-2 transform -translate-x-1/2 px-3 py-1 rounded-full text-xs font-bold z-20 transition-colors duration-300 bg-emerald-500 text-background",
|
||||||
|
)}
|
||||||
style={{ left: calculateMarkerPosition() }}
|
style={{ left: calculateMarkerPosition() }}
|
||||||
>
|
>
|
||||||
{getMonthName(currentMonth)} {currentYear}
|
{isPlaying}{getMonthName(currentMonth)} {currentYear}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Wrap button and slider in their container */}
|
{/* Wrap button and slider in their container */}
|
||||||
|
@ -168,7 +211,9 @@ export function SmoothYearTimeline({
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={handlePlayPause}
|
onClick={handlePlayPause}
|
||||||
className="text-background bg-emerald-500 rounded-full hover:text-background hover:bg-emerald-500/50 h-10 w-10 z-10"
|
className={cn(
|
||||||
|
"text-background rounded-full hover:text-background h-10 w-10 z-10 transition-colors duration-300 bg-emerald-500 hover:bg-emerald-500/50",
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
|
{isPlaying ? <Pause className="h-5 w-5" /> : <Play className="h-5 w-5" />}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -189,12 +234,12 @@ export function SmoothYearTimeline({
|
||||||
{/* Year markers */}
|
{/* Year markers */}
|
||||||
<div className="flex items-center relative h-10">
|
<div className="flex items-center relative h-10">
|
||||||
<div className="absolute inset-0 h-full flex">
|
<div className="absolute inset-0 h-full flex">
|
||||||
{yearMarkers.map((year, index) => (
|
{yearMarkers.current.map((year, index) => (
|
||||||
<div
|
<div
|
||||||
key={year}
|
key={year}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 h-full flex items-center justify-center relative",
|
"flex-1 h-full flex items-center justify-center relative",
|
||||||
index < yearMarkers.length - 1 && ""
|
index < yearMarkers.current.length - 1 && ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
|
@ -20,7 +20,7 @@ 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"
|
import { $Enums, crime_categories, crime_incidents, crimes, demographics, districts, geographics, locations } from "@prisma/client"
|
||||||
import { SmoothYearTimeline } from "./controls/year-timeline"
|
import { CrimeTimelapse } from "./controls/crime-timelapse"
|
||||||
|
|
||||||
// Updated CrimeIncident type to match the structure in crime_incidents
|
// Updated CrimeIncident type to match the structure in crime_incidents
|
||||||
interface CrimeIncident {
|
interface CrimeIncident {
|
||||||
|
@ -46,6 +46,7 @@ export default function CrimeMap() {
|
||||||
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
const [selectedMonth, setSelectedMonth] = useState<number | "all">("all")
|
||||||
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
|
const [activeControl, setActiveControl] = useState<ITopTooltipsMapId>("incidents")
|
||||||
const [yearProgress, setYearProgress] = useState(0)
|
const [yearProgress, setYearProgress] = useState(0)
|
||||||
|
const [isTimelapsePlaying, setisTimelapsePlaying] = useState(false)
|
||||||
|
|
||||||
const mapContainerRef = useRef<HTMLDivElement>(null)
|
const mapContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
@ -183,6 +184,17 @@ export default function CrimeMap() {
|
||||||
setYearProgress(progress)
|
setYearProgress(progress)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
// Handle timeline playing state change
|
||||||
|
const handleTimelinePlayingChange = useCallback((playing: boolean) => {
|
||||||
|
setisTimelapsePlaying(playing)
|
||||||
|
|
||||||
|
// When timelapse starts, close any open popups/details
|
||||||
|
if (playing) {
|
||||||
|
setSelectedIncident(null)
|
||||||
|
setSelectedDistrict(null)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
// Reset filters
|
// Reset filters
|
||||||
const resetFilters = useCallback(() => {
|
const resetFilters = useCallback(() => {
|
||||||
setSelectedYear(2024)
|
setSelectedYear(2024)
|
||||||
|
@ -248,6 +260,7 @@ export default function CrimeMap() {
|
||||||
year={selectedYear.toString()}
|
year={selectedYear.toString()}
|
||||||
month={selectedMonth.toString()}
|
month={selectedMonth.toString()}
|
||||||
filterCategory={selectedCategory}
|
filterCategory={selectedCategory}
|
||||||
|
isTimelapsePlaying={isTimelapsePlaying}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Pass onClick if you want to handle districts externally */}
|
{/* Pass onClick if you want to handle districts externally */}
|
||||||
|
@ -264,7 +277,6 @@ export default function CrimeMap() {
|
||||||
{/* Popup for selected incident */}
|
{/* Popup for selected incident */}
|
||||||
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
{selectedIncident && selectedIncident.latitude && selectedIncident.longitude && (
|
||||||
<>
|
<>
|
||||||
{/* {console.log("About to render CrimePopup with:", selectedIncident)} */}
|
|
||||||
<CrimePopup
|
<CrimePopup
|
||||||
longitude={selectedIncident.longitude}
|
longitude={selectedIncident.longitude}
|
||||||
latitude={selectedIncident.latitude}
|
latitude={selectedIncident.latitude}
|
||||||
|
@ -310,12 +322,12 @@ export default function CrimeMap() {
|
||||||
|
|
||||||
{isFullscreen && (
|
{isFullscreen && (
|
||||||
<div className="absolute flex w-full bottom-0">
|
<div className="absolute flex w-full bottom-0">
|
||||||
<SmoothYearTimeline
|
<CrimeTimelapse
|
||||||
startYear={2020}
|
startYear={2020}
|
||||||
endYear={2024}
|
endYear={2024}
|
||||||
autoPlay={false}
|
autoPlay={false}
|
||||||
autoPlaySpeed={1000}
|
|
||||||
onChange={handleTimelineChange}
|
onChange={handleTimelineChange}
|
||||||
|
onPlayingChange={handleTimelinePlayingChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -54,6 +54,7 @@ export interface DistrictLayerProps {
|
||||||
filterCategory: string | "all"
|
filterCategory: string | "all"
|
||||||
crimes: ICrimes[]
|
crimes: ICrimes[]
|
||||||
tilesetId?: string
|
tilesetId?: string
|
||||||
|
isTimelapsePlaying?: boolean // Add new prop to track timeline playing state
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DistrictLayer({
|
export default function DistrictLayer({
|
||||||
|
@ -64,6 +65,7 @@ export default function DistrictLayer({
|
||||||
filterCategory = "all",
|
filterCategory = "all",
|
||||||
crimes = [],
|
crimes = [],
|
||||||
tilesetId = MAPBOX_TILESET_ID,
|
tilesetId = MAPBOX_TILESET_ID,
|
||||||
|
isTimelapsePlaying = false, // Default to false
|
||||||
}: DistrictLayerProps) {
|
}: DistrictLayerProps) {
|
||||||
const { current: map } = useMap()
|
const { current: map } = useMap()
|
||||||
|
|
||||||
|
@ -883,6 +885,37 @@ export default function DistrictLayer({
|
||||||
if (!map || !map.getMap().getSource("crime-incidents")) return
|
if (!map || !map.getMap().getSource("crime-incidents")) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// If timeline is playing, hide all point/cluster layers to improve performance
|
||||||
|
if (isTimelapsePlaying) {
|
||||||
|
// Hide all incident points during timelapse
|
||||||
|
if (map.getMap().getLayer("clusters")) {
|
||||||
|
map.getMap().setLayoutProperty("clusters", "visibility", "none")
|
||||||
|
}
|
||||||
|
if (map.getMap().getLayer("unclustered-point")) {
|
||||||
|
map.getMap().setLayoutProperty("unclustered-point", "visibility", "none")
|
||||||
|
}
|
||||||
|
if (map.getMap().getLayer("cluster-count")) {
|
||||||
|
map.getMap().setLayoutProperty("cluster-count", "visibility", "none")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the source with empty data to free up resources
|
||||||
|
; (map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: [],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// When not playing, show all layers again
|
||||||
|
if (map.getMap().getLayer("clusters")) {
|
||||||
|
map.getMap().setLayoutProperty("clusters", "visibility", "visible")
|
||||||
|
}
|
||||||
|
if (map.getMap().getLayer("unclustered-point")) {
|
||||||
|
map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible")
|
||||||
|
}
|
||||||
|
if (map.getMap().getLayer("cluster-count")) {
|
||||||
|
map.getMap().setLayoutProperty("cluster-count", "visibility", "visible")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore detailed incidents when timelapse stops
|
||||||
const allIncidents = crimes.flatMap((crime) => {
|
const allIncidents = crimes.flatMap((crime) => {
|
||||||
if (!crime.crime_incidents) return []
|
if (!crime.crime_incidents) return []
|
||||||
|
|
||||||
|
@ -919,14 +952,76 @@ export default function DistrictLayer({
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Update the source with detailed data
|
||||||
; (map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({
|
; (map.getMap().getSource("crime-incidents") as mapboxgl.GeoJSONSource).setData({
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features: allIncidents as GeoJSON.Feature[],
|
features: allIncidents as GeoJSON.Feature[],
|
||||||
})
|
})
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error updating incident data:", error)
|
console.error("Error updating incident data:", error)
|
||||||
}
|
}
|
||||||
}, [map, crimes, filterCategory])
|
}, [map, crimes, filterCategory, isTimelapsePlaying])
|
||||||
|
|
||||||
|
// Add a new effect to update district colors even during timelapse
|
||||||
|
useEffect(() => {
|
||||||
|
if (!map || !map.getMap().getSource("districts")) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (map.getMap().getLayer("district-fill")) {
|
||||||
|
const colorEntries = focusedDistrictId
|
||||||
|
? [
|
||||||
|
[
|
||||||
|
focusedDistrictId,
|
||||||
|
crimeDataByDistrict[focusedDistrictId]?.level === "low"
|
||||||
|
? CRIME_RATE_COLORS.low
|
||||||
|
: crimeDataByDistrict[focusedDistrictId]?.level === "medium"
|
||||||
|
? CRIME_RATE_COLORS.medium
|
||||||
|
: crimeDataByDistrict[focusedDistrictId]?.level === "high"
|
||||||
|
? CRIME_RATE_COLORS.high
|
||||||
|
: CRIME_RATE_COLORS.default,
|
||||||
|
],
|
||||||
|
"rgba(0,0,0,0.05)",
|
||||||
|
]
|
||||||
|
: Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => {
|
||||||
|
if (!data || !data.level) {
|
||||||
|
return [districtId, CRIME_RATE_COLORS.default]
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
districtId,
|
||||||
|
data.level === "low"
|
||||||
|
? CRIME_RATE_COLORS.low
|
||||||
|
: data.level === "medium"
|
||||||
|
? CRIME_RATE_COLORS.medium
|
||||||
|
: data.level === "high"
|
||||||
|
? CRIME_RATE_COLORS.high
|
||||||
|
: CRIME_RATE_COLORS.default,
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const fillColorExpression = [
|
||||||
|
"case",
|
||||||
|
["has", "kode_kec"],
|
||||||
|
[
|
||||||
|
"match",
|
||||||
|
["get", "kode_kec"],
|
||||||
|
...colorEntries,
|
||||||
|
focusedDistrictId ? "rgba(0,0,0,0.05)" : CRIME_RATE_COLORS.default,
|
||||||
|
],
|
||||||
|
CRIME_RATE_COLORS.default,
|
||||||
|
] as any
|
||||||
|
|
||||||
|
// Make district fills more prominent during timelapse
|
||||||
|
map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression)
|
||||||
|
map.getMap().setPaintProperty("district-fill", "fill-opacity",
|
||||||
|
isTimelapsePlaying ? 0.85 : 0.6) // Increase opacity during timelapse
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating district fill:", error)
|
||||||
|
}
|
||||||
|
}, [map, crimeDataByDistrict, focusedDistrictId, isTimelapsePlaying])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedDistrictRef.current) {
|
if (selectedDistrictRef.current) {
|
||||||
|
@ -1122,3 +1217,4 @@ export default function DistrictLayer({
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue