"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 }