From 9c6e00583908f4b6d35b062e37749987186f711f Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Wed, 14 May 2025 10:06:09 +0700 Subject: [PATCH] fix: fix bug district pop up always show while selected district is not focus --- .../_components/map/layers/cluster-layer.tsx | 9 +- .../map/layers/district-extrusion-layer.tsx | 510 +++--- .../_components/map/layers/district-layer.tsx | 428 ++--- .../app/_components/map/layers/layers.tsx | 444 +++-- .../map/layers/recent-incidents-layer.tsx | 403 ++-- .../_components/map/layers/timeline-layer.tsx | 311 ++-- .../_components/map/layers/units-layer.tsx | 90 +- .../app/_components/map/pop-up/unit-popup.tsx | 27 +- sigap-website/app/_styles/ui.css | 1624 ++++++++--------- 9 files changed, 1915 insertions(+), 1931 deletions(-) diff --git a/sigap-website/app/_components/map/layers/cluster-layer.tsx b/sigap-website/app/_components/map/layers/cluster-layer.tsx index c07adaa..85af200 100644 --- a/sigap-website/app/_components/map/layers/cluster-layer.tsx +++ b/sigap-website/app/_components/map/layers/cluster-layer.tsx @@ -27,6 +27,7 @@ export default function ClusterLayer({ (e: any) => { if (!map) return + // Stop event propagation to prevent district layer from handling this click e.originalEvent.stopPropagation() e.preventDefault() @@ -219,6 +220,7 @@ export default function ClusterLayer({ const handleCrimePointClick = (e: any) => { if (!map) return + // Stop event propagation e.originalEvent.stopPropagation() e.preventDefault() @@ -423,6 +425,7 @@ export default function ClusterLayer({ const handleCrimePointClick = (e: any) => { if (!map) return + // Stop event propagation e.originalEvent.stopPropagation() e.preventDefault() @@ -504,19 +507,19 @@ export default function ClusterLayer({ map.off("click", "clusters", handleClusterClick) if (sourceType === "cbu" && map.getLayer("crime-points")) { // Define properly typed event handlers - const crimePointsMouseEnter = function () { + const crimePointsMouseEnter = () => { if (map && map.getCanvas()) { map.getCanvas().style.cursor = "pointer"; } }; - const crimePointsMouseLeave = function () { + const crimePointsMouseLeave = () => { if (map && map.getCanvas()) { map.getCanvas().style.cursor = ""; } }; - const crimePointsClick = function (e: mapboxgl.MapMouseEvent) { + const crimePointsClick = (e: mapboxgl.MapMouseEvent) => { if (!map) return; e.originalEvent.stopPropagation(); 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 741261c..d4c5ec5 100644 --- a/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-extrusion-layer.tsx @@ -1,7 +1,7 @@ "use client" import { getCrimeRateColor } from "@/app/_utils/map" -import { IExtrusionLayerProps } from "@/app/_utils/types/map" +import type { IExtrusionLayerProps } from "@/app/_utils/types/map" import { useEffect, useRef } from "react" export default function DistrictExtrusionLayer({ @@ -21,221 +21,227 @@ export default function DistrictExtrusionLayer({ useEffect(() => { if (!map || !visible) return - const onStyleLoad = () => { - if (!map) return + console.log("DistrictExtrusionLayer effect running, focusedDistrictId:", focusedDistrictId) - try { - const layers = map.getStyle().layers - let firstSymbolId: string | undefined - for (const layer of layers) { - if (layer.type === "symbol") { - firstSymbolId = layer.id - break - } + const onStyleLoad = () => { + if (!map) return + + try { + const layers = map.getStyle().layers + let firstSymbolId: string | undefined + for (const layer of layers) { + if (layer.type === "symbol") { + firstSymbolId = layer.id + break } - - // 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 || ""], - }, - firstSymbolId - ) - - extrusionCreatedRef.current = true - - // If a district is focused, start the animation - if (focusedDistrictId) { - lastFocusedDistrictRef.current = focusedDistrictId - animateExtrusion() - } - } catch (error) { - console.error("Error adding district extrusion layer:", error) } + + // 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}`, + }) } - if (map.isStyleLoaded()) { - onStyleLoad() - } else { - map.once("style.load", onStyleLoad) - } + // 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 || ""], + }, + firstSymbolId, + ) - return () => { - if (animationRef.current) { - cancelAnimationFrame(animationRef.current) - animationRef.current = null + 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) } - }, [map, visible, tilesetId]) + } + + 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 useEffect(() => { if (!map || !map.getLayer("district-extrusion")) return - // Skip unnecessary updates if nothing has changed - if (lastFocusedDistrictRef.current === focusedDistrictId) return; + console.log("Updating district extrusion for district:", focusedDistrictId) - // If we're unfocusing a district - if (!focusedDistrictId) { - // Stop rotation when unfocusing - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - bearingRef.current = 0 + // Skip unnecessary updates if nothing has changed + if (lastFocusedDistrictRef.current === focusedDistrictId) return - // Animate height down - const animateHeightDown = () => { - if (!map || !map.getLayer("district-extrusion")) 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 - let currentHeight = 800; - const duration = 500; - const startTime = performance.now(); + // Animate height down + const animateHeightDown = () => { + if (!map || !map.getLayer("district-extrusion")) return - 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); + const currentHeight = 800 + const duration = 500 + const startTime = performance.now() - try { - map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ - "case", - ["has", "kode_kec"], - ["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0], - 0, - ]); + 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 (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"], ""]); + try { + map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ + "case", + ["has", "kode_kec"], + ["match", ["get", "kode_kec"], lastFocusedDistrictRef.current || "", height, 0], + 0, + ]) - lastFocusedDistrictRef.current = null; + 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"], ""]) - // 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; - } - } - }; + lastFocusedDistrictRef.current = null - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - } - animationRef.current = requestAnimationFrame(animate); - }; - - animateHeightDown(); - return; + // 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 + } + } } - try { - // Update filter for the new district - map.setFilter("district-extrusion", ["==", ["get", "kode_kec"], focusedDistrictId]); + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + } + animationRef.current = requestAnimationFrame(animate) + } - // 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", - ], + 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, - ]); + // 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; + // Store current focused district + lastFocusedDistrictRef.current = focusedDistrictId - // Stop any existing animations and restart - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current); - rotationAnimationRef.current = null; - } + // Stop any existing animations and restart + if (rotationAnimationRef.current) { + cancelAnimationFrame(rotationAnimationRef.current) + rotationAnimationRef.current = null + } - if (animationRef.current) { - cancelAnimationFrame(animationRef.current); - animationRef.current = null; - } + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } - // Start animation with small delay to ensure smooth transition - setTimeout(() => { - animateExtrusion(); - }, 100); - - } catch (error) { - console.error("Error updating district extrusion:", error) - } - }, [map, focusedDistrictId, crimeDataByDistrict]) + // 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]) // Cleanup on unmount useEffect(() => { @@ -253,89 +259,95 @@ export default function DistrictExtrusionLayer({ // Animate extrusion height const animateExtrusion = () => { - if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) return + if (!map || !map.getLayer("district-extrusion") || !focusedDistrictId) { + console.log("Cannot animate extrusion: missing map, layer, or focusedDistrictId") + return + } - if (animationRef.current) { - cancelAnimationFrame(animationRef.current) - animationRef.current = null - } + console.log("Animating extrusion for district:", focusedDistrictId) - const startHeight = 0 - const targetHeight = 800 - const duration = 700 - const startTime = performance.now() + if (animationRef.current) { + cancelAnimationFrame(animationRef.current) + animationRef.current = null + } - 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 + const startHeight = 0 + const targetHeight = 800 + const duration = 700 + const startTime = performance.now() - try { - map.setPaintProperty("district-extrusion", "fill-extrusion-height", [ - "case", - ["has", "kode_kec"], - ["match", ["get", "kode_kec"], focusedDistrictId, currentHeight, 0], - 0, - ]) + 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 - if (progress < 1) { - animationRef.current = requestAnimationFrame(animate) - } else { - // Start rotation after extrusion completes - startRotation() - } - } catch (error) { - console.error("Error animating extrusion:", error) - if (animationRef.current) { - cancelAnimationFrame(animationRef.current) - animationRef.current = null - } + 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 } } - - 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 // Reset bearing at start - 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) + 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 - } + // 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 } } - - // Start the animation loop - if (rotationAnimationRef.current) { - cancelAnimationFrame(rotationAnimationRef.current) - rotationAnimationRef.current = null - } - rotationAnimationRef.current = requestAnimationFrame(animate) } + // Start the animation loop + 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 b9adca5..c108799 100644 --- a/sigap-website/app/_components/map/layers/district-layer.tsx +++ b/sigap-website/app/_components/map/layers/district-layer.tsx @@ -2,10 +2,9 @@ import { BASE_BEARING, BASE_PITCH, BASE_ZOOM } from "@/app/_utils/const/map" import { createFillColorExpression, processDistrictFeature } from "@/app/_utils/map" -import { IDistrictLayerProps } from "@/app/_utils/types/map" +import type { IDistrictLayerProps } from "@/app/_utils/types/map" import { useEffect } from "react" - export default function DistrictFillLineLayer({ visible = true, map, @@ -21,110 +20,79 @@ export default function DistrictFillLineLayer({ crimeDataByDistrict, showFill = true, activeControl, -}: IDistrictLayerProps & { onDistrictClick?: (district: any) => void }) { // Extend the type inline +}: IDistrictLayerProps & { onDistrictClick?: (district: any) => void }) { + // Extend the type inline useEffect(() => { if (!map || !visible) return - const handleDistrictClick = (e: any) => { - const incidentFeatures = map.queryRenderedFeatures(e.point, { - layers: ["unclustered-point", "clusters"], - }) + 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", + ], + }) - if (incidentFeatures && incidentFeatures.length > 0) { - return - } + 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 + 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) - } - - // 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 - }) - - // 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") - } - if (map.getLayer("unclustered-point")) { - map.setLayoutProperty("unclustered-point", "visibility", "visible") - } - - 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) + 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(district.id) + setFocusedDistrictId(null) } - // 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") - } + // 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 + }) - // Animate to a pitched view focused on the district + // 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") + } + if (map.getLayer("unclustered-point")) { + map.setLayoutProperty("unclustered-point", "visibility", "visible") + } + + 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, @@ -140,151 +108,193 @@ 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) } - const onStyleLoad = () => { - if (!map) return + // 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") + } - 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 - } - } + // 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 + }) - map.addSource("districts", { - type: "vector", - url: `mapbox://${tilesetId}`, - }) + // Use onDistrictClick if available, otherwise fall back to onClick + if (onDistrictClick) { + onDistrictClick(district) + } else if (onClick) { + onClick(district) + } + } - const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict) + const onStyleLoad = () => { + if (!map) return - // 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); + 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 } } - } catch (error) { - console.error("Error adding district layers:", error) - } - } - if (map.isStyleLoaded()) { - onStyleLoad() + 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, + ) + } + + 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 { - map.once("style.load", onStyleLoad) - } + if (map.getLayer("district-fill")) { + const fillColorExpression = createFillColorExpression(focusedDistrictId, crimeDataByDistrict) + map.setPaintProperty("district-fill", "fill-color", fillColorExpression as any) - return () => { - if (map) { - map.off("click", "district-fill", handleDistrictClick) + // 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) } - }, [ - map, - visible, - tilesetId, - crimes, - filterCategory, - year, - month, - focusedDistrictId, - crimeDataByDistrict, - onClick, - onDistrictClick, // Add to dependency array - setFocusedDistrictId, - showFill, - activeControl, - ]) + } + + 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, + ]) // 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; + if (!showFill) return 0 // Full opacity for incidents and clusters if (activeControl === "incidents" || activeControl === "clusters") { - return 0.6; - } + return 0.6 + } // Low opacity for timeline to show markers but still see district boundaries if (activeControl === "timeline") { - return 0.1; - } + return 0.1 + } // No fill for other controls, but keep boundaries visible - return 0; + return 0 } diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 03dcf93..0d5a05c 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useRef, useEffect, useCallback, act } from "react" +import { useState, useRef, useEffect, useCallback } from "react" import { useMap } from "react-map-gl/mapbox" import { BASE_BEARING, BASE_PITCH, BASE_ZOOM, MAPBOX_TILESET_ID } from "@/app/_utils/const/map" import DistrictPopup from "../pop-up/district-popup" @@ -20,17 +20,13 @@ import type { IUnits } from "@/app/_utils/types/units" import UnitsLayer from "./units-layer" import DistrictFillLineLayer from "./district-layer" import CrimePopup from "../pop-up/crime-popup" -import TimeZonesDisplay from "./timezone" import TimezoneLayer from "./timezone" import FaultLinesLayer from "./fault-lines" -import CoastlineLayer from "./coastline" import EWSAlertLayer from "./ews-alert-layer" import PanicButtonDemo from "../controls/panic-button-demo" -import { IIncidentLog } from "@/app/_utils/types/ews" +import type { IIncidentLog } from "@/app/_utils/types/ews" import { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data" - -import HistoricalIncidentsLayer from "./historical-incidents-layer" import RecentIncidentsLayer from "./recent-incidents-layer" // Interface for crime incident @@ -108,40 +104,44 @@ export default function Layers({ const [selectedIncident, setSelectedIncident] = useState(null) const [focusedDistrictId, setFocusedDistrictId] = useState(null) const selectedDistrictRef = useRef(null) + // Track if we're currently interacting with a marker to prevent district selection + const isInteractingWithMarker = useRef(false) const crimeDataByDistrict = processCrimeDataByDistrict(crimes) - const [ewsIncidents, setEwsIncidents] = useState([]); - const [showPanicDemo, setShowPanicDemo] = useState(true); + const [ewsIncidents, setEwsIncidents] = useState([]) + const [showPanicDemo, setShowPanicDemo] = useState(true) + const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) useEffect(() => { - setEwsIncidents(getAllIncidents()); - }, []); + setEwsIncidents(getAllIncidents()) + }, []) - const handleTriggerAlert = useCallback((priority: 'high' | 'medium' | 'low') => { - const newIncident = addMockIncident({ priority }); - setEwsIncidents(getAllIncidents()); - }, []); + const handleTriggerAlert = useCallback((priority: "high" | "medium" | "low") => { + const newIncident = addMockIncident({ priority }) + setEwsIncidents(getAllIncidents()) + }, []) const handleResolveIncident = useCallback((id: string) => { - resolveIncident(id); - setEwsIncidents(getAllIncidents()); - }, []); + resolveIncident(id) + setEwsIncidents(getAllIncidents()) + }, []) const handleResolveAllAlerts = useCallback(() => { - ewsIncidents.forEach(incident => { - if (incident.status === 'active') { - resolveIncident(incident.id); + ewsIncidents.forEach((incident) => { + if (incident.status === "active") { + resolveIncident(incident.id) } - }); - setEwsIncidents(getAllIncidents()); - }, [ewsIncidents]); + }) + setEwsIncidents(getAllIncidents()) + }, [ewsIncidents]) const handlePopupClose = useCallback(() => { selectedDistrictRef.current = null setSelectedDistrict(null) setSelectedIncident(null) setFocusedDistrictId(null) + isInteractingWithMarker.current = false if (map) { map.easeTo({ @@ -152,12 +152,12 @@ export default function Layers({ easing: (t) => t * (2 - t), }) - if (map.getLayer("clusters")) { - map.getMap().setLayoutProperty("clusters", "visibility", "visible") - } - if (map.getLayer("unclustered-point")) { - map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") - } + if (map.getLayer("clusters")) { + map.getMap().setLayoutProperty("clusters", "visibility", "visible") + } + if (map.getLayer("unclustered-point")) { + map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") + } if (map.getLayer("district-fill")) { const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) @@ -180,8 +180,16 @@ export default function Layers({ (feature: IDistrictFeature) => { console.log("District clicked:", feature) + // If we're currently interacting with a marker, don't process district click + if (isInteractingWithMarker.current) { + console.log("Ignoring district click because we're interacting with a marker") + return + } + + // Clear any existing incident selection setSelectedIncident(null) + // Set the district as selected setSelectedDistrict(feature) selectedDistrictRef.current = feature setFocusedDistrictId(feature.id) @@ -196,6 +204,7 @@ export default function Layers({ easing: (t) => t * (2 - t), }) + // Hide clusters when focusing on a district if (map.getLayer("clusters")) { map.getMap().setLayoutProperty("clusters", "visibility", "none") } @@ -214,16 +223,16 @@ export default function Layers({ const customEvent = e as CustomEvent if (!map || !customEvent.detail) return - const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail + const { longitude, latitude, zoom, bearing, pitch, duration } = customEvent.detail - map.flyTo({ - center: [longitude, latitude], - zoom: zoom || 15, - bearing: bearing || 0, - pitch: pitch || 45, - duration: duration || 2000, - }) - } + map.flyTo({ + center: [longitude, latitude], + zoom: zoom || 15, + bearing: bearing || 0, + pitch: pitch || 45, + duration: duration || 2000, + }) + } mapboxMap.getCanvas().addEventListener("mapbox_fly_to", handleFlyToEvent as EventListener) @@ -241,82 +250,93 @@ export default function Layers({ const customEvent = e as CustomEvent console.log("Received incident_click event in layers:", customEvent.detail) - if (!customEvent.detail) { - console.error("Empty incident click event data") - return - } + if (!customEvent.detail) { + console.error("Empty incident click event data") + return + } - const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id + // Set the marker interaction flag to prevent district selection + isInteractingWithMarker.current = true - if (!incidentId) { - console.error("No incident ID found in event data:", customEvent.detail) - return - } + const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id - console.log("Looking for incident with ID:", incidentId) + if (!incidentId) { + console.error("No incident ID found in event data:", customEvent.detail) + return + } - let foundIncident: ICrimeIncident | undefined + console.log("Looking for incident with ID:", incidentId) - if ( - customEvent.detail.latitude !== undefined && - customEvent.detail.longitude !== undefined && - customEvent.detail.category !== undefined - ) { - foundIncident = { - id: incidentId, - district: customEvent.detail.district, - category: customEvent.detail.category, - type_category: customEvent.detail.type, - description: customEvent.detail.description, - status: customEvent.detail.status || "Unknown", - timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined, - latitude: customEvent.detail.latitude, - longitude: customEvent.detail.longitude, - address: customEvent.detail.address, - } - } else { - for (const crime of crimes) { - for (const incident of crime.crime_incidents) { - if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) { - console.log("Found matching incident:", incident) - foundIncident = { - id: incident.id, - district: crime.districts.name, - description: incident.description, - status: incident.status || "unknown", - timestamp: incident.timestamp, - category: incident.crime_categories.name, - type_category: incident.crime_categories.type, - address: incident.locations.address, - latitude: incident.locations.latitude, - longitude: incident.locations.longitude, - } - break - } - } - if (foundIncident) break - } - } + let foundIncident: ICrimeIncident | undefined - if (!foundIncident) { - console.error("Could not find incident with ID:", incidentId) - return - } + if ( + customEvent.detail.latitude !== undefined && + customEvent.detail.longitude !== undefined && + customEvent.detail.category !== undefined + ) { + foundIncident = { + id: incidentId, + district: customEvent.detail.district, + category: customEvent.detail.category, + type_category: customEvent.detail.type, + description: customEvent.detail.description, + status: customEvent.detail.status || "Unknown", + timestamp: customEvent.detail.timestamp ? new Date(customEvent.detail.timestamp) : undefined, + latitude: customEvent.detail.latitude, + longitude: customEvent.detail.longitude, + address: customEvent.detail.address, + } + } else { + for (const crime of crimes) { + for (const incident of crime.crime_incidents) { + if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) { + console.log("Found matching incident:", incident) + foundIncident = { + id: incident.id, + district: crime.districts.name, + description: incident.description, + status: incident.status || "unknown", + timestamp: incident.timestamp, + category: incident.crime_categories.name, + type_category: incident.crime_categories.type, + address: incident.locations.address, + latitude: incident.locations.latitude, + longitude: incident.locations.longitude, + } + break + } + } + if (foundIncident) break + } + } - if (!foundIncident.latitude || !foundIncident.longitude) { - console.error("Found incident has invalid coordinates:", foundIncident) - return - } - - console.log("Setting selected incident:", foundIncident) - - setSelectedDistrict(null) - selectedDistrictRef.current = null - setFocusedDistrictId(null) - - setSelectedIncident(foundIncident) + if (!foundIncident) { + console.error("Could not find incident with ID:", incidentId) + isInteractingWithMarker.current = false + return } + if (!foundIncident.latitude || !foundIncident.longitude) { + console.error("Found incident has invalid coordinates:", foundIncident) + isInteractingWithMarker.current = false + return + } + + console.log("Setting selected incident:", foundIncident) + + // Clear district selection when showing an incident + setSelectedDistrict(null) + selectedDistrictRef.current = null + setFocusedDistrictId(null) + + setSelectedIncident(foundIncident) + + // Reset the marker interaction flag after a delay + setTimeout(() => { + isInteractingWithMarker.current = false + }, 1000) + } + mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener) document.addEventListener("incident_click", handleIncidentClickEvent as EventListener) @@ -331,6 +351,31 @@ export default function Layers({ } }, [mapboxMap, crimes, setFocusedDistrictId]) + // Add a listener for unit clicks to set the marker interaction flag + useEffect(() => { + if (!mapboxMap) return + + const handleUnitClickEvent = (e: Event) => { + // Set the marker interaction flag to prevent district selection + isInteractingWithMarker.current = true + + // Reset the flag after a delay + setTimeout(() => { + isInteractingWithMarker.current = false + }, 1000) + } + + mapboxMap.getCanvas().addEventListener("unit_click", handleUnitClickEvent as EventListener) + document.addEventListener("unit_click", handleUnitClickEvent as EventListener) + + return () => { + if (mapboxMap && mapboxMap.getCanvas()) { + mapboxMap.getCanvas().removeEventListener("unit_click", handleUnitClickEvent as EventListener) + } + document.removeEventListener("unit_click", handleUnitClickEvent as EventListener) + } + }, [mapboxMap]) + useEffect(() => { if (selectedDistrictRef.current) { const districtId = selectedDistrictRef.current.id @@ -339,64 +384,64 @@ export default function Layers({ if (districtCrime) { const selectedYearNum = year ? Number.parseInt(year) : new Date().getFullYear() - let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum) + let demographics = districtCrime.districts.demographics?.find((d) => d.year === selectedYearNum) - if (!demographics && districtCrime.districts.demographics?.length) { - demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0] - } + if (!demographics && districtCrime.districts.demographics?.length) { + demographics = districtCrime.districts.demographics.sort((a, b) => b.year - a.year)[0] + } - let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum) + let geographics = districtCrime.districts.geographics?.find((g) => g.year === selectedYearNum) - if (!geographics && districtCrime.districts.geographics?.length) { - const validGeographics = districtCrime.districts.geographics - .filter((g) => g.year !== null) - .sort((a, b) => (b.year || 0) - (a.year || 0)) + if (!geographics && districtCrime.districts.geographics?.length) { + const validGeographics = districtCrime.districts.geographics + .filter((g) => g.year !== null) + .sort((a, b) => (b.year || 0) - (a.year || 0)) - geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0] - } + geographics = validGeographics.length > 0 ? validGeographics[0] : districtCrime.districts.geographics[0] + } - if (!demographics || !geographics) { - console.error("Missing district data:", { demographics, geographics }) - return - } + if (!demographics || !geographics) { + console.error("Missing district data:", { demographics, geographics }) + return + } - const crime_incidents = districtCrime.crime_incidents - .filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory) - .map((incident) => ({ - id: incident.id, - timestamp: incident.timestamp, - description: incident.description, - status: incident.status || "", - category: incident.crime_categories.name, - type: incident.crime_categories.type || "", - address: incident.locations.address || "", - latitude: incident.locations.latitude, - longitude: incident.locations.longitude, - })) + const crime_incidents = districtCrime.crime_incidents + .filter((incident) => filterCategory === "all" || incident.crime_categories.name === filterCategory) + .map((incident) => ({ + id: incident.id, + timestamp: incident.timestamp, + description: incident.description, + status: incident.status || "", + category: incident.crime_categories.name, + type: incident.crime_categories.type || "", + address: incident.locations.address || "", + latitude: incident.locations.latitude, + longitude: incident.locations.longitude, + })) - const updatedDistrict: IDistrictFeature = { - ...selectedDistrictRef.current, - number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0, - level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level, - demographics: { - number_of_unemployed: demographics.number_of_unemployed, - population: demographics.population, - population_density: demographics.population_density, - year: demographics.year, - }, - geographics: { - address: geographics.address || "", - land_area: geographics.land_area || 0, - year: geographics.year || 0, - latitude: geographics.latitude, - longitude: geographics.longitude, - }, - crime_incidents, - selectedYear: year, - selectedMonth: month, - } + const updatedDistrict: IDistrictFeature = { + ...selectedDistrictRef.current, + number_of_crime: crimeDataByDistrict[districtId]?.number_of_crime || 0, + level: crimeDataByDistrict[districtId]?.level || selectedDistrictRef.current.level, + demographics: { + number_of_unemployed: demographics.number_of_unemployed, + population: demographics.population, + population_density: demographics.population_density, + year: demographics.year, + }, + geographics: { + address: geographics.address || "", + land_area: geographics.land_area || 0, + year: geographics.year || 0, + latitude: geographics.latitude, + longitude: geographics.longitude, + }, + crime_incidents, + selectedYear: year, + selectedMonth: month, + } - selectedDistrictRef.current = updatedDistrict + selectedDistrictRef.current = updatedDistrict setSelectedDistrict((prevDistrict) => { if ( @@ -414,20 +459,40 @@ export default function Layers({ const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => { console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick) + + // If this is from a marker click, set the marker interaction flag + if (isMarkerClick) { + isInteractingWithMarker.current = true + + // Reset the flag after a delay + setTimeout(() => { + isInteractingWithMarker.current = false + }, 1000) + } + setFocusedDistrictId(id) }, []) - if (!visible) return null - - const crimesVisible = activeControl === "incidents" + const crimesVisible = activeControl === "incidents" const showHeatmapLayer = activeControl === "heatmap" && sourceType !== "cbu" const showUnitsLayer = activeControl === "units" const showTimelineLayer = activeControl === "timeline" const showHistoricalLayer = activeControl === "historical" const showRecentIncidents = activeControl === "recents" - const showDistrictFill = activeControl === "incidents" || activeControl === "clusters" || activeControl === "historical" || activeControl === "recents" + const showDistrictFill = + activeControl === "incidents" || + activeControl === "clusters" || + activeControl === "historical" || + activeControl === "recents" const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" && sourceType !== "cbu" + // Ensure showPanicDemo is always defined + // const [displayPanicDemo, setDisplayPanicDemo] = useState(showEWS && showPanicDemo) + + // Always render the DistrictExtrusionLayer when a district is focused + // This ensures it's available when needed + const shouldShowExtrusion = focusedDistrictId !== null && !isInteractingWithMarker.current + return ( <> + {/* Always render the extrusion layer when a district is focused */} + {shouldShowExtrusion && ( + + )} + {/* Recent Incidents Layer (24 hours) */} - + - {selectedDistrict && !selectedIncident && ( - <> - - - - + {selectedDistrict && !selectedIncident && !isInteractingWithMarker.current && ( + )} - {/* */} + {showEWS && } - {showEWS && ( - - )} - - {showEWS && showPanicDemo && ( + {showEWS && displayPanicDemo && (
inc.status === 'active')} + activeIncidents={ewsIncidents.filter((inc) => inc.status === "active")} />
)} diff --git a/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx b/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx index 3c8cb2c..297fbbd 100644 --- a/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx +++ b/sigap-website/app/_components/map/layers/recent-incidents-layer.tsx @@ -9,234 +9,229 @@ interface RecentIncidentsLayerProps { incidents?: IIncidentLogs[] } -export default function RecentIncidentsLayer({ - visible = false, - map, - incidents = [], -}: RecentIncidentsLayerProps) { - const isInteractingWithMarker = useRef(false); +export default function RecentIncidentsLayer({ visible = false, map, incidents = [] }: RecentIncidentsLayerProps) { + const isInteractingWithMarker = useRef(false) // Filter incidents from the last 24 hours - const recentIncidents = incidents.filter(incident => { - if (!incident.timestamp) return false; - const incidentDate = new Date(incident.timestamp); - const now = new Date(); - const timeDiff = now.getTime() - incidentDate.getTime(); - // 86400000 = 24 hours in milliseconds - return timeDiff <= 86400000; - }); + const recentIncidents = incidents.filter((incident) => { + if (!incident.timestamp) return false + const incidentDate = new Date(incident.timestamp) + const now = new Date() + const timeDiff = now.getTime() - incidentDate.getTime() + // 86400000 = 24 hours in milliseconds + return timeDiff <= 86400000 + }) const handleIncidentClick = useCallback( (e: any) => { - if (!map) return; + if (!map) return - const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] }); - if (!features || features.length === 0) return; + const features = map.queryRenderedFeatures(e.point, { layers: ["recent-incidents"] }) + if (!features || features.length === 0) return - isInteractingWithMarker.current = true; + // Stop event propagation + e.originalEvent.stopPropagation() + e.preventDefault() - const incident = features[0]; - if (!incident.properties) return; + isInteractingWithMarker.current = true - e.originalEvent.stopPropagation(); - e.preventDefault(); + const incident = features[0] + if (!incident.properties) return - const incidentDetails = { - id: incident.properties.id, - description: incident.properties.description, - status: incident.properties?.status || "Active", - longitude: (incident.geometry as any).coordinates[0], - latitude: (incident.geometry as any).coordinates[1], - timestamp: new Date(incident.properties.timestamp || Date.now()), - category: incident.properties.category, - }; + e.originalEvent.stopPropagation() + e.preventDefault() - console.log("Recent incident clicked:", incidentDetails); + const incidentDetails = { + id: incident.properties.id, + description: incident.properties.description, + status: incident.properties?.status || "Active", + longitude: (incident.geometry as any).coordinates[0], + latitude: (incident.geometry as any).coordinates[1], + timestamp: new Date(incident.properties.timestamp || Date.now()), + category: incident.properties.category, + } - // Ensure markers stay visible - if (map.getLayer("recent-incidents")) { - map.setLayoutProperty("recent-incidents", "visibility", "visible"); - } + console.log("Recent incident clicked:", incidentDetails) - // First fly to the incident location - map.flyTo({ - center: [incidentDetails.longitude, incidentDetails.latitude], - zoom: 15, - bearing: 0, - pitch: 45, - duration: 2000, - }); + // Ensure markers stay visible + if (map.getLayer("recent-incidents")) { + map.setLayoutProperty("recent-incidents", "visibility", "visible") + } - // Dispatch the incident_click event to show the popup - const customEvent = new CustomEvent("incident_click", { - detail: incidentDetails, - bubbles: true, - }); + // First fly to the incident location + map.flyTo({ + center: [incidentDetails.longitude, incidentDetails.latitude], + zoom: 15, + bearing: 0, + pitch: 45, + duration: 2000, + }) - map.getCanvas().dispatchEvent(customEvent); - document.dispatchEvent(customEvent); + // Dispatch the incident_click event to show the popup + const customEvent = new CustomEvent("incident_click", { + detail: incidentDetails, + bubbles: true, + }) - // Reset the flag after a delay - setTimeout(() => { - isInteractingWithMarker.current = false; - }, 5000); - }, - [map] - ); + map.getCanvas().dispatchEvent(customEvent) + document.dispatchEvent(customEvent) + + // Reset the flag after a delay + setTimeout(() => { + isInteractingWithMarker.current = false + }, 5000) + }, + [map], + ) useEffect(() => { - if (!map || !visible) return; + if (!map || !visible) return - console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`); + console.log(`Setting up recent incidents layer with ${recentIncidents.length} incidents from the last 24 hours`) - // Convert incidents to GeoJSON - const recentData = { - type: "FeatureCollection" as const, - features: recentIncidents.map(incident => ({ - type: "Feature" as const, - geometry: { - type: "Point" as const, - coordinates: [incident.longitude, incident.latitude], - }, - properties: { - id: incident.id, - user_id: incident.user_id, - address: incident.address, - description: incident.description, - timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(), - category: incident.category, - district: incident.district, - severity: incident.severity, - status: incident.verified, - source: incident.source, - }, - })), - }; + // Convert incidents to GeoJSON + const recentData = { + type: "FeatureCollection" as const, + features: recentIncidents.map((incident) => ({ + type: "Feature" as const, + geometry: { + type: "Point" as const, + coordinates: [incident.longitude, incident.latitude], + }, + properties: { + id: incident.id, + user_id: incident.user_id, + address: incident.address, + description: incident.description, + timestamp: incident.timestamp ? incident.timestamp.toString() : new Date().toString(), + category: incident.category, + district: incident.district, + severity: incident.severity, + status: incident.verified, + source: incident.source, + }, + })), + } - const setupLayerAndSource = () => { - try { - // Check if source exists and update it - if (map.getSource("recent-incidents-source")) { - (map.getSource("recent-incidents-source") as any).setData(recentData); - } else { - // If not, add source - map.addSource("recent-incidents-source", { - type: "geojson", - data: recentData, - }); - } - - // Find first symbol layer for proper layering - const layers = map.getStyle().layers; - let firstSymbolId: string | undefined; - for (const layer of layers) { - if (layer.type === "symbol") { - firstSymbolId = layer.id; - break; - } - } - - // Check if layer exists already - if (!map.getLayer("recent-incidents")) { - map.addLayer({ - id: "recent-incidents", - type: "circle", - source: "recent-incidents-source", - paint: { - "circle-color": "#FF5252", // Red color for recent incidents - "circle-radius": [ - "interpolate", - ["linear"], - ["zoom"], - 7, 4, // Slightly larger at lower zooms for visibility - 12, 8, - 15, 12, // Larger maximum size - ], - "circle-stroke-width": 2, - "circle-stroke-color": "#FFFFFF", - "circle-opacity": 0.8, - // Add a pulsing effect - "circle-stroke-opacity": [ - "interpolate", - ["linear"], - ["zoom"], - 7, 0.5, - 15, 0.8 - ], - }, - layout: { - visibility: visible ? "visible" : "none", - } - }, firstSymbolId); - - // Add a glow effect with a larger circle behind - map.addLayer({ - id: "recent-incidents-glow", - type: "circle", - source: "recent-incidents-source", - paint: { - "circle-color": "#FF5252", - "circle-radius": [ - "interpolate", - ["linear"], - ["zoom"], - 7, 6, - 12, 12, - 15, 18, - ], - "circle-opacity": 0.2, - "circle-blur": 1, - }, - layout: { - visibility: visible ? "visible" : "none", - } - }, "recent-incidents"); - - // Add mouse events - map.on("mouseenter", "recent-incidents", () => { - map.getCanvas().style.cursor = "pointer"; - }); - - map.on("mouseleave", "recent-incidents", () => { - map.getCanvas().style.cursor = ""; - }); - } else { - // Update existing layer visibility - map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none"); - map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none"); - } - - // Ensure click handler is properly registered - map.off("click", "recent-incidents", handleIncidentClick); - map.on("click", "recent-incidents", handleIncidentClick); - - } catch (error) { - console.error("Error setting up recent incidents layer:", error); - } - }; - - // Check if style is loaded and set up layer accordingly - if (map.isStyleLoaded()) { - setupLayerAndSource(); + const setupLayerAndSource = () => { + try { + // Check if source exists and update it + if (map.getSource("recent-incidents-source")) { + ; (map.getSource("recent-incidents-source") as any).setData(recentData) } else { - map.once("style.load", setupLayerAndSource); - - // Fallback - setTimeout(() => { - if (map.isStyleLoaded()) { - setupLayerAndSource(); - } else { - console.warn("Map style still not loaded after timeout"); - } - }, 1000); + // If not, add source + map.addSource("recent-incidents-source", { + type: "geojson", + data: recentData, + }) } - return () => { - if (map) { - map.off("click", "recent-incidents", handleIncidentClick); + // Find first symbol layer for proper layering + const layers = map.getStyle().layers + let firstSymbolId: string | undefined + for (const layer of layers) { + if (layer.type === "symbol") { + firstSymbolId = layer.id + break } - }; - }, [map, visible, recentIncidents, handleIncidentClick]); + } - return null; + // Check if layer exists already + if (!map.getLayer("recent-incidents")) { + map.addLayer( + { + id: "recent-incidents", + type: "circle", + source: "recent-incidents-source", + paint: { + "circle-color": "#FF5252", // Red color for recent incidents + "circle-radius": [ + "interpolate", + ["linear"], + ["zoom"], + 7, + 4, // Slightly larger at lower zooms for visibility + 12, + 8, + 15, + 12, // Larger maximum size + ], + "circle-stroke-width": 2, + "circle-stroke-color": "#FFFFFF", + "circle-opacity": 0.8, + // Add a pulsing effect + "circle-stroke-opacity": ["interpolate", ["linear"], ["zoom"], 7, 0.5, 15, 0.8], + }, + layout: { + visibility: visible ? "visible" : "none", + }, + }, + firstSymbolId, + ) + + // Add a glow effect with a larger circle behind + map.addLayer( + { + id: "recent-incidents-glow", + type: "circle", + source: "recent-incidents-source", + paint: { + "circle-color": "#FF5252", + "circle-radius": ["interpolate", ["linear"], ["zoom"], 7, 6, 12, 12, 15, 18], + "circle-opacity": 0.2, + "circle-blur": 1, + }, + layout: { + visibility: visible ? "visible" : "none", + }, + }, + "recent-incidents", + ) + + // Add mouse events + map.on("mouseenter", "recent-incidents", () => { + map.getCanvas().style.cursor = "pointer" + }) + + map.on("mouseleave", "recent-incidents", () => { + map.getCanvas().style.cursor = "" + }) + } else { + // Update existing layer visibility + map.setLayoutProperty("recent-incidents", "visibility", visible ? "visible" : "none") + map.setLayoutProperty("recent-incidents-glow", "visibility", visible ? "visible" : "none") + } + + // Ensure click handler is properly registered + map.off("click", "recent-incidents", handleIncidentClick) + map.on("click", "recent-incidents", handleIncidentClick) + } catch (error) { + console.error("Error setting up recent incidents layer:", error) + } + } + + // Check if style is loaded and set up layer accordingly + if (map.isStyleLoaded()) { + setupLayerAndSource() + } else { + map.once("style.load", setupLayerAndSource) + + // Fallback + setTimeout(() => { + if (map.isStyleLoaded()) { + setupLayerAndSource() + } else { + console.warn("Map style still not loaded after timeout") + } + }, 1000) + } + + return () => { + if (map) { + map.off("click", "recent-incidents", handleIncidentClick) + } + } + }, [map, visible, recentIncidents, handleIncidentClick]) + + return null } diff --git a/sigap-website/app/_components/map/layers/timeline-layer.tsx b/sigap-website/app/_components/map/layers/timeline-layer.tsx index 46e619e..5eb15d6 100644 --- a/sigap-website/app/_components/map/layers/timeline-layer.tsx +++ b/sigap-website/app/_components/map/layers/timeline-layer.tsx @@ -7,7 +7,6 @@ import type mapboxgl from "mapbox-gl" import { format } from "date-fns" import { calculateAverageTimeOfDay } from "@/app/_utils/time" import TimelinePopup from "../pop-up/timeline-popup" -import TimeZonesDisplay from "./timezone" interface TimelineLayerProps { crimes: ICrimes[] @@ -45,74 +44,74 @@ export default function TimelineLayer({ } >() - crimes.forEach((crime) => { - if (!crime.districts || !crime.district_id) return + crimes.forEach((crime) => { + if (!crime.districts || !crime.district_id) return - // Initialize district group if not exists - if (!districtGroups.has(crime.district_id)) { - // Find a central location for the district from any incident - const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude) + // Initialize district group if not exists + if (!districtGroups.has(crime.district_id)) { + // Find a central location for the district from any incident + const centerIncident = crime.crime_incidents.find((inc) => inc.locations?.latitude && inc.locations?.longitude) - const center: [number, number] = centerIncident - ? [centerIncident.locations.longitude, centerIncident.locations.latitude] - : [0, 0] + const center: [number, number] = centerIncident + ? [centerIncident.locations.longitude, centerIncident.locations.latitude] + : [0, 0] - districtGroups.set(crime.district_id, { - districtId: crime.district_id, - districtName: crime.districts.name, - incidents: [], - center, + districtGroups.set(crime.district_id, { + districtId: crime.district_id, + districtName: crime.districts.name, + incidents: [], + center, + }) + } + + // Filter incidents appropriately before adding + crime.crime_incidents.forEach((incident) => { + // Skip invalid incidents + if (!incident.timestamp) return + if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return + + // Add to appropriate district group + const group = districtGroups.get(crime.district_id) + if (group) { + group.incidents.push({ + timestamp: new Date(incident.timestamp), + category: incident.crime_categories.name, }) - } + } + }) + }) - // Filter incidents appropriately before adding - crime.crime_incidents.forEach((incident) => { - // Skip invalid incidents - if (!incident.timestamp) return - if (filterCategory !== "all" && incident.crime_categories.name !== filterCategory) return + // Calculate average time for each district + const result = Array.from(districtGroups.values()) + .filter((group) => group.incidents.length > 0 && group.center[0] !== 0) + .map((group) => { + const avgTimeInfo = calculateAverageTimeOfDay(group.incidents.map((inc) => inc.timestamp)) - // Add to appropriate district group - const group = districtGroups.get(crime.district_id) - if (group) { - group.incidents.push({ - timestamp: new Date(incident.timestamp), - category: incident.crime_categories.name, - }) - } - }) - }) + return { + id: group.districtId, + name: group.districtName, + center: group.center, + avgHour: avgTimeInfo.hour, + avgMinute: avgTimeInfo.minute, + formattedTime: avgTimeInfo.formattedTime, + timeDescription: avgTimeInfo.description, + totalIncidents: group.incidents.length, + timeOfDay: avgTimeInfo.timeOfDay, + earliestTime: format(avgTimeInfo.earliest, "p"), + latestTime: format(avgTimeInfo.latest, "p"), + mostFrequentHour: avgTimeInfo.mostFrequentHour, + categoryCounts: group.incidents.reduce( + (acc, inc) => { + acc[inc.category] = (acc[inc.category] || 0) + 1 + return acc + }, + {} as Record, + ), + } + }) - // Calculate average time for each district - const result = Array.from(districtGroups.values()) - .filter((group) => group.incidents.length > 0 && group.center[0] !== 0) - .map((group) => { - const avgTimeInfo = calculateAverageTimeOfDay(group.incidents.map((inc) => inc.timestamp)) - - return { - id: group.districtId, - name: group.districtName, - center: group.center, - avgHour: avgTimeInfo.hour, - avgMinute: avgTimeInfo.minute, - formattedTime: avgTimeInfo.formattedTime, - timeDescription: avgTimeInfo.description, - totalIncidents: group.incidents.length, - timeOfDay: avgTimeInfo.timeOfDay, - earliestTime: format(avgTimeInfo.earliest, "p"), - latestTime: format(avgTimeInfo.latest, "p"), - mostFrequentHour: avgTimeInfo.mostFrequentHour, - categoryCounts: group.incidents.reduce( - (acc, inc) => { - acc[inc.category] = (acc[inc.category] || 0) + 1 - return acc - }, - {} as Record, - ), - } - }) - - return result - }, [crimes, filterCategory, year, month]) + return result + }, [crimes, filterCategory, year, month]) // Convert processed data to GeoJSON for display const timelineGeoJSON = useMemo(() => { @@ -143,30 +142,34 @@ export default function TimelineLayer({ (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return - const feature = e.features[0] - const props = feature.properties - if (!props) return + // Stop event propagation + e.originalEvent.stopPropagation() + e.preventDefault() - // Get the corresponding district data for detailed info - const districtData = districtTimeData.find((d) => d.id === props.id) - if (!districtData) return + const feature = e.features[0] + const props = feature.properties + if (!props) return - // Fly to the location - if (map) { - map.flyTo({ - center: districtData.center, - zoom: 12, - duration: 1000, - pitch: 45, - bearing: 0, - }) - } + // Get the corresponding district data for detailed info + const districtData = districtTimeData.find((d) => d.id === props.id) + if (!districtData) return - // Set the selected district for popup - setSelectedDistrict(districtData) - }, - [map, districtTimeData], - ) + // Fly to the location + if (map) { + map.flyTo({ + center: districtData.center, + zoom: 12, + duration: 1000, + pitch: 45, + bearing: 0, + }) + } + + // Set the selected district for popup + setSelectedDistrict(districtData) + }, + [map, districtTimeData], + ) // Handle popup close const handleClosePopup = useCallback(() => { @@ -177,45 +180,45 @@ export default function TimelineLayer({ useEffect(() => { if (!map || !visible) return - // Hide incident markers when timeline mode is activated - if (map.getLayer("unclustered-point")) { - map.setLayoutProperty("unclustered-point", "visibility", "none") - } + // Hide incident markers when timeline mode is activated + if (map.getLayer("unclustered-point")) { + map.setLayoutProperty("unclustered-point", "visibility", "none") + } - // Hide clusters when timeline mode is activated - if (map.getLayer("clusters")) { - map.setLayoutProperty("clusters", "visibility", "none") - } + // Hide clusters when timeline mode is activated + if (map.getLayer("clusters")) { + map.setLayoutProperty("clusters", "visibility", "none") + } - if (map.getLayer("cluster-count")) { - map.setLayoutProperty("cluster-count", "visibility", "none") - } + if (map.getLayer("cluster-count")) { + map.setLayoutProperty("cluster-count", "visibility", "none") + } - // Set up event handlers - const handleMouseEnter = () => { - if (map) map.getCanvas().style.cursor = "pointer" - } + // Set up event handlers + const handleMouseEnter = () => { + if (map) map.getCanvas().style.cursor = "pointer" + } - const handleMouseLeave = () => { - if (map) map.getCanvas().style.cursor = "" - } + const handleMouseLeave = () => { + if (map) map.getCanvas().style.cursor = "" + } - // Add event listeners - if (map.getLayer("timeline-markers")) { - map.on("click", "timeline-markers", handleMarkerClick) - map.on("mouseenter", "timeline-markers", handleMouseEnter) - map.on("mouseleave", "timeline-markers", handleMouseLeave) - } + // Add event listeners + if (map.getLayer("timeline-markers")) { + map.on("click", "timeline-markers", handleMarkerClick) + map.on("mouseenter", "timeline-markers", handleMouseEnter) + map.on("mouseleave", "timeline-markers", handleMouseLeave) + } - return () => { - // Clean up event listeners - if (map) { - map.off("click", "timeline-markers", handleMarkerClick) - map.off("mouseenter", "timeline-markers", handleMouseEnter) - map.off("mouseleave", "timeline-markers", handleMouseLeave) - } - } - }, [map, visible, handleMarkerClick]) + return () => { + // Clean up event listeners + if (map) { + map.off("click", "timeline-markers", handleMarkerClick) + map.off("mouseenter", "timeline-markers", handleMouseEnter) + map.off("mouseleave", "timeline-markers", handleMouseLeave) + } + } + }, [map, visible, handleMarkerClick]) // Clean up on unmount or when visibility changes useEffect(() => { @@ -254,44 +257,42 @@ export default function TimelineLayer({ }} /> - {/* Digital clock display */} - - + {/* Digital clock display */} + + - {/* Custom Popup Component */} - {selectedDistrict && ( - - )} - - - - ) + {/* Custom Popup Component */} + {selectedDistrict && ( + + )} + + ) } diff --git a/sigap-website/app/_components/map/layers/units-layer.tsx b/sigap-website/app/_components/map/layers/units-layer.tsx index be7b369..30bbe4f 100644 --- a/sigap-website/app/_components/map/layers/units-layer.tsx +++ b/sigap-website/app/_components/map/layers/units-layer.tsx @@ -64,40 +64,39 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible // Process units data to GeoJSON format const unitsGeoJSON = useMemo(() => { - console.log("Units data being processed:", unitsData); // Debug log + console.log("Units data being processed:", unitsData) // Debug log return { type: "FeatureCollection" as const, - features: unitsData - .map((unit) => { - // Debug log for individual units - console.log("Processing unit:", unit.code_unit, unit.name, { - longitude: unit.longitude, - latitude: unit.latitude, - district: unit.district_name - }); - - return { - type: "Feature" as const, - properties: { - id: unit.code_unit, - name: unit.name, - address: unit.address, - phone: unit.phone, - type: unit.type, - district: unit.district_name || "", - district_id: unit.district_id, - }, - geometry: { - type: "Point" as const, - coordinates: [ - parseFloat(String(unit.longitude)) || 0, - parseFloat(String(unit.latitude)) || 0 - ], - }, - }; + features: unitsData.map((unit) => { + // Debug log for individual units + console.log("Processing unit:", unit.code_unit, unit.name, { + longitude: unit.longitude, + latitude: unit.latitude, + district: unit.district_name, }) - }; + + return { + type: "Feature" as const, + properties: { + id: unit.code_unit, + name: unit.name, + address: unit.address, + phone: unit.phone, + type: unit.type, + district: unit.district_name || "", + district_id: unit.district_id, + }, + geometry: { + type: "Point" as const, + coordinates: [ + Number.parseFloat(String(unit.longitude)) || 0, + Number.parseFloat(String(unit.latitude)) || 0, + ], + }, + } + }), + } }, [unitsData]) // Process incident data to GeoJSON format @@ -225,6 +224,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return + // Stop event propagation to prevent district layer from handling this click e.originalEvent.stopPropagation() e.preventDefault() @@ -236,27 +236,26 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible // Find the unit in our data const unit = unitsData.find((u) => u.code_unit === properties.id) if (!unit) { - console.log("Unit not found in data:", properties.id); - return; + console.log("Unit not found in data:", properties.id) + return } + setIsLoading(true) // Find all incidents in the same district as the unit const districtIncidents: IDistrictIncidents[] = [] - crimes.forEach(crime => { + crimes.forEach((crime) => { // Check if this crime is in the same district as the unit - console.log("Checking district ID:", crime.district_id, "against unit district ID:", unit.district_id); - - if (crime.districts.name === unit.district_name) { - crime.crime_incidents.forEach(incident => { - if (incident.locations && typeof incident.locations.distance_to_unit !== 'undefined') { + if (selectedUnit?.code_unit === unit.code_unit) { + crime.crime_incidents.forEach((incident) => { + if (incident.locations && typeof incident.locations.distance_to_unit !== "undefined") { districtIncidents.push({ incident_id: incident.id, category_name: incident.crime_categories.name, - incident_description: incident.description || 'No description', + incident_description: incident.description || "No description", distance_meters: incident.locations.distance_to_unit!, - timestamp: incident.timestamp + timestamp: incident.timestamp, }) } }) @@ -266,12 +265,11 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible // Sort by distance (closest first) districtIncidents.sort((a, b) => a.distance_meters - b.distance_meters) - - console.log("Sorted district incidents:", districtIncidents); + console.log("Sorted district incidents:", districtIncidents) // Update the state with the distance results setUnitIncident(districtIncidents) - + setIsLoading(false) // Fly to the unit location map.flyTo({ @@ -325,6 +323,7 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible (e: mapboxgl.MapMouseEvent & { features?: mapboxgl.MapboxGeoJSONFeature[] }) => { if (!e.features || e.features.length === 0) return + // Stop event propagation e.originalEvent.stopPropagation() e.preventDefault() @@ -430,7 +429,10 @@ export default function UnitsLayer({ crimes, units = [], filterCategory, visible if (!map || !visible) return // Debug log to confirm map layers - console.log("Available map layers:", map.getStyle().layers?.map(l => l.id)); + console.log( + "Available map layers:", + map.getStyle().layers?.map((l) => l.id), + ) // Define event handlers that can be referenced for both adding and removing const handleMouseEnter = () => { diff --git a/sigap-website/app/_components/map/pop-up/unit-popup.tsx b/sigap-website/app/_components/map/pop-up/unit-popup.tsx index 38a6181..2cea43b 100644 --- a/sigap-website/app/_components/map/pop-up/unit-popup.tsx +++ b/sigap-website/app/_components/map/pop-up/unit-popup.tsx @@ -62,12 +62,12 @@ export default function UnitPopup({ closeOnClick={false} onClose={onClose} anchor="top" - maxWidth="420px" + maxWidth="320px" className="unit-popup z-50" >
{/* Custom close button */} @@ -172,7 +172,28 @@ export default function UnitPopup({
- + {/* Connection line */} +
+ {/* Connection dot */} +
) diff --git a/sigap-website/app/_styles/ui.css b/sigap-website/app/_styles/ui.css index 9b25029..491d06c 100644 --- a/sigap-website/app/_styles/ui.css +++ b/sigap-website/app/_styles/ui.css @@ -1,1265 +1,1151 @@ +/* ========================================================== + SIGAP UI CSS + ---------------------------------------------------------- + Style untuk komponen utama, animasi, dan utilitas website + ---------------------------------------------------------- + Author: SIGAP Team + Last Update: 2024 + ========================================================== +*/ + +/* ========================= + 1. ROOT COLOR VARIABLES + ========================= */ :root { - --orange: #fa0; - --red: red; - --glow-rgb: 255, 102, 0; - --text-color: #fa0; - --danger-fill-color: #f23; - --danger-glow-rgb: 255, 0, 0; - --danger-text-color: #f23; - --gutter-size: 8px; + --orange: #fa0; + --red: red; + --glow-rgb: 255, 102, 0; + --text-color: #fa0; + --danger-fill-color: #f23; + --danger-glow-rgb: 255, 0, 0; + --danger-text-color: #f23; + --gutter-size: 8px; } -.red-color { - color: var(--red); -} - -.red-bg { - background-color: var(--red); -} - -.red-border { - border: 1px solid var(--red); -} +/* ========================= + 2. COLOR UTILITY CLASSES + ========================= */ +.red-color { color: var(--red); } +.red-bg { background-color: var(--red); } +.red-border { border: 1px solid var(--red); } +/* ========================= + 3. STRIP BAR & ANIMATIONS + ========================= */ +/* --- Orange Stripe --- */ .strip-bar { - /* background-image: url("/images/strip.svg"); - background-size: 17%; */ - width: max(200vw, 2000px); - height: 30px; - display: inline-block; - margin-bottom: -5px; + width: max(200vw,2000px); + height: 30px; + display: inline-block; + margin-bottom: -5px; - --stripe-color: var(--orange); - --stripe-size: 15px; - --glow-color: rgba(255, 94, 0, 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient( - -45deg, - /* glow boundary */ var(--glow-color) calc(-1 * var(--glow-size)), - /* fade into foreground */ var(--stripe-color) 0, - /* fade from foreground */ var(--stripe-color) - calc(var(--stripe-size) - var(--glow-size) / 2), - /* glow boundary */ var(--glow-color) - calc(var(--stripe-size) + var(--glow-size) / 2), - /* fade to background */ transparent - calc(var(--stripe-size) + var(--glow-size) / 2), - /* fade from background */ transparent calc(2 * var(--stripe-size)), - /* glow boundary */ var(--glow-color) - calc(2 * var(--stripe-size) - var(--glow-size)) - ); + --stripe-color: var(--orange); + --stripe-size: 15px; + --glow-color: rgba(255, 94, 0, .8); + --glow-size: 3px; + background-image: repeating-linear-gradient(-45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); } +/* --- Red Stripe --- */ .strip-bar-red { - /* background-image: url("/images/strip.svg"); - background-size: 17%; */ - width: max(200vw, 2000px); - height: 30px; - display: inline-block; - margin-bottom: -5px; + width: max(200vw,2000px); + height: 30px; + display: inline-block; + margin-bottom: -5px; - --stripe-color: var(--red); - --stripe-size: 15px; - --glow-color: rgba(255, 17, 0, 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient( - -45deg, - /* glow boundary */ var(--glow-color) calc(-1 * var(--glow-size)), - /* fade into foreground */ var(--stripe-color) 0, - /* fade from foreground */ var(--stripe-color) - calc(var(--stripe-size) - var(--glow-size) / 2), - /* glow boundary */ var(--glow-color) - calc(var(--stripe-size) + var(--glow-size) / 2), - /* fade to background */ transparent - calc(var(--stripe-size) + var(--glow-size) / 2), - /* fade from background */ transparent calc(2 * var(--stripe-size)), - /* glow boundary */ var(--glow-color) - calc(2 * var(--stripe-size) - var(--glow-size)) - ); + --stripe-color: var(--red); + --stripe-size: 15px; + --glow-color: rgba(255, 17, 0, 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient(-45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); } +/* --- Vertical Orange Stripe --- */ .strip-bar-vertical { - height: 200vw; - transform: translate3d(0, 0, 0); - --stripe-color: var(--orange); - --stripe-size: 15px; - --glow-color: rgba(255, 94, 0, 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient( - 45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) - ); + height: 200vw; + transform: translate3d(0, 0, 0); + --stripe-color: var(--orange); + --stripe-size: 15px; + --glow-color: rgba(255, 94, 0, .8); + --glow-size: 3px; + background-image: repeating-linear-gradient(45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); } +/* --- Vertical Red Stripe --- */ .strip-bar-red-vertical { - height: 200vw; - transform: translate3d(0, 0, 0); - --stripe-color: var(--red); - --stripe-size: 15px; - --glow-color: rgba(255, 17, 0, 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient( - 45deg, - var(--glow-color) calc(-1 * var(--glow-size)), - var(--stripe-color) 0, - var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), - var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(var(--stripe-size) + var(--glow-size) / 2), - transparent calc(2 * var(--stripe-size)), - var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size)) - ); + height: 200vw; + transform: translate3d(0, 0, 0); + --stripe-color: var(--red); + --stripe-size: 15px; + --glow-color: rgba(255, 17, 0, 0.8); + --glow-size: 3px; + background-image: repeating-linear-gradient(45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); } +/* --- Animations for Stripe --- */ @keyframes slideinBg { - from { - background-position: top; - } - to { - background-position: -100px 0px; - } + from {background-position: top; } + to {background-position: -100px 0px;} } .strip-animation-vertical { - animation: stripAnimationVertical 15s infinite linear; + animation: stripAnimationVertical 15s infinite linear; } .strip-animation-vertical-reverse { - animation: stripAnimationVertical 15s infinite linear reverse; + animation: stripAnimationVertical 15s infinite linear reverse; } .strip-animation { - animation: stripAnimation 10s infinite linear; + animation: stripAnimation 10s infinite linear; } .strip-animation-reverse { - animation: stripAnimation 10s infinite linear reverse; + animation: stripAnimation 10s infinite linear reverse; } @keyframes stripAnimationVertical { - 100% { - /* background-position: 0px 100px; */ - transform: translateY(-66%); - } + 100% { + transform: translateY(-66%); + } } @keyframes stripAnimation { - 100% { - /* background-position: 0px 100px; */ - transform: translateX(-66%); - } + 100% { + transform: translateX(-66%); + } } +/* ========================= + 4. STRIP & MARQUEE EFFECT + ========================= */ .strip { - background-color: black; - width: 100vw; - border-top: 1px solid var(--red); - border-bottom: 1px solid var(--red); - position: fixed; + background-color: black; + width: 100vw; + border-top: 1px solid var(--red); + border-bottom: 1px solid var(--red); + position: fixed; } .strip-wrapper { - width: max(200vw, 2000px); - overflow: hidden; - white-space: nowrap; + width: max(200vw,2000px); + overflow: hidden; + white-space: nowrap; } @keyframes marquee1 { - 0% { - transform: translateX(100%); - } + 0% { + transform: translateX(100%); + } - 100% { - transform: translateX(-100%); - } + 100% { + transform: translateX(-100%); + } } @keyframes marquee2 { - from { - transform: translateX(0%); - } + from { + transform: translateX(0%); + } - to { - transform: translateX(-200%); - } + to { + transform: translateX(-200%); + } } .loop-strip { - animation: loopStrip infinite linear; - animation-duration: 10s; + animation: loopStrip infinite linear; + animation-duration: 10s; } .loop-strip-reverse { - animation: loopStrip infinite linear reverse; - animation-duration: 10s; + animation: loopStrip infinite linear reverse; + animation-duration: 10s; } .anim-duration-10 { - animation-duration: 10s !important; + animation-duration: 10s !important; } .anim-duration-20 { - animation-duration: 20s !important; + animation-duration: 20s !important; } @keyframes loopStrip { - from { - transform: translateX(0); - } + from { + transform: translateX(0); + } - to { - transform: translateX(-100%); - } + to { + transform: translateX(-100%); + } } +/* ========================= + 5. POPUP & TRANSITION ANIMATION + ========================= */ .show-pop-up { - animation: showPopUp 0.3s ease-in-out forwards; + animation: showPopUp 0.3s ease-in-out forwards; } -/* g.hexagon { - fill: transparent; -} */ - @keyframes showPopUp { - 0% { - opacity: 0; - transform: scale(0.5); - } + 0% { + opacity: 0; + transform: scale(0.5); + } - 100% { - opacity: 1; - transform: scale(1); - } + 100% { + opacity: 1; + transform: scale(1); + } } .close-pop-up { - animation: closePopUp 0.3s ease-in-out forwards !important; + animation: closePopUp 0.3s ease-in-out forwards !important; } @keyframes closePopUp { - 0% { - opacity: 1; - transform: scale(1); - } + 0% { + opacity: 1; + transform: scale(1); + } - 100% { - opacity: 0; - transform: scale(0.5); - } + 100% { + opacity: 0; + transform: scale(0.5); + } } .vertical-reveal { - animation: verticalReveal 0.3s ease-in-out; + animation: verticalReveal 0.3s ease-in-out; } @keyframes verticalReveal { - 0% { - transform: scaleY(0); - } + 0% { + transform: scaleY(0); + } - 100% { - transform: scaleY(1); - } + 100% { + transform: scaleY(1); + } } +/* ========================= + 6. GLOW & BLINK EFFECTS + ========================= */ .glow-effect { - animation: glowEffect 1s infinite; + animation: glowEffect 1s infinite; } @keyframes glowEffect { - 0% { - -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - } + 0% { + -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + } - 50% { - -webkit-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); - -moz-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); - box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); - } + 50% { + -webkit-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); + -moz-box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); + box-shadow: 0px 0px 66px 44px rgba(252, 60, 22, 0.9); + } - 100% { - -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); - } -} - -/* -.card { - background-color: black; - border: 3px var(--orange) solid; - transition: 0.3s; - - color: white; -} - -.card-header { - padding: 10px; - border-bottom: 3px var(--orange) solid; - color: var(--orange); -} - -.card-content { - padding: 20px; -} */ - -.marker-daerah { - width: auto; - height: 25px; - - cursor: pointer; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.marker-daerah p { - border: 1px black solid; - color: black; - background-color: red; - padding: 2px; - font-size: 8px; - text-transform: uppercase; - max-width: 75px; - line-height: 1; - text-align: center; - font-weight: bold; -} - -.marker-gempa { - /* border: 3px red solid; */ - /* width: 25px; - height: 25px; */ - font-size: 20px; - color: red; - cursor: pointer; -} - -.marker-gempa-wave { - border: 3px red solid; - border-radius: 50%; - width: 50px; - height: 50px; - font-size: 20px; - color: red; - cursor: pointer; -} - -.mapboxgl-popup { - width: auto; -} - -.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip { - border-top-color: unset; - height: 70px; - width: 3px; - background-color: red; - border: unset; -} - -.mapboxgl-popup-anchor-top .mapboxgl-popup-tip { - border-top-color: unset; - height: 70px; - width: 3px; - background-color: red; - border: unset; -} - -.mapboxgl-popup-content { - background-color: unset; - border: unset; - border-radius: 0; - padding: unset; - max-width: 256px; + 100% { + -webkit-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + -moz-box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + box-shadow: 0px 0px 66px 17px rgba(252, 60, 22, 0.59); + } } .blink { - animation: blink 1s infinite; + animation: blink 1s infinite; } @keyframes blink { - 0% { - opacity: 0; - } + 0% { + opacity: 0; + } - 50% { - opacity: 1; - } + 50% { + opacity: 1; + } - 100% { - opacity: 0; - } + 100% { + opacity: 0; + } } +/* ========================= + 7. MARKER & MAPBOX STYLES + ========================= */ +.marker-daerah { + width: auto; + height: 25px; + + cursor: pointer; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.marker-daerah p { + border: 1px black solid; + color: black; + background-color: red; + padding: 2px; + font-size: 8px; + text-transform: uppercase; + max-width: 75px; + line-height: 1; + text-align: center; + font-weight: bold; +} + +.marker-gempa { + font-size: 20px; + color: red; + cursor: pointer; +} + +.marker-gempa-wave { + border: 3px red solid; + border-radius: 50%; + width: 50px; + height: 50px; + font-size: 20px; + color: red; + cursor: pointer; +} + +.mapboxgl-popup-close-button { + display: none !important; +} + +.mapboxgl-popup { + width: auto; + +} + +.mapboxgl-popup-anchor-bottom .mapboxgl-popup-tip { + border-top-color: unset; + height: 70px; + width: 3px; + background-color: white; + border: unset; +} + +.mapboxgl-popup-anchor-top .mapboxgl-popup-tip { + border-top-color: unset; + height: 70px; + width: 3px; + background-color: white; + border: unset; +} + +.mapboxgl-popup-content { + background-color: unset; + border: unset; + border-radius: 0.5rem !important; + padding: 0 !important; + max-width: 320px; + box-shadow: + 0 10px 15px -3px rgba(0, 0, 0, 0.1), + 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; + overflow: hidden; +} + +/* ========================= + 8. WARNING & SHAPE COMPONENTS + ========================= */ .warning-wrapper { - display: flex; - justify-content: center; - position: absolute; - height: 200px; - max-width: 50%; - margin: auto; - top: 0; - bottom: 0; - left: 0; - right: 0; - animation: showWarningAlert 0.3s ease-in-out forwards; - flex-direction: column; - align-items: center; + display: flex; + justify-content: center; + position: absolute; + height: 200px; + max-width: 50%; + margin: auto; + top: 0; + bottom: 0; + left: 0; + right: 0; + animation: showWarningAlert 0.3s ease-in-out forwards; + flex-direction: column; + align-items: center; } .long-shape { - position: relative; - width: 500px; - display: flex; - justify-content: center; + position: relative; + width: 500px; + display: flex; + justify-content: center; } .long-shape .shape { - height: 150px; - width: 300px; - display: flex; - justify-content: space-between; + height: 150px; + width: 300px; + display: flex; + justify-content: space-between; } .shape { - position: absolute; - margin: auto; + position: absolute; + margin: auto; } .long-shape .bg { - background-color: #e60003; + background-color: #e60003; } .long-shape .fg { - background-color: #e60003; - scale: 0.98 0.92; + background-color: #e60003; + scale: 0.98 0.92; } .long-shape .br { - background-color: black; - scale: 0.99 0.96; + background-color: black; + scale: 0.99 0.96; } .long-shape .hex { - margin-top: 30px; - transform: scale(1.5); + margin-top: 30px; + transform: scale(1.5); } .basic-shape { - height: 100px; - width: 115px; - transform: scale(1.5); - z-index: 99; + height: 100px; + width: 115px; + transform: scale(1.5); + z-index: 99; } .basic-shape .hex { - position: absolute; - margin: auto; + position: absolute; + margin: auto; } .basic-shape .hex:nth-child(1) { - scale: 0.95; + scale: 0.95; } .basic-shape .hex:nth-child(2) { - scale: 0.9; + scale: 0.9; } .basic-shape .hex:nth-child(3) { - scale: 0.85; + scale: 0.85; } .basic-shape .hex:nth-child(4) { - scale: 0.8; + scale: 0.8; } .shape .hex:nth-child(1) { - margin-left: -20%; + margin-left: -20%; } .shape .hex:nth-child(2) { - margin-right: -20%; + margin-right: -20%; } .warning { - height: 500px; - width: 450px; + height: 500px; + width: 450px; } .long-hex { - position: relative; - height: 150px; - width: 275px; - background-image: url('/images/long_shape.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 150px; + width: 275px; + background-image: url('/images/long_shape.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-black-hex { - position: relative; - height: 100px; - width: 100px; - background-image: url('/images/warning_shape_black.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 100px; + width: 100px; + background-image: url('/images/warning_shape_black.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-black { - position: relative; - height: 40px; - width: 40px; - background-image: url('/images/warning_gempa_black.png'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 40px; + width: 40px; + background-image: url('/images/warning_gempa_black.png'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-yellow { - position: relative; - height: 80px; - width: 50px; - background-image: url('/images/warning_gempa_red_yellow.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 80px; + width: 50px; + background-image: url('/images/warning_gempa_red_yellow.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .warning-tsunami-yellow { - position: relative; - height: 80px; - width: 50px; - background-image: url('/images/warning_tsunami_yellow.png'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 80px; + width: 50px; + background-image: url('/images/warning_tsunami_yellow.png'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .basic-hex { - position: relative; - height: 100px; - width: 100px; - background-image: url('/images/hex_shape.svg'); - background-size: contain; - background-position: center; - background-repeat: no-repeat; + position: relative; + height: 100px; + width: 100px; + background-image: url('/images/hex_shape.svg'); + background-size: contain; + background-position: center; + background-repeat: no-repeat; } .animation-delay-1 { - animation-delay: 1s; + animation-delay: 1s; } .animation-delay-2 { - animation-delay: 2s; + animation-delay: 2s; } .animation-delay-3 { - animation-delay: 3s; + animation-delay: 3s; } .animation-delay-4 { - animation-delay: 4s; + animation-delay: 4s; } .warning .info .basic-hex:nth-child(1) { - /* animation: showPopUp 0.3s ease-in-out forwards; */ - animation-delay: 2s; + animation-delay: 2s; } .warning .info .basic-hex:nth-child(2) { - /* animation: showPopUp 0.3s ease-in-out forwards; */ - animation-delay: 2.2s; + animation-delay: 2.2s; } .warning .info .basic-hex:nth-child(3) { - /* animation: showPopUp 0.3s ease-in-out forwards; */ - animation-delay: 2.4s; + animation-delay: 2.4s; } .animation-fast { - animation-duration: 0.5s; + animation-duration: 0.5s; } .blink-fast { - animation-duration: 0.1s; + animation-duration: 0.1s; } +/* ========================= + 9. OVERLAY & LIST COMPONENTS + ========================= */ .overlay-bg { - background-color: rgba(0, 0, 0, 0.8); - /* background-image: url("/images/hexagons.png"); - transform: rotate(90deg); - width: 300%; - height: 300%; - opacity: 0.5; */ + background-color: rgba(0, 0, 0, 0.8); } .list-event { - display: block; - font: 400 16px 'Roboto Condensed'; - letter-spacing: -1px; - line-height: 1; - padding: 1px calc(var(--gutter-size) - 3px); - text-transform: uppercase; - user-select: none; - white-space: nowrap; - --text-glow-color: rgba(var(--glow-rgb), 0.5); - color: var(--text-color); - /* text-shadow: -1px 1px 0 var(--text-glow-color), 1px -1px 0 var(--text-glow-color); */ + display: block; + font: 400 16px 'Roboto Condensed'; + letter-spacing: -1px; + line-height: 1; + padding: 1px calc(var(--gutter-size) - 3px); + text-transform: uppercase; + user-select: none; + white-space: nowrap; + --text-glow-color: rgba(var(--glow-rgb), .5); + color: var(--text-color); } .text-glow-red { - --text-glow-color: rgba(var(--danger-glow-rgb), 0.5); - color: var(--danger-text-color); - /* text-shadow: -1px 1px 0 var(--text-glow-color), 1px -1px 0 var(--text-glow-color); */ + --text-glow-color: rgba(var(--danger-glow-rgb), 0.5); + color: var(--danger-text-color); } .text-glow { - --text-glow-color: rgba(var(--glow-rgb), 0.5); - color: var(--text-color) !important; - /* text-shadow: -1px 1px 0 var(--text-glow-color), 1px -1px 0 var(--text-glow-color); */ + --text-glow-color: rgba(var(--glow-rgb), 0.5); + color: var(--text-color) !important; } .bordered { - color: var(--text-color); - --border-glow-color: rgba(var(--glow-rgb), 0.7); - border-radius: var(--gutter-size); - border-style: solid; - border-width: 1px; - border-color: unset; - box-shadow: - inset 0 0 0 1px var(--border-glow-color), - 0 0 0 1px var(--border-glow-color); + color: var(--text-color); + --border-glow-color: rgba(var(--glow-rgb), 0.7); + border-radius: var(--gutter-size); + border-style: solid; + border-width: 1px; + border-color: unset; + box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); } .red-bordered { - color: var(--danger-text-color); - --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); - border-radius: var(--gutter-size); - border-style: solid; - border-width: 1px; - border-color: unset; - box-shadow: - inset 0 0 0 1px var(--border-glow-color), - 0 0 0 1px var(--border-glow-color); + color: var(--danger-text-color); + --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); + border-radius: var(--gutter-size); + border-style: solid; + border-width: 1px; + border-color: unset; + box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); } .red-bordered-bottom { - color: var(--danger-text-color); - --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); - border-color: unset; - border-bottom: 1px solid red; - box-shadow: - inset 0 0 0 1px var(--border-glow-color), - 0 0 0 1px var(--border-glow-color); + color: var(--danger-text-color); + --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); + border-color: unset; + border-bottom: 1px solid red; + box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); } .red-bordered-top { - color: var(--danger-text-color); - --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); - border-color: unset; - border-top: 1px solid var(--danger-glow-rgb); - box-shadow: - inset 0 0 0 1px var(--border-glow-color), - 0 0 0 1px var(--border-glow-color); + color: var(--danger-text-color); + --border-glow-color: rgba(var(--danger-glow-rgb), 0.7); + border-color: unset; + border-top: 1px solid var(--danger-glow-rgb); + box-shadow: inset 0 0 0 1px var(--border-glow-color), 0 0 0 1px var(--border-glow-color); } +/* ========================= + 10. CARD COMPONENTS + ========================= */ .card { - background-color: black; - /* border: 1px var(--red) solid; */ - transition: 0.3s; - /* color: var(--text-color); */ - /* color: white; */ + background-color: black; + transition: 0.3s; } .card-header { - padding: 6px; - /* border-bottom: 1px var(--red) solid; */ - color: var(--orange); - position: relative; - border-radius: 10px 10px 0px 0px; + padding: 6px; + color: var(--orange); + position: relative; + border-radius: 10px 10px 0px 0px; } .card-footer { - padding: 6px; - border-top: 3px var(--red) solid; - color: var(--orange); - position: relative; - border-radius: 0px 0px 10px 10px; + padding: 6px; + border-top: 3px var(--red) solid; + color: var(--orange); + position: relative; + border-radius: 0px 0px 10px 10px; } .card-content { - padding: 12px; + padding: 12px; } .card-float { - /* width: 30%; */ - transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; } .card-float .card-content { - display: block; - max-height: 45vh; - overflow-y: auto; - overflow-x: hidden; + display: block; + max-height: 45vh; + overflow-y: auto; + overflow-x: hidden; } +/* ========================= + 11. JAJAR GENJANG & DAERAH LIST + ========================= */ .jajar-genjang { - height: 30px; - width: 100%; - transform: skew(15deg); - -webkit-transform: skew(15deg); - -moz-transform: skew(15deg); - -o-transform: skew(15deg); - background-color: var(--orange); - -webkit-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); - -moz-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); - box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); - display: flex; - align-items: center; - padding: 6px; - overflow: hidden; + height: 30px; + width: 100%; + transform: skew(15deg); + -webkit-transform: skew(15deg); + -moz-transform: skew(15deg); + -o-transform: skew(15deg); + background-color: var(--orange); + -webkit-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); + -moz-box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); + box-shadow: 0px 0px 5px 0px rgba(252, 114, 22, 1); + display: flex; + align-items: center; + padding: 6px; + overflow: hidden; } -.jajar-genjang .time-countdown { -} +.jajar-genjang .time-countdown {} .jajar-genjang.danger { - background-color: var(--red); - -webkit-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); - -moz-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); - box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); + background-color: var(--red); + -webkit-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); + -moz-box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); + box-shadow: 0px 0px 5px 0px rgba(250, 23, 23, 1); } .jajar-genjang p { - transform: skew(-15deg); - -webkit-transform: skew(-15deg); - -moz-transform: skew(-15deg); - -o-transform: skew(-15deg); - color: black; - font-weight: bold; - font-size: 14px; + transform: skew(-15deg); + -webkit-transform: skew(-15deg); + -moz-transform: skew(-15deg); + -o-transform: skew(-15deg); + color: black; + font-weight: bold; + font-size: 14px; } .pinggir-jajar-genjang { - height: 30px; - width: 30px; - transform: skew(15deg); - -webkit-transform: skew(15deg); - -moz-transform: skew(15deg); - -o-transform: skew(15deg); + height: 30px; + width: 30px; + transform: skew(15deg); + -webkit-transform: skew(15deg); + -moz-transform: skew(15deg); + -o-transform: skew(15deg); } .item-daerah { - width: 100%; - position: relative; + width: 100%; + position: relative; } .list-daerah .card-content { - max-height: 50vh; - overflow-y: auto; + max-height: 50vh; + overflow-y: auto; } -.item-daerah.danger { -} +.item-daerah.danger {} .item-daerah .content { - position: absolute; - font-size: 12px; - color: black; - font-weight: bold; + position: absolute; + font-size: 12px; + color: black; + font-weight: bold; } .item-daerah .pinggir-jajar-genjang { - background-color: var(--orange); + background-color: var(--orange); } .item-daerah.danger .pinggir-jajar-genjang { - background-color: var(--red); + background-color: var(--red); } .time-countdown { - font-family: 'DS-Digital'; + font-family: 'DS-Digital'; } .text-time { - font-family: 'DS-Digital'; + font-family: 'DS-Digital'; } -.custom-scrollbar::-webkit-scrollbar-track { - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - border-radius: 10px; - background-color: rgb(61, 61, 61); +/* ========================= + 12. CUSTOM SCROLLBAR + ========================= */ +/* .custom-scrollbar::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); + border-radius: 10px; + background-color: rgb(61, 61, 61); } .custom-scrollbar::-webkit-scrollbar { - width: 12px; - background-color: rgb(61, 61, 61); + width: 12px; + background-color: rgb(61, 61, 61); } .custom-scrollbar::-webkit-scrollbar-thumb { - border-radius: 10px; - -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); - background-color: var(--red); -} + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3); + background-color: var(--red); +} */ -/* slide animation from left to right */ +/* ========================= + 13. SLIDE ANIMATION + ========================= */ .slide-in-left { - animation: slideInLeft 0.5s forwards; + animation: slideInLeft 0.5s forwards; } @keyframes slideInLeft { - 0% { - transform: translateX(-100%); - } + 0% { + transform: translateX(-100%); + } - 100% { - transform: translateX(0); - } + 100% { + transform: translateX(0); + } } +/* ========================= + 14. LABEL & RESPONSIVE + ========================= */ label#internal { - --decal-width: 50px; - --label-corner-size: 3px; - --label-gutter-size: 5px; + --decal-width: 50px; + --label-corner-size: 3px; + --label-gutter-size: 5px; } .label { - overflow: hidden; - /* padding: 1px calc(var(--gutter-size) - 3px); */ - /* width: 8rem; */ - /* display: inline-block; */ - font: 400 2rem 'Roboto Condensed'; - letter-spacing: -1px; - line-height: 1; - padding-right: 0px; - text-transform: uppercase; - user-select: none; - white-space: nowrap; - --text-glow-color: rgba(var(--glow-rgb), 0.5); - color: var(--text-color); - text-shadow: - -1px 1px 0 var(--text-glow-color), - 1px -1px 0 var(--text-glow-color), - -1px -1px 0 var(--text-glow-color), - 1px 1px 0 var(--text-glow-color); + overflow: hidden; + font: 400 2rem 'Roboto Condensed'; + letter-spacing: -1px; + line-height: 1; + padding-right: 0px; + text-transform: uppercase; + user-select: none; + white-space: nowrap; + --text-glow-color: rgba(var(--glow-rgb), .5); + color: var(--text-color); + text-shadow: -1px 1px 0 var(--text-glow-color), 1px -1px 0 var(--text-glow-color), -1px -1px 0 var(--text-glow-color), 1px 1px 0 var(--text-glow-color); } .label#internal .decal { - border-radius: calc(var(--label-corner-size) - 1px); - display: block; - height: 100px; - width: 100%; + border-radius: calc(var(--label-corner-size) - 1px); + display: block; + height: 100px; + width: 100%; } .-striped { - --stripe-color: var(--danger-fill-color); - --stripe-size: 15px; - --glow-color: rgba(var(--danger-glow-rgb), 0.8); - --glow-size: 3px; - background-image: repeating-linear-gradient( - -45deg, - /* glow boundary */ var(--glow-color) calc(-1 * var(--glow-size)), - /* fade into foreground */ var(--stripe-color) 0, - /* fade from foreground */ var(--stripe-color) - calc(var(--stripe-size) - var(--glow-size) / 2), - /* glow boundary */ var(--glow-color) - calc(var(--stripe-size) + var(--glow-size) / 2), - /* fade to background */ transparent - calc(var(--stripe-size) + var(--glow-size) / 2), - /* fade from background */ transparent calc(2 * var(--stripe-size)), - /* glow boundary */ var(--glow-color) - calc(2 * var(--stripe-size) - var(--glow-size)) - ); - box-shadow: inset 0 0 1px calc(var(--glow-size) / 2) var(--shade-3); + --stripe-color: var(--danger-fill-color); + --stripe-size: 15px; + --glow-color: rgba(var(--danger-glow-rgb), .8); + --glow-size: 3px; + background-image: repeating-linear-gradient(-45deg, + var(--glow-color) calc(-1 * var(--glow-size)), + var(--stripe-color) 0, + var(--stripe-color) calc(var(--stripe-size) - var(--glow-size) / 2), + var(--glow-color) calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(var(--stripe-size) + var(--glow-size) / 2), + transparent calc(2 * var(--stripe-size)), + var(--glow-color) calc(2 * var(--stripe-size) - var(--glow-size))); + box-shadow: inset 0 0 1px calc(var(--glow-size) / 2) var(--shade-3); } .-blink { - animation-name: blink; - animation-duration: var(--blink-duration); - animation-iteration-count: infinite; - animation-timing-function: steps(1); + animation-name: blink; + animation-duration: var(--blink-duration); + animation-iteration-count: infinite; + animation-timing-function: steps(1); } -/* @keyframes blink { - 50% { - opacity: 0; - } -} */ - .label#internal .text.-characters { - font-size: 3.5rem; - padding-top: var(--label-gutter-size); -} - -/* responsive query for mobile */ -@media (max-width: 768px) { - .card-float .card-content { - height: 0px; - padding: 0px; - } - - .card-float.open .card-content { - height: unset; - padding: 6px; - } - - .card-float { - margin: auto; - right: 0.25rem; - left: 0.25rem; - } - - .label#internal .decal { - width: 40px; - } - - .card-header { - cursor: pointer; - } -} - -/* responsive query for tablet */ -@media (min-width: 768px) and (max-width: 1024px) { - .label#internal .text.-characters { font-size: 3.5rem; - } - - .label#internal .text { - font-size: 2.5rem; - } - - .label#internal .decal { - width: 40px; - } + padding-top: var(--label-gutter-size); } +/* --- Responsive for Mobile --- */ +@media (max-width: 768px) { + .card-float .card-content { + height: 0px; + padding: 0px; + } + + .card-float.open .card-content { + height: unset; + padding: 6px; + } + + .card-float { + margin: auto; + right: 0.25rem; + left: 0.25rem; + } + + .label#internal .decal { + width: 40px; + } + + .card-header { + cursor: pointer; + } +} + +/* --- Responsive for Tablet --- */ +@media (min-width: 768px) and (max-width: 1024px) { + .label#internal .text.-characters { + font-size: 3.5rem; + } + + .label#internal .text { + font-size: 2.5rem; + } + + .label#internal .decal { + width: 40px; + } +} + +/* ========================= + 15. ICONS & LOADER + ========================= */ .github-icon { - width: 20px; - height: 20px; - border-radius: 50%; - background-image: url('https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg'); - background-color: white; - background-repeat: no-repeat; - background-position: center; + width: 20px; + height: 20px; + border-radius: 50%; + background-image: url('https://cdn.jsdelivr.net/gh/devicons/devicon/icons/github/github-original.svg'); + background-color: white; + background-repeat: no-repeat; + background-position: center; } .bmkg-icon { - width: 25px; - height: 25px; - border-radius: 50%; - background-image: url('/images/logo-bmkg.webp'); - background-size: contain; - background-repeat: no-repeat; - background-position: center; + width: 25px; + height: 25px; + border-radius: 50%; + background-image: url('/images/logo-bmkg.webp'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; } .loader { - width: 48px; - height: 48px; - display: inline-block; - position: relative; + width: 48px; + height: 48px; + display: inline-block; + position: relative; } .loader::after, .loader::before { - content: ''; - box-sizing: border-box; - width: 48px; - height: 48px; - border: 2px solid var(--orange); - position: absolute; - left: 0; - top: 0; - animation: scaleOut 2s ease-in-out infinite; + content: ''; + box-sizing: border-box; + width: 48px; + height: 48px; + border: 2px solid var(--orange); + position: absolute; + left: 0; + top: 0; + animation: scaleOut 2s ease-in-out infinite; } .loader::after { - border-color: var(--red); - animation-delay: 1s; + border-color: var(--red); + animation-delay: 1s; } #loading-screen { - background-color: black; + background-color: black; } @keyframes scaleOut { - 0% { - transform: scale(0); - } + 0% { + transform: scale(0); + } - 100% { - transform: scale(1); - } + 100% { + transform: scale(1); + } } +/* ========================= + 16. CIRCLES ANIMATION + ========================= */ .circles .circle1 { - animation-delay: 1s; + animation-delay: 1s; } .circles .circle2 { - animation-delay: 2s; + animation-delay: 2s; } .circles .circle3 { - animation-delay: 3s; + animation-delay: 3s; } .circles { - height: 200px; - width: 200px; - - margin: auto; + height: 200px; + width: 200px; + margin: auto; } .circles div { - animation: growAndFade 3s infinite ease-out; - background-color: rgb(156, 94, 0); - border-radius: 50%; - height: 200px; - width: 200px; - opacity: 0; - position: absolute; - box-shadow: 0 0 10px 5px rgba(156, 75, 0, 0.5); + animation: growAndFade 3s infinite ease-out; + background-color: rgb(156, 94, 0); + border-radius: 50%; + height: 200px; + width: 200px; + opacity: 0; + position: absolute; + box-shadow: 0 0 10px 5px rgba(156, 75, 0, 0.5); } @keyframes growAndFade { - 0% { - opacity: 0.25; - transform: scale(0); - } + 0% { + opacity: .25; + transform: scale(0); + } - 100% { - opacity: 0; - transform: scale(1); - } + 100% { + opacity: 0; + transform: scale(1); + } } +/* ========================= + 17. HEXAGON BACKGROUND + ========================= */ .main { - width: calc(max(120vh, 120vw) + 100px); - margin-left: -35vh; - transform: translateY(min(-29vw, -40vw)); - display: grid; - grid-template-columns: repeat(auto-fit, calc(var(--s) + 2 * var(--mh))); - justify-content: center; - --s: 80px; /* size */ - --r: 1.15; /* ratio */ - /* clip-path */ - --h: 0.5; - --v: 0.25; - --hc: calc(clamp(0, var(--h), 0.5) * var(--s)); - --vc: calc(clamp(0, var(--v), 0.5) * var(--s) * var(--r)); - - /*margin */ - --mv: 1px; /* vertical */ - --mh: calc(var(--mv) + (var(--s) - 2 * var(--hc)) / 2); /* horizontal */ - /* for the float*/ - --f: calc(2 * var(--s) * var(--r) + 4 * var(--mv) - 2 * var(--vc) - 2px); + width: calc(max(120vh,120vw) + 100px); + margin-left: -35vh; + transform: translateY(min(-29vw,-40vw)); + display: grid; + grid-template-columns: repeat(auto-fit,calc(var(--s) + 2*var(--mh))); + justify-content:center; + --s: 80px; /* size */ + --r: 1.15; /* ratio */ + --h: 0.5; + --v: 0.25; + --hc:calc(clamp(0,var(--h),0.5) * var(--s)) ; + --vc:calc(clamp(0,var(--v),0.5) * var(--s) * var(--r)); + --mv: 1px; /* vertical */ + --mh: calc(var(--mv) + (var(--s) - 2*var(--hc))/2); /* horizontal */ + --f: calc(2*var(--s)*var(--r) + 4*var(--mv) - 2*var(--vc) - 2px); } - + .hex-bg { - grid-column: 1/-1; - /* width: 110vh; - height: 100vw; */ - margin: 0 auto; - font-size: 0; /*disable white space between inline block element */ - position: relative; - /* padding-bottom:50px; */ - /* filter:drop-shadow(2px 2px 1px #333) */ + grid-column: 1/-1; + margin:0 auto; + font-size: 0; + position:relative; } .hex-bg div { - width: var(--s); - margin: var(--mv) var(--mh); - height: calc(var(--s) * var(--r)); - display: inline-block; - font-size: initial; - /* clip-path: polygon(var(--hc) 0, calc(100% - var(--hc)) 0,100% var(--vc),100% calc(100% - var(--vc)), calc(100% - var(--hc)) 100%,var(--hc) 100%,0 calc(100% - var(--vc)),0 var(--vc)); */ - margin-bottom: calc(var(--mv) - var(--vc)); + width: var(--s); + margin: var(--mv) var(--mh); + height: calc(var(--s)*var(--r)); + display: inline-block; + font-size:initial; + margin-bottom: calc(var(--mv) - var(--vc)); } -.hex-bg::before { - content: ''; - width: calc(var(--s) / 2 + var(--mh)); - float: left; - height: 100%; - shape-outside: repeating-linear-gradient( - transparent 0 calc(var(--f) - 2px), - #fff 0 var(--f) - ); +.hex-bg::before{ + content: ""; + width: calc(var(--s)/2 + var(--mh)); + float: left; + height: 100%; + shape-outside: repeating-linear-gradient( + transparent 0 calc(var(--f) - 2px), + #fff 0 var(--f)); } .hex-bg div { - /* background-color: var(--red); */ - justify-content: center; - align-items: center; - font-weight: bold; - text-align: center; + justify-content: center; + align-items: center; + font-weight:bold; + text-align:center; } .hex-bg div p { - text-align: center; - margin-top: 20px; - color: black; - font-size: 10px; - transform: rotate(90deg); + text-align: center; + margin-top: 20px; + color: black; + font-size: 10px; + transform: rotate(90deg); } .hex-bg img { - display: block; - position: relative; - transform: rotate(90deg) scale(1.2); + display: block; + position: relative; + transform: rotate(90deg) scale(1.2); } .hex-bg div::before { - /* padding-top:80px; */ - /* content:"DANGER"; */ - /* font-size:75px; */ - /* font-family:sans-serif; */ - position: absolute; - display: flex; - /* background-color: var(--red); */ - /* background-image: url("/images/warning_hex_red.png"); - background-position: center 5px; - background-repeat: no-repeat; - background-size: 50px; */ - /* inset:0; */ + position:absolute; + display: flex; } .hex-bg div { - /* animation:show 5s ease-in-out; */ - animation: showPopUp 0.3s ease-in-out forwards; - opacity: 0; - transform: scale(0.5); + animation:showPopUp 0.3s ease-in-out forwards; + opacity:0; + transform: scale(0.5); } -@keyframes show { - 10% { - opacity: 1; - transform: scale(1); - } - 90% { - opacity: 1; - transform: scale(1); - } +@keyframes show{ + 10% { + opacity:1; + transform: scale(1); + } + 90% { + opacity:1; + transform: scale(1); + } } -/* Mapbox Popup Styles */ -.mapboxgl-popup { - z-index: 10; -} - -.mapboxgl-popup-content { - padding: 0 !important; - border-radius: 0.5rem !important; - box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05) !important; - overflow: hidden; -} - -.mapboxgl-popup-close-button { - display: none !important; -} - -.map-popup .mapboxgl-popup-content { - max-width: 300px; -} - -/* Mapbox copyright */ +/* ========================= + 18. MAPBOX OVERRIDES + ========================= */ .mapbox-logo { - display: none !important; -} -.mapboxgl-ctrl-logo { - display: none !important; + display: none !important; } -/* .mapbox-improve-map { - display: none; - } - .mapboxgl-ctrl-compass { - display: none; - } */ +.mapboxgl-ctrl-logo { + display: none !important; +} .mapbox-gl-draw_point { - background-repeat: no-repeat; - background-position: center; - pointer-events: auto; - background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjIwIiAgIGhlaWdodD0iMjAiICAgdmlld0JveD0iMCAwIDIwIDIwIiAgIGlkPSJzdmcxOTE2NyIgICB2ZXJzaW9uPSIxLjEiICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45MStkZXZlbCtvc3htZW51IHIxMjkxMSIgICBzb2RpcG9kaTpkb2NuYW1lPSJtYXJrZXIuc3ZnIj4gIDxkZWZzICAgICBpZD0iZGVmczE5MTY5IiAvPiAgPHNvZGlwb2RpOm5hbWVkdmlldyAgICAgaWQ9ImJhc2UiICAgICBwYWdlY29sb3I9IiNmZmZmZmYiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6em9vbT0iMTYiICAgICBpbmtzY2FwZTpjeD0iMTQuMTY0MjUzIiAgICAgaW5rc2NhcGU6Y3k9IjguODg1NzIiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiICAgICBzaG93Z3JpZD0iZmFsc2UiICAgICB1bml0cz0icHgiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyODAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3NTEiICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMjA4IiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjE5MCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpvYmplY3Qtbm9kZXM9InRydWUiPiAgICA8aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMTk3MTUiIC8+ICA8L3NvZGlwb2RpOm5hbWVkdmlldz4gIDxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMTkxNzIiPiAgICA8cmRmOlJERj4gICAgICA8Y2M6V29yayAgICAgICAgIHJkZjphYm91dD0iIj4gICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PiAgICAgICAgPGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+ICAgICAgICA8ZGM6dGl0bGUgLz4gICAgICA8L2NjOldvcms+ICAgIDwvcmRmOlJERj4gIDwvbWV0YWRhdGE+ICA8ZyAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIiAgICAgaWQ9ImxheWVyMSIgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lO2NvbG9yLXJlbmRlcmluZzphdXRvO2ltYWdlLXJlbmRlcmluZzphdXRvO3NoYXBlLXJlbmRlcmluZzphdXRvO3RleHQtcmVuZGVyaW5nOmF1dG87ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSAzNiwxMDQwLjM2MjIgYyA2ZS02LDMuMzA5MyAtNS45ODg2MTIsMTAgLTUuOTg4NjEyLDEwIDAsMCAtNS45OTg3NzYsLTYuNjY4IC02LjAxMTM0NSwtOS45NzcyIC0wLjAxMjU3LC0zLjMwOTIgMi42NTY1NzYsLTYuMDAzOSA1Ljk2NTc5MiwtNi4wMjI3IDMuMzA5MTg5LC0wLjAxOSA2LjAwODg0LDIuNjQ1MiA2LjAzMzk5Miw1Ljk1NDMiICAgICAgIGlkPSJwYXRoMTI1NjEiICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc2MiIC8+ICAgIDxwYXRoICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO29wYWNpdHk6MTtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7Y29sb3ItaW50ZXJwb2xhdGlvbjpzUkdCO2NvbG9yLWludGVycG9sYXRpb24tZmlsdGVyczpsaW5lYXJSR0I7c29saWQtY29sb3I6IzAwMDAwMDtzb2xpZC1vcGFjaXR5OjE7ZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7bWFya2VyOm5vbmU7Y29sb3ItcmVuZGVyaW5nOmF1dG87aW1hZ2UtcmVuZGVyaW5nOmF1dG87c2hhcGUtcmVuZGVyaW5nOmF1dG87dGV4dC1yZW5kZXJpbmc6YXV0bztlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBkPSJtIDM0LjAwMDExNSwxMDQwLjM2MjIgYyAtNWUtNiwyLjIwNjIgLTMuOTkyNTIzLDcuMDAwMSAtMy45OTI1MjMsNy4wMDAxIDAsMCAtMy45OTkyOTEsLTQuNzc4NyAtNC4wMDc2NzksLTYuOTg0OSAtMC4wMDg0LC0yLjIwNjIgMS43NzEwODIsLTQuMDAyNyAzLjk3NzMxLC00LjAxNTMgMi4yMDYyMSwtMC4wMTMgNC4wMDYwMzcsMS43NjM1IDQuMDIyNzc3LDMuOTY5NyIgICAgICAgaWQ9InBhdGgxMjU2MyIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7b3BhY2l0eToxO2lzb2xhdGlvbjphdXRvO21peC1ibGVuZC1tb2RlOm5vcm1hbDtjb2xvci1pbnRlcnBvbGF0aW9uOnNSR0I7Y29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzOmxpbmVhclJHQjtzb2xpZC1jb2xvcjojMDAwMDAwO3NvbGlkLW9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTttYXJrZXI6bm9uZTtjb2xvci1yZW5kZXJpbmc6YXV0bztpbWFnZS1yZW5kZXJpbmc6YXV0bztzaGFwZS1yZW5kZXJpbmc6YXV0bzt0ZXh0LXJlbmRlcmluZzphdXRvO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Ik0gOS45NjY3OTY5LDEwMTQuMzYyMiBDIDYuNjU3NTgwOSwxMDE0LjM4MSAzLjk4NzQzLDEwMTcuMDc2NCA0LDEwMjAuMzg1NiBjIDAuMDEyNTY5LDMuMzA5MiA2LjAxMTcxOSw4Ljk3NjYgNi4wMTE3MTksOC45NzY2IDAsMCA1Ljk4ODI4NywtNS42OTA3IDUuOTg4MjgxLC05IGwgMCwtMC4wNDUgYyAtMC4wMjUxNSwtMy4zMDkxIC0yLjcyNDAxNCwtNS45NzQxIC02LjAzMzIwMzEsLTUuOTU1MSB6IG0gMC4wMDk3NywyIGMgMi4yMDYyMDYxLC0wLjAxMyA0LjAwNjY5MzEsMS43NjI2IDQuMDIzNDMzMSwzLjk2ODggbCAwLDAuMDMxIGMgLTVlLTYsMi4yMDYyIC0zLjk5MjE4OCw2IC0zLjk5MjE4OCw2IDAsMCAtMy45OTk0MjQsLTMuNzc4MiAtNC4wMDc4MTIsLTUuOTg0NCAtMC4wMDg0LC0yLjIwNjIgMS43NzAzMzQ1LC00LjAwMyAzLjk3NjU2MjUsLTQuMDE1NiB6IiAgICAgICBpZD0icGF0aDEyNTY4IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NjY2Njc2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9Im9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46YmV2ZWw7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lIiAgICAgICBkPSJNIDEwIDIgQyA2LjY4NjI5MiAyIDQgNC42ODYzIDQgOCBDIDQgMTEuMzEzNyAxMCAxNyAxMCAxNyBDIDEwIDE3IDE2IDExLjMxMzcgMTYgOCBDIDE2IDQuNjg2MyAxMy4zMTM3MDggMiAxMCAyIHogTSAxMCA0IEMgMTIuMDcxMDY4IDQgMTMuNzUgNS42Nzg5IDEzLjc1IDcuNzUgQyAxMy43NSA5LjIwNTMyNzggMTEuOTMxMTEgMTEuNjQ0MzkzIDEwLjgzMDA3OCAxMyBMIDkuMTY5OTIxOSAxMyBDIDguMDY4ODkwMyAxMS42NDQzOTMgNi4yNSA5LjIwNTMyNzggNi4yNSA3Ljc1IEMgNi4yNSA1LjY3ODkgNy45Mjg5MzIgNCAxMCA0IHogIiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDEwMzIuMzYyMikiICAgICAgIGlkPSJwYXRoMTczMDUiIC8+ICA8L2c+PC9zdmc+); + background-repeat: no-repeat; + background-position: center; + pointer-events: auto; + background-image: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIiAgIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyIgICB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgICB4bWxuczpzb2RpcG9kaT0iaHR0cDovL3NvZGlwb2RpLnNvdXJjZWZvcmdlLm5ldC9EVEQvc29kaXBvZGktMC5kdGQiICAgeG1sbnM6aW5rc2NhcGU9Imh0dHA6Ly93d3cuaW5rc2NhcGUub3JnL25hbWVzcGFjZXMvaW5rc2NhcGUiICAgd2lkdGg9IjIwIiAgIGhlaWdodD0iMjAiICAgdmlld0JveD0iMCAwIDIwIDIwIiAgIGlkPSJzdmcxOTE2NyIgICB2ZXJzaW9uPSIxLjEiICAgaW5rc2NhcGU6dmVyc2lvbj0iMC45MStkZXZlbCtvc3htZW51IHIxMjkxMSIgICBzb2RpcG9kaTpkb2NuYW1lPSJtYXJrZXIuc3ZnIj4gIDxkZWZzICAgICBpZD0iZGVmczE5MTY5IiAvPiAgPHNvZGlwb2RpOm5hbWVkdmlldyAgICAgaWQ9ImJhc2UiICAgICBwYWdlY29sb3I9IiNmZmZmZmYiICAgICBib3JkZXJjb2xvcj0iIzY2NjY2NiIgICAgIGJvcmRlcm9wYWNpdHk9IjEuMCIgICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwLjAiICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIiAgICAgaW5rc2NhcGU6em9vbT0iMTYiICAgICBpbmtzY2FwZTpjeD0iMTQuMTY0MjUzIiAgICAgaW5rc2NhcGU6Y3k9IjguODg1NzIiICAgICBpbmtzY2FwZTpkb2N1bWVudC11bml0cz0icHgiICAgICBpbmtzY2FwZTpjdXJyZW50LWxheWVyPSJsYXllcjEiICAgICBzaG93Z3JpZD0iZmFsc2UiICAgICB1bml0cz0icHgiICAgICBpbmtzY2FwZTp3aW5kb3ctd2lkdGg9IjEyODAiICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSI3NTEiICAgICBpbmtzY2FwZTp3aW5kb3cteD0iMjA4IiAgICAgaW5rc2NhcGU6d2luZG93LXk9IjE5MCIgICAgIGlua3NjYXBlOndpbmRvdy1tYXhpbWl6ZWQ9IjAiICAgICBpbmtzY2FwZTpvYmplY3Qtbm9kZXM9InRydWUiPiAgICA8aW5rc2NhcGU6Z3JpZCAgICAgICB0eXBlPSJ4eWdyaWQiICAgICAgIGlkPSJncmlkMTk3MTUiIC8+ICA8L3NvZGlwb2RpOm5hbWVkdmlldz4gIDxtZXRhZGF0YSAgICAgaWQ9Im1ldGFkYXRhMTkxNzIiPiAgICA8cmRmOlJERj4gICAgICA8Y2M6V29yayAgICAgICAgIHJkZjphYm91dD0iIj4gICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PiAgICAgICAgPGRjOnR5cGUgICAgICAgICAgIHJkZjpyZXNvdXJjZT0iaHR0cDovL3B1cmwub3JnL2RjL2RjbWl0eXBlL1N0aWxsSW1hZ2UiIC8+ICAgICAgICA8ZGM6dGl0bGUgLz4gICAgICA8L2NjOldvcms+ICAgIDwvcmRmOlJERj4gIDwvbWV0YWRhdGE+ICA8ZyAgICAgaW5rc2NhcGU6bGFiZWw9IkxheWVyIDEiICAgICBpbmtzY2FwZTpncm91cG1vZGU9ImxheWVyIiAgICAgaWQ9ImxheWVyMSIgICAgIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAsLTEwMzIuMzYyMikiPiAgICA8cGF0aCAgICAgICBzdHlsZT0iY29sb3I6IzAwMDAwMDtjbGlwLXJ1bGU6bm9uemVybztkaXNwbGF5OmlubGluZTtvdmVyZmxvdzp2aXNpYmxlO3Zpc2liaWxpdHk6dmlzaWJsZTtvcGFjaXR5OjE7aXNvbGF0aW9uOmF1dG87bWl4LWJsZW5kLW1vZGU6bm9ybWFsO2NvbG9yLWludGVycG9sYXRpb246c1JHQjtjb2xvci1pbnRlcnBvbGF0aW9uLWZpbHRlcnM6bGluZWFyUkdCO3NvbGlkLWNvbG9yOiMwMDAwMDA7c29saWQtb3BhY2l0eToxO2ZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtmaWxsLXJ1bGU6ZXZlbm9kZDtzdHJva2U6bm9uZTtzdHJva2Utd2lkdGg6MjtzdHJva2UtbGluZWNhcDpyb3VuZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lO2NvbG9yLXJlbmRlcmluZzphdXRvO2ltYWdlLXJlbmRlcmluZzphdXRvO3NoYXBlLXJlbmRlcmluZzphdXRvO3RleHQtcmVuZGVyaW5nOmF1dG87ZW5hYmxlLWJhY2tncm91bmQ6YWNjdW11bGF0ZSIgICAgICAgZD0ibSAzNiwxMDQwLjM2MjIgYyA2ZS02LDMuMzA5MyAtNS45ODg2MTIsMTAgLTUuOTg4NjEyLDEwIDAsMCAtNS45OTg3NzYsLTYuNjY4IC02LjAxMTM0NSwtOS45NzcyIC0wLjAxMjU3LC0zLjMwOTIgMi42NTY1NzYsLTYuMDAzOSA1Ljk2NTc5MiwtNi4wMjI3IDMuMzA5MTg5LC0wLjAxOSA2LjAwODg0LDIuNjQ1MiA2LjAzMzk5Miw1Ljk1NDMiICAgICAgIGlkPSJwYXRoMTI1NjEiICAgICAgIGlua3NjYXBlOmNvbm5lY3Rvci1jdXJ2YXR1cmU9IjAiICAgICAgIHNvZGlwb2RpOm5vZGV0eXBlcz0iY2Nzc2MiIC8+ICAgIDxwYXRoICAgICAgIHN0eWxlPSJjb2xvcjojMDAwMDAwO2NsaXAtcnVsZTpub256ZXJvO2Rpc3BsYXk6aW5saW5lO292ZXJmbG93OnZpc2libGU7dmlzaWJpbGl0eTp2aXNpYmxlO29wYWNpdHk6MTtpc29sYXRpb246YXV0bzttaXgtYmxlbmQtbW9kZTpub3JtYWw7Y29sb3ItaW50ZXJwb2xhdGlvbjpzUkdCO2NvbG9yLWludGVycG9sYXRpb24tZmlsdGVyczpsaW5lYXJSR0I7c29saWQtY29sb3I6IzAwMDAwMDtzb2xpZC1vcGFjaXR5OjE7ZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO2ZpbGwtcnVsZTpldmVub2RkO3N0cm9rZTpub25lO3N0cm9rZS13aWR0aDoyO3N0cm9rZS1saW5lY2FwOnJvdW5kO3N0cm9rZS1saW5lam9pbjpyb3VuZDtzdHJva2UtbWl0ZXJsaW1pdDo0O3N0cm9rZS1kYXNoYXJyYXk6bm9uZTtzdHJva2UtZGFzaG9mZnNldDowO3N0cm9rZS1vcGFjaXR5OjE7bWFya2VyOm5vbmU7Y29sb3ItcmVuZGVyaW5nOmF1dG87aW1hZ2UtcmVuZGVyaW5nOmF1dG87c2hhcGUtcmVuZGVyaW5nOmF1dG87dGV4dC1yZW5kZXJpbmc6YXV0bztlbmFibGUtYmFja2dyb3VuZDphY2N1bXVsYXRlIiAgICAgICBkPSJtIDM0LjAwMDExNSwxMDQwLjM2MjIgYyAtNWUtNiwyLjIwNjIgLTMuOTkyNTIzLDcuMDAwMSAtMy45OTI1MjMsNy4wMDAxIDAsMCAtMy45OTkyOTEsLTQuNzc4NyAtNC4wMDc2NzksLTYuOTg0OSAtMC4wMDg0LC0yLjIwNjIgMS43NzEwODIsLTQuMDAyNyAzLjk3NzMxLC00LjAxNTMgMi4yMDYyMSwtMC4wMTMgNC4wMDYwMzcsMS43NjM1IDQuMDIyNzc3LDMuOTY5NyIgICAgICAgaWQ9InBhdGgxMjU2MyIgICAgICAgaW5rc2NhcGU6Y29ubmVjdG9yLWN1cnZhdHVyZT0iMCIgICAgICAgc29kaXBvZGk6bm9kZXR5cGVzPSJjY2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9ImNvbG9yOiMwMDAwMDA7Y2xpcC1ydWxlOm5vbnplcm87ZGlzcGxheTppbmxpbmU7b3ZlcmZsb3c6dmlzaWJsZTt2aXNpYmlsaXR5OnZpc2libGU7b3BhY2l0eToxO2lzb2xhdGlvbjphdXRvO21peC1ibGVuZC1tb2RlOm5vcm1hbDtjb2xvci1pbnRlcnBvbGF0aW9uOnNSR0I7Y29sb3ItaW50ZXJwb2xhdGlvbi1maWx0ZXJzOmxpbmVhclJHQjtzb2xpZC1jb2xvcjojMDAwMDAwO3NvbGlkLW9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7ZmlsbC1ydWxlOmV2ZW5vZGQ7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6cm91bmQ7c3Ryb2tlLWxpbmVqb2luOnJvdW5kO3N0cm9rZS1taXRlcmxpbWl0OjQ7c3Ryb2tlLWRhc2hhcnJheTpub25lO3N0cm9rZS1kYXNob2Zmc2V0OjA7c3Ryb2tlLW9wYWNpdHk6MTttYXJrZXI6bm9uZTtjb2xvci1yZW5kZXJpbmc6YXV0bztpbWFnZS1yZW5kZXJpbmc6YXV0bztzaGFwZS1yZW5kZXJpbmc6YXV0bzt0ZXh0LXJlbmRlcmluZzphdXRvO2VuYWJsZS1iYWNrZ3JvdW5kOmFjY3VtdWxhdGUiICAgICAgIGQ9Ik0gOS45NjY3OTY5LDEwMTQuMzYyMiBDIDYuNjU3NTgwOSwxMDE0LjM4MSAzLjk4NzQzLDEwMTcuMDc2NCA0LDEwMjAuMzg1NiBjIDAuMDEyNTY5LDMuMzA5MiA2LjAxMTcxOSw4Ljk3NjYgNi4wMTE3MTksOC45NzY2IDAsMCA1Ljk4ODI4NywtNS42OTA3IDUuOTg4MjgxLC05IGwgMCwtMC4wNDUgYyAtMC4wMjUxNSwtMy4zMDkxIC0yLjcyNDAxNCwtNS45NzQxIC02LjAzMzIwMzEsLTUuOTU1MSB6IG0gMC4wMDk3NywyIGMgMi4yMDYyMDYxLC0wLjAxMyA0LjAwNjY5MzEsMS43NjI2IDQuMDIzNDMzMSwzLjk2ODggbCAwLDAuMDMxIGMgLTVlLTYsMi4yMDYyIC0zLjk5MjE4OCw2IC0zLjk5MjE4OCw2IDAsMCAtMy45OTk0MjQsLTMuNzc4MiAtNC4wMDc4MTIsLTUuOTg0NCAtMC4wMDg0LC0yLjIwNjIgMS43NzAzMzQ1LC00LjAwMyAzLjk3NjU2MjUsLTQuMDE1NiB6IiAgICAgICBpZD0icGF0aDEyNTY4IiAgICAgICBpbmtzY2FwZTpjb25uZWN0b3ItY3VydmF0dXJlPSIwIiAgICAgICBzb2RpcG9kaTpub2RldHlwZXM9ImNzY3NjY2Njc2NzYyIgLz4gICAgPHBhdGggICAgICAgc3R5bGU9Im9wYWNpdHk6MTtmaWxsOiMwMDAwMDA7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmU7c3Ryb2tlLXdpZHRoOjI7c3Ryb2tlLWxpbmVjYXA6YnV0dDtzdHJva2UtbGluZWpvaW46YmV2ZWw7c3Ryb2tlLW1pdGVybGltaXQ6NDtzdHJva2UtZGFzaGFycmF5Om5vbmU7c3Ryb2tlLWRhc2hvZmZzZXQ6MDtzdHJva2Utb3BhY2l0eToxO21hcmtlcjpub25lIiAgICAgICBkPSJNIDEwIDIgQyA2LjY4NjI5MiAyIDQgNC42ODYzIDQgOCBDIDQgMTEuMzEzNyAxMCAxNyAxMCAxNyBDIDEwIDE3IDE2IDExLjMxMzcgMTYgOCBDIDE2IDQuNjg2MyAxMy4zMTM3MDggMiAxMCAyIHogTSAxMCA0IEMgMTIuMDcxMDY4IDQgMTMuNzUgNS42Nzg5IDEzLjc1IDcuNzUgQyAxMy43NSA5LjIwNTMyNzggMTEuOTMxMTEgMTEuNjQ0MzkzIDEwLjgzMDA3OCAxMyBMIDkuMTY5OTIxOSAxMyBDIDguMDY4ODkwMyAxMS42NDQzOTMgNi4yNSA5LjIwNTMyNzggNi4yNSA3Ljc1IEMgNi4yNSA1LjY3ODkgNy45Mjg5MzIgNCAxMCA0IHogIiAgICAgICB0cmFuc2Zvcm09InRyYW5zbGF0ZSgwLDEwMzIuMzYyMikiICAgICAgIGlkPSJwYXRoMTczMDUiIC8+ICA8L2c+PC9zdmc+); } -/* Digital clock styling */ +/* ========================= + 19. DIGITAL CLOCK + ========================= */ .digital-clock { - font-family: monospace; - font-size: 1rem; - font-weight: bold; - color: #ffb700; - background-color: #000; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - border: 1px solid #333; - text-align: center; - letter-spacing: 0.05rem; - box-shadow: 0 0 5px rgba(255, 183, 0, 0.5); + font-family: monospace; + font-size: 1rem; + font-weight: bold; + color: #ffb700; + background-color: #000; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + border: 1px solid #333; + text-align: center; + letter-spacing: 0.05rem; + box-shadow: 0 0 5px rgba(255, 183, 0, 0.5); } -/* Time zone markers */ -/* .time-zone-marker { - background-color: rgba(0, 0, 0, 0.7); - color: white; - padding: 0.5rem; - border-radius: 0.25rem; - border: 1px solid #333; - font-family: monospace; -} - -.time-zone-marker .zone-name { - font-weight: bold; - text-align: center; - margin-bottom: 0.25rem; -} - -.time-zone-marker .zone-offset { - font-size: 0.75rem; - text-align: center; - color: #ccc; -} - -/* Digital Clock Styling */ -.digital-clock { - font-variant-numeric: tabular-nums; - letter-spacing: 0.05em; - background-color: rgba(0, 0, 0, 0.7); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); - display: inline-block; - margin: 0.25rem 0; -} -*/ - -/* Mapbox Container Styling */ +/* ========================= + 20. MAPBOX CONTAINER & FONT + ========================= */ .mapbox-container { - /* color: rgb(var(--foreground-rgb)); */ - /* background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); */ - margin: 0; - padding: 0; - font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; + margin: 0; + padding: 0; + font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; } -/* Override Mapbox Default Body styles */ .mapboxgl-map { - font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; + font-family: 'Roboto Condensed', Arial, Helvetica, sans-serif; } + +/* ========================================================== + END OF SIGAP UI CSS + ========================================================== +*/