diff --git a/sigap-website/app/_components/map/controls/top/additional-tooltips.tsx b/sigap-website/app/_components/map/controls/top/additional-tooltips.tsx
index fdabce4..db8841a 100644
--- a/sigap-website/app/_components/map/controls/top/additional-tooltips.tsx
+++ b/sigap-website/app/_components/map/controls/top/additional-tooltips.tsx
@@ -7,7 +7,7 @@ import { ChevronDown, Siren } from "lucide-react"
import { IconMessage } from "@tabler/icons-react"
import { useEffect, useRef, useState } from "react"
-import type { ITooltips } from "./tooltips"
+import type { ITooltipsControl } from "./tooltips"
import MonthSelector from "../month-selector"
import YearSelector from "../year-selector"
import CategorySelector from "../category-selector"
@@ -15,13 +15,13 @@ import SourceTypeSelector from "../source-type-selector"
// Define the additional tools and features
const additionalTooltips = [
- { id: "reports" as ITooltips, icon: , label: "Police Report" },
- { id: "recents" as ITooltips, icon: , label: "Recent incidents" },
+ { id: "reports" as ITooltipsControl, icon: , label: "Police Report" },
+ { id: "recents" as ITooltipsControl, icon: , label: "Recent incidents" },
]
interface AdditionalTooltipsProps {
activeControl?: string
- onControlChange?: (controlId: ITooltips) => void
+ onControlChange?: (controlId: ITooltipsControl) => void
selectedYear: number
setSelectedYear: (year: number) => void
selectedMonth: number | "all"
@@ -68,7 +68,7 @@ export default function AdditionalTooltips({
setIsClient(true)
}, [])
- const isControlDisabled = (controlId: ITooltips) => {
+ const isControlDisabled = (controlId: ITooltipsControl) => {
// When source type is CBU, disable all controls except for layers
return selectedSourceType === "cbu" && controlId !== "layers"
}
diff --git a/sigap-website/app/_components/map/controls/top/crime-tooltips.tsx b/sigap-website/app/_components/map/controls/top/crime-tooltips.tsx
index e5fcfc2..c239f8f 100644
--- a/sigap-website/app/_components/map/controls/top/crime-tooltips.tsx
+++ b/sigap-website/app/_components/map/controls/top/crime-tooltips.tsx
@@ -3,27 +3,27 @@
import { Button } from "@/app/_components/ui/button"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/app/_components/ui/tooltip"
import { AlertTriangle, Building, Car, Thermometer, History } from "lucide-react"
-import type { ITooltips } from "./tooltips"
+import type { ITooltipsControl } from "./tooltips"
import { IconChartBubble, IconClock } from "@tabler/icons-react"
// Define the primary crime data controls
const crimeTooltips = [
- // { id: "incidents" as ITooltips, icon: , label: "All Incidents" },
- { id: "heatmap" as ITooltips, icon: , label: "Density Heatmap" },
- { id: "units" as ITooltips, icon: , label: "Police Units" },
- { id: "clusters" as ITooltips, icon: , label: "Clustered Incidents" },
- { id: "patrol" as ITooltips, icon: , label: "Patrol Areas" },
- { id: "timeline" as ITooltips, icon: , label: "Time Analysis" },
+ // { id: "incidents" as ITooltipsControl, icon: , label: "All Incidents" },
+ { id: "heatmap" as ITooltipsControl, icon: , label: "Density Heatmap" },
+ { id: "units" as ITooltipsControl, icon: , label: "Police Units" },
+ { id: "clusters" as ITooltipsControl, icon: , label: "Clustered Incidents" },
+ { id: "patrol" as ITooltipsControl, icon: , label: "Patrol Areas" },
+ { id: "timeline" as ITooltipsControl, icon: , label: "Time Analysis" },
]
interface CrimeTooltipsProps {
activeControl?: string
- onControlChange?: (controlId: ITooltips) => void
+ onControlChange?: (controlId: ITooltipsControl) => void
sourceType?: string
}
export default function CrimeTooltips({ activeControl, onControlChange, sourceType = "cbt" }: CrimeTooltipsProps) {
- const handleControlClick = (controlId: ITooltips) => {
+ const handleControlClick = (controlId: ITooltipsControl) => {
// If control is disabled, don't do anything
if (isDisabled(controlId)) {
return
@@ -37,7 +37,7 @@ export default function CrimeTooltips({ activeControl, onControlChange, sourceTy
}
// Determine which controls should be disabled based on source type
- const isDisabled = (controlId: ITooltips) => {
+ const isDisabled = (controlId: ITooltipsControl) => {
return sourceType === "cbu" && controlId !== "clusters"
}
diff --git a/sigap-website/app/_components/map/controls/top/search-control.tsx b/sigap-website/app/_components/map/controls/top/search-control.tsx
index d4354b2..e4b250b 100644
--- a/sigap-website/app/_components/map/controls/top/search-control.tsx
+++ b/sigap-website/app/_components/map/controls/top/search-control.tsx
@@ -20,7 +20,7 @@ import { AnimatePresence, motion } from "framer-motion"
import ActionSearchBar from "@/app/_components/action-search-bar"
import { Card } from "@/app/_components/ui/card"
import { format } from "date-fns"
-import type { ITooltips } from "./tooltips"
+import type { ITooltipsControl } from "./tooltips"
// Define types based on the crime data structure
interface ICrimeIncident {
@@ -95,7 +95,7 @@ const ACTIONS = [
]
interface SearchTooltipProps {
- onControlChange?: (controlId: ITooltips) => void
+ onControlChange?: (controlId: ITooltipsControl) => void
activeControl?: string
crimes?: ICrime[]
sourceType?: string
@@ -336,7 +336,7 @@ export default function SearchTooltip({
setShowSearch(!showSearch)
if (!showSearch && onControlChange) {
- onControlChange("search" as ITooltips)
+ onControlChange("search" as ITooltipsControl)
setSelectedSearchType(null)
setSearchValue("")
setSuggestions([])
diff --git a/sigap-website/app/_components/map/controls/top/tooltips.tsx b/sigap-website/app/_components/map/controls/top/tooltips.tsx
index 338a710..ce0af33 100644
--- a/sigap-website/app/_components/map/controls/top/tooltips.tsx
+++ b/sigap-website/app/_components/map/controls/top/tooltips.tsx
@@ -8,7 +8,7 @@ import SearchTooltip from "./search-control"
import type { ReactNode } from "react"
// Define the possible control IDs for the crime map
-export type ITooltips =
+export type ITooltipsControl =
// Crime data views
| "incidents"
| "historical"
@@ -30,14 +30,14 @@ export type ITooltips =
// Map tools type definition
export interface IMapTools {
- id: ITooltips
+ id: ITooltipsControl
label: string
icon: ReactNode
description?: string
}
interface TooltipProps {
- onControlChange?: (controlId: ITooltips) => void
+ onControlChange?: (controlId: ITooltipsControl) => void
activeControl?: string
selectedSourceType: string
setSelectedSourceType: (sourceType: string) => void
diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx
index fcad740..c4e737d 100644
--- a/sigap-website/app/_components/map/crime-map.tsx
+++ b/sigap-website/app/_components/map/crime-map.tsx
@@ -18,7 +18,7 @@ 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 { ITooltipsControl } from "./controls/top/tooltips"
import CrimeSidebar from "./controls/left/sidebar/map-sidebar"
import Tooltips from "./controls/top/tooltips"
import Layers from "./layers/layers"
@@ -29,7 +29,7 @@ export default function CrimeMap() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(true)
const [selectedDistrict, setSelectedDistrict] = useState(null)
const [showLegend, setShowLegend] = useState(true)
- const [activeControl, setActiveControl] = useState("clusters")
+ const [activeControl, setActiveControl] = useState("clusters")
const [selectedSourceType, setSelectedSourceType] = useState("cbu")
const [selectedYear, setSelectedYear] = useState(2024)
const [selectedMonth, setSelectedMonth] = useState("all")
@@ -190,7 +190,7 @@ export default function CrimeMap() {
return title;
}
- const handleControlChange = (controlId: ITooltips) => {
+ const handleControlChange = (controlId: ITooltipsControl) => {
if (selectedSourceType === "cbu" &&
!["clusters", "reports", "layers", "search", "alerts"].includes(controlId as string)) {
return;
diff --git a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx
index d4c5ec5..8acd156 100644
--- a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx
+++ b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx
@@ -1,6 +1,6 @@
"use client"
-import { getCrimeRateColor } from "@/app/_utils/map"
+import { getCrimeRateColor, getFillOpacity } from "@/app/_utils/map"
import type { IExtrusionLayerProps } from "@/app/_utils/types/map"
import { useEffect, useRef } from "react"
@@ -17,231 +17,232 @@ export default function DistrictExtrusionLayer({
const extrusionCreatedRef = useRef(false)
const lastFocusedDistrictRef = useRef(null)
- // Handle extrusion layer creation and updates
- useEffect(() => {
- if (!map || !visible) return
+ // Helper to (re)create the extrusion layer
+ const createExtrusionLayer = () => {
+ if (!map) return
- console.log("DistrictExtrusionLayer effect running, focusedDistrictId:", focusedDistrictId)
+ const fillOpacity = getFillOpacity('units', true)
- const onStyleLoad = () => {
- if (!map) return
+ // Remove existing layer if exists
+ if (map.getLayer("district-extrusion")) {
+ map.removeLayer("district-extrusion")
+ extrusionCreatedRef.current = false
+ }
- try {
- const layers = map.getStyle().layers
- let firstSymbolId: string | undefined
- for (const layer of layers) {
- if (layer.type === "symbol") {
- firstSymbolId = layer.id
- break
- }
+ // Make sure the districts source exists
+ if (!map.getSource("districts")) {
+ if (!tilesetId) {
+ console.error("No tileset ID provided for districts source")
+ return
}
-
- // Remove existing layer if it exists to avoid conflicts
- if (map.getLayer("district-extrusion")) {
- map.removeLayer("district-extrusion")
- extrusionCreatedRef.current = false
- }
-
- // Make sure the districts source exists
- if (!map.getSource("districts")) {
- if (!tilesetId) {
- console.error("No tileset ID provided for districts source")
- return
- }
-
map.addSource("districts", {
type: "vector",
url: `mapbox://${tilesetId}`,
})
}
- // Create the extrusion layer
- map.addLayer(
- {
- id: "district-extrusion",
- type: "fill-extrusion",
- source: "districts",
- "source-layer": "Districts",
- paint: {
- "fill-extrusion-color": [
- "case",
- ["has", "kode_kec"],
- [
- "match",
- ["get", "kode_kec"],
- focusedDistrictId || "",
- getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
- "transparent",
- ],
- "transparent",
- ],
- "fill-extrusion-height": [
- "case",
- ["has", "kode_kec"],
- ["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0], // Start at 0 for animation
- 0,
- ],
- "fill-extrusion-base": 0,
- "fill-extrusion-opacity": 0.8,
- },
- filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
- },
+ // Find first symbol layer for correct layer order
+ const layers = map.getStyle().layers
+ let firstSymbolId: string | undefined
+ for (const layer of layers) {
+ if (layer.type === "symbol") {
+ firstSymbolId = layer.id
+ break
+ }
+ }
+
+
+ // Create the extrusion layer
+ map.addLayer(
+ {
+ id: "district-extrusion",
+ type: "fill-extrusion",
+ source: "districts",
+ "source-layer": "Districts",
+ paint: {
+ "fill-extrusion-color": [
+ "case",
+ ["has", "kode_kec"],
+ [
+ "match",
+ ["get", "kode_kec"],
+ focusedDistrictId || "",
+ getCrimeRateColor(crimeDataByDistrict[focusedDistrictId || ""]?.level),
+ "transparent",
+ ],
+ "transparent",
+ ],
+ "fill-extrusion-height": [
+ "case",
+ ["has", "kode_kec"],
+ ["match", ["get", "kode_kec"], focusedDistrictId || "", 0, 0], // Start at 0 for animation
+ 0,
+ ],
+ "fill-extrusion-base": 0,
+ "fill-extrusion-opacity": fillOpacity,
+ },
+ filter: ["==", ["get", "kode_kec"], focusedDistrictId || ""],
+ },
firstSymbolId,
)
-
- extrusionCreatedRef.current = true
- console.log("District extrusion layer created")
-
- // If a district is focused, start the animation
- if (focusedDistrictId) {
- console.log("Starting animation for district:", focusedDistrictId)
- lastFocusedDistrictRef.current = focusedDistrictId
- animateExtrusion()
- }
- } catch (error) {
- console.error("Error adding district extrusion layer:", error)
- }
+ extrusionCreatedRef.current = true
}
- if (map.isStyleLoaded()) {
- onStyleLoad()
- } else {
- map.once("style.load", onStyleLoad)
- }
-
- return () => {
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current)
- animationRef.current = null
- }
- }
- }, [map, visible, tilesetId, focusedDistrictId, crimeDataByDistrict])
-
- // Update filter and color when focused district changes
+ // Handle extrusion layer creation and updates
useEffect(() => {
- if (!map || !map.getLayer("district-extrusion")) return
-
- console.log("Updating district extrusion for district:", focusedDistrictId)
-
- // Skip unnecessary updates if nothing has changed
- if (lastFocusedDistrictRef.current === focusedDistrictId) return
-
- // If we're unfocusing a district
- if (!focusedDistrictId) {
- // Stop rotation when unfocusing
- if (rotationAnimationRef.current) {
- cancelAnimationFrame(rotationAnimationRef.current)
- rotationAnimationRef.current = null
- }
- bearingRef.current = 0
-
- // Animate height down
- const animateHeightDown = () => {
- if (!map || !map.getLayer("district-extrusion")) return
-
- const currentHeight = 800
- const duration = 500
- const startTime = performance.now()
-
- const animate = (time: number) => {
- const elapsed = time - startTime
- const progress = Math.min(elapsed / duration, 1)
- const easedProgress = progress * (2 - progress) // easeOutQuad
- const height = 800 - 800 * easedProgress
+ if (!map || !visible) return
+ const onStyleLoad = () => {
+ if (!map) return
try {
- map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
- "case",
- ["has", "kode_kec"],
- ["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0],
- 0,
+ createExtrusionLayer()
+ // Start animation if focusedDistrictId ada
+ if (focusedDistrictId) {
+ lastFocusedDistrictRef.current = focusedDistrictId
+ // setTimeout(() => {
+ if (map.getLayer("district-extrusion")) {
+ animateExtrusion()
+ }
+ // }, 50)
+ }
+ } catch (error) {
+ console.error("Error adding district extrusion layer:", error)
+ }
+ }
+
+ if (map.isStyleLoaded()) {
+ onStyleLoad()
+ } else {
+ map.once("style.load", onStyleLoad)
+ }
+
+ return () => {
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current)
+ animationRef.current = null
+ }
+ }
+ // Tambahkan crimeDataByDistrict ke deps agar update color jika data berubah
+ }, [map, visible, tilesetId, focusedDistrictId, crimeDataByDistrict])
+
+ // Update filter dan color ketika focusedDistrictId berubah
+ useEffect(() => {
+ if (!map) return
+
+ // Jika layer belum ada, buat dulu
+ if (!map.getLayer("district-extrusion")) {
+ createExtrusionLayer()
+ }
+
+ // Tunggu layer benar-benar ada
+ if (!map.getLayer("district-extrusion")) return
+
+ // Skip unnecessary updates if nothing has changed
+ if (lastFocusedDistrictRef.current === focusedDistrictId) return
+
+ // Jika unfocus
+ if (!focusedDistrictId) {
+ if (rotationAnimationRef.current) {
+ cancelAnimationFrame(rotationAnimationRef.current)
+ rotationAnimationRef.current = null
+ }
+ bearingRef.current = 0
+
+ // Animate height down
+ const animateHeightDown = () => {
+ if (!map || !map.getLayer("district-extrusion")) return
+
+ const currentHeight = 800
+ const duration = 500
+ const startTime = performance.now()
+
+ const animate = (time: number) => {
+ const elapsed = time - startTime
+ const progress = Math.min(elapsed / duration, 1)
+ const easedProgress = progress * (2 - progress)
+ const height = 800 - 800 * easedProgress
+
+ try {
+ map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
+ "case",
+ ["has", "kode_kec"],
+ ["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0],
+ 0,
+ ])
+
+ if (progress < 1) {
+ animationRef.current = requestAnimationFrame(animate)
+ } else {
+ map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
+ "case",
+ ["has", "kode_kec"],
+ ["match", ["get", "kode_kec"], "", 0, 0],
+ 0,
+ ])
+ map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""])
+ lastFocusedDistrictRef.current = null
+ map.setBearing(0)
+ }
+ } catch (error) {
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current)
+ animationRef.current = null
+ }
+ }
+ }
+
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current)
+ }
+ animationRef.current = requestAnimationFrame(animate)
+ }
+
+ animateHeightDown()
+ return
+ }
+
+ try {
+ // Update filter dan color
+ map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId])
+ map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
+ "case",
+ ["has", "kode_kec"],
+ [
+ "match",
+ ["get", "kode_kec"],
+ focusedDistrictId,
+ getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
+ "transparent",
+ ],
+ "transparent",
+ ])
+ map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
+ "case",
+ ["has", "kode_kec"],
+ ["match", ["get", "kode_kec"], focusedDistrictId, 0, 0],
+ 0,
])
- if (progress < 1) {
- animationRef.current = requestAnimationFrame(animate)
- } else {
- // Reset when animation completes
- map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
- "case",
- ["has", "kode_kec"],
- ["match", ["get", "kode_kec"], "", 0, 0],
- 0,
- ])
- map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], ""])
+ lastFocusedDistrictRef.current = focusedDistrictId
- lastFocusedDistrictRef.current = null
+ if (rotationAnimationRef.current) {
+ cancelAnimationFrame(rotationAnimationRef.current)
+ rotationAnimationRef.current = null
+ }
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current)
+ animationRef.current = null
+ }
- // Ensure bearing is reset
- map.setBearing(0)
- }
- } catch (error) {
- console.error("Error animating extrusion down:", error)
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current)
- animationRef.current = null
- }
- }
+ setTimeout(() => {
+ if (map.getLayer("district-extrusion")) {
+ animateExtrusion()
+ }
+ }, 50)
+ } catch (error) {
+ console.error("Error updating district extrusion:", error)
}
-
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current)
- }
- animationRef.current = requestAnimationFrame(animate)
- }
-
- animateHeightDown()
- return
- }
-
- try {
- // Update filter for the new district
- map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId])
-
- // Update the extrusion color
- map.setPaintProperty("district-extrusion", "fill-extrusion-color", [
- "case",
- ["has", "kode_kec"],
- [
- "match",
- ["get", "kode_kec"],
- focusedDistrictId,
- getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
- "transparent",
- ],
- "transparent",
- ])
-
- // Reset height for animation
- map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
- "case",
- ["has", "kode_kec"],
- ["match", ["get", "kode_kec"], focusedDistrictId, 0, 0],
- 0,
- ])
-
- // Store current focused district
- lastFocusedDistrictRef.current = focusedDistrictId
-
- // Stop any existing animations and restart
- if (rotationAnimationRef.current) {
- cancelAnimationFrame(rotationAnimationRef.current)
- rotationAnimationRef.current = null
- }
-
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current)
- animationRef.current = null
- }
-
- // Start animation with small delay to ensure smooth transition
- setTimeout(() => {
- console.log("Starting animation after district update")
- animateExtrusion()
- }, 100)
- } catch (error) {
- console.error("Error updating district extrusion:", error)
- }
- }, [map, focusedDistrictId, crimeDataByDistrict])
+ }, [map, focusedDistrictId, crimeDataByDistrict])
// Cleanup on unmount
useEffect(() => {
@@ -259,95 +260,84 @@ export default function DistrictExtrusionLayer({
// Animate extrusion height
const animateExtrusion = () => {
- if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) {
- console.log("Cannot animate extrusion: missing map, layer, or focusedDistrictId")
- return
- }
+ if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) {
+ return
+ }
- console.log("Animating extrusion for district:", focusedDistrictId)
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current)
+ animationRef.current = null
+ }
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current)
- animationRef.current = null
- }
+ const startHeight = 0
+ const targetHeight = 800
+ const duration = 700
+ const startTime = performance.now()
- const startHeight = 0
- const targetHeight = 800
- const duration = 700
- const startTime = performance.now()
+ const animate = (currentTime: number) => {
+ const elapsed = currentTime - startTime
+ const progress = Math.min(elapsed / duration, 1)
+ const easedProgress = progress * (2 - progress)
+ const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
- const animate = (currentTime: number) => {
- const elapsed = currentTime - startTime
- const progress = Math.min(elapsed / duration, 1)
- const easedProgress = progress * (2 - progress) // easeOutQuad
- const currentHeight = startHeight + (targetHeight - startHeight) * easedProgress
+ try {
+ map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
+ "case",
+ ["has", "kode_kec"],
+ ["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
+ 0,
+ ])
- try {
- map.setPaintProperty("district-extrusion", "fill-extrusion-height", [
- "case",
- ["has", "kode_kec"],
- ["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0],
- 0,
- ])
-
- if (progress < 1) {
- animationRef.current = requestAnimationFrame(animate)
- } else {
- console.log("Extrusion animation complete, starting rotation")
- // Start rotation after extrusion completes
- startRotation()
- }
- } catch (error) {
- console.error("Error animating extrusion:", error)
- if (animationRef.current) {
- cancelAnimationFrame(animationRef.current)
- animationRef.current = null
+ if (progress < 1) {
+ animationRef.current = requestAnimationFrame(animate)
+ } else {
+ startRotation()
+ }
+ } catch (error) {
+ if (animationRef.current) {
+ cancelAnimationFrame(animationRef.current)
+ animationRef.current = null
+ }
}
}
- }
- animationRef.current = requestAnimationFrame(animate)
- }
+ animationRef.current = requestAnimationFrame(animate)
+ }
// Start rotation animation
const startRotation = () => {
if (!map || !focusedDistrictId) return
- const rotationSpeed = 0.05 // degrees per frame
- bearingRef.current = 0 // Reset bearing at start
+ const rotationSpeed = 0.05 // degrees per frame
+ bearingRef.current = 0
- const animate = () => {
- if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) {
- if (rotationAnimationRef.current) {
- cancelAnimationFrame(rotationAnimationRef.current)
- rotationAnimationRef.current = null
- }
- return
- }
+ const animate = () => {
+ if (!map || !focusedDistrictId || focusedDistrictId !== lastFocusedDistrictRef.current) {
+ if (rotationAnimationRef.current) {
+ cancelAnimationFrame(rotationAnimationRef.current)
+ rotationAnimationRef.current = null
+ }
+ return
+ }
- try {
- // Update bearing with smooth increment
- bearingRef.current = (bearingRef.current + rotationSpeed) % 360
- map.setBearing(bearingRef.current)
-
- // Continue the animation
- rotationAnimationRef.current = requestAnimationFrame(animate)
- } catch (error) {
- console.error("Error during rotation animation:", error)
- if (rotationAnimationRef.current) {
- cancelAnimationFrame(rotationAnimationRef.current)
- rotationAnimationRef.current = null
+ try {
+ bearingRef.current = (bearingRef.current + rotationSpeed) % 360
+ map.setBearing(bearingRef.current)
+ rotationAnimationRef.current = requestAnimationFrame(animate)
+ } catch (error) {
+ if (rotationAnimationRef.current) {
+ cancelAnimationFrame(rotationAnimationRef.current)
+ rotationAnimationRef.current = null
+ }
}
}
- }
- // Start the animation loop
- if (rotationAnimationRef.current) {
- cancelAnimationFrame(rotationAnimationRef.current)
- rotationAnimationRef.current = null
- }
- rotationAnimationRef.current = requestAnimationFrame(animate)
- }
+ if (rotationAnimationRef.current) {
+ cancelAnimationFrame(rotationAnimationRef.current)
+ rotationAnimationRef.current = null
+ }
+ rotationAnimationRef.current = requestAnimationFrame(animate)
+ }
return null
}
diff --git a/sigap-website/app/_components/map/layers/district-layer.tsx b/sigap-website/app/_components/map/layers/district-layer.tsx
index c108799..e404de5 100644
--- a/sigap-website/app/_components/map/layers/district-layer.tsx
+++ b/sigap-website/app/_components/map/layers/district-layer.tsx
@@ -1,7 +1,7 @@
"use client"
import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
-import { createFillColorExpression, processDistrictFeature } from "@/app/_utils/map"
+import { createFillColorExpression, getFillOpacity, processDistrictFeature } from "@/app/_utils/map"
import type { IDistrictLayerProps } from "@/app/_utils/types/map"
import { useEffect } from "react"
@@ -25,74 +25,118 @@ export default function DistrictFillLineLayer({
useEffect(() => {
if (!map || !visible) return
- const handleDistrictClick = (e: any) => {
- // First check if the click was on a marker or cluster
- const incidentFeatures = map.queryRenderedFeatures(e.point, {
- layers: [
- "unclustered-point",
- "clusters",
- "crime-points",
- "units-points",
- "incidents-points",
- "timeline-markers",
- "recent-incidents",
- ],
- })
+ const handleDistrictClick = (e: any) => {
+ // Only include layers that exist in the map style
+ const possibleLayers = [
+ "unclustered-point",
+ "clusters",
+ "crime-points",
+ "units-points",
+ "incidents-points",
+ "timeline-markers",
+ "recent-incidents",
+ ]
+ const availableLayers = possibleLayers.filter(layer => map.getLayer(layer))
+ const incidentFeatures = map.queryRenderedFeatures(e.point, {
+ layers: availableLayers,
+ })
- if (incidentFeatures && incidentFeatures.length > 0) {
- // Click was on a marker or cluster, so don't process it as a district click
- return
- }
-
- if (!map || !e.features || e.features.length === 0) return
-
- const feature = e.features[0]
- const districtId = feature.properties.kode_kec
-
- // If clicking the same district, deselect it
- if (focusedDistrictId === districtId) {
- // Add null check for setFocusedDistrictId
- if (setFocusedDistrictId) {
- setFocusedDistrictId(null)
+ if (incidentFeatures && incidentFeatures.length > 0) {
+ // Click was on a marker or cluster, so don't process it as a district click
+ return
}
- // Reset pitch and bearing with animation
- map.easeTo({
- zoom: BASE_ZOOM,
- pitch: BASE_PITCH,
- bearing: BASE_BEARING,
- duration: 1500,
- easing: (t) => t * (2 - t), // easeOutQuad
- })
+ if (!map || !e.features || e.features.length === 0) return
- // Restore fill color for all districts when unfocusing
- const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
- map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
+ const feature = e.features[0]
+ const districtId = feature.properties.kode_kec
- // Show all clusters again when unfocusing
- if (map.getLayer("clusters")) {
- map.setLayoutProperty("clusters", "visibility", "visible")
- }
- if (map.getLayer("unclustered-point")) {
- map.setLayoutProperty("unclustered-point", "visibility", "visible")
- }
+ // If clicking the same district, deselect it
+ if (focusedDistrictId === districtId) {
+ // Add null check for setFocusedDistrictId
+ if (setFocusedDistrictId) {
+ setFocusedDistrictId(null)
+ }
- return
- } else if (focusedDistrictId) {
- // If we're already focusing on a district and clicking a different one,
- // we need to reset the current one and move to the new one
- if (setFocusedDistrictId) {
- setFocusedDistrictId(null)
- }
+ // Reset pitch and bearing with animation
+ map.easeTo({
+ zoom: BASE_ZOOM,
+ pitch: BASE_PITCH,
+ bearing: BASE_BEARING,
+ duration: 1500,
+ easing: (t) => t * (2 - t), // easeOutQuad
+ })
- // Wait a moment before selecting the new district to ensure clean transitions
- setTimeout(() => {
- const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
- if (!district || !setFocusedDistrictId) return
+ // Restore fill color for all districts when unfocusing
+ const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
+ map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
- setFocusedDistrictId(district.id)
+ // Show all clusters again when unfocusing
+ if (map.getLayer("clusters")) {
+ map.setLayoutProperty("clusters", "visibility", "visible")
+ }
+ if (map.getLayer("unclustered-point")) {
+ map.setLayoutProperty("unclustered-point", "visibility", "visible")
+ }
- // Fly to the new district
+ return
+ } else if (focusedDistrictId) {
+ // If we're already focusing on a district and clicking a different one,
+ // we need to reset the current one and move to the new one
+ if (setFocusedDistrictId) {
+ setFocusedDistrictId(null)
+ }
+
+ // Wait a moment before selecting the new district to ensure clean transitions
+ setTimeout(() => {
+ const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
+ if (!district || !setFocusedDistrictId) return
+
+ setFocusedDistrictId(district.id)
+
+ // Fly to the new district
+ map.flyTo({
+ center: [district.longitude, district.latitude],
+ zoom: 12.5,
+ pitch: 75,
+ bearing: 0,
+ duration: 1500,
+ easing: (t) => t * (2 - t), // easeOutQuad
+ })
+
+ // Use onDistrictClick if available, otherwise fall back to onClick
+ if (onDistrictClick) {
+ onDistrictClick(district)
+ } else if (onClick) {
+ onClick(district)
+ }
+ }, 100)
+
+ return
+ }
+
+ const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
+
+ if (!district) return
+
+ // Set the fill color expression immediately to show the focus
+ const focusedFillColorExpression = createFillColorExpression(district.id, crimeDataByDistrict)
+ map.setPaintProperty("district-fill", "fill-color", focusedFillColorExpression as any)
+
+ // Add null check for setFocusedDistrictId
+ if (setFocusedDistrictId) {
+ setFocusedDistrictId(district.id)
+ }
+
+ // Hide clusters when focusing on a district
+ if (map.getLayer("clusters")) {
+ map.setLayoutProperty("clusters", "visibility", "none")
+ }
+ if (map.getLayer("unclustered-point")) {
+ map.setLayoutProperty("unclustered-point", "visibility", "none")
+ }
+
+ // Animate to a pitched view focused on the district
map.flyTo({
center: [district.longitude, district.latitude],
zoom: 12.5,
@@ -108,193 +152,134 @@ export default function DistrictFillLineLayer({
} else if (onClick) {
onClick(district)
}
- }, 100)
-
- return
- }
-
- const district = processDistrictFeature(feature, e, districtId, crimeDataByDistrict, crimes, year, month)
-
- if (!district) return
-
- // Set the fill color expression immediately to show the focus
- const focusedFillColorExpression = createFillColorExpression(district.id, crimeDataByDistrict)
- map.setPaintProperty("district-fill", "fill-color", focusedFillColorExpression as any)
-
- // Add null check for setFocusedDistrictId
- if (setFocusedDistrictId) {
- setFocusedDistrictId(district.id)
}
- // Hide clusters when focusing on a district
- if (map.getLayer("clusters")) {
- map.setLayoutProperty("clusters", "visibility", "none")
- }
- if (map.getLayer("unclustered-point")) {
- map.setLayoutProperty("unclustered-point", "visibility", "none")
- }
+ const onStyleLoad = () => {
+ if (!map) return
- // Animate to a pitched view focused on the district
- map.flyTo({
- center: [district.longitude, district.latitude],
- zoom: 12.5,
- pitch: 75,
- bearing: 0,
- duration: 1500,
- easing: (t) => t * (2 - t), // easeOutQuad
- })
+ try {
+ if (!map.getSource("districts")) {
+ const layers = map.getStyle().layers
+ let firstSymbolId: string | undefined
+ for (const layer of layers) {
+ if (layer.type === "symbol") {
+ firstSymbolId = layer.id
+ break
+ }
+ }
- // Use onDistrictClick if available, otherwise fall back to onClick
- if (onDistrictClick) {
- onDistrictClick(district)
- } else if (onClick) {
- onClick(district)
- }
- }
+ map.addSource("districts", {
+ type: "vector",
+ url: `mapbox://${tilesetId}`,
+ })
- const onStyleLoad = () => {
- if (!map) return
+ const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
- try {
- if (!map.getSource("districts")) {
- const layers = map.getStyle().layers
- let firstSymbolId: string | undefined
- for (const layer of layers) {
- if (layer.type === "symbol") {
- firstSymbolId = layer.id
- break
+ // Determine fill opacity based on active control
+ const fillOpacity = getFillOpacity(activeControl, showFill)
+
+ if (!map.getLayer("district-fill")) {
+ map.addLayer(
+ {
+ id: "district-fill",
+ type: "fill",
+ source: "districts",
+ "source-layer": "Districts",
+ paint: {
+ "fill-color": fillColorExpression as any,
+ "fill-opacity": fillOpacity,
+ },
+ },
+ firstSymbolId,
+ )
+ }
+
+ if (!map.getLayer("district-line")) {
+ map.addLayer(
+ {
+ id: "district-line",
+ type: "line",
+ source: "districts",
+ "source-layer": "Districts",
+ paint: {
+ "line-color": "#ffffff",
+ "line-width": 1,
+ "line-opacity": 0.5,
+ },
+ },
+ firstSymbolId,
+ )
+ }
+
+ map.on("mouseenter", "district-fill", () => {
+ map.getCanvas().style.cursor = "pointer"
+ })
+
+ map.on("mouseleave", "district-fill", () => {
+ map.getCanvas().style.cursor = ""
+ })
+
+ map.off("click", "district-fill", handleDistrictClick)
+ map.on("click", "district-fill", handleDistrictClick)
+ } else {
+ if (map.getLayer("district-fill")) {
+ const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
+
+ // Update fill opacity when active control changes
+ const fillOpacity = getFillOpacity(activeControl, showFill)
+ map.setPaintProperty("district-fill", "fill-opacity", fillOpacity)
}
}
-
- map.addSource("districts", {
- type: "vector",
- url: `mapbox://${tilesetId}`,
- })
-
- const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
-
- // Determine fill opacity based on active control
- const fillOpacity = getFillOpacity(activeControl, showFill)
-
- if (!map.getLayer("district-fill")) {
- map.addLayer(
- {
- id: "district-fill",
- type: "fill",
- source: "districts",
- "source-layer": "Districts",
- paint: {
- "fill-color": fillColorExpression as any,
- "fill-opacity": fillOpacity,
- },
- },
- firstSymbolId,
- )
+ } catch (error) {
+ console.error("Error adding district layers:", error)
}
-
- if (!map.getLayer("district-line")) {
- map.addLayer(
- {
- id: "district-line",
- type: "line",
- source: "districts",
- "source-layer": "Districts",
- paint: {
- "line-color": "#ffffff",
- "line-width": 1,
- "line-opacity": 0.5,
- },
- },
- firstSymbolId,
- )
- }
-
- map.on("mouseenter", "district-fill", () => {
- map.getCanvas().style.cursor = "pointer"
- })
-
- map.on("mouseleave", "district-fill", () => {
- map.getCanvas().style.cursor = ""
- })
-
- map.off("click", "district-fill", handleDistrictClick)
- map.on("click", "district-fill", handleDistrictClick)
- } else {
- if (map.getLayer("district-fill")) {
- const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
- map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
-
- // Update fill opacity when active control changes
- const fillOpacity = getFillOpacity(activeControl, showFill)
- map.setPaintProperty("district-fill", "fill-opacity", fillOpacity)
- }
- }
- } catch (error) {
- console.error("Error adding district layers:", error)
}
- }
- if (map.isStyleLoaded()) {
- onStyleLoad()
- } else {
- map.once("style.load", onStyleLoad)
- }
+ if (map.isStyleLoaded()) {
+ onStyleLoad()
+ } else {
+ map.once("style.load", onStyleLoad)
+ }
- return () => {
- if (map) {
- map.off("click", "district-fill", handleDistrictClick)
- }
- }
- }, [
- map,
- visible,
- tilesetId,
- crimes,
- filterCategory,
- year,
- month,
- focusedDistrictId,
- crimeDataByDistrict,
- onClick,
- onDistrictClick, // Add to dependency array
- setFocusedDistrictId,
- showFill,
- activeControl,
- ])
+ return () => {
+ if (map) {
+ map.off("click", "district-fill", handleDistrictClick)
+ }
+ }
+ }, [
+ map,
+ visible,
+ tilesetId,
+ crimes,
+ filterCategory,
+ year,
+ month,
+ focusedDistrictId,
+ crimeDataByDistrict,
+ onClick,
+ onDistrictClick, // Add to dependency array
+ setFocusedDistrictId,
+ showFill,
+ activeControl,
+ ])
// Add an effect to update the fill color and opacity whenever relevant props change
useEffect(() => {
- if (!map || !map.getLayer("district-fill")) return
+ if (!map || !map.getLayer("district-fill")) return
- try {
- const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
- map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
+ try {
+ const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
- // Update fill opacity when active control changes
- const fillOpacity = getFillOpacity(activeControl, showFill)
- map.setPaintProperty("district-fill", "fill-opacity", fillOpacity)
- } catch (error) {
- console.error("Error updating district fill colors or opacity:", error)
- }
- }, [map, focusedDistrictId, crimeDataByDistrict, activeControl, showFill])
+ // Update fill opacity when active control changes
+ const fillOpacity = getFillOpacity(activeControl, showFill)
+ map.setPaintProperty("district-fill", "fill-opacity", fillOpacity)
+ } catch (error) {
+ console.error("Error updating district fill colors or opacity:", error)
+ }
+ }, [map, focusedDistrictId, crimeDataByDistrict, activeControl, showFill])
return null
}
-// Helper function to determine fill opacity based on active control
-function getFillOpacity(activeControl?: string, showFill?: boolean): number {
- if (!showFill) return 0
-
- // Full opacity for incidents and clusters
- if (activeControl === "incidents" || activeControl === "clusters") {
- return 0.6
- }
-
- // Low opacity for timeline to show markers but still see district boundaries
- if (activeControl === "timeline") {
- return 0.1
- }
-
- // No fill for other controls, but keep boundaries visible
- return 0
-}
diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx
index 0d5a05c..82f13a2 100644
--- a/sigap-website/app/_components/map/layers/layers.tsx
+++ b/sigap-website/app/_components/map/layers/layers.tsx
@@ -15,7 +15,7 @@ import { createFillColorExpression, processCrimeDataByDistrict } from "@/app/_ut
import UnclusteredPointLayer from "./uncluster-layer"
import { toast } from "sonner"
-import type { ITooltips } from "../controls/top/tooltips"
+import type { ITooltipsControl } from "../controls/top/tooltips"
import type { IUnits } from "@/app/_utils/types/units"
import UnitsLayer from "./units-layer"
import DistrictFillLineLayer from "./district-layer"
@@ -59,7 +59,7 @@ export interface IDistrictLayerProps {
setFocusedDistrictId?: (id: string | null) => void
crimeDataByDistrict?: Record
showFill?: boolean
- activeControl?: ITooltips
+ activeControl?: ITooltipsControl
}
interface LayersProps {
@@ -70,7 +70,7 @@ interface LayersProps {
year: string
month: string
filterCategory: string | "all"
- activeControl: ITooltips
+ activeControl: ITooltipsControl
tilesetId?: string
useAllData?: boolean
showEWS?: boolean
@@ -132,7 +132,7 @@ export default function Layers({
if (incident.status === "active") {
resolveIncident(incident.id)
}
- })
+ })
setEwsIncidents(getAllIncidents())
}, [ewsIncidents])
diff --git a/sigap-website/app/_styles/ui.css b/sigap-website/app/_styles/ui.css
index 491d06c..000db04 100644
--- a/sigap-website/app/_styles/ui.css
+++ b/sigap-website/app/_styles/ui.css
@@ -984,7 +984,7 @@ label#internal {
margin: auto;
}
-.circles div {
+/* .circles div {
animation: growAndFade 3s infinite ease-out;
background-color: rgb(156, 94, 0);
border-radius: 50%;
@@ -993,7 +993,7 @@ label#internal {
opacity: 0;
position: absolute;
box-shadow: 0 0 10px 5px rgba(156, 75, 0, 0.5);
-}
+} */
@keyframes growAndFade {
0% {
diff --git a/sigap-website/app/_utils/map.ts b/sigap-website/app/_utils/map.ts
index 0601df5..e19197d 100644
--- a/sigap-website/app/_utils/map.ts
+++ b/sigap-website/app/_utils/map.ts
@@ -2,6 +2,7 @@ import { $Enums } from '@prisma/client';
import { CRIME_RATE_COLORS } from '@/app/_utils/const/map';
import type { ICrimes } from '@/app/_utils/types/crimes';
import { IDistrictFeature } from './types/map';
+import { ITooltipsControl } from '../_components/map/controls/top/tooltips';
// Process crime data by district
export const processCrimeDataByDistrict = (crimes: ICrimes[]) => {
@@ -262,4 +263,22 @@ export function formatDistance(meters: number): string {
} else {
return `${(meters / 1000).toFixed(1)} km`;
}
-}
\ No newline at end of file
+}
+
+// Helper function to determine fill opacity based on active control
+export function getFillOpacity(activeControl?: ITooltipsControl, showFill?: boolean): number {
+ if (!showFill) return 0
+
+ // Full opacity for incidents and clusters
+ if (activeControl === "incidents" || activeControl === "clusters" || activeControl === "units") {
+ return 0.6
+ }
+
+ // Low opacity for timeline to show markers but still see district boundaries
+ if (activeControl === "timeline") {
+ return 0.1
+ }
+
+ // No fill for other controls, but keep boundaries visible
+ return 0
+}
diff --git a/sigap-website/app/_utils/types/map.ts b/sigap-website/app/_utils/types/map.ts
index 0f42b82..44e080c 100644
--- a/sigap-website/app/_utils/types/map.ts
+++ b/sigap-website/app/_utils/types/map.ts
@@ -19,6 +19,7 @@ export interface IGeoJSONFeatureCollection {
import { $Enums } from '@prisma/client';
import type { ICrimes } from '@/app/_utils/types/crimes';
import mapboxgl from 'mapbox-gl';
+import { ITooltipsControl } from "@/app/_components/map/controls/top/tooltips";
// Types for district properties
export interface IDistrictFeature {
@@ -75,7 +76,7 @@ export interface IDistrictLayerProps {
filterCategory: string | 'all';
crimes: ICrimes[];
tilesetId?: string;
- activeControl?: string;
+ activeControl: ITooltipsControl;
focusedDistrictId: string | null;
setFocusedDistrictId?: (id: string | null) => void;
crimeDataByDistrict?: any;