Spaces:
Runtime error
Runtime error
Claude
Add resource intelligence layer: mining/RAM datasets, macro & utilities APIs, map layers
5aeda2b unverified | 'use client'; | |
| import { useEffect, useRef, useState, useCallback, memo } from 'react'; | |
| import maplibregl from 'maplibre-gl'; | |
| import 'maplibre-gl/dist/maplibre-gl.css'; | |
| interface OsirisMapProps { | |
| data: any; | |
| activeLayers: Record<string, boolean>; | |
| onEntityClick?: (entity: any) => void; | |
| onMouseCoords?: (coords: { lat: number; lng: number }) => void; | |
| onRightClick?: (coords: { lat: number; lng: number }) => void; | |
| onViewStateChange?: (vs: { zoom: number; latitude: number }) => void; | |
| flyToLocation?: { lat: number; lng: number; zoom?: number; ts: number } | null; | |
| projection?: 'mercator' | 'globe'; | |
| mapStyle?: string; | |
| sweepData?: any; | |
| scanTargets?: any[]; | |
| demoMode?: boolean; | |
| theme?: 'core' | 'ghost'; | |
| } | |
| function computeSolarTerminator(): [number, number][] { | |
| const now = new Date(); | |
| const dayOfYear = Math.floor((now.getTime() - new Date(now.getFullYear(), 0, 0).getTime()) / 86400000); | |
| const declination = -23.44 * Math.cos((2 * Math.PI / 365) * (dayOfYear + 10)); | |
| const decRad = declination * Math.PI / 180; | |
| const utcHours = now.getUTCHours() + now.getUTCMinutes() / 60; | |
| const subsolarLng = (12 - utcHours) * 15; | |
| const points: [number, number][] = []; | |
| for (let lng = -180; lng <= 180; lng += 2) { | |
| const lngRad = (lng - subsolarLng) * Math.PI / 180; | |
| const lat = Math.atan(-Math.cos(lngRad) / Math.tan(decRad)) * 180 / Math.PI; | |
| points.push([lng, lat]); | |
| } | |
| const darkSide = declination >= 0 ? -90 : 90; | |
| points.push([180, darkSide]); | |
| points.push([-180, darkSide]); | |
| points.push(points[0]); | |
| return points; | |
| } | |
| const EMPTY_FC = { type: 'FeatureCollection' as const, features: [] }; | |
| function OsirisMap({ data, activeLayers, onEntityClick, onMouseCoords, onRightClick, onViewStateChange, flyToLocation, projection = 'globe', mapStyle = 'dark', sweepData, scanTargets = [], demoMode = false, theme = 'core' }: OsirisMapProps) { | |
| const containerRef = useRef<HTMLDivElement>(null); | |
| const mapRef = useRef<maplibregl.Map | null>(null); | |
| const popupRef = useRef<maplibregl.Popup | null>(null); | |
| const [mapReady, setMapReady] = useState(false); | |
| const prevStyleRef = useRef(mapStyle); | |
| // Create aircraft icon on canvas (for WebGL symbol layer) | |
| const createIcon = useCallback((map: maplibregl.Map, id: string, color: string, size: number) => { | |
| if (map.hasImage(id)) return; | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = size; canvas.height = size; | |
| const ctx = canvas.getContext('2d')!; | |
| const cx = size / 2, cy = size / 2; | |
| ctx.fillStyle = color; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy - size * 0.4); | |
| ctx.lineTo(cx - size * 0.12, cy + size * 0.1); | |
| ctx.lineTo(cx - size * 0.4, cy + size * 0.2); | |
| ctx.lineTo(cx - size * 0.4, cy + size * 0.3); | |
| ctx.lineTo(cx - size * 0.12, cy + size * 0.15); | |
| ctx.lineTo(cx, cy + size * 0.35); | |
| ctx.lineTo(cx + size * 0.12, cy + size * 0.15); | |
| ctx.lineTo(cx + size * 0.4, cy + size * 0.3); | |
| ctx.lineTo(cx + size * 0.4, cy + size * 0.2); | |
| ctx.lineTo(cx + size * 0.12, cy + size * 0.1); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| map.addImage(id, { width: size, height: size, data: new Uint8Array(ctx.getImageData(0, 0, size, size).data) }); | |
| }, []); | |
| const createDot = useCallback((map: maplibregl.Map, id: string, color: string, size: number) => { | |
| if (map.hasImage(id)) return; | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = size; canvas.height = size; | |
| const ctx = canvas.getContext('2d')!; | |
| ctx.fillStyle = color; | |
| ctx.beginPath(); | |
| ctx.arc(size/2, size/2, size/2 - 1, 0, Math.PI * 2); | |
| ctx.fill(); | |
| map.addImage(id, { width: size, height: size, data: new Uint8Array(ctx.getImageData(0, 0, size, size).data) }); | |
| }, []); | |
| useEffect(() => { | |
| if (!mapRef.current) return; | |
| const map = mapRef.current; | |
| // ── DEMO MODE SPINNING ── | |
| let spinReq: number | undefined = undefined; | |
| let isSpinning = false; | |
| const startSpinning = () => { | |
| if (!map) return; | |
| isSpinning = true; | |
| let lastTime = performance.now(); | |
| const frame = (time: number) => { | |
| if (!isSpinning) return; | |
| // Only spin if the user is not actively dragging or zooming the map | |
| if (!map.isMoving() && !map.isZooming()) { | |
| const dt = time - lastTime; | |
| const center = map.getCenter(); | |
| // Adjust spin speed: 0.5 degrees per second | |
| center.lng += (0.5 * dt) / 1000; | |
| map.setCenter(center); | |
| } | |
| lastTime = time; | |
| spinReq = requestAnimationFrame(frame); | |
| }; | |
| spinReq = requestAnimationFrame(frame); | |
| }; | |
| if (demoMode) { | |
| startSpinning(); | |
| } else { | |
| isSpinning = false; | |
| if (spinReq) cancelAnimationFrame(spinReq); | |
| } | |
| return () => { | |
| isSpinning = false; | |
| if (spinReq) cancelAnimationFrame(spinReq); | |
| if (typeof window !== 'undefined' && (window as any)._globeSpinTimer) { | |
| clearInterval((window as any)._globeSpinTimer); | |
| } | |
| }; | |
| }, [mapReady, demoMode]); | |
| useEffect(() => { | |
| if (!containerRef.current || mapRef.current) return; | |
| // Select basemap style | |
| const styleUrl = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json'; | |
| const map = new maplibregl.Map({ | |
| container: containerRef.current, | |
| style: styleUrl, | |
| center: [25.48, 42.70], zoom: 6.5, minZoom: 1.5, maxZoom: 18, | |
| attributionControl: false, | |
| maxPitch: 85, | |
| transformRequest: (url: string) => { | |
| // Route all CARTO CDN requests through the internal Next.js proxy API | |
| if (url.includes('cartocdn.com')) { | |
| const baseUrl = typeof window !== 'undefined' ? window.location.origin : ''; | |
| return { url: `${baseUrl}/api/proxy-tiles?url=${encodeURIComponent(url)}` }; | |
| } | |
| return { url }; | |
| }, | |
| }); | |
| map.on('load', () => { | |
| mapRef.current = map; | |
| // Theme colors | |
| const isGhost = theme === 'ghost'; | |
| const phantomPurple = '#B388FF'; | |
| const phantomDark = '#1A0040'; | |
| const cameraColor = isGhost ? '#B388FF' : '#00E676'; | |
| const flightCom = isGhost ? phantomPurple : '#00E5FF'; | |
| const flightPriv = isGhost ? phantomPurple : '#FFD700'; | |
| const flightGov = isGhost ? phantomPurple : '#FF9500'; | |
| const flightMil = isGhost ? phantomPurple : '#FF3D3D'; | |
| // Create icons — OSIRIS Unified Palette | |
| createIcon(map, 'plane-cyan', flightCom, 24); | |
| createIcon(map, 'plane-green', flightPriv, 24); | |
| createIcon(map, 'plane-pink', flightGov, 24); | |
| createIcon(map, 'plane-red', flightMil, 24); | |
| createIcon(map, 'plane-grey', isGhost ? phantomPurple : '#546E7A', 24); | |
| createDot(map, 'dot-gold', isGhost ? phantomPurple : '#D4AF37', 8); | |
| createDot(map, 'dot-red', isGhost ? phantomPurple : '#D32F2F', 10); | |
| createDot(map, 'dot-orange', isGhost ? phantomPurple : '#E65100', 10); | |
| createDot(map, 'dot-green', isGhost ? phantomPurple : '#26A69A', 10); | |
| createDot(map, 'dot-fire', isGhost ? phantomPurple : '#E65100', 10); | |
| createDot(map, 'dot-cctv', cameraColor, 10); | |
| const sources = ['flights','military','jets','private-fl','satellites','earthquakes','gdelt','gps-jamming','day-night','cctv','fires','weather','infrastructure','maritime','maritime-choke','maritime-ships','live-news','sigint-news','conflict-zones', 'war-alerts-targets', 'war-alerts-lines', 'balloons', 'radiation', 'ip-sweep-devices', 'ip-sweep-pulse', 'ip-sweep-connections', 'scan-targets', 'sdk-entities', 'sdk-links', 'malware-nodes', 'network-mesh', 'mining-sites', 'ram-sites']; | |
| sources.forEach(s => map.addSource(s, { type: 'geojson', data: EMPTY_FC })); | |
| // Warning icon generator (parameterized — eliminates 3x copy-paste) | |
| const createWarningIcon = (id: string, color: string) => { | |
| const s = 20; | |
| const c = document.createElement('canvas'); | |
| c.width = s; c.height = s; | |
| const ctx = c.getContext('2d')!; | |
| ctx.fillStyle = color; | |
| ctx.beginPath(); | |
| ctx.moveTo(s/2, 1); | |
| ctx.lineTo(s - 1, s - 1); | |
| ctx.lineTo(1, s - 1); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| ctx.fillStyle = '#000'; | |
| ctx.font = 'bold 11px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText('!', s/2, s - 4); | |
| map.addImage(id, { width: s, height: s, data: new Uint8Array(ctx.getImageData(0, 0, s, s).data) }); | |
| }; | |
| createWarningIcon('warn-icon', '#D32F2F'); | |
| createWarningIcon('warn-orange', '#E65100'); | |
| createWarningIcon('warn-yellow', '#F9A825'); | |
| map.addLayer({ id: 'conflict-icons', type: 'symbol', source: 'conflict-zones', layout: { | |
| 'icon-image': ['match', ['get','severity'], 'war','warn-icon', 'high','warn-orange', 'warn-yellow'], | |
| 'icon-size': ['interpolate',['linear'],['zoom'], 1,0.6, 4,0.8, 8,1], | |
| 'icon-allow-overlap': true, | |
| 'text-field': ['get','label'], | |
| 'text-size': ['interpolate',['linear'],['zoom'], 1,7, 4,9, 8,11], | |
| 'text-font': ['Open Sans Bold'], | |
| 'text-offset': [0, 1.4], | |
| 'text-allow-overlap': false, | |
| }, paint: { | |
| 'text-color': ['match', ['get','severity'], 'war','#D32F2F', 'high','#E65100', '#F9A825'], | |
| 'text-halo-color': '#000', 'text-halo-width': 1.5, 'text-opacity': 0.9, | |
| }}); | |
| // Day/Night | |
| map.addLayer({ id: 'day-night-fill', type: 'fill', source: 'day-night', paint: { 'fill-color': isGhost ? '#0D0030' : '#000022', 'fill-opacity': 0.35 }}); | |
| // Earthquakes — amber threat spectrum | |
| map.addLayer({ id: 'eq-circles', type: 'circle', source: 'earthquakes', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['get','magnitude'], 2.5,4, 5,12, 7,24], | |
| 'circle-color': ['interpolate',['linear'],['get','magnitude'], 2.5,'#F9A825', 4,'#E65100', 6,'#D32F2F'], | |
| 'circle-opacity': 0.55, 'circle-blur': 0.3, 'circle-stroke-width': 1, 'circle-stroke-color': '#F9A825', 'circle-stroke-opacity': 0.25, | |
| }}); | |
| map.addLayer({ id: 'eq-label', type: 'symbol', source: 'earthquakes', filter: ['>=',['get','magnitude'],4.5], layout: { | |
| 'text-field': ['concat','M',['to-string',['get','magnitude']]], 'text-size': 9, 'text-font': ['Open Sans Regular'], 'text-offset': [0,1.5], | |
| }, paint: { 'text-color': '#F9A825', 'text-halo-color': '#000', 'text-halo-width': 1 }}); | |
| // Fires — burnt sienna | |
| map.addLayer({ id: 'fires-heat', type: 'circle', source: 'fires', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,2, 5,4, 10,8], | |
| 'circle-color': '#E65100', 'circle-opacity': 0.45, 'circle-blur': 0.5, | |
| }}); | |
| // CCTV — outer glow ring (black/white depending on theme) | |
| map.addLayer({ id: 'cctv-glow', type: 'circle', source: 'cctv', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,5, 5,8, 10,14, 14,20], | |
| 'circle-color': '#000000', 'circle-opacity': 0.35, 'circle-blur': 1, | |
| }}); | |
| // CCTV — main dot | |
| map.addLayer({ id: 'cctv-dots', type: 'circle', source: 'cctv', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,3, 5,5, 10,8, 14,12], | |
| 'circle-color': cameraColor, 'circle-opacity': 0.9, | |
| 'circle-stroke-width': 2.5, 'circle-stroke-color': '#000000', 'circle-stroke-opacity': 0.9, | |
| }}); | |
| // CCTV — labels at zoom 10+ | |
| map.addLayer({ id: 'cctv-label', type: 'symbol', source: 'cctv', minzoom: 10, layout: { | |
| 'text-field': ['get','name'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 1.8], 'text-max-width': 12, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': cameraColor, 'text-halo-color': '#000000', 'text-halo-width': 1.5, 'text-opacity': 0.8 }}); | |
| // GDELT | |
| // ══ NETWORK INTEL — Live Malware (abuse.ch) — crimson threat ══ | |
| map.addLayer({ id: 'malware-glow', type: 'circle', source: 'malware-nodes', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,6, 5,12, 10,20], | |
| 'circle-color': '#D32F2F', 'circle-opacity': 0.06, 'circle-blur': 0.5, | |
| }}); | |
| map.addLayer({ id: 'malware-dots', type: 'circle', source: 'malware-nodes', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,2, 5,4, 10,6], | |
| 'circle-color': '#D32F2F', | |
| 'circle-opacity': 0.9, | |
| 'circle-stroke-width': 1, 'circle-stroke-color': '#000000', 'circle-stroke-opacity': 0.8, | |
| }}); | |
| map.addLayer({ id: 'malware-label', type: 'symbol', source: 'malware-nodes', minzoom: 5, layout: { | |
| 'text-field': ['get','malware'], 'text-size': 8, 'text-font': ['JetBrains Mono Bold', 'Open Sans Bold'], | |
| 'text-offset': [0, 1.5], 'text-max-width': 10, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#D32F2F', 'text-halo-color': '#111', 'text-halo-width': 1.5, 'text-opacity': 0.85 }}); | |
| // ── NETWORK INTEL MESH (SDK STYLE) ── | |
| map.addLayer({ id: 'network-mesh-atmo', type: 'line', source: 'network-mesh', paint: { | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 2, 5, 4, 10, 8], | |
| 'line-opacity': 0.08, | |
| 'line-blur': 4, | |
| }}); | |
| map.addLayer({ id: 'network-mesh-glow', type: 'line', source: 'network-mesh', paint: { | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 1, 5, 2, 10, 4], | |
| 'line-opacity': 0.2, | |
| 'line-blur': 1.5, | |
| }}); | |
| map.addLayer({ id: 'network-mesh-core', type: 'line', source: 'network-mesh', paint: { | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 0.2, 5, 0.5, 10, 1.5], | |
| 'line-opacity': 0.4, | |
| }}); | |
| map.addLayer({ id: 'gdelt-dots', type: 'circle', source: 'gdelt', paint: { | |
| 'circle-radius': 4, 'circle-color': '#D32F2F', 'circle-opacity': 0.5, 'circle-stroke-width': 1, 'circle-stroke-color': '#D32F2F', 'circle-stroke-opacity': 0.25, | |
| }}); | |
| // GPS Jamming — crimson | |
| map.addLayer({ id: 'jam-fill', type: 'circle', source: 'gps-jamming', paint: { 'circle-radius': 30, 'circle-color': '#D32F2F', 'circle-opacity': 0.12, 'circle-blur': 1 }}); | |
| map.addLayer({ id: 'jam-label', type: 'symbol', source: 'gps-jamming', layout: { | |
| 'text-field': ['concat','GPS JAM ',['to-string',['get','severity']],'%'], 'text-size': 10, 'text-font': ['Open Sans Bold'], 'text-allow-overlap': true, | |
| }, paint: { 'text-color': '#D32F2F', 'text-halo-color': '#000', 'text-halo-width': 1 }}); | |
| // Weather Events (NASA EONET) — deep violet | |
| map.addLayer({ id: 'weather-glow', type: 'circle', source: 'weather', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,12, 5,20, 10,30], | |
| 'circle-color': '#7E57C2', 'circle-opacity': 0.08, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'weather-dots', type: 'circle', source: 'weather', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,5, 5,8, 10,14], | |
| 'circle-color': ['match', ['get','icon'], 'cyclone','#7E57C2', 'volcano','#D32F2F', '#7E57C2'], | |
| 'circle-opacity': 0.75, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': '#7E57C2', 'circle-stroke-opacity': 0.35, | |
| }}); | |
| map.addLayer({ id: 'weather-label', type: 'symbol', source: 'weather', layout: { | |
| 'text-field': ['get','title'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 2], 'text-max-width': 14, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#7E57C2', 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.8 }}); | |
| // Nuclear Infrastructure — teal / amber risk | |
| map.addLayer({ id: 'infra-glow', type: 'circle', source: 'infrastructure', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,8, 5,14, 10,22], | |
| 'circle-color': ['case', ['in', 'SEISMIC RISK', ['get', 'status']], '#E65100', '#26A69A'], | |
| 'circle-opacity': 0.08, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'infra-dots', type: 'circle', source: 'infrastructure', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,4, 5,6, 10,10], | |
| 'circle-color': ['case', | |
| ['in', 'SEISMIC RISK', ['get', 'status']], '#E65100', | |
| ['==', ['get','status'], 'Active Conflict Zone'], '#D32F2F', | |
| ['==', ['get','status'], 'Destroyed / Decommissioning'], '#546E7A', | |
| '#26A69A' | |
| ], | |
| 'circle-opacity': 0.75, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': ['case', ['in', 'SEISMIC RISK', ['get', 'status']], '#E65100', '#26A69A'], 'circle-stroke-opacity': 0.35, | |
| }}); | |
| map.addLayer({ id: 'infra-label', type: 'symbol', source: 'infrastructure', minzoom: 5, layout: { | |
| 'text-field': ['get','name'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 2], 'text-max-width': 14, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': ['case', ['in', 'SEISMIC RISK', ['get', 'status']], '#E65100', '#26A69A'], 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.7 }}); | |
| // Mining Companies — colored by commodity category | |
| const miningColor: any = ['match', ['get','category'], | |
| 'copper','#FF8A65', 'gold','#FFD54F', 'silver','#CFD8DC', | |
| 'pgm','#4DB6AC', 'rare_earth','#BA68C8', '#FFB74D']; | |
| map.addLayer({ id: 'mining-glow', type: 'circle', source: 'mining-sites', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,8, 5,14, 10,22], | |
| 'circle-color': miningColor, 'circle-opacity': 0.08, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'mining-dots', type: 'circle', source: 'mining-sites', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,4, 5,6, 10,10], | |
| 'circle-color': miningColor, 'circle-opacity': 0.8, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': miningColor, 'circle-stroke-opacity': 0.35, | |
| }}); | |
| map.addLayer({ id: 'mining-label', type: 'symbol', source: 'mining-sites', minzoom: 4, layout: { | |
| 'text-field': ['get','site'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 2], 'text-max-width': 14, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': miningColor, 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.75 }}); | |
| // Memory / RAM Fabs — colored by ecosystem role | |
| const ramColor: any = ['match', ['get','category'], | |
| 'dram_hbm','#4FC3F7', 'nand','#81C784', 'controller_ip','#FFF176', | |
| 'module_maker','#90A4AE', 'packaging_test','#F06292', 'equipment','#9575CD', '#4FC3F7']; | |
| map.addLayer({ id: 'ram-glow', type: 'circle', source: 'ram-sites', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,8, 5,14, 10,22], | |
| 'circle-color': ramColor, 'circle-opacity': 0.08, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'ram-dots', type: 'circle', source: 'ram-sites', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,4, 5,6, 10,10], | |
| 'circle-color': ramColor, 'circle-opacity': 0.8, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': ramColor, 'circle-stroke-opacity': 0.35, | |
| }}); | |
| map.addLayer({ id: 'ram-label', type: 'symbol', source: 'ram-sites', minzoom: 4, layout: { | |
| 'text-field': ['get','company'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 2], 'text-max-width': 14, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': ramColor, 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.75 }}); | |
| // Satellites | |
| map.addLayer({ id: 'sat-glow', type: 'circle', source: 'satellites', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,3, 5,6], 'circle-color': ['get','color'], 'circle-opacity': 0.3, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'sat-dots', type: 'circle', source: 'satellites', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,1.5, 5,3], 'circle-color': ['get','color'], 'circle-opacity': 1.0, | |
| }}); | |
| // Maritime — ports & naval bases — ocean teal | |
| map.addLayer({ id: 'maritime-glow', type: 'circle', source: 'maritime', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,6, 5,12, 10,20], | |
| 'circle-color': ['match', ['get','type'], 'naval','#D32F2F', 'energy','#E65100', '#26C6DA'], | |
| 'circle-opacity': 0.08, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'maritime-dots', type: 'circle', source: 'maritime', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,3, 5,5, 10,9], | |
| 'circle-color': ['match', ['get','type'], 'naval','#D32F2F', 'energy','#E65100', '#26C6DA'], | |
| 'circle-opacity': 0.8, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': ['match', ['get','type'], 'naval','#D32F2F', 'energy','#E65100', '#26C6DA'], 'circle-stroke-opacity': 0.35, | |
| }}); | |
| map.addLayer({ id: 'maritime-label', type: 'symbol', source: 'maritime', minzoom: 4, layout: { | |
| 'text-field': ['get','name'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 1.8], 'text-max-width': 12, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#26C6DA', 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.7 }}); | |
| // Maritime chokepoints — amber threat spectrum | |
| map.addLayer({ id: 'choke-glow', type: 'circle', source: 'maritime-choke', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,10, 5,18, 10,28], | |
| 'circle-color': '#E65100', 'circle-opacity': 0.1, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'choke-dots', type: 'circle', source: 'maritime-choke', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,4, 5,7, 10,12], | |
| 'circle-color': ['match', ['get','risk'], 'CRITICAL','#D32F2F', 'HIGH','#E65100', 'ELEVATED','#F9A825', '#26A69A'], | |
| 'circle-opacity': 0.85, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': '#E65100', 'circle-stroke-opacity': 0.4, | |
| }}); | |
| map.addLayer({ id: 'choke-label', type: 'symbol', source: 'maritime-choke', minzoom: 3, layout: { | |
| 'text-field': ['get','name'], 'text-size': 10, 'text-font': ['Open Sans Bold'], | |
| 'text-offset': [0, 2], 'text-max-width': 14, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#E65100', 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.9 }}); | |
| // Live News — muted rose | |
| map.addLayer({ id: 'news-glow', type: 'circle', source: 'live-news', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,8, 5,14, 10,22], | |
| 'circle-color': '#EC407A', 'circle-opacity': 0.08, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'news-dots', type: 'circle', source: 'live-news', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,4, 5,6, 10,10], | |
| 'circle-color': '#EC407A', 'circle-opacity': 0.8, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': '#EC407A', 'circle-stroke-opacity': 0.4, | |
| }}); | |
| map.addLayer({ id: 'news-label', type: 'symbol', source: 'live-news', minzoom: 4, layout: { | |
| 'text-field': ['get','name'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 1.8], 'text-max-width': 12, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#EC407A', 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.8 }}); | |
| // SIGINT RSS news - gold markers | |
| map.addLayer({ id: 'sigint-news-glow', type: 'circle', source: 'sigint-news', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,6, 5,10, 10,18], | |
| 'circle-color': '#D4AF37', 'circle-opacity': 0.12, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'sigint-news-dots', type: 'circle', source: 'sigint-news', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,3, 5,5, 10,8], | |
| 'circle-color': '#D4AF37', 'circle-opacity': 0.9, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': '#FFF8DC', 'circle-stroke-opacity': 0.6, | |
| }}); | |
| map.addLayer({ id: 'sigint-news-label', type: 'symbol', source: 'sigint-news', minzoom: 5, layout: { | |
| 'text-field': ['get','source'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 1.6], 'text-max-width': 10, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#D4AF37', 'text-halo-color': '#000', 'text-halo-width': 1, 'text-opacity': 0.85 }}); | |
| // ══ IP SWEEP — Neighborhood device visualization ══ | |
| map.addLayer({ id: 'sweep-connections', type: 'line', source: 'ip-sweep-connections', paint: { | |
| 'line-color': ['get', 'color'], 'line-width': 1, 'line-opacity': 0.3, 'line-dasharray': [2, 4], | |
| }}); | |
| map.addLayer({ id: 'sweep-pulse-ring', type: 'circle', source: 'ip-sweep-pulse', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 8,40, 12,80, 16,160], | |
| 'circle-color': 'transparent', 'circle-opacity': 0.6, | |
| 'circle-stroke-width': 2, 'circle-stroke-color': '#FF3D3D', 'circle-stroke-opacity': 0.4, | |
| }}); | |
| map.addLayer({ id: 'sweep-device-glow', type: 'circle', source: 'ip-sweep-devices', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 8,8, 12,16, 16,30], | |
| 'circle-color': ['get', 'color'], 'circle-opacity': 0.15, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'sweep-device-dots', type: 'circle', source: 'ip-sweep-devices', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 8,3, 12,6, 16,10], | |
| 'circle-color': ['get', 'color'], 'circle-opacity': 0.95, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': '#FFFFFF', 'circle-stroke-opacity': 0.6, | |
| }}); | |
| map.addLayer({ id: 'sweep-device-labels', type: 'symbol', source: 'ip-sweep-devices', minzoom: 13, layout: { | |
| 'text-field': ['concat', ['get', 'device_type'], '\n', ['get', 'ip']], | |
| 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 2.2], 'text-max-width': 12, 'text-allow-overlap': false, | |
| }, paint: { | |
| 'text-color': ['get', 'color'], 'text-halo-color': '#000', 'text-halo-width': 1.5, 'text-opacity': 0.9, | |
| }}); | |
| // ══ SCAN TARGETS — Geolocated individual scans ══ | |
| map.addLayer({ id: 'scan-targets-glow', type: 'circle', source: 'scan-targets', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,12, 5,25, 10,40], | |
| 'circle-color': '#D32F2F', 'circle-opacity': 0.15, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'scan-targets-dots', type: 'circle', source: 'scan-targets', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,5, 5,8, 10,12], | |
| 'circle-color': '#D32F2F', 'circle-opacity': 0.9, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': '#ECEFF1', 'circle-stroke-opacity': 0.7, | |
| }}); | |
| map.addLayer({ id: 'scan-targets-label', type: 'symbol', source: 'scan-targets', layout: { | |
| 'text-field': ['get', 'id'], 'text-size': 11, 'text-font': ['Open Sans Bold'], | |
| 'text-offset': [0, 2], 'text-max-width': 14, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': '#D32F2F', 'text-halo-color': '#000', 'text-halo-width': 1.5, 'text-opacity': 0.9 }}); | |
| // Flight layers (WebGL symbol — GPU rendered, handles 50K+ smooth) | |
| const flightLayers = [ | |
| { id: 'fl-commercial', src: 'flights', icon: 'plane-cyan' }, | |
| { id: 'fl-private', src: 'private-fl', icon: 'plane-green' }, | |
| { id: 'fl-jets', src: 'jets', icon: 'plane-pink' }, | |
| { id: 'fl-military', src: 'military', icon: 'plane-red' }, | |
| ]; | |
| flightLayers.forEach(l => { | |
| map.addLayer({ id: l.id, type: 'symbol', source: l.src, layout: { | |
| 'icon-image': l.icon, 'icon-size': ['interpolate',['linear'],['zoom'], 1,0.4, 5,0.7, 10,1], | |
| 'icon-rotate': ['get','heading'], 'icon-rotation-alignment': 'map', 'icon-allow-overlap': true, 'icon-ignore-placement': true, | |
| }, paint: { 'icon-opacity': 0.85 }}); | |
| }); | |
| // Balloons (moving entities) | |
| map.addLayer({ id: 'balloon-dots', type: 'circle', source: 'balloons', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,3, 5,5, 10,7], | |
| 'circle-color': ['get', 'color'], | |
| 'circle-opacity': 0.8, | |
| 'circle-stroke-width': 1, 'circle-stroke-color': '#fff', 'circle-stroke-opacity': 0.5, | |
| }}); | |
| map.addLayer({ id: 'balloon-label', type: 'symbol', source: 'balloons', minzoom: 4, layout: { | |
| 'text-field': ['get','callsign'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 1.2], 'text-max-width': 12, 'text-allow-overlap': false, | |
| }, paint: { 'text-color': ['get', 'color'], 'text-halo-color': '#000', 'text-halo-width': 1 }}); | |
| // Radiation — violet base, threat spectrum for danger/warning | |
| map.addLayer({ id: 'rad-glow', type: 'circle', source: 'radiation', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,10, 5,20, 10,40], | |
| 'circle-color': ['match', ['get','status'], 'DANGER','#D32F2F', 'WARNING','#E65100', '#7E57C2'], | |
| 'circle-opacity': 0.12, 'circle-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'rad-dots', type: 'circle', source: 'radiation', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,4, 5,6, 10,8], | |
| 'circle-color': ['match', ['get','status'], 'DANGER','#D32F2F', 'WARNING','#E65100', '#7E57C2'], | |
| 'circle-opacity': 0.85, | |
| 'circle-stroke-width': 1.5, 'circle-stroke-color': ['match', ['get','status'], 'DANGER','#D32F2F', 'WARNING','#E65100', '#7E57C2'], 'circle-stroke-opacity': 0.35, | |
| }}); | |
| map.addLayer({ id: 'rad-label', type: 'symbol', source: 'radiation', minzoom: 5, layout: { | |
| 'text-field': ['concat', ['to-string', ['get','reading']], ' nSv/h'], 'text-size': 9, 'text-font': ['Open Sans Bold'], | |
| 'text-offset': [0, 1.5], 'text-allow-overlap': false, | |
| }, paint: { 'text-color': ['match', ['get','status'], 'DANGER','#D32F2F', 'WARNING','#E65100', '#7E57C2'], 'text-halo-color': '#000', 'text-halo-width': 1 }}); | |
| // ══ OSIRIS SDK — Lattice Intelligence Mesh ══ | |
| // Polybolos Style: Delicate, translucent, steel-blue splined mesh | |
| // ── SEA domain (Distinct Solid Lines) ── | |
| // Removed glow to match the clean, diagrammatic look of submarinecablemap.com | |
| map.addLayer({ id: 'sdk-sea', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'SEA'], paint: { | |
| 'line-color': ['coalesce', ['get', 'color'], '#1976D2'], // Single solid color from properties | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 0.8, 5, 1.5, 10, 2.5], | |
| 'line-opacity': ['interpolate',['linear'],['zoom'], 1, 0.3, 5, 0.5, 10, 0.7], | |
| }}); | |
| // ── AIR domain (Steel Gray / Cyan) ── | |
| map.addLayer({ id: 'sdk-air-atmo', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'AIR'], paint: { | |
| 'line-color': '#4DD0E1', | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 1.5, 5, 5, 10, 8], | |
| 'line-opacity': 0.04, | |
| 'line-blur': 3, | |
| }}); | |
| map.addLayer({ id: 'sdk-air-glow', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'AIR'], paint: { | |
| 'line-color': '#80DEEA', | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 0.8, 5, 2, 10, 4], | |
| 'line-opacity': ['interpolate',['linear'],['zoom'], 1, 0.08, 5, 0.12, 10, 0.18], | |
| 'line-blur': 1, | |
| }}); | |
| map.addLayer({ id: 'sdk-air', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'AIR'], paint: { | |
| 'line-color': '#B2EBF2', | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 0.15, 5, 0.6, 10, 1.2], | |
| 'line-opacity': ['interpolate',['linear'],['zoom'], 1, 0.2, 5, 0.35, 10, 0.5], | |
| }}); | |
| // ── INTEL domain (Deep Steel / Violet) ── | |
| map.addLayer({ id: 'sdk-intel-atmo', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'INTEL'], paint: { | |
| 'line-color': '#7986CB', | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 2.5, 5, 7, 10, 12], | |
| 'line-opacity': 0.06, | |
| 'line-blur': 5, | |
| }}); | |
| map.addLayer({ id: 'sdk-intel-glow', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'INTEL'], paint: { | |
| 'line-color': '#9FA8DA', | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 1.2, 5, 3, 10, 6], | |
| 'line-opacity': ['interpolate',['linear'],['zoom'], 1, 0.12, 5, 0.18, 10, 0.25], | |
| 'line-blur': 2, | |
| }}); | |
| map.addLayer({ id: 'sdk-intel', type: 'line', source: 'sdk-links', filter: ['==',['get','domain'],'INTEL'], paint: { | |
| 'line-color': '#C5CAE9', | |
| 'line-width': ['interpolate',['linear'],['zoom'], 1, 0.3, 5, 1, 10, 2], | |
| 'line-opacity': ['interpolate',['linear'],['zoom'], 1, 0.3, 5, 0.45, 10, 0.7], | |
| }}); | |
| // Maritime Ships (moving entities) — ocean teal family | |
| map.addLayer({ id: 'ship-dots', type: 'circle', source: 'maritime-ships', paint: { | |
| 'circle-radius': ['interpolate',['linear'],['zoom'], 1,2, 5,4, 10,6], | |
| 'circle-color': ['match', ['get','type'], 'military','#D32F2F', 'tanker','#E65100', 'cargo','#26C6DA', '#B0BEC5'], | |
| 'circle-opacity': 0.75, | |
| }}); | |
| map.addLayer({ id: 'ship-label', type: 'symbol', source: 'maritime-ships', minzoom: 5, layout: { | |
| 'text-field': ['get','name'], 'text-size': 9, 'text-font': ['Open Sans Regular'], | |
| 'text-offset': [0, 1.2], 'text-allow-overlap': false, | |
| }, paint: { 'text-color': ['match', ['get','type'], 'military','#D32F2F', 'tanker','#E65100', 'cargo','#26C6DA', '#B0BEC5'], 'text-halo-color': '#000', 'text-halo-width': 1 }}); | |
| setMapReady(true); | |
| }); | |
| // Events | |
| let lastMove = 0; | |
| map.on('mousemove', e => { | |
| const now = Date.now(); | |
| if (now - lastMove > 100) { | |
| lastMove = now; | |
| onMouseCoords?.({ lat: e.lngLat.lat, lng: e.lngLat.lng }); | |
| } | |
| }); | |
| map.on('contextmenu', e => { e.preventDefault(); onRightClick?.({ lat: e.lngLat.lat, lng: e.lngLat.lng }); }); | |
| map.on('moveend', () => { const c = map.getCenter(); onViewStateChange?.({ zoom: map.getZoom(), latitude: c.lat }); }); | |
| // ── POPUP HELPER ── | |
| const popup = (coords: any, html: string) => { | |
| popupRef.current?.remove(); | |
| popupRef.current = new maplibregl.Popup({ closeButton: true, maxWidth: '420px', offset: 14 }).setLngLat(coords).setHTML(html).addTo(map); | |
| }; | |
| const pStyle = `background:rgba(12,14,26,0.95);backdrop-filter:blur(16px);border-radius:10px;padding:16px;font-family:'JetBrains Mono',monospace;`; | |
| const linkStyle = `display:inline-block;margin-top:8px;padding:5px 12px;font-size:10px;letter-spacing:0.12em;text-decoration:none;border-radius:5px;font-family:'JetBrains Mono',monospace;`; | |
| // ── XSS PROTECTION HELPERS ── | |
| const htmlEsc = (s: any): string => String(s ?? '').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); | |
| const idSafe = (s: any): string => String(s ?? '').replace(/[^a-zA-Z0-9_\.\-]/g, ''); | |
| const urlSafe = (s: any): string => { const u = String(s ?? ''); return /^https?:\/\//i.test(u) ? u : '#'; }; | |
| const colorSafe = (s: any): string => /^#[0-9a-fA-F]{3,8}$/.test(String(s ?? '')) ? String(s) : '#aaa'; | |
| // ── Flights (with FlightAware + ADS-B Exchange links) ── | |
| ['fl-commercial','fl-private','fl-jets','fl-military'].forEach(layer => { | |
| map.on('click', layer, e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const cs = (p.callsign||'').trim(); | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(212,175,55,0.3);"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;"> | |
| <span style="color:#D4AF37;font-size:16px;font-weight:700;letter-spacing:0.1em;">${htmlEsc(cs)}</span> | |
| <span style="color:#5C5A54;font-size:10px;">${htmlEsc(p.icao24||'')}</span> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px;font-size:11px;"> | |
| <div><span style="color:#5C5A54;font-size:9px;">MODEL</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.model||'—')}</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">ALT</span><br/><span style="color:#00E5FF;">${p.alt?Math.round(p.alt)+'m':'—'}</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">SPEED</span><br/><span style="color:#E8E6E0;">${p.speed_knots||'—'}kt</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">HDG</span><br/><span style="color:#E8E6E0;">${Math.round(p.heading||0)}°</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">REG</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.registration||'—')}</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">POS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(2)},${coords[0].toFixed(2)}</span></div> | |
| </div> | |
| <div style="margin-top:12px;display:flex;gap:6px;flex-wrap:wrap;"> | |
| <a href="https://www.flightaware.com/live/flight/${encodeURIComponent(cs)}" target="_blank" style="${linkStyle}color:#D4AF37;border:1px solid rgba(212,175,55,0.4);background:rgba(212,175,55,0.1);">⚡ FLIGHTAWARE</a> | |
| <a href="https://globe.adsbexchange.com/?icao=${encodeURIComponent(p.icao24||'')}" target="_blank" style="${linkStyle}color:#00E5FF;border:1px solid rgba(0,229,255,0.4);background:rgba(0,229,255,0.1);">📡 ADS-B</a> | |
| <a href="https://www.radarbox.com/data/flights/${encodeURIComponent(cs)}" target="_blank" style="${linkStyle}color:#FF69B4;border:1px solid rgba(255,105,180,0.4);background:rgba(255,105,180,0.1);">📍 RADARBOX</a> | |
| </div> | |
| <button onclick="window.openOsirisIntel({ callsign: '${idSafe(cs)}', icao24: '${idSafe(p.icao24||'')}', model: '${idSafe(p.model||'')}', registration: '${idSafe(p.registration||'')}' })" style="width:100%;margin-top:8px;padding:6px 12px;background:rgba(212,175,55,0.15);border:1px solid rgba(212,175,55,0.5);color:#D4AF37;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:bold;letter-spacing:0.1em;border-radius:4px;cursor:pointer;">[ DEEP DIVE INTEL ]</button> | |
| </div>`); | |
| }); | |
| map.on('mouseenter', layer, () => { map.getCanvas().style.cursor = 'pointer'; }); | |
| map.on('mouseleave', layer, () => { map.getCanvas().style.cursor = ''; }); | |
| }); | |
| // ── CCTV (opens CameraViewer panel) ── | |
| map.on('click', 'cctv-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| // Emit the camera data so the CameraViewer opens | |
| onEntityClick?.({ | |
| type: 'cctv', | |
| id: p.id, | |
| name: p.name, | |
| city: p.city, | |
| country: p.country, | |
| source: p.source, | |
| feed_url: p.feed_url, | |
| stream_url: p.stream_url, | |
| stream_type: p.stream_type, | |
| external_url: p.external_url, | |
| lat: coords[1], | |
| lng: coords[0], | |
| }); | |
| // Also fly to the camera | |
| map.flyTo({ center: coords, zoom: Math.max(map.getZoom(), 13), duration: 1000 }); | |
| }); | |
| // ── Earthquakes (with USGS link) ── | |
| map.on('click', 'eq-circles', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(255,149,0,0.3);"> | |
| <div style="color:#FF9500;font-size:14px;font-weight:700;margin-bottom:4px;">M${p.magnitude} EARTHQUAKE</div> | |
| <div style="font-size:9px;color:#E8E6E0;margin-bottom:8px;">${htmlEsc(p.place||'Unknown location')}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:9px;"> | |
| <div><span style="color:#5C5A54;">DEPTH</span><br/><span style="color:#E8E6E0;">${p.depth||'—'}km</span></div> | |
| <div><span style="color:#5C5A54;">COORDS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(3)}, ${coords[0].toFixed(3)}</span></div> | |
| </div> | |
| <a href="${p.source === 'NIGGG-BAS' ? 'https://ndc.niggg.bas.bg/' : `https://earthquake.usgs.gov/earthquakes/eventpage/${encodeURIComponent(p.id||'')}`}" target="_blank" style="${linkStyle}color:#FF9500;border:1px solid rgba(255,149,0,0.4);background:rgba(255,149,0,0.1);">📊 ${p.source === 'NIGGG-BAS' ? 'NIGGG-BAS' : 'USGS DETAILS'}</a> | |
| </div>`); | |
| }); | |
| // ── Satellites (SatNOGS powered) ── | |
| map.on('click', 'sat-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(212,175,55,0.3);"> | |
| <div style="color:#D4AF37;font-size:12px;font-weight:700;letter-spacing:0.1em;margin-bottom:4px;">🛰️ ${htmlEsc(p.name)}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:4px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">MISSION</span><br/><span style="color:${colorSafe(p.color)};">${htmlEsc(p.mission||'Unknown')}</span></div> | |
| <div><span style="color:#5C5A54;">ALT</span><br/><span style="color:#00E5FF;">${p.alt ? p.alt+' km' : '—'}</span></div> | |
| <div><span style="color:#5C5A54;">POS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(2)}°, ${coords[0].toFixed(2)}°</span></div> | |
| </div> | |
| ${p.noradId ? `<a href="https://www.n2yo.com/satellite/?s=${p.noradId}" target="_blank" style="display:block;text-align:center;padding:4px;margin-top:6px;font-size:8px;font-family:monospace;letter-spacing:0.1em;text-decoration:none;color:#00E5FF;border:1px solid rgba(0,229,255,0.4);background:rgba(0,229,255,0.1);border-radius:2px;cursor:pointer;">📡 TRACK ON N2YO</a>` : ''} | |
| </div>`); | |
| }); | |
| // ── Fires (with NASA FIRMS link) ── | |
| map.on('click', 'fires-heat', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(255,107,0,0.3);"> | |
| <div style="color:#FF6B00;font-size:12px;font-weight:700;margin-bottom:6px;">🔥 ACTIVE FIRE DETECTED</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">BRIGHTNESS</span><br/><span style="color:#FF6B00;">${p.brightness||'—'}K</span></div> | |
| <div><span style="color:#5C5A54;">COORDS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(3)}°, ${coords[0].toFixed(3)}°</span></div> | |
| </div> | |
| <a href="https://firms.modaps.eosdis.nasa.gov/map/#d:24hrs;l:noaa20-viirs,viirs,modis_a,modis_t;@${coords[0]},${coords[1]},10z" target="_blank" style="${linkStyle}color:#FF6B00;border:1px solid rgba(255,107,0,0.4);background:rgba(255,107,0,0.1);">🛰️ NASA FIRMS MAP</a> | |
| </div>`); | |
| }); | |
| // ── Malware Threats (Abuse.ch) ── | |
| map.on('click', 'malware-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const tType = (p.threat_type || 'MALWARE').toUpperCase(); | |
| const statusColor = p.status === 'online' ? '#39FF14' : '#FF1744'; | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(255,23,68,0.4);box-shadow:inset 0 0 12px rgba(255,23,68,0.1);"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid rgba(255,23,68,0.3);padding-bottom:6px;margin-bottom:8px;"> | |
| <div style="color:#FF1744;font-size:12px;font-weight:700;letter-spacing:0.1em;text-shadow:0 0 4px rgba(255,23,68,0.5);">[ ${htmlEsc(tType)} ]</div> | |
| <div style="color:#5C5A54;font-size:9px;">${htmlEsc(p.country || 'UNKNOWN')}</div> | |
| </div> | |
| <div style="color:#E8E6E0;font-size:11px;font-weight:bold;margin-bottom:10px;">${htmlEsc(p.malware || 'Unidentified Threat Payload')}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:9px;margin-bottom:12px;background:rgba(0,0,0,0.3);padding:6px;border-radius:4px;"> | |
| <div><span style="color:#5C5A54;">TARGET IP</span><br/><span style="color:#00E5FF;font-family:monospace;">${htmlEsc(p.ip)}</span></div> | |
| <div><span style="color:#5C5A54;">STATUS</span><br/><span style="color:${statusColor};">${(p.status||'UNKNOWN').toUpperCase()}</span></div> | |
| </div> | |
| <div style="display:flex;gap:6px;"> | |
| <a href="https://feodotracker.abuse.ch/browse/" target="_blank" style="${linkStyle}flex:1;text-align:center;color:#E8E6E0;border:1px solid rgba(255,255,255,0.2);background:rgba(255,255,255,0.05);">THREAT INTEL ↗</a> | |
| </div> | |
| <button onclick="window.openOsirisIntel({ type: 'ip', ip: '${idSafe(p.ip)}', threat_type: '${idSafe(p.malware || p.threat_type || '')}', status: '${idSafe(p.status || '')}' })" style="width:100%;margin-top:8px;padding:8px 12px;background:linear-gradient(90deg, rgba(255,23,68,0.1) 0%, rgba(255,23,68,0.2) 100%);border:1px solid rgba(255,23,68,0.6);color:#FF1744;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:bold;letter-spacing:0.15em;border-radius:4px;cursor:pointer;transition:all 0.2s;">DEEP DIVE ANALYTICS</button> | |
| </div>`); | |
| }); | |
| // ── GDELT Conflicts (with source article) ── | |
| map.on('click', 'gdelt-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| // Map coordinates to Liveuamap regions | |
| let sourceUrl = p.url || ''; | |
| if (!sourceUrl || sourceUrl.includes('google.com')) { | |
| const [lng, lat] = coords; | |
| if (lat > 44 && lat < 53 && lng > 22 && lng < 40) sourceUrl = 'https://liveuamap.com/'; // Ukraine | |
| else if (lat > 30 && lat < 33 && lng > 34 && lng < 36) sourceUrl = 'https://israelpalestine.liveuamap.com/'; // Gaza | |
| else if (lat > 33 && lat < 34.5 && lng > 35 && lng < 36.5) sourceUrl = 'https://lebanon.liveuamap.com/'; // Lebanon | |
| else if (lat > 32 && lat < 37 && lng > 35 && lng < 42) sourceUrl = 'https://syria.liveuamap.com/'; // Syria | |
| else if (lat > 10 && lat < 22 && lng > 22 && lng < 38) sourceUrl = 'https://sudan.liveuamap.com/'; // Sudan | |
| else if (lat > 12 && lat < 20 && lng > 42 && lng < 55) sourceUrl = 'https://yemen.liveuamap.com/'; // Yemen | |
| else sourceUrl = 'https://liveuamap.com/'; // Global fallback | |
| } | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(255,61,61,0.3);"> | |
| <div style="color:#FF3D3D;font-size:12px;font-weight:700;margin-bottom:6px;">⚠️ CONFLICT EVENT</div> | |
| <div style="font-size:9px;color:#E8E6E0;margin-bottom:8px;line-height:1.4;">${htmlEsc(p.name||'Unclassified incident')}</div> | |
| <a href="${urlSafe(sourceUrl)}" target="_blank" style="${linkStyle}flex:1;text-align:center;color:#FF3D3D;border:1px solid rgba(255,61,61,0.4);background:rgba(255,61,61,0.15);display:inline-block;width:100%;box-sizing:border-box;margin-top:4px;">[ OPEN SOURCE ↗ ]</a> | |
| </div>`); | |
| }); | |
| // ── Global Event / Conflict Markers ── | |
| map.on('click', 'conflict-icons', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const color = p.severity === 'war' ? '#FF1744' : p.severity === 'high' ? '#FF9500' : '#FFD500'; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${color}40;"> | |
| <div style="color:${color};font-size:12px;font-weight:700;margin-bottom:6px;">⚠️ ${htmlEsc(p.label || 'WARNING EVENT')}</div> | |
| <div style="font-size:10px;color:#E8E6E0;margin-bottom:8px;line-height:1.4;">${htmlEsc(p.description || 'Global event detected at this location.')}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">SEVERITY</span><br/><span style="color:${color};">${(p.severity||'unknown').toUpperCase()}</span></div> | |
| <div><span style="color:#5C5A54;">COORDS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(3)}°, ${coords[0].toFixed(3)}°</span></div> | |
| </div> | |
| ${p.sourceUrl ? `<a href="${urlSafe(p.sourceUrl)}" target="_blank" style="${linkStyle}flex:1;text-align:center;color:${color};border:1px solid ${color}40;background:${color}15;display:inline-block;width:100%;box-sizing:border-box;margin-top:4px;">[ OPEN SOURCE ↗ ]</a>` : ''} | |
| </div>`); | |
| }); | |
| // ── OSIRIS SDK link click ── | |
| const SDK_SOURCE_URLS: Record<string, string> = { | |
| 'AIS Maritime': 'https://www.marinetraffic.com', | |
| 'AIS Stream': 'https://aisstream.io', | |
| 'AIS → Lattice': 'https://aisstream.io', | |
| 'ADS-B / OpenSky': 'https://opensky-network.org', | |
| 'ADS-B → Lattice': 'https://opensky-network.org', | |
| 'Naval Intelligence': 'https://www.odni.gov', | |
| }; | |
| ['sdk-sea','sdk-sea-glow','sdk-air','sdk-air-glow','sdk-intel','sdk-intel-glow'].forEach(layer => { | |
| map.on('click', layer, e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = e.lngLat; | |
| const srcUrl = p.url || SDK_SOURCE_URLS[p.source] || 'https://osirisai.live'; | |
| const domainLabel = p.domain === 'SEA' ? '⚓ MARITIME' : p.domain === 'AIR' ? '✈ AIR CORRIDOR' : '🛡 NAVAL INTEL'; | |
| const domainColor = p.domain === 'SEA' ? '#4FC3F7' : p.domain === 'AIR' ? '#B3E5FC' : '#81D4FA'; | |
| const linkStyle = 'text-decoration:none;padding:3px 8px;border-radius:4px;font-size:9px;font-weight:700;letter-spacing:0.05em;'; | |
| popup([coords.lng, coords.lat], `<div style="${pStyle}border:1px solid ${domainColor}40;"> | |
| <div style="display:flex;align-items:center;gap:6px;margin-bottom:8px;"> | |
| <div style="width:8px;height:8px;border-radius:50%;background:${domainColor};box-shadow:0 0 8px ${domainColor};"></div> | |
| <span style="color:${domainColor};font-size:11px;font-weight:700;letter-spacing:0.1em;">${domainLabel}</span> | |
| </div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">FROM</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.fromName || 'Origin')}</span></div> | |
| <div><span style="color:#5C5A54;">TO</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.toName || 'Destination')}</span></div> | |
| <div><span style="color:#5C5A54;">DOMAIN</span><br/><span style="color:${domainColor};">${p.domain}</span></div> | |
| <div><span style="color:#5C5A54;">SOURCE</span><br/><a href="${urlSafe(srcUrl)}" target="_blank" style="color:${domainColor};text-decoration:underline;cursor:pointer;">${htmlEsc(p.source || 'OSIRIS')}</a></div> | |
| </div> | |
| <a href="${urlSafe(srcUrl)}" target="_blank" style="${linkStyle}color:${domainColor};border:1px solid ${domainColor}40;background:${domainColor}18;display:inline-block;margin-top:4px;">OPEN SOURCE ↗</a> | |
| </div>`); | |
| }); | |
| }); | |
| // ── Generic hover for clickables ── | |
| ['conflict-icons','cctv-dots','eq-circles','sat-dots','fires-heat','gdelt-dots','weather-dots','infra-dots','maritime-dots','choke-dots','news-dots','sigint-news-dots','balloon-dots','rad-dots','ship-dots','sweep-device-dots','scan-targets-dots','sdk-sea','sdk-sea-glow','sdk-sea-atmo','sdk-air','sdk-air-glow','sdk-air-atmo','sdk-intel','sdk-intel-glow','sdk-intel-atmo','malware-dots','mining-dots','ram-dots'].forEach(layer => { | |
| map.on('mouseenter', layer, () => { map.getCanvas().style.cursor = 'pointer'; }); | |
| map.on('mouseleave', layer, () => { map.getCanvas().style.cursor = ''; }); | |
| }); | |
| // ── Scan Targets click ── | |
| map.on('click', 'scan-targets-dots', (e: any) => { | |
| const p = e.features?.[0]?.properties; | |
| if (!p) return; | |
| const coords = e.features[0].geometry.coordinates.slice(); | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(255,61,61,0.5);"> | |
| <div style="color:#FF3D3D;font-size:12px;font-weight:700;margin-bottom:6px;">🎯 TARGET: ${htmlEsc(p.id)}</div> | |
| <div style="font-size:9px;color:#E8E6E0;margin-bottom:8px;">${htmlEsc(p.city || 'Unknown')}, ${htmlEsc(p.country || 'Unknown')} — ${htmlEsc(p.isp || 'Unknown ISP')}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:9px;"> | |
| <div><span style="color:#5C5A54;">TYPE</span><br/><span style="color:#00E5FF;">${(p.type || 'UNKNOWN').toUpperCase()}</span></div> | |
| <div><span style="color:#5C5A54;">COORDS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(3)}°, ${coords[0].toFixed(3)}°</span></div> | |
| </div> | |
| <button onclick="window.openOsirisIntel({ type: 'ip', ip: '${idSafe(p.id)}' })" style="width:100%;margin-top:8px;padding:6px 12px;background:rgba(255,109,0,0.15);border:1px solid rgba(255,109,0,0.5);color:#FF6D00;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:bold;letter-spacing:0.1em;border-radius:4px;cursor:pointer;">[ IP INTEL DEEP DIVE ]</button> | |
| </div>`); | |
| }); | |
| // ── SCM Suppliers ── | |
| map.on('click', 'scm-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const color = p.risk_level === 'CRITICAL' ? '#FF1744' : p.risk_level === 'HIGH' ? '#FF9500' : '#00BCD4'; | |
| const activeThreats = p.active_threats ? JSON.parse(p.active_threats) : []; | |
| let threatsHtml = ''; | |
| if (activeThreats.length > 0) { | |
| threatsHtml = `<div style="margin-top:8px;padding-top:6px;border-top:1px solid ${color}40;color:${color};font-size:9px;font-weight:bold;"> | |
| ACTIVE THREATS:<br/>${activeThreats.map((t: string) => `⚠ ${htmlEsc(t)}`).join('<br/>')} | |
| </div>`; | |
| } | |
| popup(coords, `<div style="${pStyle}border:1px solid ${color}40;"> | |
| <div style="color:${color};font-size:12px;font-weight:700;margin-bottom:4px;">🏢 ${htmlEsc(p.name)}</div> | |
| <div style="font-size:9px;color:#aaa;margin-bottom:8px;">${htmlEsc(p.category)} | ${htmlEsc(p.city)}, ${htmlEsc(p.country)}</div> | |
| <div style="display:grid;grid-template-columns:1fr;gap:4px;font-size:11px;"> | |
| <div><span style="color:#5C5A54;font-size:9px;">SCM RISK LEVEL</span><br/><span style="color:${color};font-weight:bold;">${p.risk_level}</span></div> | |
| </div> | |
| ${threatsHtml} | |
| </div>`); | |
| }); | |
| // ── Mining Companies ── | |
| const MINING_COLORS: Record<string, string> = { copper: '#FF8A65', gold: '#FFD54F', silver: '#CFD8DC', pgm: '#4DB6AC', rare_earth: '#BA68C8', specialty: '#FFB74D' }; | |
| map.on('click', 'mining-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const color = MINING_COLORS[p.category] || '#FFB74D'; | |
| const commodities = p.commodities ? JSON.parse(p.commodities) : []; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${color}40;"> | |
| <div style="color:${color};font-size:12px;font-weight:700;margin-bottom:4px;">⛏️ ${htmlEsc(p.company)}</div> | |
| <div style="font-size:9px;color:#aaa;margin-bottom:8px;">${htmlEsc(p.site)} — ${htmlEsc(p.country)}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">CATEGORY</span><br/><span style="color:${color};font-weight:bold;">${htmlEsc((p.category || '').toUpperCase().replace('_',' '))}</span></div> | |
| <div><span style="color:#5C5A54;">TICKER</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.ticker || '—')}${p.exchange ? ' · ' + htmlEsc(p.exchange) : ''}</span></div> | |
| <div><span style="color:#5C5A54;">SITE TYPE</span><br/><span style="color:#E8E6E0;">${htmlEsc((p.site_type || 'mine').toUpperCase())}</span></div> | |
| <div><span style="color:#5C5A54;">HQ</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.hq || '—')}</span></div> | |
| </div> | |
| <div style="font-size:9px;color:#8A8880;margin-bottom:6px;">COMMODITIES: <span style="color:${color};">${commodities.map((c: string) => htmlEsc(c)).join(', ')}</span></div> | |
| ${p.notes ? `<div style="font-size:9px;color:#aaa;line-height:1.4;margin-bottom:6px;">${htmlEsc(p.notes)}</div>` : ''} | |
| <a href="https://www.google.com/maps/@${coords[1]},${coords[0]},13z/data=!3m1!1e3" target="_blank" style="${linkStyle}color:${color};border:1px solid ${color}40;background:${color}15;">SATELLITE VIEW</a> | |
| </div>`); | |
| }); | |
| // ── Memory / RAM Fabs ── | |
| const RAM_COLORS: Record<string, string> = { dram_hbm: '#4FC3F7', nand: '#81C784', controller_ip: '#FFF176', module_maker: '#90A4AE', packaging_test: '#F06292', equipment: '#9575CD' }; | |
| map.on('click', 'ram-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const color = RAM_COLORS[p.category] || '#4FC3F7'; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${color}40;"> | |
| <div style="color:${color};font-size:12px;font-weight:700;margin-bottom:4px;">💾 ${htmlEsc(p.company)}</div> | |
| <div style="font-size:9px;color:#aaa;margin-bottom:8px;">${htmlEsc(p.site)} — ${htmlEsc(p.country)}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">ROLE</span><br/><span style="color:${color};font-weight:bold;">${htmlEsc((p.category || '').toUpperCase().replace(/_/g,' '))}</span></div> | |
| <div><span style="color:#5C5A54;">TICKER</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.ticker || '—')}${p.exchange ? ' · ' + htmlEsc(p.exchange) : ''}</span></div> | |
| <div><span style="color:#5C5A54;">SITE TYPE</span><br/><span style="color:#E8E6E0;">${htmlEsc((p.site_type || 'fab').toUpperCase())}</span></div> | |
| <div><span style="color:#5C5A54;">HQ</span><br/><span style="color:#E8E6E0;">${htmlEsc(p.hq || '—')}</span></div> | |
| </div> | |
| ${p.notes ? `<div style="font-size:9px;color:#aaa;line-height:1.4;">${htmlEsc(p.notes)}</div>` : ''} | |
| </div>`); | |
| }); | |
| // ── IP Sweep device click ── | |
| map.on('click', 'sweep-device-dots', (e: any) => { | |
| const p = e.features?.[0]?.properties; | |
| if (!p) return; | |
| const coords = e.features[0].geometry.coordinates.slice(); | |
| const ports = JSON.parse(p.ports || '[]'); | |
| const vulns = JSON.parse(p.vulns || '[]'); | |
| const hostnames = JSON.parse(p.hostnames || '[]'); | |
| const riskColors: Record<string, string> = { CRITICAL: '#FF3D3D', HIGH: '#FF6B00', MEDIUM: '#FFD700', LOW: '#76FF03', INFO: '#5C5A54' }; | |
| popup(coords, `<div style="font-family:monospace;font-size:11px;color:#E8E6E0;"> | |
| <div style="font-size:13px;font-weight:bold;margin-bottom:6px;color:${p.color};">${p.device_type}</div> | |
| <div style="font-size:12px;margin-bottom:8px;color:#fff;">${p.ip}</div> | |
| ${hostnames.length > 0 ? `<div style="font-size:9px;color:#8A8880;margin-bottom:6px;">${hostnames.join(', ')}</div>` : ''} | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">PORTS</span><br/><span style="color:#E8E6E0;">${ports.length}</span></div> | |
| <div><span style="color:#5C5A54;">RISK</span><br/><span style="color:${riskColors[p.risk_level] || '#666'};">${p.risk_level}</span></div> | |
| </div> | |
| <div style="font-size:9px;color:#8A8880;margin-bottom:6px;">Open: ${ports.slice(0, 12).join(', ')}${ports.length > 12 ? ' ...' : ''}</div> | |
| ${vulns.length > 0 ? `<div style="font-size:9px;color:#FF3D3D;margin-bottom:6px;">⚠ CVEs: ${vulns.slice(0, 5).join(', ')}${vulns.length > 5 ? ` +${vulns.length - 5} more` : ''}</div>` : ''} | |
| <button onclick="window.openOsirisIntel({ type: 'ip', ip: '${p.ip}' })" style="width:100%;margin-top:6px;padding:6px 12px;background:rgba(255,109,0,0.15);border:1px solid rgba(255,109,0,0.5);color:#FF6D00;font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:bold;letter-spacing:0.1em;border-radius:4px;cursor:pointer;">[ IP INTEL DEEP DIVE ]</button> | |
| </div>`); | |
| }); | |
| // ── Balloons / Sondes ── | |
| map.on('click', 'balloon-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${p.color}40;"> | |
| <div style="color:${p.color};font-size:12px;font-weight:700;letter-spacing:0.1em;margin-bottom:4px;">🎈 ${p.callsign}</div> | |
| <div style="font-size:9px;color:#aaa;margin-bottom:8px;">${p.type.toUpperCase()} / STATUS: ${p.status.toUpperCase()}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:9px;"> | |
| <div><span style="color:#5C5A54;">ALTITUDE</span><br/><span style="color:#E8E6E0;">${p.altitude} m</span></div> | |
| <div><span style="color:#5C5A54;">SPEED</span><br/><span style="color:#E8E6E0;">${Math.round(p.speed)} km/h</span></div> | |
| <div><span style="color:#5C5A54;">VERT RATE</span><br/><span style="color:${p.verticalRate > 0 ? '#00E676' : '#FF3D3D'};">${p.verticalRate.toFixed(1)} m/s</span></div> | |
| <div><span style="color:#5C5A54;">TEMP</span><br/><span style="color:#E8E6E0;">${p.temperature}°C</span></div> | |
| </div> | |
| </div>`); | |
| }); | |
| // ── Radiation ── | |
| map.on('click', 'rad-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const color = p.status === 'DANGER' ? '#FF1744' : p.status === 'WARNING' ? '#FF9500' : '#AB47BC'; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${color}40;"> | |
| <div style="color:${color};font-size:12px;font-weight:700;margin-bottom:4px;">☢️ ${p.name}</div> | |
| <div style="font-size:9px;color:#aaa;margin-bottom:8px;">${p.city}, ${p.country}</div> | |
| <div style="display:grid;grid-template-columns:1fr;gap:4px;font-size:11px;"> | |
| <div><span style="color:#5C5A54;font-size:9px;">READING</span><br/><span style="color:${color};font-weight:bold;">${p.reading} nSv/h</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">STATUS</span><br/><span style="color:${color};">${p.status}</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">NETWORK</span><br/><span style="color:#E8E6E0;">${p.network}</span></div> | |
| </div> | |
| </div>`); | |
| }); | |
| // ── Maritime Ships ── | |
| map.on('click', 'ship-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const color = p.type === 'military' ? '#FF1744' : p.type === 'tanker' ? '#FF9500' : '#00E5FF'; | |
| const icon = p.type === 'military' ? '⚔️' : p.type === 'tanker' ? '🛢️' : '🚢'; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${color}60;box-shadow:inset 0 0 12px ${color}15;"> | |
| <div style="display:flex;justify-content:space-between;align-items:center;border-bottom:1px solid ${color}40;padding-bottom:6px;margin-bottom:8px;"> | |
| <div style="color:${color};font-size:12px;font-weight:700;letter-spacing:0.1em;">${icon} [ ${(p.type||'VESSEL').toUpperCase()} ]</div> | |
| <div style="color:#5C5A54;font-size:9px;">FLAG: ${p.flag||'UNK'}</div> | |
| </div> | |
| <div style="color:#E8E6E0;font-size:11px;font-weight:bold;margin-bottom:10px;">${p.name || 'UNIDENTIFIED VESSEL'}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:9px;margin-bottom:8px;background:rgba(0,0,0,0.3);padding:6px;border-radius:4px;"> | |
| <div><span style="color:#5C5A54;">SPEED</span><br/><span style="color:${color};font-family:monospace;">${Number(p.speed).toFixed(1)} kn</span></div> | |
| <div><span style="color:#5C5A54;">HEADING</span><br/><span style="color:${color};font-family:monospace;">${Number(p.heading).toFixed(0)}°</span></div> | |
| <div><span style="color:#5C5A54;">LATITUDE</span><br/><span style="color:#E8E6E0;font-family:monospace;">${coords[1].toFixed(4)}°</span></div> | |
| <div><span style="color:#5C5A54;">LONGITUDE</span><br/><span style="color:#E8E6E0;font-family:monospace;">${coords[0].toFixed(4)}°</span></div> | |
| </div> | |
| <div><span style="color:#5C5A54;font-size:9px;">DESTINATION: </span><span style="color:#E8E6E0;font-size:9px;">${p.destination || 'UNKNOWN'}</span></div> | |
| <a href="https://www.marinetraffic.com/en/ais/details/ships/mmsi:${p.mmsi}" target="_blank" style="${linkStyle}flex:1;text-align:center;color:${color};border:1px solid ${color}40;background:${color}15;display:inline-block;width:100%;box-sizing:border-box;margin-top:4px;">[ OPEN SOURCE ↗ ]</a> | |
| </div>`); | |
| }); | |
| // ── Weather Events (NASA EONET + NOAA/NWS + GDACS) ── | |
| map.on('click', 'weather-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const iconEmoji = p.icon === 'cyclone' ? '🌀' : p.icon === 'volcano' ? '🌋' : p.icon === 'flood' ? '🌊' : p.icon === 'drought' ? '🏜️' : p.icon === 'ice' ? '🧊' : p.icon === 'weather' ? '⚠️' : '⚡'; | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(224,64,251,0.3);"> | |
| <div style="color:#E040FB;font-size:14px;font-weight:700;margin-bottom:6px;">${iconEmoji} ${p.type || 'Weather Event'}</div> | |
| <div style="font-size:10px;color:#E8E6E0;margin-bottom:8px;line-height:1.4;">${p.title || 'Unknown event'}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">SEVERITY</span><br/><span style="color:${p.severity === 'high' ? '#FF1744' : '#FFD700'};">${(p.severity||'low').toUpperCase()}</span></div> | |
| <div><span style="color:#5C5A54;">COORDS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(3)}°, ${coords[0].toFixed(3)}°</span></div> | |
| </div> | |
| <div style="display:flex;gap:6px;"> | |
| ${p.source ? `<a href="${p.source}" target="_blank" style="${linkStyle}color:#E040FB;border:1px solid rgba(224,64,251,0.4);background:rgba(224,64,251,0.1);">📡 SOURCE</a>` : ''} | |
| </div> | |
| </div>`); | |
| }); | |
| // ── Nuclear Infrastructure ── | |
| map.on('click', 'infra-dots', e => { | |
| if (!e.features?.length) return; | |
| const p = e.features[0].properties as any; | |
| const coords = (e.features[0].geometry as any).coordinates; | |
| const statusColor = p.status.includes('SEISMIC RISK') ? '#FF9500' : p.status === 'Active Conflict Zone' ? '#FF1744' : p.status === 'Operational' ? '#76FF03' : '#757575'; | |
| popup(coords, `<div style="${pStyle}border:1px solid rgba(118,255,3,0.3);"> | |
| <div style="color:#76FF03;font-size:14px;font-weight:700;margin-bottom:4px;">☢️ ${p.name || 'Nuclear Facility'}</div> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;font-size:9px;margin-bottom:8px;"> | |
| <div><span style="color:#5C5A54;">STATUS</span><br/><span style="color:${statusColor};">${p.status || '—'}</span></div> | |
| <div><span style="color:#5C5A54;">CITY</span><br/><span style="color:#E8E6E0;">${p.city || '—'}, ${p.country || ''}</span></div> | |
| <div><span style="color:#5C5A54;">REACTORS</span><br/><span style="color:#76FF03;">${p.reactors || '—'}</span></div> | |
| <div><span style="color:#5C5A54;">CAPACITY</span><br/><span style="color:#E8E6E0;">${p.capacityMW ? p.capacityMW.toLocaleString() + ' MW' : '—'}</span></div> | |
| <div><span style="color:#5C5A54;">OWNER</span><br/><span style="color:#E8E6E0;">${p.owner || '—'}</span></div> | |
| <div><span style="color:#5C5A54;">COORDS</span><br/><span style="color:#E8E6E0;">${coords[1].toFixed(3)}°, ${coords[0].toFixed(3)}°</span></div> | |
| </div> | |
| <a href="https://www.google.com/maps/@${coords[1]},${coords[0]},14z/data=!3m1!1e3" target="_blank" style="${linkStyle}color:#76FF03;border:1px solid rgba(118,255,3,0.4);background:rgba(118,255,3,0.1);">SATELLITE VIEW</a> | |
| </div>`); | |
| }); | |
| // ── Maritime Ports & Naval Bases ── | |
| map.on('click', 'maritime-dots', e => { | |
| const p = e.features?.[0]?.properties; | |
| if (!p) return; | |
| const coords = (e.features![0].geometry as any).coordinates; | |
| const typeColor = p.type === 'naval' ? '#FF3D3D' : p.type === 'energy' ? '#FF9500' : '#00BCD4'; | |
| const typeLabel = p.type === 'naval' ? 'NAVAL BASE' : p.type === 'energy' ? 'ENERGY PORT' : 'CONTAINER PORT'; | |
| const congestionHtml = p.congestion ? ` | |
| <div style="margin-top:8px;padding-top:6px;border-top:1px solid rgba(255,255,255,0.1);"> | |
| <div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;"> | |
| <div><span style="color:#5C5A54;font-size:9px;">CONGESTION</span><br/><span style="color:${p.congestion === 'SEVERE' ? '#FF1744' : p.congestion === 'CONGESTED' ? '#FF9500' : '#00E676'};font-weight:bold;font-size:10px;">${p.congestion}</span></div> | |
| <div><span style="color:#5C5A54;font-size:9px;">EST. DWELL TIME</span><br/><span style="color:#E8E6E0;font-weight:bold;font-size:10px;">${p.dwell_time || 'Unknown'}</span></div> | |
| </div> | |
| </div>` : ''; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${typeColor}40;"> | |
| <div style="color:${typeColor};font-weight:bold;font-size:11px;margin-bottom:4px;">${p.name}</div> | |
| <div style="color:#999;font-size:9px;margin-bottom:6px;">${typeLabel} — ${p.country}</div> | |
| ${p.volume ? `<div style="font-size:9px;color:#aaa;">Volume: <span style="color:${typeColor};font-weight:bold;">${p.volume}</span></div>` : ''} | |
| ${p.fleet ? `<div style="font-size:9px;color:#aaa;">Fleet: <span style="color:${typeColor};font-weight:bold;">${p.fleet}</span></div>` : ''} | |
| ${p.rank ? `<div style="font-size:9px;color:#aaa;">Global Rank: <span style="color:${typeColor};font-weight:bold;">#${p.rank}</span></div>` : ''} | |
| ${congestionHtml} | |
| </div>`); | |
| }); | |
| // ── Maritime Chokepoints ── | |
| map.on('click', 'choke-dots', e => { | |
| const p = e.features?.[0]?.properties; | |
| if (!p) return; | |
| const coords = (e.features![0].geometry as any).coordinates; | |
| const riskCol = p.risk === 'CRITICAL' ? '#FF1744' : p.risk === 'HIGH' ? '#FF9500' : p.risk === 'ELEVATED' ? '#FFD700' : '#00E676'; | |
| popup(coords, `<div style="${pStyle}border:1px solid ${riskCol}40;"> | |
| <div style="color:#FF9500;font-weight:bold;font-size:11px;margin-bottom:4px;">${p.name}</div> | |
| <div style="font-size:9px;color:#aaa;">Traffic: <span style="color:#fff;">${p.traffic}</span></div> | |
| <div style="font-size:9px;color:#aaa;">Risk: <span style="color:${riskCol};font-weight:bold;">${p.risk}</span></div> | |
| </div>`); | |
| }); | |
| // ── Live News (opens feed viewer) ── | |
| map.on('click', 'news-dots', e => { | |
| const p = e.features?.[0]?.properties; | |
| if (!p) return; | |
| onEntityClick?.({ | |
| type: 'live_news', | |
| name: p.name, | |
| city: p.city, | |
| country: p.country, | |
| url: p.url, | |
| category: p.category, | |
| embed_allowed: p.embed_allowed !== false && p.embed_allowed !== 'false', | |
| }); | |
| }); | |
| return () => { map.remove(); mapRef.current = null; }; | |
| }, []); | |
| // Day/Night | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current) return; | |
| const map = mapRef.current; | |
| const update = () => { | |
| const src = map.getSource('day-night') as any; | |
| if (!src) return; | |
| if (!activeLayers.day_night) { src.setData(EMPTY_FC); return; } | |
| src.setData({ type: 'FeatureCollection', features: [{ type: 'Feature', geometry: { type: 'Polygon', coordinates: [computeSolarTerminator()] }, properties: {} }] }); | |
| }; | |
| update(); | |
| const iv = setInterval(update, 300000); // 5 min (was 1 min — shadow barely moves) | |
| return () => clearInterval(iv); | |
| }, [mapReady, activeLayers.day_night]); | |
| // Helper to set GeoJSON | |
| const setGeo = useCallback((source: string, features: any[]) => { | |
| const src = mapRef.current?.getSource(source) as any; | |
| if (src) src.setData({ type: 'FeatureCollection', features }); | |
| }, []); | |
| const setVis = useCallback((ids: string[], visible: boolean) => { | |
| const map = mapRef.current; | |
| if (!map) return; | |
| ids.forEach(id => { if (map.getLayer(id)) map.setLayoutProperty(id, 'visibility', visible ? 'visible' : 'none'); }); | |
| }, []); | |
| // Flight data → GeoJSON (GPU rendered) | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| const toFeatures = (arr: any[], decimate: number = 1) => { | |
| let filtered = arr || []; | |
| if (decimate > 1) { | |
| filtered = filtered.filter((_, i) => i % decimate === 0); | |
| } | |
| return filtered.map((f: any) => ({ | |
| type: 'Feature' as const, geometry: { type: 'Point' as const, coordinates: [f.lng, f.lat] }, | |
| properties: { callsign: f.callsign, heading: f.heading || 0, alt: f.alt, model: f.model, speed_knots: f.speed_knots, registration: f.registration, icao24: f.icao24 }, | |
| })); | |
| }; | |
| setGeo('flights', activeLayers.flights ? toFeatures(data.commercial_flights, 10) : []); | |
| setGeo('private-fl', activeLayers.private ? toFeatures(data.private_flights, 2) : []); | |
| setGeo('jets', activeLayers.jets ? toFeatures(data.private_jets, 2) : []); | |
| setGeo('military', activeLayers.military ? toFeatures(data.military_flights) : []); | |
| }, [mapReady, data.commercial_flights, data.private_flights, data.private_jets, data.military_flights, activeLayers.flights, activeLayers.private, activeLayers.jets, activeLayers.military]); | |
| // Update aircraft icon colors dynamically on theme switch | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current) return; | |
| const map = mapRef.current; | |
| const isGhost = theme === 'ghost'; | |
| const phantomPurple = '#B388FF'; | |
| const ghostPriv = '#CE93D8'; | |
| const ghostGov = '#D500F9'; | |
| const flightCom = isGhost ? phantomPurple : '#00E5FF'; | |
| const flightPriv = isGhost ? ghostPriv : '#FFD700'; | |
| const flightGov = isGhost ? ghostGov : '#FF9500'; | |
| const flightMil = '#FF0000'; | |
| const updateMapIcon = (id: string, color: string, size: number) => { | |
| if (!map.hasImage(id)) return; | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = size; canvas.height = size; | |
| const ctx = canvas.getContext('2d')!; | |
| const cx = size / 2, cy = size / 2; | |
| ctx.fillStyle = color; | |
| ctx.beginPath(); | |
| ctx.moveTo(cx, cy - size * 0.4); | |
| ctx.lineTo(cx - size * 0.12, cy + size * 0.1); | |
| ctx.lineTo(cx - size * 0.4, cy + size * 0.2); | |
| ctx.lineTo(cx - size * 0.4, cy + size * 0.3); | |
| ctx.lineTo(cx - size * 0.12, cy + size * 0.15); | |
| ctx.lineTo(cx, cy + size * 0.35); | |
| ctx.lineTo(cx + size * 0.12, cy + size * 0.15); | |
| ctx.lineTo(cx + size * 0.4, cy + size * 0.3); | |
| ctx.lineTo(cx + size * 0.4, cy + size * 0.2); | |
| ctx.lineTo(cx + size * 0.12, cy + size * 0.1); | |
| ctx.closePath(); | |
| ctx.fill(); | |
| map.updateImage(id, { width: size, height: size, data: new Uint8Array(ctx.getImageData(0, 0, size, size).data) }); | |
| }; | |
| updateMapIcon('plane-cyan', flightCom, 24); | |
| updateMapIcon('plane-green', flightPriv, 24); | |
| updateMapIcon('plane-pink', flightGov, 24); | |
| updateMapIcon('plane-red', flightMil, 24); | |
| updateMapIcon('plane-grey', isGhost ? phantomPurple : '#546E7A', 24); | |
| }, [mapReady, theme]); | |
| // ── DECOUPLED LAYER RENDERERS (Performance Optimized) ── | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('earthquakes', activeLayers.earthquakes && data.earthquakes ? data.earthquakes.map((eq: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [eq.lng, eq.lat] }, properties: { magnitude: eq.magnitude, place: eq.place } })) : []); | |
| }, [mapReady, data.earthquakes, activeLayers.earthquakes, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| const sats = data.satellites || []; | |
| const al = activeLayers as any; | |
| // If 'All Satellites' is on, show everything | |
| if (al.satellites) { | |
| setGeo('satellites', sats.map((s: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [s.lng, s.lat] }, properties: { name: s.name, color: s.color, mission: s.mission, alt: s.alt, noradId: s.noradId, category: s.category } }))); | |
| return; | |
| } | |
| // Otherwise filter by enabled sub-layers | |
| const enabledCategories: string[] = []; | |
| if (al.sat_comms) enabledCategories.push('comms'); | |
| if (al.sat_military) enabledCategories.push('military'); | |
| if (al.sat_navigation) enabledCategories.push('navigation'); | |
| if (al.sat_earth) enabledCategories.push('earth_obs'); | |
| if (al.sat_science) enabledCategories.push('science'); | |
| if (enabledCategories.length === 0) { | |
| setGeo('satellites', []); | |
| return; | |
| } | |
| const filtered = sats.filter((s: any) => enabledCategories.includes(s.category)); | |
| setGeo('satellites', filtered.map((s: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [s.lng, s.lat] }, properties: { name: s.name, color: s.color, mission: s.mission, alt: s.alt, noradId: s.noradId, category: s.category } }))); | |
| }, [mapReady, data.satellites, activeLayers.satellites, (activeLayers as any).sat_comms, (activeLayers as any).sat_military, (activeLayers as any).sat_navigation, (activeLayers as any).sat_earth, (activeLayers as any).sat_science, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('gdelt', activeLayers.global_incidents && data.gdelt ? data.gdelt.map((e: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [e.lng, e.lat] }, properties: { name: e.name } })) : []); | |
| }, [mapReady, data.gdelt, activeLayers.global_incidents, setGeo]); | |
| // Malware Threats | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('malware-nodes', activeLayers.malware && data.malware_threats ? data.malware_threats.map((t: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [t.lng, t.lat] }, properties: { ip: t.ip, malware: t.malware, status: t.status, threat_type: t.threat_type, country: t.country } })) : []); | |
| }, [mapReady, data.malware_threats, activeLayers.malware, setGeo]); | |
| // Network Mesh Generation (Nearest Neighbor Lattice) | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| const meshLinks: any[] = []; | |
| // Generate Malware Botnet Mesh | |
| if (activeLayers.malware && data.malware_threats && data.malware_threats.length > 1) { | |
| const nodes = data.malware_threats; | |
| for (let i = 0; i < nodes.length; i++) { | |
| // Connect each to next 2 for a global web | |
| for (let j = 1; j <= 2; j++) { | |
| const target = nodes[(i + j) % nodes.length]; | |
| meshLinks.push({ | |
| type: 'Feature', | |
| geometry: { type: 'LineString', coordinates: [[nodes[i].lng, nodes[i].lat], [target.lng, target.lat]] }, | |
| properties: { threat_type: 'malware' } | |
| }); | |
| } | |
| } | |
| } | |
| setGeo('network-mesh', meshLinks); | |
| }, [mapReady, activeLayers.malware, data.malware_threats, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('gps-jamming', activeLayers.gps_jamming && data.gps_jamming ? data.gps_jamming.map((z: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [z.lng, z.lat] }, properties: { severity: z.severity } })) : []); | |
| }, [mapReady, data.gps_jamming, activeLayers.gps_jamming, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('cctv', activeLayers.cctv && data.cameras ? data.cameras.map((c: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [c.lng, c.lat] }, properties: { id: c.id, name: c.name, city: c.city, country: c.country, source: c.source, feed_url: c.feed_url, stream_url: c.stream_url, stream_type: c.stream_type, external_url: c.external_url } })) : []); | |
| }, [mapReady, data.cameras, activeLayers.cctv, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('fires', activeLayers.fires && data.fires ? data.fires.map((f: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [f.lng, f.lat] }, properties: { brightness: f.brightness } })) : []); | |
| }, [mapReady, data.fires, activeLayers.fires, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('weather', activeLayers.weather && data.weather_events ? data.weather_events.map((w: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [w.lng, w.lat] }, properties: { title: w.title, type: w.type, icon: w.icon, severity: w.severity, source: w.source, id: w.id } })) : []); | |
| }, [mapReady, data.weather_events, activeLayers.weather, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('infrastructure', activeLayers.infrastructure && data.infrastructure ? data.infrastructure.map((i: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [i.lng, i.lat] }, properties: { name: i.name, city: i.city, country: i.country, status: i.status, reactors: i.reactors, capacityMW: i.capacityMW, owner: i.owner } })) : []); | |
| }, [mapReady, data.infrastructure, activeLayers.infrastructure, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('mining-sites', activeLayers.mining && data.mining_companies ? data.mining_companies.map((m: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [m.lng, m.lat] }, properties: { company: m.company, site: m.site, site_type: m.site_type, country: m.country, category: m.category, commodities: m.commodities, ticker: m.ticker, exchange: m.exchange, hq: m.hq, notes: m.notes } })) : []); | |
| }, [mapReady, data.mining_companies, activeLayers.mining, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('ram-sites', activeLayers.ram_fabs && data.ram_companies ? data.ram_companies.map((m: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [m.lng, m.lat] }, properties: { company: m.company, site: m.site, site_type: m.site_type, country: m.country, category: m.category, commodities: m.commodities, ticker: m.ticker, exchange: m.exchange, hq: m.hq, notes: m.notes } })) : []); | |
| }, [mapReady, data.ram_companies, activeLayers.ram_fabs, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('maritime', activeLayers.maritime && data.maritime_ports ? data.maritime_ports.map((p: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [p.lng, p.lat] }, properties: { name: p.name, country: p.country, type: p.type, volume: p.volume, fleet: p.fleet, rank: p.rank } })) : []); | |
| setGeo('maritime-choke', activeLayers.maritime && data.maritime_chokepoints ? data.maritime_chokepoints.map((c: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [c.lng, c.lat] }, properties: { name: c.name, traffic: c.traffic, risk: c.risk } })) : []); | |
| setGeo('maritime-ships', activeLayers.maritime && data.maritime_ships ? data.maritime_ships.map((s: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [s.lng, s.lat] }, properties: { name: s.name || s.mmsi?.toString(), type: s.type || 'cargo', speed: s.speed, heading: s.heading, destination: s.destination, flag: s.flag } })) : []); | |
| }, [mapReady, data.maritime_ports, data.maritime_chokepoints, data.maritime_ships, activeLayers.maritime, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('balloons', activeLayers.balloons && data.balloons ? data.balloons.map((b: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [b.lng, b.lat] }, properties: { callsign: b.callsign, type: b.type, status: b.status, altitude: b.altitude, speed: b.speed, verticalRate: b.verticalRate, temperature: b.temperature, color: b.color } })) : []); | |
| }, [mapReady, data.balloons, activeLayers.balloons, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('radiation', activeLayers.radiation && data.radiation ? data.radiation.map((r: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [r.lng, r.lat] }, properties: { name: r.name, city: r.city, country: r.country, reading: r.reading, status: r.status, network: r.network } })) : []); | |
| }, [mapReady, data.radiation, activeLayers.radiation, setGeo]); | |
| // ══ OSIRIS SDK — Lattice Sensor Mesh ══ | |
| // Uses real submarine cable data for SEA domain, curated routes for AIR/INTEL | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('sdk-entities', []); | |
| const anySDK = activeLayers.sdk_sea || activeLayers.sdk_air || activeLayers.sdk_naval; | |
| if (!anySDK) { | |
| setGeo('sdk-links', []); | |
| return; | |
| } | |
| const links: any[] = []; | |
| // ── SEA DOMAIN: Real submarine cable data (1-for-1 Match) ── | |
| if (activeLayers.sdk_sea && data.submarine_cables) { | |
| const ignoredColors = new Set(['#9BB5CC', '#A0B8CD', '#8EABC2', '#9bb5cc', '#a0b8cd', '#8eabc2']); | |
| for (const cable of data.submarine_cables) { | |
| if (!cable.geometry) continue; | |
| // Remove the light blue background arcs | |
| if (cable.properties?.color && ignoredColors.has(cable.properties.color)) continue; | |
| links.push({ | |
| type: 'Feature', | |
| geometry: cable.geometry, // Raw topographic paths exactly from Submarine Map | |
| properties: { | |
| domain: 'SEA', | |
| fromName: cable.properties?.name || 'Submarine Cable', | |
| toName: cable.properties?.landing_points || '', | |
| source: 'Global Subsea Cable Network', | |
| url: 'https://www.submarinecablemap.com/', | |
| ...cable.properties, | |
| color: '#1976D2', // Darker blue as requested, more transparent in layer paint | |
| }, | |
| }); | |
| } | |
| } | |
| setGeo('sdk-links', links); | |
| }, [mapReady, activeLayers.sdk_sea, activeLayers.sdk_air, activeLayers.sdk_naval, data.submarine_cables, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setGeo('live-news', activeLayers.live_news && data.live_feeds ? data.live_feeds.map((f: any) => ({ type: 'Feature', geometry: { type: 'Point', coordinates: [f.lng, f.lat] }, properties: { name: f.name, city: f.city, country: f.country, url: f.url, category: f.category, embed_allowed: f.embed_allowed !== false } })) : []); | |
| }, [mapReady, data.live_feeds, activeLayers.live_news, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| const items = data.news || []; | |
| setGeo('sigint-news', activeLayers.news_intel && items.length > 0 | |
| ? items.filter((n: any) => n.coords?.length === 2).map((n: any) => ({ | |
| type: 'Feature', | |
| geometry: { type: 'Point', coordinates: [n.coords[1], n.coords[0]] }, | |
| properties: { title: n.title, source: n.source, risk_score: n.risk_score, link: n.link } | |
| })) | |
| : []); | |
| }, [mapReady, data.news, activeLayers.news_intel, setGeo]); | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| // 🔴 CONFLICT ZONES - Live from /api/conflicts 🔴 | |
| let cancelled = false; | |
| (async () => { | |
| try { | |
| const res = await fetch('/api/conflicts'); | |
| if (cancelled) return; | |
| if (!res.ok) throw new Error(`HTTP error! status: ${res.status}`); | |
| const conflictData = await res.json(); | |
| if (cancelled) return; | |
| // Zone anchor markers (war/high/elevated labels) | |
| const zoneFeatures = (conflictData.zones || []).map((z: any) => ({ | |
| type: 'Feature' as const, | |
| geometry: { type: 'Point' as const, coordinates: [z.lng, z.lat] }, | |
| properties: { | |
| label: z.label, | |
| severity: z.severity, | |
| description: `${z.description}${z.eventCount > 0 ? ` [${z.eventCount} live events detected]` : ''}`, | |
| sourceUrl: z.sourceUrl, | |
| eventCount: z.eventCount, | |
| }, | |
| })); | |
| // Individual live conflict events (scatter dots across conflict zones) | |
| const eventFeatures = (conflictData.liveEvents || []) | |
| .filter((e: any) => e.lat && e.lng) | |
| .map((e: any) => ({ | |
| type: 'Feature' as const, | |
| geometry: { type: 'Point' as const, coordinates: [e.lng, e.lat] }, | |
| properties: { | |
| label: (e.title || 'CONFLICT EVENT').substring(0, 60).toUpperCase(), | |
| severity: 'war', | |
| description: e.title || 'Live conflict event detected by GDELT.', | |
| sourceUrl: e.url || '', | |
| }, | |
| })); | |
| setGeo('conflict-zones', [...zoneFeatures, ...eventFeatures]); | |
| } catch (e) { | |
| // Fallback: if API fails, use minimal known zones | |
| const FALLBACK_ZONES = [ | |
| { label: 'UKRAINE WAR', severity: 'war', lat: 48.5, lng: 31.2, description: 'Ongoing Russian invasion of Ukraine.', sourceUrl: 'https://liveuamap.com/' }, | |
| { label: 'GAZA CONFLICT', severity: 'war', lat: 31.35, lng: 34.35, description: 'Active military operations in Gaza.', sourceUrl: 'https://israelpalestine.liveuamap.com/' }, | |
| { label: 'SUDAN CIVIL WAR', severity: 'war', lat: 15.0, lng: 30.0, description: 'SAF vs RSF armed conflict.', sourceUrl: 'https://sudan.liveuamap.com/' }, | |
| { label: 'YEMEN WAR', severity: 'war', lat: 15.5, lng: 48.0, description: 'Houthi operations and Red Sea threats.', sourceUrl: 'https://yemen.liveuamap.com/' }, | |
| { label: 'MYANMAR CONFLICT', severity: 'war', lat: 19.5, lng: 96.5, description: 'Military junta vs opposition forces.', sourceUrl: 'https://myanmar.liveuamap.com/' }, | |
| { label: 'SYRIA', severity: 'high', lat: 35.0, lng: 38.5, description: 'Ongoing civil conflict.', sourceUrl: 'https://syria.liveuamap.com/' }, | |
| ]; | |
| const fallbackFeatures = FALLBACK_ZONES.map(z => ({ | |
| type: 'Feature' as const, | |
| geometry: { type: 'Point' as const, coordinates: [z.lng, z.lat] }, | |
| properties: { label: z.label, severity: z.severity, description: z.description, sourceUrl: z.sourceUrl }, | |
| })); | |
| setGeo('conflict-zones', fallbackFeatures); | |
| } | |
| })(); | |
| return () => { cancelled = true; }; | |
| }, [mapReady, setGeo]); | |
| // Visibility | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| setVis(['eq-circles','eq-label'], activeLayers.earthquakes); | |
| const anySat = activeLayers.satellites || (activeLayers as any).sat_comms || (activeLayers as any).sat_military || (activeLayers as any).sat_navigation || (activeLayers as any).sat_earth || (activeLayers as any).sat_science; | |
| setVis(['sat-glow','sat-dots'], anySat); | |
| setVis(['gdelt-dots'], activeLayers.global_incidents); | |
| setVis(['malware-glow','malware-dots','malware-label'], activeLayers.malware); | |
| setVis(['network-mesh-atmo', 'network-mesh-glow', 'network-mesh-core'], activeLayers.internet_outages || activeLayers.malware); | |
| setVis(['jam-fill','jam-label'], activeLayers.gps_jamming); | |
| setVis(['day-night-fill'], activeLayers.day_night); | |
| setVis(['fl-commercial'], activeLayers.flights); | |
| setVis(['fl-private'], activeLayers.private); | |
| setVis(['fl-jets'], activeLayers.jets); | |
| setVis(['fl-military'], activeLayers.military); | |
| setVis(['cctv-glow','cctv-dots','cctv-label'], activeLayers.cctv); | |
| setVis(['fires-heat'], activeLayers.fires); | |
| setVis(['weather-glow','weather-dots','weather-label'], activeLayers.weather); | |
| setVis(['infra-glow','infra-dots','infra-label'], activeLayers.infrastructure); | |
| setVis(['mining-glow','mining-dots','mining-label'], activeLayers.mining); | |
| setVis(['ram-glow','ram-dots','ram-label'], activeLayers.ram_fabs); | |
| setVis(['maritime-glow','maritime-dots','maritime-label'], activeLayers.maritime); | |
| setVis(['choke-glow','choke-dots','choke-label'], activeLayers.maritime); | |
| setVis(['ship-dots','ship-label'], activeLayers.maritime); | |
| setVis(['news-glow','news-dots','news-label'], activeLayers.live_news); | |
| setVis(['sigint-news-glow','sigint-news-dots','sigint-news-label'], activeLayers.news_intel); | |
| setVis(['conflict-icons'], activeLayers.conflict_zones !== false); | |
| setVis(['balloon-dots','balloon-label'], activeLayers.balloons); | |
| setVis(['rad-glow','rad-dots','rad-label'], activeLayers.radiation); | |
| setVis(['sdk-sea','sdk-sea-glow','sdk-sea-atmo'], activeLayers.sdk_sea !== false); | |
| setVis(['sdk-air','sdk-air-glow','sdk-air-atmo'], activeLayers.sdk_air !== false); | |
| setVis(['sdk-intel','sdk-intel-glow','sdk-intel-atmo'], activeLayers.sdk_naval !== false); | |
| // Sweep layers always visible when data is present (controlled by useEffect) | |
| setVis(['sweep-connections','sweep-pulse-ring','sweep-device-glow','sweep-device-dots','sweep-device-labels'], true); | |
| }, [mapReady, activeLayers, setVis]); | |
| // IP Sweep visualization | |
| useEffect(() => { | |
| if (!mapReady) return; | |
| if (!sweepData?.devices?.length) { | |
| setGeo('ip-sweep-devices', []); | |
| setGeo('ip-sweep-pulse', []); | |
| setGeo('ip-sweep-connections', []); | |
| return; | |
| } | |
| const map = mapRef.current; | |
| if (!map) return; | |
| const { center, devices } = sweepData; | |
| const centerCoord: [number, number] = [center.lng, center.lat]; | |
| // Switch to globe and fly to the sweep location | |
| try { | |
| (map as any).setProjection({ type: 'globe' }); | |
| map.setSky({ 'sky-color': '#0A0A0F', 'sky-horizon-blend': 0.02, 'horizon-color': '#0A0A0F', 'horizon-fog-blend': 0.02 }); | |
| } catch { /* projection may not be supported */ } | |
| map.flyTo({ center: centerCoord, zoom: 14, pitch: 50, bearing: -20, duration: 3000, essential: true }); | |
| // Set center pulse | |
| setGeo('ip-sweep-pulse', [{ | |
| type: 'Feature' as const, | |
| geometry: { type: 'Point' as const, coordinates: centerCoord }, | |
| properties: { ip: sweepData.target_ip }, | |
| }]); | |
| // Build device features spread in a circle around center | |
| const allDeviceFeatures = devices.map((d: any, i: number) => { | |
| const angle = (i / devices.length) * Math.PI * 2; | |
| const radius = 0.001 + ((i % 7 + 1) * 0.0004); | |
| const dLng = centerCoord[0] + Math.cos(angle) * radius * (1 / Math.cos(center.lat * Math.PI / 180)); | |
| const dLat = centerCoord[1] + Math.sin(angle) * radius; | |
| return { | |
| type: 'Feature' as const, | |
| geometry: { type: 'Point' as const, coordinates: [dLng, dLat] }, | |
| properties: { | |
| ip: d.ip, device_type: d.device_type, device_icon: d.device_icon, | |
| color: d.device_color, risk_level: d.risk_level, | |
| ports: JSON.stringify(d.ports), hostnames: JSON.stringify(d.hostnames), | |
| vulns: JSON.stringify(d.vulns), cpes: JSON.stringify(d.cpes), tags: JSON.stringify(d.tags), | |
| }, | |
| }; | |
| }); | |
| // Connection lines from center to each device | |
| const connectionFeatures = allDeviceFeatures.map((f: any) => ({ | |
| type: 'Feature' as const, | |
| geometry: { type: 'LineString' as const, coordinates: [centerCoord, f.geometry.coordinates] }, | |
| properties: { color: f.properties.color }, | |
| })); | |
| // Stagger the appearance after 3s flyTo completes | |
| const timer = setTimeout(() => { | |
| setGeo('ip-sweep-connections', connectionFeatures); | |
| const batchSize = 5; | |
| const batches = Math.ceil(allDeviceFeatures.length / batchSize); | |
| for (let b = 0; b < batches; b++) { | |
| setTimeout(() => { | |
| setGeo('ip-sweep-devices', allDeviceFeatures.slice(0, (b + 1) * batchSize)); | |
| }, b * 100); | |
| } | |
| }, 3000); | |
| return () => clearTimeout(timer); | |
| }, [mapReady, sweepData, setGeo]); | |
| // Scan Targets visualization | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current || !scanTargets) return; | |
| const map = mapRef.current; | |
| const features = scanTargets.map(t => ({ | |
| type: 'Feature' as const, | |
| geometry: { type: 'Point' as const, coordinates: [t.lng, t.lat] }, | |
| properties: { ...t } | |
| })); | |
| const src = map.getSource('scan-targets') as maplibregl.GeoJSONSource; | |
| if (src) src.setData({ type: 'FeatureCollection', features }); | |
| }, [scanTargets, mapReady]); | |
| // Fly-to | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current || !flyToLocation) return; | |
| mapRef.current.flyTo({ center: [flyToLocation.lng, flyToLocation.lat], zoom: flyToLocation.zoom || 8, duration: 2000 }); | |
| }, [mapReady, flyToLocation]); | |
| // Dynamic projection switching (lightweight — no terrain DEM) | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current) return; | |
| const map = mapRef.current; | |
| try { | |
| (map as any).setProjection({ type: projection }); | |
| if (projection === 'globe') { | |
| map.easeTo({ pitch: 20, duration: 1200 }); | |
| try { | |
| (map as any).setSky({ | |
| 'sky-color': '#04040A', | |
| 'sky-horizon-blend': 0.5, | |
| 'horizon-color': '#0a0a1a', | |
| 'horizon-fog-blend': 0.3, | |
| 'fog-color': '#04040A', | |
| 'fog-ground-blend': 0.9, | |
| }); | |
| } catch (e) { console.warn('[OSIRIS] Suppressed error:', e instanceof Error ? e.message : e); } | |
| } else { | |
| map.easeTo({ pitch: 0, duration: 800 }); | |
| } | |
| } catch (e) { | |
| console.warn('Projection switch failed:', e); | |
| } | |
| }, [mapReady, projection]); | |
| // 3D Terrain & Buildings layer | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current) return; | |
| const map = mapRef.current; | |
| const enabled = activeLayers.terrain_3d; | |
| try { | |
| if (enabled) { | |
| // ── 3D BUILDINGS SOURCE (OpenFreeMap CDN — no API key, globally cached) ── | |
| if (!map.getSource('osiris-buildings')) { | |
| map.addSource('osiris-buildings', { | |
| type: 'vector', | |
| url: 'https://tiles.openfreemap.org/planet', | |
| }); | |
| } | |
| // ── 3D BUILDING EXTRUSION LAYER ── | |
| if (!map.getLayer('osiris-3d-buildings')) { | |
| map.addLayer({ | |
| id: 'osiris-3d-buildings', | |
| source: 'osiris-buildings', | |
| 'source-layer': 'building', | |
| type: 'fill-extrusion', | |
| minzoom: 14.5, | |
| paint: { | |
| 'fill-extrusion-color': [ | |
| 'interpolate', ['linear'], ['get', 'render_height'], | |
| 0, '#1a1a2e', | |
| 20, '#16213e', | |
| 50, '#0f3460', | |
| 120, '#533483', | |
| 300, '#e94560', | |
| ], | |
| 'fill-extrusion-height': [ | |
| 'interpolate', ['linear'], ['zoom'], | |
| 14.5, 0, | |
| 15.5, ['get', 'render_height'] | |
| ], | |
| 'fill-extrusion-base': [ | |
| 'interpolate', ['linear'], ['zoom'], | |
| 14.5, 0, | |
| 15.5, ['get', 'render_min_height'] | |
| ], | |
| 'fill-extrusion-opacity': [ | |
| 'interpolate', ['linear'], ['zoom'], | |
| 14.5, 0, | |
| 15, 0.7, | |
| ], | |
| }, | |
| }); | |
| } | |
| // Pitch the camera to reveal the 3D skyline | |
| if (map.getPitch() < 40) { | |
| map.easeTo({ pitch: 50, duration: 1200 }); | |
| } | |
| } else { | |
| // ── DISABLE 3D ── | |
| if (map.getLayer('osiris-3d-buildings')) map.removeLayer('osiris-3d-buildings'); | |
| } | |
| } catch (e) { | |
| console.warn('[OSIRIS] 3D terrain toggle error:', e); | |
| } | |
| }, [mapReady, activeLayers.terrain_3d]); | |
| // Satellite / Dark style switching | |
| useEffect(() => { | |
| if (!mapReady || !mapRef.current) return; | |
| if (mapStyle === prevStyleRef.current) return; | |
| prevStyleRef.current = mapStyle; | |
| const map = mapRef.current; | |
| try { | |
| if (mapStyle !== 'dark') { | |
| // Add satellite raster tiles | |
| if (!map.getSource('satellite-tiles')) { | |
| map.addSource('satellite-tiles', { | |
| type: 'raster', | |
| tiles: ['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], | |
| tileSize: 256, | |
| maxzoom: 18, | |
| }); | |
| map.addLayer({ id: 'satellite-layer', type: 'raster', source: 'satellite-tiles', paint: { 'raster-opacity': 0.85 } }, 'day-night-fill'); | |
| } else { | |
| map.setLayoutProperty('satellite-layer', 'visibility', 'visible'); | |
| } | |
| } else { | |
| if (map.getLayer('satellite-layer')) { | |
| map.setLayoutProperty('satellite-layer', 'visibility', 'none'); | |
| } | |
| } | |
| } catch (e) { | |
| console.warn('Style switch failed:', e); | |
| } | |
| }, [mapReady, mapStyle]); | |
| return <div ref={containerRef} className="absolute inset-0 w-full h-full" />; | |
| } | |
| export default memo(OsirisMap); | |