import { useState, useEffect, useRef } from 'react'; // Lightning Detection Plugin - Real-time lightning strike visualization // Data source: Simulated lightning strikes (can be replaced with Blitzortung.org API) // Update: Real-time (every 30 seconds) export const metadata = { id: 'lightning', name: 'Lightning Detection(Testing-Simulated)', description: 'Real-time lightning strike detection and visualization', icon: '⚡', category: 'weather', defaultEnabled: false, defaultOpacity: 0.9, version: '1.0.0' }; // Strike age colors (fading over time) function getStrikeColor(ageMinutes) { if (ageMinutes < 1) return '#FFD700'; // Gold (fresh, <1 min) if (ageMinutes < 5) return '#FFA500'; // Orange (recent, <5 min) if (ageMinutes < 15) return '#FF6B6B'; // Red (aging, <15 min) if (ageMinutes < 30) return '#CD5C5C'; // Dark red (old, <30 min) return '#8B4513'; // Brown (very old, >30 min) } // Generate simulated lightning strikes (demo data) // In production, this would fetch from a real API function generateSimulatedStrikes(count = 50) { const strikes = []; const now = Date.now(); // Generate strikes across the globe with realistic clustering const stormCenters = [ { lat: 28.5, lon: -81.5, name: 'Florida' }, // Florida { lat: 40.7, lon: -74.0, name: 'New York' }, // New York { lat: 51.5, lon: -0.1, name: 'London' }, // London { lat: -23.5, lon: -46.6, name: 'São Paulo' }, // São Paulo { lat: 1.3, lon: 103.8, name: 'Singapore' }, // Singapore { lat: -33.9, lon: 151.2, name: 'Sydney' }, // Sydney { lat: 19.4, lon: -99.1, name: 'Mexico City' }, // Mexico City { lat: 13.7, lon: 100.5, name: 'Bangkok' }, // Bangkok ]; // Use strike INDEX as seed for completely stable positions // Each strike always appears at the same location for (let i = 0; i < count; i++) { const seed = i * 12345; // Each strike has fixed seed based on index const seededRandom = seed * 9301 + 49297; // Simple LCG const r1 = (seededRandom % 233280) / 233280.0; const r2 = ((seededRandom * 7) % 233280) / 233280.0; const r3 = ((seededRandom * 13) % 233280) / 233280.0; // Pick a storm center (always same center for this index) const center = stormCenters[Math.floor(r1 * stormCenters.length)]; // Create strike near the center (always same offset for this index) const latOffset = (r2 - 0.5) * 2.0; // ~220 km spread const lonOffset = (r3 - 0.5) * 2.0; // Calculate fixed position for this strike const lat = Math.round((center.lat + latOffset) * 10) / 10; const lon = Math.round((center.lon + lonOffset) * 10) / 10; // Age cycles over time (strikes "age out" and "reappear" as fresh) const cycleMs = 30 * 60 * 1000; // 30 minute cycle const ageMs = ((now + (i * 10000)) % cycleMs); // Stagger ages const timestamp = now - ageMs; const roundedTime = Math.floor(timestamp / 10000) * 10000; // Round to 10s for ID changes // Intensity fixed for this strike const intensity = (r2 * 200) - 50; // -50 to +150 kA const polarity = intensity >= 0 ? 'positive' : 'negative'; strikes.push({ id: `strike_${i}_${lat}_${lon}_${roundedTime}`, // Include time for ID changes lat, // Fixed position lon, // Fixed position timestamp, age: ageMs / 1000, intensity: Math.abs(intensity), polarity, region: center.name }); } return strikes.sort((a, b) => b.timestamp - a.timestamp); // Newest first } export function useLayer({ enabled = false, opacity = 0.9, map = null }) { const [strikeMarkers, setStrikeMarkers] = useState([]); const [lightningData, setLightningData] = useState([]); const [statsControl, setStatsControl] = useState(null); const previousStrikeIds = useRef(new Set()); const updateIntervalRef = useRef(null); const isFirstLoad = useRef(true); // Fetch lightning data (simulated for now) useEffect(() => { if (!enabled) return; const fetchLightning = () => { try { // In production, this would be: // const response = await fetch('/api/lightning/strikes?minutes=30'); // const data = await response.json(); // For now, generate simulated data const strikes = generateSimulatedStrikes(50); console.log('[Lightning] Generated', strikes.length, 'strikes at', new Date().toLocaleTimeString()); setLightningData(strikes); } catch (err) { console.error('Lightning data fetch error:', err); } }; fetchLightning(); // Refresh every 30 seconds updateIntervalRef.current = setInterval(fetchLightning, 30000); return () => { if (updateIntervalRef.current) { clearInterval(updateIntervalRef.current); } }; }, [enabled]); // Render strike markers with animation useEffect(() => { if (!map || typeof L === 'undefined') return; // Clear old markers strikeMarkers.forEach(marker => { try { map.removeLayer(marker); } catch (e) { // Already removed } }); setStrikeMarkers([]); if (!enabled || lightningData.length === 0) return; const newMarkers = []; const currentStrikeIds = new Set(); lightningData.forEach(strike => { const { id, lat, lon, timestamp, age, intensity, polarity, region } = strike; currentStrikeIds.add(id); // Check if this is a new strike (but not on first load) const isNew = !isFirstLoad.current && !previousStrikeIds.current.has(id); // Calculate age in minutes const ageMinutes = age / 60; const color = getStrikeColor(ageMinutes); // Size based on intensity (12-32px) const size = Math.min(Math.max(intensity / 8, 12), 32); // Create lightning bolt icon marker with high visibility const icon = L.divIcon({ className: 'lightning-strike-icon', html: `
`, iconSize: [size, size], iconAnchor: [size/2, size/2] }); const marker = L.marker([lat, lon], { icon, opacity, zIndexOffset: 10000 // Ensure markers appear on top }); // Add to map first marker.addTo(map); // Add pulsing animation for new strikes ONLY if (isNew) { // Wait for DOM element to be created, then add animation class setTimeout(() => { try { const iconElement = marker.getElement(); if (iconElement) { const iconDiv = iconElement.querySelector('div'); if (iconDiv) { iconDiv.classList.add('lightning-strike-new'); // Remove animation class after it completes (0.8s) setTimeout(() => { try { iconDiv.classList.remove('lightning-strike-new'); } catch (e) {} }, 800); } } } catch (e) { console.warn('Could not animate lightning marker:', e); } }, 10); // Create pulsing ring effect const pulseRing = L.circle([lat, lon], { radius: 30000, // 30km radius in meters fillColor: color, fillOpacity: 0, color: color, weight: 2, opacity: 0.9, className: 'lightning-pulse-ring' }); pulseRing.addTo(map); // Remove pulse ring after animation completes setTimeout(() => { try { map.removeLayer(pulseRing); } catch (e) {} }, 2000); } // Format time const strikeTime = new Date(timestamp); const timeStr = strikeTime.toLocaleString(); const ageStr = ageMinutes < 1 ? `${Math.floor(age)} sec ago` : `${Math.floor(ageMinutes)} min ago`; // Add popup with details marker.bindPopup(`
${isNew ? '🆕 ' : ''}⚡ Lightning Strike
Region:${region || 'Unknown'}
Time:${timeStr}
Age:${ageStr}
Intensity:${intensity.toFixed(1)} kA
Polarity:${polarity}
Coordinates:${lat.toFixed(3)}°, ${lon.toFixed(3)}°
`); // Already added to map above (before animation) newMarkers.push(marker); }); // Update previous strike IDs for next comparison previousStrikeIds.current = currentStrikeIds; // After first load, allow animations for new strikes if (isFirstLoad.current) { isFirstLoad.current = false; } setStrikeMarkers(newMarkers); return () => { newMarkers.forEach(marker => { try { map.removeLayer(marker); } catch (e) { // Already removed } }); }; }, [enabled, lightningData, map, opacity]); // Add statistics control useEffect(() => { if (!map || typeof L === 'undefined') return; // Remove existing control if (statsControl) { try { map.removeControl(statsControl); } catch (e) {} setStatsControl(null); } if (!enabled || lightningData.length === 0) return; // Create stats control const StatsControl = L.Control.extend({ options: { position: 'topleft' }, onAdd: function () { const div = L.DomUtil.create('div', 'lightning-stats'); // Calculate statistics const fresh = lightningData.filter(s => s.age < 60).length; // <1 min const recent = lightningData.filter(s => s.age < 300).length; // <5 min const total = lightningData.length; const avgIntensity = lightningData.reduce((sum, s) => sum + s.intensity, 0) / total; const positiveStrikes = lightningData.filter(s => s.polarity === 'positive').length; const negativeStrikes = total - positiveStrikes; console.log('[Lightning] Stats panel updated:', { fresh, recent, total }); div.innerHTML = `
⚡ Lightning Activity
Fresh (<1 min):${fresh}
Recent (<5 min):${recent}
Total (30 min):${total}
Avg Intensity:${avgIntensity.toFixed(1)} kA
Positive:+${positiveStrikes}
Negative:-${negativeStrikes}
Updates every 30s
`; // Add minimize/maximize functionality const header = div.querySelector('.lightning-stats-header'); const content = div.querySelector('.lightning-stats-content'); const toggle = div.querySelector('.lightning-stats-toggle'); const minimized = localStorage.getItem('lightning-stats-minimized') === 'true'; if (minimized) { content.style.display = 'none'; toggle.textContent = '▶'; } header.addEventListener('click', () => { const isMinimized = content.style.display === 'none'; content.style.display = isMinimized ? 'block' : 'none'; toggle.textContent = isMinimized ? '▼' : '▶'; localStorage.setItem('lightning-stats-minimized', !isMinimized); }); // Prevent map interaction on control L.DomEvent.disableClickPropagation(div); L.DomEvent.disableScrollPropagation(div); return div; } }); const control = new StatsControl(); control.addTo(map); setStatsControl(control); return () => { if (control && map) { try { map.removeControl(control); } catch (e) {} } }; }, [enabled, lightningData, map]); // Cleanup on disable useEffect(() => { if (!enabled && map) { // Remove stats control if (statsControl) { try { map.removeControl(statsControl); } catch (e) {} setStatsControl(null); } // Clear all markers strikeMarkers.forEach(marker => { try { map.removeLayer(marker); } catch (e) {} }); setStrikeMarkers([]); // Clear data setLightningData([]); previousStrikeIds.current.clear(); } }, [enabled, map]); return { markers: strikeMarkers, strikeCount: lightningData.length, freshCount: lightningData.filter(s => s.age < 60).length }; }