From 8a1a4320c22676b204a7831186cdfec7b06bb787 Mon Sep 17 00:00:00 2001 From: vergiLgood1 Date: Wed, 7 May 2025 10:23:54 +0700 Subject: [PATCH] feat: Add Panic Button Demo component and EWS Alert Layer for incident management - Implemented PanicButtonDemo component for triggering alerts with varying priorities. - Created EWSAlertLayer to manage and display active incidents on the map. - Added styles for alert markers and status indicators. - Developed mock data utilities for generating and managing incident logs. - Defined TypeScript interfaces for incident logs and locations. --- sigap-website/app/(pages)/layout.tsx | 1 - .../app/_components/map/controls/example.tsx | 2266 +++++++++++++++++ .../map/controls/panic-button-demo.tsx | 108 + .../app/_components/map/crime-map.tsx | 149 +- .../app/_components/map/layers/coastline.tsx | 78 +- .../map/layers/ews-alert-layer.tsx | 318 +++ .../app/_components/map/layers/layers.tsx | 127 +- .../app/_components/map/layers/timezone.tsx | 1 + sigap-website/app/_styles/ews.css | 50 + sigap-website/app/_styles/globals.css | 3 + sigap-website/app/_utils/mock/ews-data.ts | 74 + sigap-website/app/_utils/types/crimes.ts | 14 + sigap-website/app/_utils/types/ews.ts | 24 + 13 files changed, 3046 insertions(+), 167 deletions(-) create mode 100644 sigap-website/app/_components/map/controls/example.tsx create mode 100644 sigap-website/app/_components/map/controls/panic-button-demo.tsx create mode 100644 sigap-website/app/_components/map/layers/ews-alert-layer.tsx create mode 100644 sigap-website/app/_styles/ews.css create mode 100644 sigap-website/app/_utils/mock/ews-data.ts create mode 100644 sigap-website/app/_utils/types/ews.ts diff --git a/sigap-website/app/(pages)/layout.tsx b/sigap-website/app/(pages)/layout.tsx index d2cce73..effd8c5 100644 --- a/sigap-website/app/(pages)/layout.tsx +++ b/sigap-website/app/(pages)/layout.tsx @@ -1,7 +1,6 @@ import { Geist } from "next/font/google"; import { ThemeProvider } from "next-themes"; import "@/app/_styles/globals.css"; -import "@/app/_styles/ui.css"; import ReactQueryProvider from "@/app/_lib/react-query-provider"; import { Toaster } from "@/app/_components/ui/sonner"; diff --git a/sigap-website/app/_components/map/controls/example.tsx b/sigap-website/app/_components/map/controls/example.tsx new file mode 100644 index 0000000..d349cd0 --- /dev/null +++ b/sigap-website/app/_components/map/controls/example.tsx @@ -0,0 +1,2266 @@ +// 'use client' +// import 'mapbox-gl/dist/mapbox-gl.css'; +// import mapboxgl from "mapbox-gl"; +// import './ui.css'; +// import React, { useRef, useEffect, useState } from 'react'; +// import io from 'socket.io-client' +// import Worker from 'web-worker'; +// import TitikGempa from './components/mapbox_marker/titik_gempa'; +// import GempaBumiAlert from './components/GempaBumiAlert'; +// import * as turf from '@turf/turf' +// import Card from './components/card/card'; +// import { createRoot } from 'react-dom/client'; +// import AnimatedPopup from 'mapbox-gl-animated-popup'; +// import ItemKotaTerdampak from './components/ItemKotaTerdampak'; +// import { KotaTerdampak, InfoGempa, InfoTsunami } from "../libs/interface"; +// import Jam from './components/Jam'; +// const { DateTime } = require("luxon"); +// import { IoLocationSharp } from "react-icons/io5"; +// import { XMLParser, XMLBuilder, XMLValidator } from "fast-xml-parser"; +// import TitikTsunami from './components/mapbox_marker/titik_tsunami'; + +// // import { HexGrid, Layout, Hexagon, Text, Pattern, Path, Hex, GridGenerator, HexUtils } from 'react-hexgrid'; +// // import { css } from "@emotion/react" + +// // const initialConfig: any = { +// // width: 1000, +// // height: 800, +// // layout: { width: 8, height: 8, flat: false, spacing: 1.02 }, +// // origin: { x: 0, y: 0 }, +// // map: "parallelogram", +// // mapProps: [10], +// // } +// // const generator = GridGenerator.getGenerator(initialConfig.map) + +// // const initialHexagons: Hex[] = generator(initialConfig.mapProps) + + +// mapboxgl.accessToken = 'pk.eyJ1IjoiYmFndXNpbmRyYXlhbmEiLCJhIjoiY2p0dHMxN2ZhMWV5bjRlbnNwdGY4MHFuNSJ9.0j5UAU7dprNjZrouWnoJyg'; + + + +// let socket; +// export default function Home() { +// // const [hexagons, setHexagons] = React.useState(initialHexagons) +// // const [config, setConfig] = React.useState(initialConfig) + +// // const layout = config.layout +// // const size = { x: layout.width, y: layout.height } + +// const dangerSound = "/sounds/siren-alarm-96503.mp3" +// const smallEarthQuakeSound = "/sounds/wrong-answer-129254.mp3" +// const tsunamiAlertSound = "sounds/security-alarm-80493.mp3" +// const mapContainer = useRef(null); // Update the type of mapContainer ref +// const map = useRef(null); // Update the type of the map ref +// const [lng, setLng] = useState(123.90146694265115); +// const [lat, setLat] = useState(-1.370489908625089); +// const [zoom, setZoom] = useState(5); + +// const geoJsonData = useRef(null); +// const geoJsonCoastline = useRef(null); +// const geoJsonTitikGempa = useRef(null); +// const worker = useRef(null); +// const adaGempa = useRef(false); +// const tgs = useRef([]); +// const titikGempaBaru = useRef([]); + +// const tts = useRef([]); + +// const kts = useRef([]); +// const markerDaerahs = useRef([]); +// const daerahTsunami = useRef([]); + +// const lastGempaId = useRef(''); +// const lastGempaKecilId = useRef(''); + +// const [detailInfoGempa, setDetailInfoGempa] = useState(null); +// const [loadingScreen, setLoadingScreen] = useState(true); + +// const [gempaDirasakan, setGempaDirasakan] = useState(null); +// const [gempaTerakhir, setGempaTerakhir] = useState(null); +// const [events, setEvents] = useState([]); +// const [alertGempaBumi, setAlertGempaBumi] = useState(null); + +// const [alertGempaBumis, setAlertGempaBumis] = useState([]); + +// const [alertTsunami, setAlertTsunami] = useState(null); + + +// const [infoTsunami, setInfoTsunami] = useState(null); +// const blinkInterval = useRef(null); + + +// const warningHandler = async (data: any) => { +// console.log("WARNING!!!"); +// const time = new Date().toLocaleTimeString(); +// const id = data.id ?? `tg-${time}`; + + +// if (!map.current) return; +// // map.current.flyTo({ +// // center: [data.lng, data.lat], +// // zoom: 7, +// // essential: true +// // }); + +// const nig: InfoGempa = { +// id: id, +// lng: parseFloat(data.lng), +// lat: parseFloat(data.lat), +// mag: parseFloat(data.mag || 9.0), +// depth: data.depth || "10 Km", +// message: data.message, +// place: data.place, +// time: data.time || new Date().toLocaleString(), +// listKotaTerdampak: [] +// }; + +// const tg = new TitikGempa(id, nig, { +// pWaveSpeed: 6000, +// sWaveSpeed: 3000, +// map: map.current!, +// showMarker: true, +// description: data.message, +// showPopup: true, +// showPopUpInSecond: 6, +// zoomToPosition: true +// }); +// tgs.current.push(tg); +// titikGempaBaru.current.push(tg); + + +// setAlertGempaBumis([...alertGempaBumis, nig]); + + +// // tgs.current.push(tg); +// // tgs.current.sort(function (a: any, b: any) { +// // return new Date(b.time).getTime() - new Date(a.time).getTime(); +// // }); + +// const audioDangerElement = document.getElementById('danger'); +// setTimeout(() => { + +// if (audioDangerElement) { +// (audioDangerElement as HTMLAudioElement).play(); +// } +// setTimeout(() => { +// var voice = new Audio("/voice/gempabumi.wav"); +// voice.play(); +// }, 2000); +// }, 2000); + + +// await new Promise(r => setTimeout(r, 6000)); + +// setEvents(tgs.current); +// if (worker.current != null) { +// adaGempa.current = true; +// console.log("Send Wave"); +// sendWave(); +// } +// if (audioDangerElement) { +// //set volume down +// (audioDangerElement as HTMLAudioElement).volume = 0.5; +// } + +// } + +// function blinkCoastline() { +// if (blinkInterval.current) { +// clearInterval(blinkInterval.current); +// } +// blinkInterval.current = setInterval(() => { +// const visibility = map.current!.getLayoutProperty( +// 'outline-coastline', +// 'visibility' +// ); +// map.current!.setLayoutProperty( +// 'outline-coastline', +// 'visibility', +// visibility == 'visible' ? 'none' : 'visible' +// ); +// }, 1000); + +// } + +// const warningTsunamiHandler = async (data: any) => { +// // setInfoTsunami(data); +// if (blinkInterval.current) { +// clearInterval(blinkInterval.current); +// } +// const results: any = []; +// daerahTsunami.current = []; + +// const time = new Date().toLocaleTimeString(); +// const id = data.id ?? `tg-${time}`; + +// const coordinates = data.point.coordinates.split(","); +// const nit: InfoTsunami = { +// id: id, +// lng: parseFloat(coordinates[0]), +// lat: parseFloat(coordinates[1]), +// message: data.description + "\n" + data.instruction, +// level: data.subject, +// time: data.time || new Date().toLocaleString(), +// listKotaTerdampak: [] +// }; + +// let level = "WASPADA"; + +// for (let x = 0; x < data.wzarea.length; x++) { +// const wz = data.wzarea[x]; +// const cek = geoJsonCoastline.current.features.find((f) => +// wz.district.replaceAll("-", " ") +// .replaceAll("PULAU ", "") +// .replaceAll("KEPULAUAN ", "") +// .replaceAll(" BAGIAN UTARA", "") +// .replaceAll(" BAGIAN BARAT", "") +// .replaceAll(" BAGIAN SELATAN", "") +// .replaceAll(" BAGIAN TIMUR", "") +// === +// f.properties.alt_name.replaceAll("KABUPATEN ", "") +// .replaceAll("PULAU ", "") +// .replaceAll("KEPULAUAN ", "")); +// if (cek) { + +// let color = "yellow"; +// if (wz.level == "SIAGA") { +// color = "orange"; +// } else if (wz.level == "AWAS") { +// color = "red"; +// } +// if (level == "WASPADA" && wz.level == "SIAGA") { +// level = wz.level; +// } + +// if (level == "SIAGA" && wz.level == "AWAS") { +// level = wz.level; +// } + + +// cek.properties.color = color; +// results.push(cek); + +// const dist = turf.distance(turf.point([nit.lng, nit.lat]), turf.point([cek.properties.longitude, cek.properties.latitude])); +// const timeDist = Math.floor(dist / 3) * 1000; +// nit.listKotaTerdampak!.push({ +// lng: cek.properties.longitude, +// lat: cek.properties.latitude, +// distance: dist, +// name: cek.properties.alt_name, +// hit: false, +// timeArrival: new Date(new Date().getTime() + timeDist) +// }); +// } else { +// // console.log(info.wzarea); +// console.log(wz); +// } + +// nit.listKotaTerdampak!.sort((a, b) => a.distance - b.distance); + +// } + +// for (let x = 0; x < results.length; x++) { +// const element = results[x]; +// const p: number[] = turf.centroid(element).geometry.coordinates; +// if (markerDaerahs.current.findIndex((el) => el[0] == p[0] && el[1] == p[1]) == -1) { +// markerDaerahs.current.push([p[0], p[1]]); +// const markerParent = document.createElement('div'); +// const markerEl = document.createElement('div'); +// markerEl.innerHTML = '

' + element.properties.alt_name + '

'; +// markerEl.classList.add('marker-daerah'); +// markerEl.classList.add('show-pop-up'); +// markerParent.appendChild(markerEl); +// new mapboxgl.Marker(markerParent) +// .setLngLat([p[0], p[1]]) +// .addTo(map.current!) + +// } + +// } + +// const tt = new TitikTsunami(id, nit, { +// pWaveSpeed: 6000, +// sWaveSpeed: 3000, +// map: map.current!, +// showMarker: true, +// description: data.description + "\n" + data.instruction, +// showPopup: true, +// showPopUpInSecond: 6, +// zoomToPosition: true, +// closePopUpInSecond: 13 +// }); +// tts.current.push(tt); + +// setAlertTsunami(tt); + + +// daerahTsunami.current = results; +// if (results.length > 0) { +// if (map.current!.getSource('coastline')) { +// (map.current!.getSource('coastline') as mapboxgl.GeoJSONSource).setData({ "type": "FeatureCollection", "features": results }); +// } else { +// map.current!.addSource('coastline', { +// 'type': 'geojson', +// 'data': { "type": "FeatureCollection", "features": results } +// }); +// } +// map.current!.setLayoutProperty( +// 'outline-coastline', +// 'visibility', +// 'visible' +// ); + +// } else { +// testDemoTsunami(); +// return; +// } + + + +// if (!map.current) return; +// // map.current.flyTo({ +// // center: [coordinates[0], coordinates[1]], +// // zoom: 7, +// // essential: true +// // }); + + +// blinkCoastline(); +// map.current!.moveLayer('outline-coastline'); + +// // tgs.current.push(nt); +// // setEvents(tgs.current); + +// console.log("WARNING TSUNAMI!!!"); + +// var notif = new Audio(tsunamiAlertSound); +// notif.loop = true; +// notif.play(); +// setTimeout(() => { +// var voice = new Audio("/voice/terdeteksi.wav"); +// voice.play(); + +// setTimeout(() => { +// var voice = new Audio("/voice/" + level.toLowerCase() + ".wav"); +// voice.play(); +// setTimeout(() => { +// var voice = new Audio("/voice/potensi.wav"); +// voice.play(); + +// if (level == "AWAS") { +// setTimeout(() => { +// var voice = new Audio("/voice/evakuasi.wav"); +// voice.play(); +// setTimeout(() => { +// notif.pause(); +// }, 2000); +// }, 6000); +// } else { +// setTimeout(() => { +// var voice = new Audio("/voice/informasi.wav"); +// voice.play(); +// setTimeout(() => { +// notif.pause(); +// }, 2000); +// }, 6000); +// } + +// }, 5000); +// }, 5000); +// }, 2000); + + +// setTimeout(() => { +// const tsunamiWarning: HTMLDivElement = document.querySelector("#tsunami-warning") as HTMLDivElement; +// //find div inside bg-tsunami +// if (tsunamiWarning) { +// const divs = tsunamiWarning.querySelectorAll(".show-pop-up"); +// //loop and add class close-pop-up +// divs.forEach((v) => { +// v.classList.add("close-pop-up"); +// }); +// } +// }, 9000); +// setTimeout(() => { + + +// const bgTsunami: HTMLDivElement = document.querySelector("#bg-tsunami .hex-bg") as HTMLDivElement; +// //find div inside bg-tsunami +// if (bgTsunami) { +// const divs = bgTsunami.querySelectorAll("div"); +// //loop and add class close-pop-up +// divs.forEach((v) => { +// v.classList.add("close-pop-up"); +// }); +// } +// setTimeout(() => { +// setAlertTsunami(null); +// }, 1000); +// setInfoTsunami(tt); +// }, 10000); +// } + +// const socketInitializer = () => { +// if (socket != null) return; +// fetch('/api/socket') +// .then(() => { + +// console.log('Socket is initializing'); + +// socket = io(); + +// socket.on('connect', () => { +// console.log('connected'); +// }); +// socket.on('warning', (v: any) => { + +// warningHandler(v); +// }); +// socket.on('message', (v: any) => { + +// console.log(v); +// }); + +// }) +// .catch((error) => { +// console.error('Error initializing socket:', error); +// }); +// }; + +// const initWorker = () => { +// worker.current = new Worker( +// new URL('./worker.mjs', import.meta.url), +// { type: 'module' } +// ); + +// worker.current.postMessage({ type: 'geoJsonData', data: geoJsonData.current, coastline: geoJsonCoastline.current }); + +// worker.current.addEventListener('message', (event: any) => { +// const data = event.data; +// if (data.type == "checkMultiHighlightArea" && data.id == "wave") { +// recieveWave(data); +// } +// }); +// } + + + + +// useEffect(() => { +// if (map.current) return () => { }; +// map.current = new mapboxgl.Map({ +// container: mapContainer.current!, +// style: 'mapbox://styles/mapbox/dark-v11', +// center: [lng, lat], +// zoom: zoom, +// maxZoom: 22, +// }); + +// map.current.on('load', () => { +// // loadGeoJsonData(); +// loadGeoJsonCoastline(); +// }); + + +// }); + +// useEffect(() => { + +// socketInitializer(); + +// if (socket) return () => { +// socket!.disconnect(); +// }; + +// }); + + + +// const sendWave = () => { +// let t: any = []; +// for (let i = 0; i < titikGempaBaru.current.length; i++) { +// const v = titikGempaBaru.current[i]; +// if (!v.finish) { +// t.push({ +// id: v.id, +// center: v.center, +// mag: v.mag, +// depth: v.depth, +// pWaveRadius: v.pWaveRadius, +// sWaveRadius: v.sWaveRadius, +// areaTerdampak: [], +// message: v.description +// }) +// } + +// } +// if (t.length > 0) { +// worker.current!.postMessage({ type: 'checkMultiHighlightArea', titikGempa: t, id: "wave" }); +// } else { +// console.log("Not Send Wave"); +// } +// } + + +// const isEqual = (a, b) => a.id === b.id && a.name === b.name; + + +// const recieveWave = async (data: any) => { +// let alerts: InfoGempa[] = []; +// for (let x = 0; x < data.titikGempa.length; x++) { +// const tg = data.titikGempa[x]; + +// const nig: InfoGempa = { +// id: tg.id, +// lng: parseFloat(tg.center[1]), +// lat: parseFloat(tg.center[0]), +// mag: tg.mag, +// depth: tg.depth, +// message: tg.message, +// place: tg.place, +// time: new Date().toLocaleString(), +// listKotaTerdampak: [] +// }; + +// for (let il = 0; il < tg.areaTerdampak.length; il++) { +// const at = tg.areaTerdampak[il]; +// const dist = turf.distance(turf.point([tg.center[0], tg.center[1]]), turf.point([at.center[0], at.center[1]])) - (tg.sWaveRadius / 1000); +// const time = Math.floor(dist / 3) * 1000; +// nig.listKotaTerdampak!.push({ +// lng: at.center[1], +// lat: at.center[0], +// distance: dist, +// name: at.alt_name, +// hit: at.hit, +// timeArrival: new Date(new Date().getTime() + time) +// }); + +// } + +// //sort nig.listKotaTerdampak by distance +// nig.listKotaTerdampak!.sort((a, b) => a.distance - b.distance); + +// alerts.push(nig); +// } + +// //get last alert +// if (alerts.length > 0) { +// const fig = alerts.slice(-1).pop()!; +// setAlertGempaBumi(new TitikGempa(fig.id, fig)); +// } else { +// setAlertGempaBumi(null); +// } + +// const areas = data.area; + +// // Hapus data array objek yang sama +// // const uniqueData = areas.filter((obj, index, self) => +// // index === self.findIndex((t) => isEqual(t.properties.mhid, obj.properties.mhid)) +// // ); + +// const uniqueData = areas; + +// for (let x = 0; x < uniqueData.length; x++) { +// const element = uniqueData[x]; +// const p: number[] = turf.centroid(element).geometry.coordinates; +// if (markerDaerahs.current.findIndex((el) => el[0] == p[0] && el[1] == p[1]) == -1) { +// markerDaerahs.current.push([p[0], p[1]]); +// const markerParent = document.createElement('div'); +// const markerEl = document.createElement('div'); +// markerEl.innerHTML = '

' + element.properties.alt_name + '

'; +// markerEl.classList.add('marker-daerah'); +// markerEl.classList.add('show-pop-up'); +// markerParent.appendChild(markerEl); +// new mapboxgl.Marker(markerParent) +// .setLngLat([p[0], p[1]]) +// .addTo(map.current!) + +// } else { + +// const index = kts.current.findIndex((el) => el.lng == p[0] && el.lat == p[1]); +// if (index != -1) { + +// // kts.current[index].distance += 16; +// // kts.current[index].hit = element.properties.hit; +// // setKotaTerdampak([...kotaTerdampak, ...kts.current]); +// // countdownTime(); +// } +// } + + +// } + +// if (map.current!.getSource('hightlight-wave')) { +// (map.current!.getSource('hightlight-wave') as mapboxgl.GeoJSONSource).setData({ "type": "FeatureCollection", "features": uniqueData }); +// } else { +// map.current!.addSource('hightlight-wave', { +// 'type': 'geojson', +// 'data': { "type": "FeatureCollection", "features": uniqueData } +// }); +// } + +// if (!map.current!.getLayer('hightlight-wave-layer')) { +// map.current!.addLayer({ +// 'id': 'hightlight-wave-layer', +// 'type': 'fill', +// 'source': 'hightlight-wave', +// 'layout': {}, +// 'paint': { +// 'fill-color': ['get', 'color'], +// 'fill-opacity': 0.8 +// } +// }); + +// map.current!.moveLayer('outline'); +// map.current!.moveLayer('outline-coastline'); +// for (let tg of tgs.current) { +// if (map.current!.getLayer(tg.id)) { +// map.current!.moveLayer(tg.id); +// } +// } +// } + +// // if (map.current!.getSource('coastline')) { +// // (map.current!.getSource('coastline') as mapboxgl.GeoJSONSource).setData({ "type": "FeatureCollection", "features": data.line }); +// // } else { +// // map.current!.addSource('coastline', { +// // 'type': 'geojson', +// // 'data': { "type": "FeatureCollection", "features": data.line } +// // }); +// // } + +// sendWave(); +// } + +// const hoverWilayah = useRef(null); + +// function loadGeoJsonCoastline() { +// fetch('/geojson/garis_pantai.geojson') +// .then(response => response.json()) +// .then(data => { +// geoJsonCoastline.current = data; +// if (!map.current!.getSource('coastline')) { +// map.current!.addSource('coastline', { +// type: 'geojson', +// generateId: true, +// data: data +// }); +// map.current!.addLayer({ +// 'id': 'outline-coastline', +// 'type': 'line', +// 'source': 'coastline', +// 'layout': { +// 'visibility': 'none', +// }, +// 'paint': { +// 'line-color': ['get', 'color'], +// 'line-width': 5, +// 'line-opacity': 1 +// } +// }); + +// } +// loadGeoJsonData(); +// }).catch((error) => { +// alert("Failed load geojson data : " + error); +// console.error('Error fetching data:', error); +// }); +// }; + +// function loadGeoJsonData() { +// fetch('/geojson/all_kabkota_ind_reduce.geojson') +// .then(response => response.json()) +// .then(data => { +// geoJsonData.current = data; +// if (!map.current!.getSource('wilayah')) { +// map.current!.addSource('wilayah', { +// type: 'geojson', +// generateId: true, +// data: data +// }); +// map.current!.addLayer({ +// 'id': 'outline', +// 'type': 'line', +// 'source': 'wilayah', +// 'layout': {}, +// 'paint': { +// 'line-color': '#807a72', +// 'line-width': 1, +// 'line-opacity': 0.7 +// } +// }); + +// map.current!.addLayer({ +// 'id': 'wilayah-fill', +// 'type': 'fill', +// 'source': 'wilayah', +// 'layout': { + +// }, +// 'paint': { +// 'fill-color': 'red', +// 'fill-opacity': [ +// 'case', +// ['boolean', ['feature-state', 'hover'], false], +// 0.1, +// 0 +// ], + +// } +// }); + +// // map.current!.on('mousemove', 'wilayah-fill', (e: any) => { +// // if (e.features.length > 0) { +// // if (hoverWilayah.current !== null) { +// // map.current!.setFeatureState( +// // { source: 'wilayah', id: hoverWilayah.current }, +// // { hover: false } +// // ); +// // } +// // hoverWilayah.current = e.features[0].id; +// // map.current!.setFeatureState( +// // { source: 'wilayah', id: hoverWilayah.current }, +// // { hover: true } +// // ); +// // } +// // }); + +// // map.current!.on('mouseleave', 'wilayah-fill', () => { +// // if (hoverWilayah.current !== null) { +// // map.current!.setFeatureState( +// // { source: 'wilayah', id: hoverWilayah.current }, +// // { hover: false } +// // ); +// // } +// // hoverWilayah.current = null; +// // }); +// } +// // getTitikStationJson(); +// getTitikGempaJson(); +// getTimezoneGeojson(); +// getFaultLineGeojson(); +// initWorker(); +// }).catch((error) => { +// alert("Failed load geojson data : " + error); +// console.error('Error fetching data:', error); +// }); +// }; + +// function getTitikStationJson() { +// const url = "https://bmkg-content-inatews.storage.googleapis.com/sensor_seismic.json"; +// if (map.current) { +// map.current.loadImage( +// '/images/triangle-filled-svgrepo-com.png', +// (error, image: any) => { +// if (error) throw error; + +// // Add the image to the map style. +// map.current!.addImage('station-icon', image); + +// // Add a data source containing one point feature. +// map.current!.addSource('station', { +// 'type': 'geojson', +// 'data': url +// }); + +// // Add a layer to use the image to represent the data. +// map.current!.addLayer({ +// 'id': 'stations', +// 'type': 'symbol', +// 'source': 'station', // reference the data source +// 'layout': { +// 'icon-image': 'station-icon', // reference the image +// 'icon-size': 0.05 +// } +// }); + +// map.current!.on('click', 'stations', (e: any) => { +// // Copy coordinates array. +// const coordinates = e.features[0].geometry.coordinates.slice(); +// const d = e.features[0].properties; +// const placeholder = document.createElement('div'); +// const root = createRoot(placeholder) +// root.render(SENSOR SEISMIK

+// } className='min-h-48 min-w-48 whitespace-pre-wrap' > +//
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
ID{d.id}
Stakeholder{d.stakeholder}
UPTBMKG{d.uptbmkg}
Lokasi (Lat,Lng){coordinates[0]} , {coordinates[1]}
+//
+//
); + +// new AnimatedPopup({ +// openingAnimation: { +// duration: 100, +// easing: 'easeOutSine', +// transform: 'scale' +// }, +// closingAnimation: { +// duration: 100, +// easing: 'easeInOutSine', +// transform: 'scale' +// } +// }).setDOMContent(placeholder).setLngLat(coordinates).addTo(map.current!); +// }); + +// map.current!.on('mouseenter', 'stations', () => { +// map.current!.getCanvas().style.cursor = 'pointer'; +// }); + +// // Change it back to a pointer when it leaves. +// map.current!.on('mouseleave', 'stations', () => { +// map.current!.getCanvas().style.cursor = ''; +// }); +// } +// ); + +// } +// } + +// function getTitikGempaJson() { +// const url = "https://bmkg-content-inatews.storage.googleapis.com/gempaQL.json?t=" + new Date().getTime(); +// fetch(url) +// .then(response => response.json()) +// .then((data) => { +// geoJsonTitikGempa.current = data; +// setTimeout(() => { +// document.getElementById("loading-screen")!.style.display = "none"; +// setLoadingScreen(false); +// }, 1000); +// let ntg: TitikGempa[] = []; +// for (let index = 0; index < data.features.length; index++) { +// const feature = data.features[index]; +// const dt = DateTime.fromSQL(feature.properties.time, { zone: 'UTC' }).setZone("Asia/Jakarta"); +// const readAbleTime = dt.toISODate() + " " + dt.toLocaleString(DateTime.TIME_24_WITH_SECONDS) +// ntg.push(new TitikGempa(feature.properties.id, { +// id: feature.properties.id, +// lng: feature.geometry.coordinates[0], +// lat: feature.geometry.coordinates[1], +// mag: feature.properties.mag, +// depth: feature.properties.depth, +// place: feature.properties.place, +// time: readAbleTime +// })); +// } +// tgs.current = ntg; +// setEvents(tgs.current); +// console.log('load titik gempa 1'); +// //check earthquakes layer +// if (map.current!.getLayer('earthquakes-layer')) { +// //update source +// (map.current!.getSource('earthquakes') as mapboxgl.GeoJSONSource).setData(data); +// } else { +// //add source +// map.current!.addSource('earthquakes', { +// type: 'geojson', +// data: data +// }); + +// map.current!.addLayer({ +// 'id': 'earthquakes-layer', +// 'type': 'circle', +// 'source': 'earthquakes', +// 'paint': { +// 'circle-radius': ["to-number", ['get', 'mag']], +// 'circle-stroke-width': 2, + +// 'circle-color': [ +// "case", +// //depth <= 50 red, depth <= 100 orange, depth <= 250 yellow, depth <= 600 green, depth > 600 blue +// ['<=', ["to-number", ['get', 'depth']], 50], +// "red", +// ['<=', ["to-number", ['get', 'depth']], 100], +// "orange", +// ['<=', ["to-number", ['get', 'depth']], 250], +// "yellow", +// ['<=', ["to-number", ['get', 'depth']], 600], +// "green", +// "blue", +// ], +// 'circle-stroke-color': 'white' +// } +// }); +// } + +// map.current!.on('click', 'earthquakes-layer', (e: any) => { +// // Copy coordinates array. +// const coordinates = e.features[0].geometry.coordinates.slice(); +// const d = e.features[0].properties; +// const placeholder = document.createElement('div'); +// const root = createRoot(placeholder) +// root.render( +//
+//
+//

GEMPA BUMI

+//
+// +// } className='min-h-48 min-w-48 whitespace-pre-wrap' > +//
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
Magnitudo{d.mag}
Kedalaman{d.depth}
Waktu{new Date(d.time!).toLocaleString()}
Lokasi (Lat,Lng){coordinates[0]} , {coordinates[1]}
+//
+//
); + +// new AnimatedPopup({ +// openingAnimation: { +// duration: 100, +// easing: 'easeOutSine', +// transform: 'scale' +// }, +// closingAnimation: { +// duration: 100, +// easing: 'easeInOutSine', +// transform: 'scale' +// } +// }).setDOMContent(placeholder).setLngLat(coordinates).addTo(map.current!); +// }); + +// map.current!.on('mouseenter', 'earthquakes-layer', () => { +// map.current!.getCanvas().style.cursor = 'pointer'; +// }); + +// // Change it back to a pointer when it leaves. +// map.current!.on('mouseleave', 'earthquakes-layer', () => { +// map.current!.getCanvas().style.cursor = ''; +// }); + +// console.log('load titik gempa 2'); + +// getGempa(); +// getGempaKecil(); + +// }) +// .catch((error) => { +// console.error('Error initializing socket:', error); +// }); + +// } + +// const hoverTimezone = useRef(null); +// function getTimezoneGeojson() { +// const url = "/geojson/timezones_wVVG8.geojson"; +// map.current!.addSource('timezone', { +// 'type': 'geojson', +// 'generateId': true, +// 'data': url +// }); + +// // Add a layer to use the image to represent the data. +// // map.current!.addLayer({ +// // 'id': 'timezone-fill', +// // 'type': 'fill', +// // 'source': 'timezone', // reference the data source +// // 'layout': { + +// // }, +// // 'paint': { +// // // 'line-color': 'blue', +// // // 'line-width': 1 +// // 'fill-color': 'red', +// // 'fill-opacity': [ +// // 'case', +// // ['boolean', ['feature-state', 'hover'], false], +// // 0.1, +// // 0 +// // ], + +// // } +// // }); + +// map.current!.addLayer({ +// 'id': 'timezone-line', +// 'type': 'line', +// 'source': 'timezone', // reference the data source +// 'layout': { + +// }, +// 'paint': { +// 'line-color': 'orange', +// 'line-width': 1, +// 'line-opacity': 0.5 +// } +// }); + +// const markerParent1 = document.createElement('div'); +// const gmt7Marker = createRoot(markerParent1) +// gmt7Marker.render( +//
+//

+// +//

+//

WIB / GMT+7

+//
+// ); + +// new mapboxgl.Marker(markerParent1) +// .setLngLat([107.4999769225339, 3.4359354227361933]) +// .addTo(map.current!); + + +// const markerParent2 = document.createElement('div'); +// const gmt8Marker = createRoot(markerParent2) +// gmt8Marker.render( +//
+//

+// +//

+//

WITA / GMT+8

+//
+// ); +// new mapboxgl.Marker(markerParent2) +// .setLngLat([119.1174733337183, 3.4359354227361933]) +// .addTo(map.current!); + +// const markerParent3 = document.createElement('div'); +// const gmt9Marker = createRoot(markerParent3) +// gmt9Marker.render( +//
+//

+// +//

+//

WIT / GMT+9

+//
+// ); +// new mapboxgl.Marker(markerParent3) +// .setLngLat([131.58387377752751, 3.4359354227361933]) +// .addTo(map.current!) + +// // map.current!.on('click', 'timezone-fill', (e: any) => { +// // console.log(e); +// // }); + +// // map.current!.on('mousemove', 'timezone-fill', (e: any) => { +// // if (e.features.length > 0) { +// // if (hoverTimezone.current !== null) { +// // map.current!.setFeatureState( +// // { source: 'timezone', id: hoverTimezone.current }, +// // { hover: false } +// // ); +// // } +// // hoverTimezone.current = e.features[0].id; +// // map.current!.setFeatureState( +// // { source: 'timezone', id: hoverTimezone.current }, +// // { hover: true } +// // ); +// // } +// // }); + +// // map.current!.on('mouseleave', 'timezone-fill', () => { +// // if (hoverTimezone.current !== null) { +// // map.current!.setFeatureState( +// // { source: 'timezone', id: hoverTimezone.current }, +// // { hover: false } +// // ); +// // } +// // hoverTimezone.current = null; +// // }); +// } + +// function getFaultLineGeojson() { +// const url = "/geojson/indo_faults_lines.geojson"; +// map.current!.addSource('indo_faults_lines', { +// 'type': 'geojson', +// 'generateId': true, +// 'data': url +// }); + +// // Add a layer to use the image to represent the data. +// map.current!.addLayer({ +// 'id': 'indo_faults_line_layer', +// 'type': 'line', +// 'source': 'indo_faults_lines', // reference the data source +// 'layout': { + +// }, +// 'paint': { +// 'line-color': 'red', +// 'line-width': 1, +// 'line-opacity': 0.5 +// // 'fill-color': 'red', +// // 'fill-opacity': [ +// // 'case', +// // ['boolean', ['feature-state', 'hover'], false], +// // 0.1, +// // 0 +// // ], + +// } +// }); + + + +// } + +// function getGempa() { +// if (lastGempaId.current) { +// return +// } +// console.log("getGempa"); +// const url = "https://bmkg-content-inatews.storage.googleapis.com/datagempa.json?t=" + new Date().getTime(); +// fetch(url) +// .then(response => response.json()) +// .then((data) => { +// const coordinates = data.info.point.coordinates.split(","); +// lastGempaId.current = data.identifier; +// const sentTime = DateTime.fromISO(data.sent.replace("WIB", ""), { zone: "Asia/Jakarta" }); +// const currentTime = DateTime.now().setZone("Asia/Jakarta"); +// const readAbleTime = sentTime.toISODate() + " " + sentTime.toLocaleString(DateTime.TIME_24_WITH_SECONDS) + +// const nig: InfoGempa = { +// id: data.identifier, +// lng: parseFloat(coordinates[0]), +// lat: parseFloat(coordinates[1]), +// mag: data.info.magnitude || 9.0, +// depth: data.info.depth || "10 Km", +// message: data.info.description, +// time: readAbleTime +// }; + +// const cek = tgs.current.find((v) => v.id == data.identifier); +// if ((currentTime.toMillis() - sentTime.toMillis()) < 600000) { +// warningHandler({ +// id: data.identifier, +// lng: parseFloat(coordinates[0]), +// lat: parseFloat(coordinates[1]), +// mag: parseFloat(data.info.magnitude), +// depth: data.info.depth, +// message: data.info.description + "\n" + data.info.instruction, +// time: readAbleTime, +// }); +// const ntg = new TitikGempa(nig.id, nig, { +// map: map.current!, +// showMarker: true +// }); + +// setTimeout(() => { + +// // setAlertGempaBumi(ntg); +// setGempaDirasakan(ntg); +// }, 6000); +// } else if (!cek) { +// tgs.current.push(new TitikGempa(nig.id, nig)); +// //sort by time +// tgs.current.sort(function (a: any, b: any) { +// return new Date(b.time).getTime() - new Date(a.time).getTime(); +// }) +// geoJsonTitikGempa.current.features.push({ +// "geometry": { +// "type": "Point", +// "coordinates": [ +// nig.lng, +// nig.lat, +// 1 +// ] +// }, +// "type": "Feature", +// "properties": { +// id: nig.id, +// depth: parseFloat(nig.depth.replaceAll(" Km", "")).toFixed(2), +// mag: nig.mag, +// time: nig.time, +// place: nig.place, +// } +// }); +// (map.current!.getSource('earthquakes') as mapboxgl.GeoJSONSource).setData(geoJsonTitikGempa.current); +// setEvents(tgs.current); + +// } + +// const ntg = new TitikGempa(nig.id, nig, { +// map: map.current! +// }); +// setGempaDirasakan(ntg); + +// // getGempaPeriodik(); +// }) +// .catch((error) => { +// console.error('Error initializing socket:', error); +// }); + + +// } + +// function getGempaKecil() { +// if (lastGempaKecilId.current) { +// return; +// } +// console.log("getGempaKecil"); +// const url = "https://bmkg-content-inatews.storage.googleapis.com/lastQL.json?t=" + new Date().getTime(); +// fetch(url) +// .then(response => response.json()) +// .then((data) => { +// if (data.features.length > 0) { +// const feature = data.features[0]; +// lastGempaKecilId.current = feature.properties.id; + +// const sentTime = DateTime.fromSQL(feature.properties.time, { zone: 'UTC' }); +// const currentTime = DateTime.now().setZone("UTC"); + +// const msg = `${feature.properties.place} +// Magnitudo : ${feature.properties.mag} +// Kedalaman : ${feature.properties.depth} +// Lokasi (Lat,Lng) : +// ${feature.geometry.coordinates[0]} , ${feature.geometry.coordinates[1]}`; + +// const dt = DateTime.fromSQL(feature.properties.time, { zone: 'UTC' }).setZone("Asia/Jakarta"); +// const readAbleTime = dt.toISODate() + " " + dt.toLocaleString(DateTime.TIME_24_WITH_SECONDS) +// const nig: InfoGempa = { +// id: feature.properties.id, +// lng: parseFloat(feature.geometry.coordinates[0]), +// lat: parseFloat(feature.geometry.coordinates[1]), +// mag: parseFloat(feature.properties.mag), +// depth: feature.properties.depth || "10 Km", +// message: msg, +// place: feature.properties.place, +// time: readAbleTime +// }; + + + + +// //if sent time is less than 10 minutes +// if ((currentTime.toMillis() - sentTime.toMillis()) < 600000) { + +// if (map.current) { +// var notif = new Audio(smallEarthQuakeSound); +// notif.play(); +// // map.current!.flyTo({ +// // center: [feature.geometry.coordinates[0], feature.geometry.coordinates[1]], +// // zoom: 7, +// // essential: true +// // }); + +// const tg = new TitikGempa(feature.properties.id, nig, { +// pWaveSpeed: 6000, +// sWaveSpeed: 3000, +// map: map.current!, +// description: msg, +// zoomToPosition: true, +// showMarker: true, +// showPopup: true, +// showPopUpInSecond: 1, +// }); + +// setGempaTerakhir(tg); +// setAlertGempaBumi(new TitikGempa(feature.properties.id, nig)); + +// } + + + + +// } else { +// setGempaTerakhir(new TitikGempa(feature.properties.id, nig)); +// } + +// const cek = tgs.current.find((v) => v.id == feature.properties.id); +// if (!cek) { + +// tgs.current.unshift(new TitikGempa(feature.properties.id, nig)); +// geoJsonTitikGempa.current.features.push(feature); +// (map.current!.getSource('earthquakes') as mapboxgl.GeoJSONSource).setData(geoJsonTitikGempa.current); + + +// } + + +// setEvents(tgs.current); + + + + +// } +// getGempaPeriodik(); +// }) +// .catch((error) => { +// console.error('Error initializing socket:', error); +// }); +// } + +// function getGempaPeriodik() { +// setInterval(() => { +// const url = "https://bmkg-content-inatews.storage.googleapis.com/datagempa.json?t=" + new Date().getTime() +// //await fetch +// fetch(url) +// .then(response => response.json()) +// .then((data) => { +// if (lastGempaId.current != data.identifier) { +// lastGempaId.current = data.identifier; +// const coordinates = data.info.point.coordinates.split(","); +// const sentTime = DateTime.fromISO(data.sent.replace("WIB", ""), { zone: "Asia/Jakarta" }); +// const readAbleTime = sentTime.toISODate() + " " + sentTime.toLocaleString(DateTime.TIME_24_WITH_SECONDS); + +// warningHandler({ +// id: data.identifier, +// lng: parseFloat(coordinates[0]), +// lat: parseFloat(coordinates[1]), +// mag: parseFloat(parseFloat(data.info.magnitude).toFixed(1)), +// depth: data.info.depth, +// message: data.info.description + "\n" + data.info.instruction, +// time: readAbleTime +// }); + +// if (data.info.wzarea != undefined && data.info.wzarea.length > 0) { +// if (data.info.subject == "Warning Tsunami PD-4") { +// //delete outline-costline layer +// try { +// map.current!.removeLayer('outline-coastline'); +// map.current!.removeLayer('outline'); +// } catch (error) { + +// } +// } else if (data.info.subject.includes("Warning Tsunami")) { +// warningTsunamiHandler(data.info); +// } + +// } + +// } +// }) +// .catch((error) => { +// console.error('Error initializing socket:', error); +// }); +// }, 5000); + +// setInterval(() => { +// const url = "https://bmkg-content-inatews.storage.googleapis.com/lastQL.json?t=" + new Date().getTime(); +// fetch(url) +// .then(response => response.json()) +// .then((data) => { +// const feature = data.features[0]; +// const msg = `${feature.properties.place} +// Magnitudo : ${feature.properties.mag} +// Kedalaman : ${feature.properties.depth} +// Lokasi (Lat,Lng) : +// ${feature.geometry.coordinates[0]} , ${feature.geometry.coordinates[1]}`; + +// const dt = DateTime.fromSQL(feature.properties.time, { zone: 'UTC' }).setZone("Asia/Jakarta"); +// const readAbleTime = dt.toISODate() + " " + dt.toLocaleString(DateTime.TIME_24_WITH_SECONDS) +// const nig: InfoGempa = { +// id: feature.properties.id, +// lng: parseFloat(feature.geometry.coordinates[0]), +// lat: parseFloat(feature.geometry.coordinates[1]), +// mag: parseFloat(feature.properties.mag), +// depth: feature.properties.depth || "10 Km", +// message: msg, +// place: feature.properties.place, +// time: readAbleTime +// }; +// if (lastGempaKecilId.current != feature.properties.id) { +// lastGempaKecilId.current = feature.properties.id; +// var notif = new Audio(smallEarthQuakeSound); +// notif.play(); +// if (!map.current) return; + + +// if (gempaTerakhir != null && gempaTerakhir.setting != null && gempaTerakhir.setting.map != null) { +// gempaTerakhir.removeAllRender(); +// gempaTerakhir.removeMarker(); +// if (tgs.current.length > 0) { +// const ig = tgs.current[0].infoGempa +// geoJsonTitikGempa.current.features.push({ +// "geometry": { +// "type": "Point", +// "coordinates": [ +// ig.lng, +// ig.lat, +// 1 +// ] +// }, +// "type": "Feature", +// "properties": { +// id: ig.id, +// depth: ig.depth, +// mag: ig.mag, +// time: ig.time, +// place: ig.place, +// } +// }); +// (map.current!.getSource('earthquakes') as mapboxgl.GeoJSONSource).setData(geoJsonTitikGempa.current); +// } +// } + +// tgs.current.push(new TitikGempa(nig.id, nig, { +// map: map.current!, +// zoomToPosition: true, +// showMarker: true, +// showPopup: true, +// showPopUpInSecond: 1 +// })) +// tgs.current.sort(function (a: any, b: any) { +// return new Date(b.time).getTime() - new Date(a.time).getTime(); +// }); +// setEvents(tgs.current); +// setAlertGempaBumi(new TitikGempa(nig.id, nig)) + +// const tg = new TitikGempa(lastGempaKecilId.current, nig, { +// pWaveSpeed: 6000, +// sWaveSpeed: 3000, +// map: map.current!, +// description: msg, +// }); + +// // titikGempaKecil.current = tg; +// setGempaTerakhir(new TitikGempa(nig.id, nig)); +// } else { +// const cek = tgs.current.find((v) => v.id == feature.properties.id); +// if (cek && cek.infoGempa.mag != parseFloat(feature.properties.mag)) { +// console.log(cek.infoGempa.mag, parseFloat(feature.properties.mag)) +// setGempaTerakhir(new TitikGempa(nig.id, nig)); +// setAlertGempaBumi(new TitikGempa(nig.id, nig)); +// const indextgs = tgs.current.findIndex((v) => v.id == feature.properties.id); +// tgs.current[indextgs].infoGempa = nig; + +// } +// } + + +// }) +// .catch((error) => { +// console.error('Error initializing socket:', error); +// }); +// }, 5000); +// } + +// const selectedPopup = useRef(null); + +// function selectEvent(d: InfoGempa) { +// setDetailInfoGempa(d); +// if (selectedPopup.current) { +// selectedPopup.current.remove(); +// } + +// map.current!.flyTo({ +// center: [d.lng, d.lat], +// zoom: 6, +// essential: true +// }); +// const placeholder = document.createElement('div'); +// const root = createRoot(placeholder) +// root.render( +//
+//
+//

GEMPA BUMI

+//
+// +// } className='min-h-48 min-w-48 whitespace-pre-wrap ' > +//
    +//
  • +// Magnitudo : {d.mag} +//
  • +//
  • +// Kedalaman : {d.depth} +//
  • +//
  • +// Waktu : {new Date(d.time!).toLocaleString()} +//
  • +//
  • +// Lokasi (Lat,Lng) :
    {d.lat} , {d.lng} +//
  • +//
+//
); + +// selectedPopup.current = new AnimatedPopup({ +// closeOnClick: false, +// openingAnimation: { +// duration: 100, +// easing: 'easeOutSine', +// transform: 'scale' +// }, +// closingAnimation: { +// duration: 100, +// easing: 'easeInOutSine', +// transform: 'scale' +// } +// }).setDOMContent(placeholder).setLngLat([d.lng, d.lat]).addTo(map.current!); +// const cekTable = document.querySelector("#histori_tabel tbody"); +// if (cekTable) { +// cekTable.innerHTML = ""; +// } +// setTimeout(() => { +// readTextFile("https://bmkg-content-inatews.storage.googleapis.com/history." + d.id + ".txt"); +// }, 500); +// } + +// function testDemoGempa() { +// if (geoJsonData.current == null) { +// alert("Wait loading geojson"); +// return; +// }; +// const bbox = turf.bbox(geoJsonData.current); +// const randomPosition = turf.randomPosition(bbox); +// const mag = (Math.random() * (10 - 5) + 5).toFixed(1); +// const depth = (Math.random() * 20).toFixed(1) + " Km"; +// const message = "Gempa Bumi Test Pada Lokasi : Lat : " + randomPosition[1].toFixed(4) + " Lng : " + randomPosition[0].toFixed(4) + " Magnitudo : " + mag + " Kedalaman : " + depth; +// const id = `tg-${new Date().getTime()}`; + +// const dt = DateTime.now(); +// const readAbleTime = dt.toISODate() + " " + dt.toLocaleString(DateTime.TIME_24_WITH_SECONDS) +// const nig: InfoGempa = { +// id: id, +// lng: randomPosition[0].toFixed(4), +// lat: randomPosition[1].toFixed(4), +// mag: parseFloat(mag), +// depth: depth || "10 Km", +// message: message, +// time: readAbleTime +// }; + + +// warningHandler(nig); + +// // setTimeout(() => { +// // setInfoGempaDirasakanTerakhir(nig); +// // }, 6000); + +// } + +// function testDemoTsunami() { +// const url = "https://bmkg-content-inatews.storage.googleapis.com/last30tsunamievent.xml"; +// //fecth and parse xml +// fetch(url) +// .then(response => response.text()) +// .then(data => { +// const parser = new XMLParser(); +// let jObj = parser.parse(data); +// var infos = jObj.alert.info; +// infos = infos.filter((v) => v.wzarea != undefined) +// var randInfo = infos[(Math.random() * infos.length) | 0] + +// warningTsunamiHandler(randInfo); + +// }).catch((error) => { +// alert("Failed load geojson data : " + error); +// console.error('Error fetching data:', error); +// }); + +// //warningTsunamiHandler(null); +// } + +// function readTextFile(e: string) { + +// var t = new XMLHttpRequest; +// t.open("GET", e, !1), t.onreadystatechange = function () { +// if (4 === t.readyState && (200 === t.status || 0 == t.status)) { +// let u = t.responseText.split("\n"); +// var table = document.getElementById("histori_tabel") as HTMLTableElement; +// //clear tbody + +// let T = u.length - 1; +// for (let t = 1; t < T; t++) { +// let T = u[t].split("|"); +// var n = table.insertRow(t), +// a = n.insertCell(0), +// l = n.insertCell(1), +// s = n.insertCell(2), +// i = n.insertCell(3), +// o = n.insertCell(4), +// r = n.insertCell(5), +// d = n.insertCell(6), +// m = n.insertCell(7), +// g = n.insertCell(8), +// c = n.insertCell(9); +// a.innerHTML = T[0], l.innerHTML = T[1], s.innerHTML = T[2], i.innerHTML = T[3], o.innerHTML = T[4], r.innerHTML = T[5], d.innerHTML = T[6], m.innerHTML = T[7], g.innerHTML = T[8], c.innerHTML = T[9] +// } +// } +// }, t.send(null) +// } + +// function generateDiv(max) { +// let arrayDivs: any = []; +// for (let index = 0; index < max; index++) { +// arrayDivs.push(
+// +//
); +// } + +// return arrayDivs; +// } + + +// return ( +//
+// + +//
+ +// {!loadingScreen && alertGempaBumi && +// +//
+//
+//

GEMPA BUMI

+//
+//
+// } className='hidden md:block show-pop-up fixed top-12 md:top-6 left-0 card-float right-0 md:left-6 md:w-1/3 lg:w-1/4 xl:w-1/5 2xl:w-1/6'> +//
+//
+//
+//
+//
+//
{alertGempaBumi.readableMag}
+//
MAG
+//
+//
+//
+//

DEPTH : {alertGempaBumi.readableDepth} KM

+//
+//
+// +// + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
TIME{alertGempaBumi.readableTime} WIB
MAG{alertGempaBumi.mag}
DEPTH{alertGempaBumi.depth}
LAT{alertGempaBumi.infoGempa.lat}
LNG{alertGempaBumi.infoGempa.lng}
+//
+ +//
+//
+//

{alertGempaBumi.infoGempa.message}

+//
+//
+// {alertGempaBumi.mag >= 5 &&
+//
    +// {alertGempaBumi.infoGempa.listKotaTerdampak && alertGempaBumi.infoGempa.listKotaTerdampak.map((kota, i) => { +// if (kota.hit) { +// return
  • +// +//
  • +// } else { +// return
  • +// +//
  • +// } +// })} +//
+//
} +// } + +// {!loadingScreen && infoTsunami && +// +//
+//
+//

PERINGATAN TSUNAMI

+//
+//
+// } +// footer={ +//
+// {infoTsunami.infoTsunami.level} +//
+// } +// className='hidden md:block show-pop-up fixed top-12 md:top-6 left-0 card-float right-0 md:left-6 md:w-1/3 lg:w-1/4 xl:w-1/5 2xl:w-1/6'> +//
+ +//
+//

{infoTsunami.infoTsunami.message}

+//
+//
+// {(infoTsunami.infoTsunami.level?.includes("PD-1") || infoTsunami.infoTsunami.level?.includes("PD-2")) &&
+//
    +// {infoTsunami.infoTsunami.listKotaTerdampak && infoTsunami.infoTsunami.listKotaTerdampak.map((kota, i) => { +// return
  • +// +//
  • +// })} +//
+//
} +// } + +//
+// + +// +//
+ + + +// {!loadingScreen && EVENT LOG

+// } className=' fixed right-0 md:right-6 top-1 md:top-6 card-float md:w-1/3 lg:w-1/5 show-pop-up'> +//
    +// {events.map((v: TitikGempa, i) => { + +// return
  • { +// selectEvent(v.infoGempa); + +// }} +// className='flex flex-col mb-2 list-event cursor-pointer slide-in-left' style={{ +// animationDelay: `${i * 0.01}s`, +// transform: 'translateX(-110%)' +// }}> +// {v.infoGempa.time} WIB +//
    +// {v.readableMag} M - {v.infoGempa.place || "uknown"} +//
    +//
  • +// })} +//
+ +//
} + + + + + +// {!loadingScreen && gempaTerakhir && +//

+// GEMPA TERDETEKSI TERAKHIR +//

+ +// +// } +// footer={ +//
{ +// selectEvent(gempaTerakhir.infoGempa); +// }}> +// +//
+// } + +// className='hidden md:block show-pop-up fixed bottom-28 md:bottom-6 card-float left-1 right-1 m-auto md:w-1/4 lg:w-1/6'> +//
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
PLACE{gempaTerakhir.infoGempa.place}
TIME{gempaTerakhir.readableTime} WIB
MAG{gempaTerakhir.infoGempa.mag}
DEPTH{gempaTerakhir.readableDepth} KM
LAT{gempaTerakhir.infoGempa.lat}
LNG{gempaTerakhir.infoGempa.lng}
+//
+//
} + +// {!loadingScreen && gempaDirasakan && +//

+// GEMPA DIRASAKAN TERAKHIR +//

+ +// +// } +// footer={ +//
{ +// selectEvent(gempaDirasakan.infoGempa); +// }}> +// +//
+// } + +// className='hidden md:block show-pop-up fixed bottom-10 left-1 right-1 md:right-0 md:left-6 card-float md:w-1/3 lg:w-1/4 xl:w-1/5 2xl:w-1/6'> +//
+//
+//
+//
+//
+//
{gempaDirasakan.readableMag}
+//
MAG
+//
+//
+//
+//

DEPTH : {gempaDirasakan.readableDepth} KM

+//
+//
+// +// + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
TIME{gempaDirasakan.infoGempa.time} WIB
MAG{gempaDirasakan.infoGempa.mag}
DEPTH{gempaDirasakan.infoGempa.depth}
LAT{gempaDirasakan.infoGempa.lat}
LNG{gempaDirasakan.infoGempa.lng}
+//
+ +//
+//
+//

{gempaDirasakan.infoGempa.message}

+//
+//
+//
} + +// {!loadingScreen && detailInfoGempa && +//

+// DETAIL EVENT +//

+// +// +// } +// className='right-6 bottom-10 md:bottom-6 fixed card-float show-pop-up '> +//
+//
+// {/* */} + +//
+//
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
PLACE{detailInfoGempa.place}
TIME{detailInfoGempa.time} WIB
MAG{detailInfoGempa.mag}
DEPTH{parseFloat(detailInfoGempa.depth.replace(" Km", "")).toFixed(2)} KM
LAT{detailInfoGempa.lat}
LNG{detailInfoGempa.lng}
+//
+//
+ +//
+// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
+// Time(UTC) +// +// +OT(min) +// +// Lat +// +// Lng +// +// Depth +// +// Phase +// +// MagType +// +// Mag +// +// MagCount +// +// Status +//
+//
+//
+ +//
+ +//
} + +// {!loadingScreen && alertGempaBumi && gempaDirasakan && +//
+//
+//

GEMPA BUMI

+//
+// +// } className='block md:hidden show-pop-up fixed bottom-10 md:top-6 left-0 card-warning right-0 md:left-6 md:w-1/4 lg:w-1/5'> +//
+//
+//
+//
+//
+//
{gempaDirasakan.readableMag}
+//
MAG
+//
+//
+//
+//

DEPTH : {gempaDirasakan.readableDepth} KM

+//
+//
+// +// + +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
TIME{gempaDirasakan.infoGempa.time} WIB
MAG{gempaDirasakan.infoGempa.mag}
DEPTH{gempaDirasakan.infoGempa.depth}
LAT{gempaDirasakan.infoGempa.lat}
LNG{gempaDirasakan.infoGempa.lng}
+//
+ +//
+//
+//

{gempaDirasakan.infoGempa.message}

+//
+//
+ +//
} + +// {!loadingScreen && alertGempaBumis.map((v, i) => { +// return
+// +//
+// })} + +// {/* {!loadingScreen && alertGempaBumi && } */} + + +// + +// {!loadingScreen && alertTsunami &&
+ +//
+//
+//
+// {generateDiv(window.screen.width + (window.screen.width / 3))} + +//
+//
+//
+ +//
+//
+//
+//
+//
+//
+// TSUNAMI +// Peringatan Dini Tsunami +//
+//
+//
+//
+//
+ +//
+//
+//
+//
+//
+//
+//
+//
+ +//
+ +//
+//
+ +//
+//
+//
+//
+//
+//
+//

POTENSI TSUNAMI

+//
+//
+//
+// +//
+//
+//

{alertTsunami.infoTsunami.level}

+//
+//
+// } +// className='w-full h-auto'> +//

+// {alertTsunami.infoTsunami.message} +//

+// +//
+ +//
+//
+ + + +//
+//
+//
+//
+//
+//
+//
+//
+//
+ +//
+//
+//
+//
+//
+//
+//
+ +//
+//
+//
+//
+//
+//
+//
+ + +//
+//
+//
+//
+//
+//
+//
+ +//
+//
+//
+//
+//
+//
+//
+ +//
+//
+//
+//
+//
+//
+//
+ + + +//
+ + + +// } + + +//
+// +//

INI MERUPAKAN DESAIN KONSEP - DATA GEMPA DARI BMKG

+//
+ +// + +// ); +// } diff --git a/sigap-website/app/_components/map/controls/panic-button-demo.tsx b/sigap-website/app/_components/map/controls/panic-button-demo.tsx new file mode 100644 index 0000000..4781e18 --- /dev/null +++ b/sigap-website/app/_components/map/controls/panic-button-demo.tsx @@ -0,0 +1,108 @@ +"use client" + +import { useState } from 'react'; +import { Button } from '@/app/_components/ui/button'; +import { + AlertTriangle, + Bell, + ShieldAlert, + Radio, + RadioTower, + Shield +} from 'lucide-react'; +import { cn } from '@/app/_lib/utils'; +import { Badge } from '@/app/_components/ui/badge'; +import { IIncidentLog } from '@/app/_utils/types/ews'; + +interface PanicButtonDemoProps { + onTriggerAlert: (priority: 'high' | 'medium' | 'low') => void; + onResolveAllAlerts: () => void; + activeIncidents: IIncidentLog[]; + className?: string; +} + +export default function PanicButtonDemo({ + onTriggerAlert, + onResolveAllAlerts, + activeIncidents, + className +}: PanicButtonDemoProps) { + const [isTriggering, setIsTriggering] = useState(false); + + const handleTriggerPanic = (priority: 'high' | 'medium' | 'low') => { + setIsTriggering(true); + onTriggerAlert(priority); + + // Reset animation + setTimeout(() => { + setIsTriggering(false); + }, 1000); + }; + + return ( +
+
+

+ + EWS Panic Button Demo +

+ + {activeIncidents.length > 0 && ( + + {activeIncidents.length} Active + + )} +
+ +
+ + +
+ + + +
+ + {activeIncidents.length > 0 && ( + + )} +
+ +

+ Simulates a mobile app panic button activation in the Jember area. +

+
+ ); +} diff --git a/sigap-website/app/_components/map/crime-map.tsx b/sigap-website/app/_components/map/crime-map.tsx index 5d082e9..59e4831 100644 --- a/sigap-website/app/_components/map/crime-map.tsx +++ b/sigap-website/app/_components/map/crime-map.tsx @@ -42,6 +42,7 @@ export default function CrimeMap() { const [showUnclustered, setShowUnclustered] = useState(true) const [useAllYears, setUseAllYears] = useState(false) const [useAllMonths, setUseAllMonths] = useState(false) + const [showEWS, setShowEWS] = useState(true) const mapContainerRef = useRef(null) @@ -185,6 +186,9 @@ export default function CrimeMap() { setUseAllYears(false); setUseAllMonths(false); } + + // Enable EWS in all modes for demo purposes + setShowEWS(true); } const showTimelineLayer = activeControl === "timeline"; @@ -218,87 +222,90 @@ export default function CrimeMap() { ) : ( -
+
- - +
+ + - {isFullscreen && ( - <> -
- -
- - - {isFullscreen && ( -
- {showClusters && ( - - )} - {showUnclustered && !showClusters && ( - - )} -
- )} - - {isFullscreen && showUnitsLayer && ( -
- +
+
- )} - {isFullscreen && showTimelineLayer && ( -
- -
- )} - - )} + + {isFullscreen && ( +
+ {showClusters && ( + + )} + {showUnclustered && !showClusters && ( + + )} +
+ )} - {isFullscreen && ( -
- -
- )} - + {isFullscreen && showUnitsLayer && ( +
+ +
+ )} + + {isFullscreen && showTimelineLayer && ( +
+ +
+ )} + + )} + + {isFullscreen && ( +
+ +
+ )} + +
)} diff --git a/sigap-website/app/_components/map/layers/coastline.tsx b/sigap-website/app/_components/map/layers/coastline.tsx index 98b8c37..3a22df9 100644 --- a/sigap-website/app/_components/map/layers/coastline.tsx +++ b/sigap-website/app/_components/map/layers/coastline.tsx @@ -14,40 +14,50 @@ export default function CoastlineLayer({ map, visible = true }: CoastlineLayerPr // Function to add coastline layer function addCoastline() { - const sourceId = 'coastline'; + const sourceId = 'coastline_id'; const layerId = 'outline-coastline'; // Make sure map is defined if (!map) return; try { - // Check if the source already exists - if (!map.getSource(sourceId)) { + // More robust check if the source already exists + let sourceExists = false; + try { + sourceExists = !!map.getSource(sourceId); + } catch (e) { + sourceExists = false; + } + + if (!sourceExists) { // Add coastline data source fetch('/geojson/garis_pantai.geojson') .then(response => response.json()) .then(data => { - // Add coastline data source - map.addSource(sourceId, { - type: 'geojson', - generateId: true, - data: data - }); + // Double-check the source doesn't exist right before adding + if (!map.getSource(sourceId)) { + // Add coastline data source + map.addSource(sourceId, { + type: 'geojson', + generateId: true, + data: data + }); - // Add coastline layer - map.addLayer({ - 'id': layerId, - 'type': 'line', - 'source': sourceId, - 'layout': { - 'visibility': visible ? 'visible' : 'none', - }, - 'paint': { - 'line-color': ['get', 'color'], - 'line-width': 5, - 'line-opacity': 1 - } - }); + // Add coastline layer + map.addLayer({ + 'id': layerId, + 'type': 'line', + 'source': sourceId, + 'layout': { + 'visibility': visible ? 'visible' : 'none', + }, + 'paint': { + 'line-color': '#1a1a1a', // dull white color instead of ['get', 'color'] + 'line-width': 5, + 'line-opacity': 1 + } + }); + } }) .catch((error) => { console.error('Error fetching coastline data:', error); @@ -71,7 +81,7 @@ export default function CoastlineLayer({ map, visible = true }: CoastlineLayerPr if (!map || !map.getStyle()) return; const layerId = 'outline-coastline'; - const sourceId = 'coastline'; + const sourceId = 'coastline_id'; if (map.getLayer(layerId)) { map.removeLayer(layerId); @@ -92,14 +102,24 @@ export default function CoastlineLayer({ map, visible = true }: CoastlineLayerPr if (map.loaded() && map.isStyleLoaded()) { addCoastline(); } else { - // Use multiple events to catch map ready state - map.on('load', addCoastline); - map.on('style.load', addCoastline); - map.on('styledata', addCoastline); + // Reduce event listeners to minimize duplicates + const addLayerOnce = () => { + // Remove all listeners after first successful execution + map.off('load', addLayerOnce); + map.off('style.load', addLayerOnce); + map.off('styledata', addLayerOnce); + clearTimeout(timeoutId); + + addCoastline(); + }; + + map.on('load', addLayerOnce); + map.on('style.load', addLayerOnce); + map.on('styledata', addLayerOnce); // Fallback timeout timeoutId = setTimeout(() => { - addCoastline(); + addLayerOnce(); }, 2000); } } catch (error) { diff --git a/sigap-website/app/_components/map/layers/ews-alert-layer.tsx b/sigap-website/app/_components/map/layers/ews-alert-layer.tsx new file mode 100644 index 0000000..cf06de0 --- /dev/null +++ b/sigap-website/app/_components/map/layers/ews-alert-layer.tsx @@ -0,0 +1,318 @@ +"use client" + +import { useEffect, useState, useRef } from 'react'; +import mapboxgl from 'mapbox-gl'; +import { IIncidentLog, EWSStatus } from '@/app/_utils/types/ews'; +import { createRoot } from 'react-dom/client'; +import { AlertTriangle, X } from 'lucide-react'; +import DigitalClock from '../markers/digital-clock'; +import { Badge } from '@/app/_components/ui/badge'; +import { Button } from '@/app/_components/ui/button'; + +interface EWSAlertLayerProps { + map: mapboxgl.Map | null; + incidents?: IIncidentLog[]; + onIncidentResolved?: (id: string) => void; + visible?: boolean; +} + +export default function EWSAlertLayer({ + map, + incidents = [], + onIncidentResolved, + visible = true +}: EWSAlertLayerProps) { + const [ewsStatus, setEwsStatus] = useState('idle'); + const [activeIncidents, setActiveIncidents] = useState([]); + const markersRef = useRef>(new Map()); + const animationFrameRef = useRef(null); + const alertAudioRef = useRef(null); + const pulsingDotsRef = useRef>({}); // For animation reference + + // Initialize audio + useEffect(() => { + try { + // Try multiple possible audio sources in order of preference + const possibleSources = [ + '/sounds/alert.mp3', + '/alert.mp3', + '/sounds/error.mp3', + '/error.mp3', + '/sounds/notification.mp3' + ]; + + // Try to load the first audio source + alertAudioRef.current = new Audio("/sounds/error-2-126514.mp3"); + + // Add error handling to try alternative sources + alertAudioRef.current.addEventListener('error', (e) => { + // console.warn(`Failed to load audio from ${possibleSources[0]}, trying fallback sources`, e); + + // Try each source in succession + // for (let i = 1; i < possibleSources.length; i++) { + // try { + // alertAudioRef.current = new Audio(possibleSources[i]); + // console.log(`Using fallback audio source: ${possibleSources[i]}`); + // break; + // } catch (err) { + // console.warn(`Fallback audio ${possibleSources[i]} also failed`, err); + // } + // } + }); + + alertAudioRef.current.volume = 0.5; + + // Loop handling - stop after 1 minute + let loopStartTime: number | null = null; + + alertAudioRef.current.addEventListener('ended', () => { + // Initialize start time on first play + if (loopStartTime === null) { + loopStartTime = Date.now(); + } + + // Check if 1 minute has passed + if (Date.now() - loopStartTime < 60000) { // 60000ms = 1 minute + alertAudioRef.current?.play().catch(err => + console.error("Error playing looped alert sound:", err)); + } else { + loopStartTime = null; // Reset for future alerts + } + }); + + // Preload the audio + alertAudioRef.current.load(); + } catch (err) { + console.error("Could not initialize alert audio:", err); + } + + return () => { + if (alertAudioRef.current) { + alertAudioRef.current.pause(); + alertAudioRef.current = null; + } + }; + }, []); + + // Update active incidents when incidents prop changes + useEffect(() => { + const newActiveIncidents = incidents.filter(inc => inc.status === 'active'); + setActiveIncidents(newActiveIncidents); + + // Update EWS status + if (newActiveIncidents.length > 0) { + setEwsStatus('alert'); + // Play alert sound with error handling + if (alertAudioRef.current) { + alertAudioRef.current.play() + .catch(err => { + console.error("Error playing alert sound:", err); + // Create a simple beep as fallback + try { + const audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); + const oscillator = audioContext.createOscillator(); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(800, audioContext.currentTime); + oscillator.connect(audioContext.destination); + oscillator.start(); + oscillator.stop(audioContext.currentTime + 0.5); + } catch (fallbackErr) { + console.error("Fallback audio also failed:", fallbackErr); + } + }); + } + } else { + setEwsStatus('idle'); + } + }, [incidents]); + + // Handle marker creation, animation, and cleanup + useEffect(() => { + if (!map || !visible) return; + + // Clear any existing markers + markersRef.current.forEach(marker => marker.remove()); + markersRef.current.clear(); + pulsingDotsRef.current = {}; + + // Cancel any ongoing animations + if (animationFrameRef.current !== null) { + cancelAnimationFrame(animationFrameRef.current); + animationFrameRef.current = null; + } + + // Create new markers for all active incidents + activeIncidents.forEach(incident => { + // Don't add if marker already exists + if (markersRef.current.has(incident.id)) return; + + const { latitude, longitude } = incident.location; + + // Create marker element + const el = document.createElement('div'); + el.className = 'ews-alert-marker'; + + // Create a wrapper for the pulsing effect + const pulsingDot = document.createElement('div'); + pulsingDot.className = 'pulsing-dot'; + pulsingDotsRef.current[incident.id] = pulsingDot; + + // Create the content for the marker + const contentElement = document.createElement('div'); + contentElement.className = 'ews-alert-content'; + + // Use React for the popup content + const root = createRoot(contentElement); + root.render( +
+
+
+
+ + {incident.priority.toUpperCase()} PRIORITY + +
+ +
+
+ +

+ + {incident.category || "Emergency Alert"} +

+ +
+

{incident.location.district}

+

{incident.location.address}

+

Reported by: {incident.reporter.name}

+
+ +
+ + ID: {incident.id} + + +
+
+
+ ); + + // Add the elements to the marker + el.appendChild(pulsingDot); + el.appendChild(contentElement); + + // Create and add the marker + const marker = new mapboxgl.Marker({ + element: el, + anchor: 'center' + }) + .setLngLat([longitude, latitude]) + .addTo(map); + + markersRef.current.set(incident.id, marker); + + // Fly to the incident if it's new + const isNewIncident = activeIncidents.length > 0 && + incident.id === activeIncidents[activeIncidents.length - 1].id; + + if (isNewIncident) { + // Dispatch custom flyTo event + const flyToEvent = new CustomEvent('mapbox_fly_to', { + detail: { + longitude, + latitude, + zoom: 15, + bearing: 0, + pitch: 60, + duration: 2000 + } + }); + + map.getContainer().dispatchEvent(flyToEvent); + } + }); + + // Setup animation for pulsing dots + const animatePulsingDots = () => { + Object.entries(pulsingDotsRef.current).forEach(([id, el]) => { + const scale = 1 + 0.5 * Math.sin(Date.now() / 200); // Pulsing effect + el.style.transform = `scale(${scale})`; + + // Add rotation for more visual effect + const rotation = (Date.now() / 50) % 360; + el.style.transform += ` rotate(${rotation}deg)`; + }); + + animationFrameRef.current = requestAnimationFrame(animatePulsingDots); + }; + + animationFrameRef.current = requestAnimationFrame(animatePulsingDots); + + // Cleanup function + return () => { + if (animationFrameRef.current !== null) { + cancelAnimationFrame(animationFrameRef.current); + } + + markersRef.current.forEach(marker => marker.remove()); + markersRef.current.clear(); + }; + }, [map, activeIncidents, visible, onIncidentResolved]); + + // Create a floating EWS status indicator when in alert mode + useEffect(() => { + if (!map || ewsStatus === 'idle') return; + + // Create status indicator element if it doesn't exist + let statusContainer = document.getElementById('ews-status-indicator'); + + if (!statusContainer) { + statusContainer = document.createElement('div'); + statusContainer.id = 'ews-status-indicator'; + statusContainer.className = 'absolute top-16 left-1/2 transform -translate-x-1/2 z-50'; + map.getContainer().appendChild(statusContainer); + + // Use React for the status indicator + const root = createRoot(statusContainer); + root.render( +
+ + + {ewsStatus === 'alert' + ? `Alert: ${activeIncidents.length} Active Emergencies` + : 'System Active'} + +
+ ); + } + + // Cleanup function + return () => { + if (statusContainer && statusContainer.parentNode) { + statusContainer.parentNode.removeChild(statusContainer); + } + }; + }, [map, ewsStatus, activeIncidents.length]); + + return null; // This component doesn't render anything directly +} diff --git a/sigap-website/app/_components/map/layers/layers.tsx b/sigap-website/app/_components/map/layers/layers.tsx index 22cd202..fb08e78 100644 --- a/sigap-website/app/_components/map/layers/layers.tsx +++ b/sigap-website/app/_components/map/layers/layers.tsx @@ -23,6 +23,12 @@ 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 { addMockIncident, getAllIncidents, resolveIncident } from "@/app/_utils/mock/ews-data" // Interface for crime incident interface ICrimeIncident { @@ -67,6 +73,7 @@ interface LayersProps { activeControl: ITooltips tilesetId?: string useAllData?: boolean + showEWS?: boolean } export default function Layers({ @@ -79,6 +86,7 @@ export default function Layers({ activeControl, tilesetId = MAPBOX_TILESET_ID, useAllData = false, + showEWS = true, }: LayersProps) { const { current: map } = useMap() @@ -96,25 +104,47 @@ export default function Layers({ const crimeDataByDistrict = processCrimeDataByDistrict(crimes) - // Handle popup close with a common reset pattern + const [ewsIncidents, setEwsIncidents] = useState([]); + const [showPanicDemo, setShowPanicDemo] = useState(true); + + useEffect(() => { + setEwsIncidents(getAllIncidents()); + }, []); + + const handleTriggerAlert = useCallback((priority: 'high' | 'medium' | 'low') => { + const newIncident = addMockIncident({ priority }); + setEwsIncidents(getAllIncidents()); + }, []); + + const handleResolveIncident = useCallback((id: string) => { + resolveIncident(id); + setEwsIncidents(getAllIncidents()); + }, []); + + const handleResolveAllAlerts = useCallback(() => { + ewsIncidents.forEach(incident => { + if (incident.status === 'active') { + resolveIncident(incident.id); + } + }); + setEwsIncidents(getAllIncidents()); + }, [ewsIncidents]); + const handlePopupClose = useCallback(() => { - // Reset selected state selectedDistrictRef.current = null setSelectedDistrict(null) setSelectedIncident(null) setFocusedDistrictId(null) - // Reset map view/camera if (map) { map.easeTo({ zoom: BASE_ZOOM, pitch: BASE_PITCH, bearing: BASE_BEARING, duration: 1500, - easing: (t) => t * (2 - t), // easeOutQuad + easing: (t) => t * (2 - t), }) - // Show all clusters again when closing popup if (map.getLayer("clusters")) { map.getMap().setLayoutProperty("clusters", "visibility", "visible") } @@ -122,7 +152,6 @@ export default function Layers({ map.getMap().setLayoutProperty("unclustered-point", "visibility", "visible") } - // Update fill color for all districts if (map.getLayer("district-fill")) { const fillColorExpression = createFillColorExpression(null, crimeDataByDistrict) map.getMap().setPaintProperty("district-fill", "fill-color", fillColorExpression as any) @@ -130,32 +159,26 @@ export default function Layers({ } }, [map, crimeDataByDistrict]) - // Handle district popup close specifically const handleCloseDistrictPopup = useCallback(() => { console.log("Closing district popup") handlePopupClose() }, [handlePopupClose]) - // Handle incident popup close specifically const handleCloseIncidentPopup = useCallback(() => { console.log("Closing incident popup") handlePopupClose() }, [handlePopupClose]) - // Handle district clicks const handleDistrictClick = useCallback( (feature: IDistrictFeature) => { console.log("District clicked:", feature) - // Clear any incident selection when showing a district setSelectedIncident(null) - // Set the selected district setSelectedDistrict(feature) selectedDistrictRef.current = feature setFocusedDistrictId(feature.id) - // Fly to the district if (map && feature.longitude && feature.latitude) { map.flyTo({ center: [feature.longitude, feature.latitude], @@ -166,7 +189,6 @@ export default function Layers({ easing: (t) => t * (2 - t), }) - // Hide clusters when focusing on district if (map.getLayer("clusters")) { map.getMap().setLayoutProperty("clusters", "visibility", "none") } @@ -178,7 +200,6 @@ export default function Layers({ [map], ) - // Set up custom event handler for fly-to events useEffect(() => { if (!mapboxMap) return @@ -206,7 +227,6 @@ export default function Layers({ } }, [mapboxMap, map]) - // Handle incident click events useEffect(() => { if (!mapboxMap) return @@ -214,13 +234,11 @@ export default function Layers({ const customEvent = e as CustomEvent console.log("Received incident_click event in layers:", customEvent.detail) - // Enhanced error checking if (!customEvent.detail) { console.error("Empty incident click event data") return } - // Allow for different property names in the event data const incidentId = customEvent.detail.id || customEvent.detail.incidentId || customEvent.detail.incident_id if (!incidentId) { @@ -230,10 +248,8 @@ export default function Layers({ console.log("Looking for incident with ID:", incidentId) - // Improved incident finding let foundIncident: ICrimeIncident | undefined - // First try to use the data directly from the event if it has all needed properties if ( customEvent.detail.latitude !== undefined && customEvent.detail.longitude !== undefined && @@ -252,7 +268,6 @@ export default function Layers({ address: customEvent.detail.address, } } else { - // Otherwise search through the crimes data for (const crime of crimes) { for (const incident of crime.crime_incidents) { if (incident.id === incidentId || incident.id?.toString() === incidentId?.toString()) { @@ -288,20 +303,16 @@ export default function Layers({ console.log("Setting selected incident:", foundIncident) - // Clear any existing district selection first setSelectedDistrict(null) selectedDistrictRef.current = null setFocusedDistrictId(null) - // Set the selected incident setSelectedIncident(foundIncident) } - // Add event listeners to both the map canvas and document mapboxMap.getCanvas().addEventListener("incident_click", handleIncidentClickEvent as EventListener) document.addEventListener("incident_click", handleIncidentClickEvent as EventListener) - // For debugging purposes, log when this effect runs console.log("Set up incident click event listener") return () => { @@ -313,7 +324,6 @@ export default function Layers({ } }, [mapboxMap, crimes, setFocusedDistrictId]) - // Update selected district when year/month/filter changes useEffect(() => { if (selectedDistrictRef.current) { const districtId = selectedDistrictRef.current.id @@ -395,7 +405,6 @@ export default function Layers({ } }, [crimes, filterCategory, year, month, crimeDataByDistrict]) - // Make sure we have a defined handler for setFocusedDistrictId const handleSetFocusedDistrictId = useCallback((id: string | null, isMarkerClick = false) => { console.log("Setting focused district ID:", id, "from marker click:", isMarkerClick) setFocusedDistrictId(id) @@ -403,23 +412,18 @@ export default function Layers({ if (!visible) return null - // Determine which layers should be visible based on the active control const showDistrictLayer = activeControl === "incidents" const showHeatmapLayer = activeControl === "heatmap" const showClustersLayer = activeControl === "clusters" const showUnitsLayer = activeControl === "units" const showTimelineLayer = activeControl === "timeline" - // District fill should only be visible for incidents and clusters const showDistrictFill = activeControl === "incidents" || activeControl === "clusters" - // Show incident markers for incidents, clusters, AND units modes - // But hide for heatmap and timeline const showIncidentMarkers = activeControl !== "heatmap" && activeControl !== "timeline" return ( <> - {/* Standard District Layer with incident points */} - {/* Heatmap Layer */} - {/* Timeline Layer - make sure this is the only visible layer in timeline mode */} - {/* Units Layer - always show incidents when Units is active */} - {/* Cluster Layer - only enable when the clusters control is active and NOT in timeline mode */} - {/* Unclustered Points Layer - now show for both incidents and units modes */} - {/* District Popup */} {selectedDistrict && !selectedIncident && ( <> )} - {/* Timeline Layer - only show if active control is timeline */} - {/* Fault line layer */} - - {/* Incident Popup */} - {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( - + )} + + {showEWS && showPanicDemo && ( +
+ inc.status === 'active')} /> - )} +
+ )} - {/* Debug info for development */} - {/*
-
Selected District: {selectedDistrict ? selectedDistrict.name : "None"}
-
Selected Incident: {selectedIncident ? selectedIncident.id : "None"}
-
Focused District ID: {focusedDistrictId || "None"}
-
*/} + {selectedIncident && selectedIncident.latitude && selectedIncident.longitude && ( + + )} ) } diff --git a/sigap-website/app/_components/map/layers/timezone.tsx b/sigap-website/app/_components/map/layers/timezone.tsx index d34fcfa..060596f 100644 --- a/sigap-website/app/_components/map/layers/timezone.tsx +++ b/sigap-website/app/_components/map/layers/timezone.tsx @@ -19,6 +19,7 @@ function Jam({ timeZone }: { timeZone: string }) { const options: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit', + second: '2-digit', // Added seconds display hour12: false, timeZone }; diff --git a/sigap-website/app/_styles/ews.css b/sigap-website/app/_styles/ews.css new file mode 100644 index 0000000..bcd2ff6 --- /dev/null +++ b/sigap-website/app/_styles/ews.css @@ -0,0 +1,50 @@ +/* EWS Alert Marker */ +.ews-alert-marker { + position: relative; + display: flex; + justify-content: center; + align-items: center; + z-index: 2; +} + +.pulsing-dot { + width: 20px; + height: 20px; + border-radius: 50%; + background: rgb(220, 38, 38); + box-shadow: 0 0 0 rgba(220, 38, 38, 0.4); + transform-origin: center center; + position: absolute; + z-index: 3; +} + +.ews-alert-content { + position: absolute; + min-width: 200px; + z-index: 1; + top: -8px; + left: 15px; +} + +/* Animation for alert transitions */ +@keyframes alert-pulse { + 0% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7); + } + + 70% { + transform: scale(1); + box-shadow: 0 0 0 10px rgba(255, 82, 82, 0); + } + + 100% { + transform: scale(0.95); + box-shadow: 0 0 0 0 rgba(255, 82, 82, 0); + } +} + +/* EWS Status Indicator */ +#ews-status-indicator { + transition: all 0.3s ease-in-out; +} diff --git a/sigap-website/app/_styles/globals.css b/sigap-website/app/_styles/globals.css index 60189fd..3766d5b 100644 --- a/sigap-website/app/_styles/globals.css +++ b/sigap-website/app/_styles/globals.css @@ -2,6 +2,9 @@ @tailwind components; @tailwind utilities; +@import "./ews.css"; +@import "./ui.css"; + @import url(//fonts.googleapis.com/css?family=Roboto+Condensed:400,600,700); @layer base { diff --git a/sigap-website/app/_utils/mock/ews-data.ts b/sigap-website/app/_utils/mock/ews-data.ts new file mode 100644 index 0000000..337770e --- /dev/null +++ b/sigap-website/app/_utils/mock/ews-data.ts @@ -0,0 +1,74 @@ +import { IIncidentLog } from "../types/ews"; + +// Jember area coordinates +const JEMBER_LOCATIONS = [ + { latitude: -8.172380, longitude: 113.702588, district: "Kaliwates", address: "Jl. Gajah Mada No. 233, Kaliwates" }, + { latitude: -8.184859, longitude: 113.668811, district: "Sumbersari", address: "Jl. Kalimantan No.37, Sumbersari" }, + { latitude: -8.166498, longitude: 113.722759, district: "Patrang", address: "Jl. Mastrip No. 49, Patrang" }, + { latitude: -8.159021, longitude: 113.713175, district: "Jemberlor", address: "Jl. Letjen Panjaitan No. 55, Jemberlor" }, + { latitude: -8.192226, longitude: 113.669716, district: "Kebonsari", address: "Perumahan Kebonsari Indah, Blok C-15" }, +]; + +// Generate mock incident log +export const generateMockIncident = (override: Partial = {}): IIncidentLog => { + const locationIndex = Math.floor(Math.random() * JEMBER_LOCATIONS.length); + const location = JEMBER_LOCATIONS[locationIndex]; + const priorityOptions = ['high', 'medium', 'low'] as const; + const priority = override.priority || priorityOptions[Math.floor(Math.random() * priorityOptions.length)]; + + const reporters = [ + { id: "USR001", name: "Budi Santoso", phone: "081234567890" }, + { id: "USR002", name: "Dewi Putri", phone: "085678901234" }, + { id: "USR003", name: "Ahmad Rizki", phone: "087890123456" }, + { id: "USR004", name: "Siti Nurhaliza", phone: "089012345678" } + ]; + + const reporterIndex = Math.floor(Math.random() * reporters.length); + + return { + id: override.id || `INC${Math.floor(Math.random() * 10000).toString().padStart(4, '0')}`, + timestamp: override.timestamp || new Date(), + location: override.location || { + latitude: location.latitude + (Math.random() * 0.01 - 0.005), + longitude: location.longitude + (Math.random() * 0.01 - 0.005), + address: location.address, + district: location.district + }, + status: override.status || 'active', + reporter: override.reporter || reporters[reporterIndex], + description: override.description || "Panic button activated", + category: override.category || "Emergency Alert", + priority, + response_time: override.response_time, + }; +}; + +// List of mock incidents (initially empty) +export const mockIncidents: IIncidentLog[] = []; + +// Add a new incident to the mock data +export const addMockIncident = (incident: Partial = {}): IIncidentLog => { + const newIncident = generateMockIncident(incident); + mockIncidents.push(newIncident); + return newIncident; +}; + +// Get all incidents +export const getAllIncidents = (): IIncidentLog[] => { + return [...mockIncidents]; +}; + +// Get active incidents +export const getActiveIncidents = (): IIncidentLog[] => { + return mockIncidents.filter(incident => incident.status === 'active'); +}; + +// Resolve an incident +export const resolveIncident = (id: string): IIncidentLog | undefined => { + const incident = mockIncidents.find(inc => inc.id === id); + if (incident) { + incident.status = 'resolved'; + incident.response_time = Math.floor(Math.random() * 300) + 60; // 1-5 minutes response time + } + return incident; +}; diff --git a/sigap-website/app/_utils/types/crimes.ts b/sigap-website/app/_utils/types/crimes.ts index da1090d..010d300 100644 --- a/sigap-website/app/_utils/types/crimes.ts +++ b/sigap-website/app/_utils/types/crimes.ts @@ -7,6 +7,7 @@ import { districts, geographics, locations, + incident_logs, } from '@prisma/client'; export interface ICrimes extends crimes { @@ -90,4 +91,17 @@ export interface IDistanceResult { category_name: string; district_name: string; distance_meters: number; +} + +export interface IIncidentLogs { + id: string; + created_at: Date; + updated_at: Date; + source: string; + time: Date; + description: string; + user_id: string; + location_id: string; + category_id: string; + verified: boolean; } \ No newline at end of file diff --git a/sigap-website/app/_utils/types/ews.ts b/sigap-website/app/_utils/types/ews.ts new file mode 100644 index 0000000..e26a80e --- /dev/null +++ b/sigap-website/app/_utils/types/ews.ts @@ -0,0 +1,24 @@ +export interface IEWSLocation { + latitude: number; + longitude: number; + address?: string; + district?: string; +} + +export interface IIncidentLog { + id: string; + timestamp: Date; + location: IEWSLocation; + status: 'active' | 'resolved' | 'false_alarm'; + reporter: { + id: string; + name: string; + phone?: string; + }; + description?: string; + category?: string; + priority: 'high' | 'medium' | 'low'; + response_time?: number; // in seconds +} + +export type EWSStatus = 'idle' | 'alert' | 'responding';