diff --git a/sigap-website/app/_components/map/layers/cluster-layer.tsx b/sigap-website/app/_components/map/layers/cluster-layer.tsx
index 20e401c..78ea95e 100644
--- a/sigap-website/app/_components/map/layers/cluster-layer.tsx
+++ b/sigap-website/app/_components/map/layers/cluster-layer.tsx
@@ -4,7 +4,7 @@ import { useEffect, useCallback } from "react"
import mapboxgl from "mapbox-gl"
import type { GeoJSON } from "geojson"
import type { IClusterLayerProps } from "@/app/_utils/types/map"
-import { extractCrimeIncidents } from "@/app/_utils/map"
+import { extractCrimeIncidents } from "@/app/_utils/map/common"
import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility"
interface ExtendedClusterLayerProps extends IClusterLayerProps {
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 8eb8115..7d7230f 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, getFillOpacity } from "@/app/_utils/map"
+import { getCrimeRateColor, getFillOpacity } from "@/app/_utils/map/common"
import type { IExtrusionLayerProps } from "@/app/_utils/types/map"
import { useEffect, useRef } from "react"
import { manageLayerVisibility } from "@/app/_utils/map/layer-visibility"
diff --git a/sigap-website/app/_components/map/layers/district-layer.tsx b/sigap-website/app/_components/map/layers/district-layer.tsx
index dde7966..20b2e0f 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_DURATION, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map"
-import { createFillColorExpression, getFillOpacity, processDistrictFeature } from "@/app/_utils/map"
+import { createFillColorExpression, getFillOpacity, processDistrictFeature } from "@/app/_utils/map/common"
import type { IDistrictLayerProps } from "@/app/_utils/types/map"
import { useEffect } from "react"
@@ -9,7 +9,7 @@ export default function DistrictFillLineLayer({
visible = true,
map,
onClick,
- onDistrictClick, // Add the new prop
+ onDistrictClick,
year,
month,
filterCategory = "all",
@@ -21,12 +21,10 @@ export default function DistrictFillLineLayer({
showFill = true,
activeControl,
}: IDistrictLayerProps & { onDistrictClick?: (district: any) => void }) {
- // Extend the type inline
useEffect(() => {
if (!map || !visible) return
const handleDistrictClick = (e: any) => {
- // Only include layers that exist in the map style
const possibleLayers = [
"unclustered-point",
"clusters",
@@ -42,7 +40,6 @@ export default function DistrictFillLineLayer({
})
if (incidentFeatures && incidentFeatures.length > 0) {
- // Click was on a marker or cluster, so don't process it as a district click
return
}
@@ -51,27 +48,22 @@ export default function DistrictFillLineLayer({
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)
}
- // Reset pitch and bearing with animation
map.easeTo({
zoom: BASE_ZOOM,
pitch: BASE_PITCH,
bearing: BASE_BEARING,
duration: BASE_DURATION,
- easing: (t) => t * (2 - t), // easeOutQuad
+ easing: (t) => t * (2 - t),
})
- // Restore fill color for all districts when unfocusing
const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict)
map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any)
- // Show all clusters again when unfocusing
if (map.getLayer("clusters")) {
map.setLayoutProperty("clusters", "visibility", "visible")
}
@@ -81,37 +73,10 @@ export default function DistrictFillLineLayer({
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
}
@@ -119,16 +84,13 @@ export default function DistrictFillLineLayer({
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")
}
@@ -136,17 +98,6 @@ export default function DistrictFillLineLayer({
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,
- // 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) {
@@ -173,9 +124,21 @@ export default function DistrictFillLineLayer({
url: `mapbox://${tilesetId}`,
})
- const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ let fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ if (
+ Array.isArray(fillColorExpression) &&
+ fillColorExpression[0] === "match" &&
+ fillColorExpression.length < 4
+ ) {
+ fillColorExpression = [
+ "match",
+ ["get", "kode_kec"],
+ "",
+ "#90a4ae",
+ "#90a4ae"
+ ]
+ }
- // Determine fill opacity based on active control
const fillOpacity = getFillOpacity(activeControl, showFill)
if (!map.getLayer("district-fill")) {
@@ -223,10 +186,22 @@ export default function DistrictFillLineLayer({
map.on("click", "district-fill", handleDistrictClick)
} else {
if (map.getLayer("district-fill")) {
- const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ let fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ if (
+ Array.isArray(fillColorExpression) &&
+ fillColorExpression[0] === "match" &&
+ fillColorExpression.length < 4
+ ) {
+ fillColorExpression = [
+ "match",
+ ["get", "kode_kec"],
+ "",
+ "#90a4ae",
+ "#90a4ae"
+ ]
+ }
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)
}
@@ -258,21 +233,32 @@ export default function DistrictFillLineLayer({
focusedDistrictId,
crimeDataByDistrict,
onClick,
- onDistrictClick, // Add to dependency array
+ onDistrictClick,
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
try {
- const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ let fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict)
+ if (
+ Array.isArray(fillColorExpression) &&
+ fillColorExpression[0] === "match" &&
+ fillColorExpression.length < 4
+ ) {
+ fillColorExpression = [
+ "match",
+ ["get", "kode_kec"],
+ "",
+ "#90a4ae",
+ "#90a4ae"
+ ]
+ }
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) {
diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx
index 7916309..d993b2a 100644
--- a/sigap-website/app/_components/map/layers/layers.tsx
+++ b/sigap-website/app/_components/map/layers/layers.tsx
@@ -18,12 +18,12 @@ import HeatmapLayer from "./heatmap-layer";
import TimelineLayer from "./timeline-layer";
import type { ICrimes, IIncidentLogs } from "@/app/_utils/types/crimes";
-import type { IDistrictFeature } from "@/app/_utils/types/map";
+import type { ICrimeSourceTypes, IDistrictFeature } from "@/app/_utils/types/map";
import {
createFillColorExpression,
getCrimeRateColor,
processCrimeDataByDistrict,
-} from "@/app/_utils/map";
+} from "@/app/_utils/map/common";
import { toast } from "sonner";
import type { ITooltipsControl } from "../controls/top/tooltips";
@@ -83,7 +83,7 @@ interface LayersProps {
tilesetId?: string;
useAllData?: boolean;
showEWS?: boolean;
- sourceType?: string;
+ sourceType?: ICrimeSourceTypes;
}
export default function Layers({
diff --git a/sigap-website/app/_components/map/pop-up/distance-info.tsx b/sigap-website/app/_components/map/pop-up/distance-info.tsx
index d27ce2a..1092a1a 100644
--- a/sigap-website/app/_components/map/pop-up/distance-info.tsx
+++ b/sigap-website/app/_components/map/pop-up/distance-info.tsx
@@ -3,7 +3,7 @@
import { calculateDistances } from '@/app/(pages)/(admin)/dashboard/crime-management/crime-overview/action'
import { useState, useEffect } from 'react'
import { Skeleton } from '../../ui/skeleton'
-import { formatDistance } from '@/app/_utils/map'
+import { formatDistance } from '@/app/_utils/map/common'
import { useQuery } from '@tanstack/react-query'
diff --git a/sigap-website/app/_utils/map.ts b/sigap-website/app/_utils/map/common.tsx
similarity index 65%
rename from sigap-website/app/_utils/map.ts
rename to sigap-website/app/_utils/map/common.tsx
index e19197d..b89e3e2 100644
--- a/sigap-website/app/_utils/map.ts
+++ b/sigap-website/app/_utils/map/common.tsx
@@ -1,8 +1,10 @@
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';
+
+import { AlertTriangle, Shield } from "lucide-react";
+import { IDistrictFeature } from '../types/map';
+import { ITooltipsControl } from '@/app/_components/map/controls/top/tooltips';
// Process crime data by district
export const processCrimeDataByDistrict = (crimes: ICrimes[]) => {
@@ -49,15 +51,15 @@ export const createFillColorExpression = (
) => {
const colorEntries = focusedDistrictId
? [
- [
- focusedDistrictId,
- getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
- ],
- 'rgba(0,0,0,0.05)',
- ]
+ [
+ focusedDistrictId,
+ getCrimeRateColor(crimeDataByDistrict[focusedDistrictId]?.level),
+ ],
+ 'rgba(0,0,0,0.05)',
+ ]
: Object.entries(crimeDataByDistrict).flatMap(([districtId, data]) => {
- return [districtId, getCrimeRateColor(data.level)];
- });
+ return [districtId, getCrimeRateColor(data.level)];
+ });
return [
'case',
@@ -282,3 +284,124 @@ export function getFillOpacity(activeControl?: ITooltipsControl, showFill?: bool
// No fill for other controls, but keep boundaries visible
return 0
}
+
+// Helper to normalize severity to a number
+export const normalizeSeverity = (sev: number | "Low" | "Medium" | "High" | "Critical"): number => {
+ if (typeof sev === "number") return sev
+ switch (sev) {
+ case "Critical":
+ return 4
+ case "High":
+ return 3
+ case "Medium":
+ return 2
+ case "Low":
+ default:
+ return 1
+ }
+}
+
+export const normalizeSeverityNumber = (sev?: number | "Low" | "Medium" | "High" | "Critical"): string => {
+ if (!sev) return "Medium";
+
+ if (typeof sev === "number") {
+ switch (sev) {
+ case 4: return "Critical";
+ case 3: return "High";
+ case 2: return "Medium";
+ case 1: return "Low";
+ default: return "Medium";
+ }
+ }
+ return sev;
+};
+
+
+export const getSeverityGradient = (severity: number) => {
+ switch (severity) {
+ case 4:
+ return "bg-gradient-to-r from-purple-500/10 to-purple-600/5 dark:from-purple-900/20 dark:to-purple-800/10"
+ case 3:
+ return "bg-gradient-to-r from-red-500/10 to-red-600/5 dark:from-red-900/20 dark:to-red-800/10"
+ case 2:
+ return "bg-gradient-to-r from-yellow-500/10 to-yellow-600/5 dark:from-yellow-900/20 dark:to-yellow-800/10"
+ case 1:
+ default:
+ return "bg-gradient-to-r from-blue-500/10 to-blue-600/5 dark:from-blue-900/20 dark:to-blue-800/10"
+ }
+}
+
+export const getSeverityIconColor = (severity: number) => {
+ switch (severity) {
+ case 4:
+ return "text-purple-600 dark:text-purple-400"
+ case 3:
+ return "text-red-500 dark:text-red-400"
+ case 2:
+ return "text-yellow-600 dark:text-yellow-400"
+ case 1:
+ default:
+ return "text-blue-500 dark:text-blue-400"
+ }
+}
+
+export const getSeverityIcon = (severity: number) => {
+ switch (severity) {
+ case 4:
+ case 3:
+ return
+ case 2:
+ case 1:
+ default:
+ return
+ }
+}
+
+export const getStatusInfo = (status?: string | true) => {
+ if (status === true) {
+ return {
+ text: "Verified",
+ color: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400"
+ };
+ }
+
+ if (!status) {
+ return {
+ text: "Pending",
+ color: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400"
+ };
+ }
+
+ switch (status.toLowerCase()) {
+ case "resolved":
+ return {
+ text: "Resolved",
+ color: "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400"
+ };
+ case "in progress":
+ return {
+ text: "In Progress",
+ color: "bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-900/30 dark:text-amber-400"
+ };
+ case "pending":
+ return {
+ text: "Pending",
+ color: "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400"
+ };
+ default:
+ return {
+ text: status.charAt(0).toUpperCase() + status.slice(1),
+ color: "bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-900/30 dark:text-gray-400"
+ };
+ }
+};
+// Get appropriate color for severity badge
+export const getSeverityColor = (severity: string) => {
+ switch (severity) {
+ case "Critical": return "bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-400";
+ case "High": return "bg-orange-100 text-orange-800 border-orange-200 dark:bg-orange-900/30 dark:text-orange-400";
+ case "Medium": return "bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-400";
+ case "Low": return "bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-400";
+ default: return "bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-400";
+ }
+};
\ No newline at end of file
diff --git a/sigap-website/app/_utils/types/map.ts b/sigap-website/app/_utils/types/map.ts
index 44e080c..7c72648 100644
--- a/sigap-website/app/_utils/types/map.ts
+++ b/sigap-website/app/_utils/types/map.ts
@@ -107,3 +107,6 @@ export interface IUnclusteredPointLayerProps extends IBaseLayerProps {
focusedDistrictId: string | null;
showIncidentMarkers?: boolean; // Add this prop to control marker visibility
}
+
+// Source type crimes
+export type ICrimeSourceTypes = "cbt" | "cbu"