"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'; import { IconCancel } from '@tabler/icons-react'; 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 notificationAudioRef = useRef(null); const warningAudioRef = useRef(null); const pulsingDotsRef = useRef>({}); // For animation reference const sireneAudioRef = useRef(null); const [showAlert, setShowAlert] = useState(false); const [currentAlert, setCurrentAlert] = useState(null); // Initialize audio useEffect(() => { try { // Initialize different audio elements for different purposes // alertAudioRef.current = new Audio("/sounds/error-2-126514.mp3"); // notificationAudioRef.current = new Audio("/sounds/system-notification-199277.mp3"); // warningAudioRef.current = new Audio("/sounds/error-call-to-attention-129258.mp3"); sireneAudioRef.current = new Audio("/sounds/security-alarm-80493.mp3"); // Configure audio elements [alertAudioRef, notificationAudioRef, warningAudioRef].forEach(audioRef => { if (audioRef.current) { audioRef.current.volume = 0.5; audioRef.current.load(); } }); // Loop handling for main alert let loopStartTime: number | null = null; if (alertAudioRef.current) { 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 } }); } } catch (err) { console.error("Could not initialize alert audio:", err); } return () => { // Cleanup all audio elements [alertAudioRef, notificationAudioRef, warningAudioRef].forEach(audioRef => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } }); }; }, []); // Update active incidents when incidents prop changes useEffect(() => { const newActiveIncidents = incidents.filter(inc => inc.status === 'active'); setActiveIncidents(newActiveIncidents); // Update EWS status and trigger alert display if (newActiveIncidents.length > 0) { setEwsStatus('alert'); // Play notification sound first if (notificationAudioRef.current) { notificationAudioRef.current.play() .catch(err => console.error("Error playing notification sound:", err)); } // Set the most recent incident as current alert const newestIncident = newActiveIncidents[newActiveIncidents.length - 1]; setCurrentAlert(newestIncident); setShowAlert(true); // Play warning sound after a delay // setTimeout(() => { // if (warningAudioRef.current) { // warningAudioRef.current.play() // .catch(err => console.error("Error playing warning sound:", err)); // } // }, 1000); // Auto-close the alert after 15 seconds setTimeout(() => { setShowAlert(false); }, 5000); } else { setEwsStatus('idle'); setShowAlert(false); setCurrentAlert(null); } }, [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 with animated circles (similar to TitikGempa) const el = document.createElement('div'); el.className = 'ews-alert-marker'; // Create the content for the marker const contentElement = document.createElement('div'); contentElement.className = 'marker-gempa'; // Use React for the marker content with animated circles const markerRoot = createRoot(contentElement); markerRoot.render(
); // Add the content element to the marker el.appendChild(contentElement); // Create and add the marker const marker = new mapboxgl.Marker({ element: el, anchor: 'center' }) .setLngLat([longitude, latitude]) .addTo(map); // Create the popup content const popupElement = document.createElement('div'); const popupRoot = createRoot(popupElement); popupRoot.render(

{incident.priority} PRIORITY

{incident.priority.toUpperCase()} PRIORITY

{incident.category || "Emergency Alert"}

{incident.location.district}

{incident.location.address}

Reported by: {incident.reporter.name}

Auto-closing in 10s

ID: {incident.id}
); // Create and attach the animated popup // const popup = new CustomAnimatedPopup({ // closeOnClick: false, // openingAnimation: { // duration: 300, // easing: 'ease-out', // transform: 'scale' // }, // closingAnimation: { // duration: 200, // easing: 'ease-in-out', // transform: 'scale' // } // }).setDOMContent(popupElement); // marker.setPopup(popup); // markersRef.current.set(incident.id, marker); // // Add wave circles around the incident point // if (map) { // popup.addWaveCircles(map, new mapboxgl.LngLat(longitude, latitude), { // color: incident.priority === 'high' ? '#ff0000' : // incident.priority === 'medium' ? '#ff9900' : '#0066ff', // maxRadius: 300, // count: 4 // }); // } // 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); // Auto-open popup for the newest incident // setTimeout(() => { // popup.addTo(map); // }, 2000); } }); // 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'); // Cleanup function return () => { if (statusContainer && statusContainer.parentNode) { statusContainer.parentNode.removeChild(statusContainer); } }; }, [map, ewsStatus, activeIncidents.length]); // Render the full-screen alert overlay when a new incident is detected return ( <> {showAlert && currentAlert && (
PERINGATAN {currentAlert.category || "Emergency Alert"}

{currentAlert.priority}

PRIORITY

{currentAlert.location.district}

LOCATION

)} ); }