Spaces:
Running
Running
| <html lang="en" class="dark"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MoStar Industries — Phantom POE Map | 3D Intelligence View</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Lucide Icons --> | |
| <script src="https://unpkg.com/lucide@latest"></script> | |
| <!-- Google Fonts --> | |
| <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
| <!-- CesiumJS --> | |
| <script src="https://cesium.com/downloads/cesiumjs/releases/1.114/Build/Cesium/Cesium.js"></script> | |
| <link href="https://cesium.com/downloads/cesiumjs/releases/1.114/Build/Cesium/Widgets/widgets.css" rel="stylesheet"> | |
| <script> | |
| tailwind.config = { | |
| darkMode: 'class', | |
| theme: { | |
| extend: { | |
| fontFamily: { | |
| mono: ['JetBrains Mono', 'monospace'], | |
| sans: ['Inter', 'sans-serif'], | |
| }, | |
| colors: { | |
| 'risk-critical': '#ef4444', | |
| 'risk-high': '#f97316', | |
| 'risk-medium': '#eab308', | |
| 'lane-live': '#00CC66', | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| <style> | |
| body { | |
| margin: 0; | |
| overflow: hidden; | |
| background: #0f172a; | |
| font-family: 'Inter', sans-serif; | |
| } | |
| #cesiumContainer { | |
| width: 100%; | |
| height: 100vh; | |
| } | |
| .glass-panel { | |
| background: rgba(15, 23, 42, 0.9); | |
| backdrop-filter: blur(12px); | |
| border: 1px solid rgba(51, 65, 85, 0.8); | |
| } | |
| .cesium-viewer-toolbar { | |
| display: none ; | |
| } | |
| .cesium-viewer-bottom { | |
| display: none ; | |
| } | |
| .cesium-viewer-fullscreenContainer { | |
| display: none ; | |
| } | |
| /* Custom scrollbar for panels */ | |
| ::-webkit-scrollbar { | |
| width: 6px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: #1e293b; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #475569; | |
| border-radius: 3px; | |
| } | |
| </style> | |
| </head> | |
| <body class="text-slate-200"> | |
| <!-- Header Overlay --> | |
| <div class="absolute top-0 left-0 right-0 z-50 glass-panel border-b border-slate-700 h-16 flex items-center justify-between px-4"> | |
| <div class="flex items-center gap-4"> | |
| <a href="index.html" class="flex items-center gap-2 hover:opacity-80 transition-opacity"> | |
| <span class="text-2xl font-bold tracking-tighter text-white">◉⟁⬡</span> | |
| <div> | |
| <h1 class="font-bold text-lg leading-tight text-white">MoStar Industries</h1> | |
| <p class="text-xs text-slate-400 font-mono">Phantom POE 3D Map v2.1</p> | |
| </div> | |
| </a> | |
| <div class="h-8 w-px bg-slate-700 mx-2"></div> | |
| <div class="flex items-center gap-2"> | |
| <span class="px-3 py-1 rounded-full bg-lane-live/20 text-lane-live border border-lane-live/30 text-xs font-bold flex items-center gap-2"> | |
| <span class="w-2 h-2 rounded-full bg-lane-live animate-pulse"></span> | |
| LIVE MODE | |
| </span> | |
| <span class="text-xs text-slate-500 font-mono">14 Corridors | 91 Nodes | 157 Signals</span> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <button onclick="resetView()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 border border-slate-600"> | |
| <i data-lucide="globe" class="w-4 h-4"></i> | |
| Africa Overview | |
| </button> | |
| <button onclick="toggleCascadeMenu()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 border border-slate-600"> | |
| <i data-lucide="play-circle" class="w-4 h-4 text-emerald-400"></i> | |
| Cascade Animation | |
| </button> | |
| <button onclick="clearCascade()" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 border border-slate-600"> | |
| <i data-lucide="square" class="w-4 h-4 text-red-400"></i> | |
| Stop | |
| </button> | |
| <a href="index.html" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 rounded-lg text-xs font-medium transition-colors flex items-center gap-2 border border-slate-600"> | |
| <i data-lucide="layout-dashboard" class="w-4 h-4"></i> | |
| Dashboard | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Cascade Menu (Hidden by default) --> | |
| <div id="cascade-menu" class="hidden absolute top-20 left-4 z-40 glass-panel rounded-xl border border-slate-700 w-64 max-h-[70vh] overflow-y-auto"> | |
| <div class="p-3 border-b border-slate-700 bg-slate-800/50"> | |
| <h3 class="text-xs font-bold text-slate-400 uppercase tracking-wider">Select Corridor</h3> | |
| </div> | |
| <div class="p-2 space-y-1"> | |
| <button onclick="startCascade('C-ET-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-critical"> | |
| <div class="font-bold text-white">Gambela → Malakal</div> | |
| <div class="text-slate-500">280km | CRITICAL</div> | |
| </button> | |
| <button onclick="startCascade('C-NG-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-critical"> | |
| <div class="font-bold text-white">Baga → Diffa</div> | |
| <div class="text-slate-500">147km | CRITICAL</div> | |
| </button> | |
| <button onclick="startCascade('C-CD-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-critical"> | |
| <div class="font-bold text-white">Mbandaka → Impfondo</div> | |
| <div class="text-slate-500">190km | CRITICAL</div> | |
| </button> | |
| <button onclick="startCascade('C-CF-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-critical"> | |
| <div class="font-bold text-white">Birao → Nyala</div> | |
| <div class="text-slate-500">300km | CRITICAL</div> | |
| </button> | |
| <button onclick="startCascade('C-SS-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-critical"> | |
| <div class="font-bold text-white">Yambio → Bangui</div> | |
| <div class="text-slate-500">527km | CRITICAL</div> | |
| </button> | |
| <button onclick="startCascade('C-CD-002')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-high"> | |
| <div class="font-bold text-white">Gbadolite → Bangui</div> | |
| <div class="text-slate-500">280km | HIGH</div> | |
| </button> | |
| <button onclick="startCascade('C-ET-002')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-high"> | |
| <div class="font-bold text-white">Dolo Ado → Baidoa</div> | |
| <div class="text-slate-500">210km | HIGH</div> | |
| </button> | |
| <button onclick="startCascade('C-SS-002')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-high"> | |
| <div class="font-bold text-white">Renk → Ed Damazin</div> | |
| <div class="text-slate-500">175km | HIGH</div> | |
| </button> | |
| <button onclick="startCascade('C-ET-003')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-high"> | |
| <div class="font-bold text-white">Humera → Kassala</div> | |
| <div class="text-slate-500">155km | HIGH</div> | |
| </button> | |
| <button onclick="startCascade('C-NG-002')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-high"> | |
| <div class="font-bold text-white">Gwoza → Mora</div> | |
| <div class="text-slate-500">52km | HIGH</div> | |
| </button> | |
| <button onclick="startCascade('C-SD-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-high"> | |
| <div class="font-bold text-white">Metema → Gallabat</div> | |
| <div class="text-slate-500">8km | HIGH</div> | |
| </button> | |
| <button onclick="startCascade('C-SS-003')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-medium"> | |
| <div class="font-bold text-white">Kapoeta → Lodwar</div> | |
| <div class="text-slate-500">245km | MEDIUM</div> | |
| </button> | |
| <button onclick="startCascade('C-NG-003')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-medium"> | |
| <div class="font-bold text-white">Banki → Amchide</div> | |
| <div class="text-slate-500">73km | MEDIUM</div> | |
| </button> | |
| <button onclick="startCascade('C-GL-001')" class="w-full text-left px-3 py-2 rounded-lg hover:bg-slate-800 text-xs transition-colors border-l-2 border-risk-critical"> | |
| <div class="font-bold text-white">Goma → Gisenyi</div> | |
| <div class="text-slate-500">4km | CRITICAL</div> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Legend --> | |
| <div class="absolute bottom-6 left-6 z-40 glass-panel rounded-xl border border-slate-700 p-4 max-w-xs pointer-events-none"> | |
| <div class="font-bold text-sm mb-2 text-white flex items-center gap-2"> | |
| <i data-lucide="map" class="w-4 h-4"></i> | |
| PHANTOM POE ENGINE | |
| </div> | |
| <div class="text-xs text-slate-400 mb-3">Hidden & Undocumented Border Crossings</div> | |
| <div class="space-y-2 mb-3"> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <div class="w-3 h-1 bg-risk-critical rounded"></div> | |
| <span class="text-slate-300">CRITICAL Risk</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <div class="w-3 h-1 bg-risk-high rounded"></div> | |
| <span class="text-slate-300">HIGH Risk</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <div class="w-3 h-1 bg-risk-medium rounded"></div> | |
| <span class="text-slate-300">MEDIUM Risk</span> | |
| </div> | |
| </div> | |
| <div class="border-t border-slate-700 pt-2 space-y-1.5 mb-3"> | |
| <div class="flex items-center gap-2 text-xs text-slate-400"> | |
| <span class="w-2 h-2 rounded-full bg-lime-500"></span> | |
| <span>Start Node</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs text-slate-400"> | |
| <span class="w-2 h-2 rounded-full bg-red-500"></span> | |
| <span>End Node</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs text-slate-400"> | |
| <span class="w-2 h-2 rounded-full bg-amber-400 border border-amber-600"></span> | |
| <span>Phantom POE ⚡</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs text-slate-400"> | |
| <span class="w-2 h-2 rounded-full bg-orange-500"></span> | |
| <span>Border Zone</span> | |
| </div> | |
| </div> | |
| <div class="border-t border-slate-700 pt-2 space-y-1.5"> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <span class="w-2 h-2 rounded-full bg-red-500"></span> | |
| <span class="text-slate-400">ACLED Conflict</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <span class="w-2 h-2 rounded-full bg-blue-500"></span> | |
| <span class="text-slate-400">IOM-DTM Displacement</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <span class="w-2 h-2 rounded-full bg-emerald-500"></span> | |
| <span class="text-slate-400">DHIS2 Health</span> | |
| </div> | |
| <div class="flex items-center gap-2 text-xs"> | |
| <span class="w-2 h-2 rounded-full bg-yellow-500"></span> | |
| <span class="text-slate-400">AFRO-Sentinel</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Active Cascade Info --> | |
| <div id="cascade-info" class="hidden absolute bottom-6 right-6 z-40 glass-panel rounded-xl border border-slate-700 p-4 w-64"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h3 class="text-xs font-bold text-slate-400 uppercase">Cascade Animation</h3> | |
| <div class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div> | |
| </div> | |
| <div id="cascade-corridor-name" class="font-bold text-white text-sm mb-1">--</div> | |
| <div class="flex justify-between text-xs mb-2"> | |
| <span class="text-slate-400">Day:</span> | |
| <span id="cascade-day" class="text-white font-mono">0/7</span> | |
| </div> | |
| <div class="flex justify-between text-xs mb-2"> | |
| <span class="text-slate-400">Score:</span> | |
| <span id="cascade-score" class="text-emerald-400 font-mono">0.000</span> | |
| </div> | |
| <div class="w-full bg-slate-800 rounded-full h-1.5"> | |
| <div id="cascade-progress" class="bg-emerald-500 h-1.5 rounded-full transition-all duration-500" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Cesium Container --> | |
| <div id="cesiumContainer"></div> | |
| <script> | |
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| // Cesium Ion token (replace with your own for production) | |
| Cesium.Ion.defaultAccessToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiJlYWEzNmJkOC1iMzQ2LTRmOTYtODVlNy1iZGU4M2M2YjE1MTYiLCJpZCI6MjQ0MTU4LCJpYXQiOjE3MzA4MjY1NzB9.ZH9mWGt46kZ1RGbK8R6K7ZvC7P9R8Q5F3M2N1L0K9J8'; | |
| // Initialize viewer | |
| const viewer = new Cesium.Viewer("cesiumContainer", { | |
| timeline: false, | |
| animation: false, | |
| sceneModePicker: false, | |
| baseLayerPicker: false, | |
| geocoder: false, | |
| homeButton: false, | |
| fullscreenButton: false, | |
| navigationHelpButton: false, | |
| skyAtmosphere: true, | |
| globe: false // Google Tiles include terrain | |
| }); | |
| // Enable sky | |
| viewer.scene.skyAtmosphere.show = true; | |
| // Add Google Photorealistic 3D Tiles | |
| let googleTileset; | |
| async function loadGoogleTiles() { | |
| try { | |
| googleTileset = await Cesium.createGooglePhotorealistic3DTileset({ | |
| onlyUsingWithGoogleGeocoder: false | |
| }); | |
| viewer.scene.primitives.add(googleTileset); | |
| } catch (error) { | |
| console.log('Error loading Google Tiles:', error); | |
| // Fallback to default terrain | |
| viewer.globe = true; | |
| } | |
| } | |
| loadGoogleTiles(); | |
| // Source Colors for Signals | |
| const SOURCE_COLORS = { | |
| 'ACLED': Cesium.Color.fromCssColorString('#EF4444'), | |
| 'IOM-DTM': Cesium.Color.fromCssColorString('#3B82F6'), | |
| 'DHIS2': Cesium.Color.fromCssColorString('#22C55E'), | |
| 'AFRO-SENTINEL': Cesium.Color.fromCssColorString('#EAB308'), | |
| 'MANUAL': Cesium.Color.fromCssColorString('#A855F7'), | |
| 'MOCK': Cesium.Color.fromCssColorString('#64748B') | |
| }; | |
| // Risk Colors | |
| const RISK_COLORS = { | |
| 'CRITICAL': Cesium.Color.fromCssColorString('#EF4444'), | |
| 'HIGH': Cesium.Color.fromCssColorString('#F97316'), | |
| 'MEDIUM': Cesium.Color.fromCssColorString('#EAB308'), | |
| 'LOW': Cesium.Color.fromCssColorString('#22C55E') | |
| }; | |
| // Corridor Definitions | |
| const corridors = [ | |
| { id: 'C-ET-001', name: 'Gambela → Malakal', risk: 'CRITICAL', distance: 280, mode: 'foot', color: '#EF4444', width: 6, glow: 0.6 }, | |
| { id: 'C-NG-001', name: 'Baga → Diffa', risk: 'CRITICAL', distance: 147, mode: 'canoe', color: '#EF4444', width: 6, glow: 0.6 }, | |
| { id: 'C-CD-001', name: 'Mbandaka → Impfondo', risk: 'CRITICAL', distance: 190, mode: 'canoe', color: '#EF4444', width: 6, glow: 0.6 }, | |
| { id: 'C-CF-001', name: 'Birao → Nyala', risk: 'CRITICAL', distance: 300, mode: 'foot', color: '#EF4444', width: 6, glow: 0.6 }, | |
| { id: 'C-SS-001', name: 'Yambio → Bangui', risk: 'CRITICAL', distance: 527, mode: 'foot', color: '#EF4444', width: 6, glow: 0.6 }, | |
| { id: 'C-GL-001', name: 'Goma → Gisenyi', risk: 'CRITICAL', distance: 4, mode: 'foot', color: '#EF4444', width: 6, glow: 0.6 }, | |
| { id: 'C-CD-002', name: 'Gbadolite → Bangui', risk: 'HIGH', distance: 280, mode: 'canoe', color: '#F97316', width: 4, glow: 0.4 }, | |
| { id: 'C-ET-002', name: 'Dolo Ado → Baidoa', risk: 'HIGH', distance: 210, mode: 'foot', color: '#F97316', width: 4, glow: 0.4 }, | |
| { id: 'C-SS-002', name: 'Renk → Ed Damazin', risk: 'HIGH', distance: 175, mode: 'foot', color: '#F97316', width: 4, glow: 0.4 }, | |
| { id: 'C-ET-003', name: 'Humera → Kassala', risk: 'HIGH', distance: 155, mode: 'foot', color: '#F97316', width: 4, glow: 0.4 }, | |
| { id: 'C-NG-002', name: 'Gwoza → Mora', risk: 'HIGH', distance: 52, mode: 'foot', color: '#F97316', width: 4, glow: 0.4 }, | |
| { id: 'C-SD-001', name: 'Metema → Gallabat', risk: 'HIGH', distance: 8, mode: 'foot', color: '#F97316', width: 4, glow: 0.4 }, | |
| { id: 'C-SS-003', name: 'Kapoeta → Lodwar', risk: 'MEDIUM', distance: 245, mode: 'livestock', color: '#EAB308', width: 3, glow: 0.2 }, | |
| { id: 'C-NG-003', name: 'Banki → Amchide', risk: 'MEDIUM', distance: 73, mode: 'foot', color: '#EAB308', width: 3, glow: 0.2 } | |
| ]; | |
| // Path coordinates for corridors | |
| const corridorPaths = { | |
| 'C-ET-001': [34.5833, 8.25, 50, 34.2081, 8.4008, 50, 33.8703, 8.5677, 50, 33.4757, 8.7387, 50, 33.1118, 8.9070, 50, 32.7688, 9.0444, 50, 32.3657, 9.2253, 50, 32.0143, 9.3893, 50, 31.65, 9.5333, 50], | |
| 'C-NG-001': [13.7833, 12.8167, 50, 13.4679, 12.9490, 50, 13.2232, 13.0500, 50, 12.8940, 13.2123, 50, 12.6167, 13.3167, 50], | |
| 'C-CD-001': [18.2558, -0.0478, 50, 18.2230, 0.3079, 50, 18.1575, 0.6325, 50, 18.1304, 0.9764, 50, 18.0988, 1.2734, 50, 18.0594, 1.6306, 50], | |
| 'C-CF-001': [22.7833, 10.2833, 50, 23.0229, 10.5124, 50, 23.2891, 10.7105, 50, 23.5850, 10.9526, 50, 23.8500, 11.1632, 50, 24.1049, 11.3950, 50, 24.3412, 11.6140, 50, 24.6381, 11.8336, 50, 24.8833, 12.05, 50], | |
| 'C-SS-001': [28.3833, 4.5667, 50, 27.7003, 4.5390, 50, 26.9809, 4.5535, 50, 26.2815, 4.5090, 50, 25.5847, 4.5050, 50, 24.8779, 4.5011, 50, 24.1815, 4.4978, 50, 23.4633, 4.4523, 50, 22.7698, 4.4584, 50, 22.0954, 4.4280, 50, 21.4034, 4.4410, 50, 20.7043, 4.3847, 50, 19.9889, 4.4152, 50, 19.2866, 4.3599, 50, 18.5833, 4.3667, 50], | |
| 'C-GL-001': [29.2333, -1.6833, 50, 29.2697, -1.6903, 50, 29.2667, -1.7, 50], | |
| 'C-CD-002': [21.0167, 4.2833, 50, 20.7319, 4.3157, 50, 20.3842, 4.2801, 50, 20.1108, 4.3306, 50, 19.7991, 4.3292, 50, 19.5079, 4.3163, 50, 19.1945, 4.3542, 50, 18.8895, 4.3354, 50, 18.5833, 4.3667, 50], | |
| 'C-ET-002': [42.0667, 4.1833, 50, 42.3439, 3.9934, 50, 42.6012, 3.8294, 50, 42.8797, 3.6443, 50, 43.1219, 3.4561, 50, 43.3886, 3.2831, 50, 43.65, 3.1167, 50], | |
| 'C-SS-002': [32.7833, 11.7667, 50, 33.0720, 11.7647, 50, 33.3905, 11.7466, 50, 33.7326, 11.7421, 50, 34.0386, 11.7580, 50, 34.35, 11.7667, 50], | |
| 'C-ET-003': [36.6333, 14.3, 50, 36.5867, 14.5750, 50, 36.5130, 14.8646, 50, 36.4728, 15.1642, 50, 36.4, 15.45, 50], | |
| 'C-NG-002': [13.6833, 11.0833, 50, 13.8960, 11.0299, 50, 14.1333, 11.0167, 50], | |
| 'C-SD-001': [36.2, 12.95, 50, 36.1677, 12.9741, 50, 36.15, 12.9667, 50], | |
| 'C-SS-003': [33.5833, 4.7667, 50, 33.8537, 4.5274, 50, 34.1758, 4.3103, 50, 34.4249, 4.0643, 50, 34.7470, 3.8198, 50, 35.0036, 3.6073, 50, 35.3191, 3.3284, 50, 35.6, 3.1167, 50], | |
| 'C-NG-003': [13.5833, 11.05, 50, 13.9212, 10.8955, 50, 14.2167, 10.75, 50] | |
| }; | |
| // Evidence/Simulated Signal Data | |
| const EVIDENCE = [ | |
| // C-ET-001: Gambela → Malakal | |
| {id:"C-ET-001-E0-0",cid:"C-ET-001",day:0,lat:8.25,lng:34.5833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Gambela",loc:"Gambela",score:0.391,precision:"PRECISE"}, | |
| {id:"C-ET-001-E0-1",cid:"C-ET-001",day:0,lat:8.4008,lng:34.2081,km:35,src:"IOM-DTM",type:"DISPLACEMENT",tag:"DISPLACEMENT: Border Zone",loc:"Border Zone ET/SS",score:0.361,precision:"SETTLEMENT"}, | |
| {id:"C-ET-001-E2-2",cid:"C-ET-001",day:2,lat:8.9070,lng:33.1118,km:140,src:"IOM-DTM",type:"DISPLACEMENT",tag:"DISPLACEMENT: Phantom Crossing",loc:"Phantom Crossing C-ET-001",score:0.169,precision:"INFERRED"}, | |
| {id:"C-ET-001-E5-2",cid:"C-ET-001",day:5,lat:9.5333,lng:31.65,km:280,src:"IOM-DTM",type:"DISPLACEMENT",tag:"DISPLACEMENT: Malakal",loc:"Malakal",score:0.367,precision:"PRECISE"}, | |
| // C-NG-001: Baga → Diffa | |
| {id:"C-NG-001-E0-0",cid:"C-NG-001",day:0,lat:12.8167,lng:13.7833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Baga",loc:"Baga",score:0.443,precision:"PRECISE"}, | |
| {id:"C-NG-001-E3-0",cid:"C-NG-001",day:3,lat:13.0500,lng:13.2232,km:74,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Phantom Crossing",loc:"Phantom Crossing C-NG-001",score:0.453,precision:"INFERRED"}, | |
| {id:"C-NG-001-E6-1",cid:"C-NG-001",day:6,lat:13.3167,lng:12.6167,km:147,src:"IOM-DTM",type:"DISPLACEMENT",tag:"DISPLACEMENT: Diffa",loc:"Diffa",score:0.391,precision:"PRECISE"}, | |
| // C-CD-001: Mbandaka → Impfondo | |
| {id:"C-CD-001-E0-0",cid:"C-CD-001",day:0,lat:-0.0478,lng:18.2558,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Mbandaka",loc:"Mbandaka",score:0.466,precision:"PRECISE"}, | |
| {id:"C-CD-001-E0-2",cid:"C-CD-001",day:0,lat:0.6325,lng:18.1575,km:76,src:"DHIS2",type:"HEALTH",tag:"HEALTH: Phantom Crossing",loc:"Phantom Crossing C-CD-001",score:0.378,precision:"INFERRED"}, | |
| {id:"C-CD-001-E5-1",cid:"C-CD-001",day:5,lat:1.6306,lng:18.0594,km:190,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Impfondo",loc:"Impfondo",score:0.491,precision:"PRECISE"}, | |
| // C-CF-001: Birao → Nyala | |
| {id:"C-CF-001-E0-0",cid:"C-CF-001",day:0,lat:10.2833,lng:22.7833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Birao",loc:"Birao",score:0.368,precision:"PRECISE"}, | |
| {id:"C-CF-001-E4-2",cid:"C-CF-001",day:4,lat:11.8336,lng:24.6381,km:263,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Waypoint",loc:"Waypoint C-CF-001-7",score:0.474,precision:"SETTLEMENT"}, | |
| {id:"C-CF-001-E6-1",cid:"C-CF-001",day:6,lat:12.05,lng:24.8833,km:300,src:"IOM-DTM",type:"DISPLACEMENT",tag:"DISPLACEMENT: Nyala",loc:"Nyala",score:0.344,precision:"PRECISE"}, | |
| // C-GL-001: Goma → Gisenyi | |
| {id:"C-GL-001-E0-0",cid:"C-GL-001",day:0,lat:-1.6833,lng:29.2333,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Goma",loc:"Goma",score:0.386,precision:"PRECISE"}, | |
| {id:"C-GL-001-E3-0",cid:"C-GL-001",day:3,lat:-1.6903,lng:29.2697,km:2,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Phantom Crossing",loc:"Phantom Crossing C-GL-001",score:0.478,precision:"INFERRED"}, | |
| // Additional signals for other corridors | |
| {id:"C-CD-002-E0-0",cid:"C-CD-002",day:0,lat:4.2833,lng:21.0167,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Gbadolite",loc:"Gbadolite",score:0.371,precision:"PRECISE"}, | |
| {id:"C-ET-002-E0-0",cid:"C-ET-002",day:0,lat:4.1833,lng:42.0667,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Dolo Ado",loc:"Dolo Ado",score:0.203,precision:"PRECISE"}, | |
| {id:"C-SS-002-E0-0",cid:"C-SS-002",day:0,lat:11.7667,lng:32.7833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Renk",loc:"Renk",score:0.195,precision:"PRECISE"}, | |
| {id:"C-ET-003-E0-0",cid:"C-ET-003",day:0,lat:14.3,lng:36.6333,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Humera",loc:"Humera",score:0.368,precision:"PRECISE"}, | |
| {id:"C-NG-002-E0-0",cid:"C-NG-002",day:0,lat:11.0833,lng:13.6833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Gwoza",loc:"Gwoza",score:0.449,precision:"PRECISE"}, | |
| {id:"C-SD-001-E0-0",cid:"C-SD-001",day:0,lat:12.95,lng:36.2,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Metema",loc:"Metema",score:0.468,precision:"PRECISE"}, | |
| {id:"C-SS-003-E0-0",cid:"C-SS-003",day:0,lat:4.7667,lng:33.5833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Kapoeta",loc:"Kapoeta",score:0.255,precision:"PRECISE"}, | |
| {id:"C-NG-003-E0-0",cid:"C-NG-003",day:0,lat:11.05,lng:13.5833,km:0,src:"ACLED",type:"CONFLICT",tag:"CONFLICT: Banki",loc:"Banki",score:0.208,precision:"PRECISE"} | |
| ]; | |
| // Create Corridor Ribbons | |
| function createCorridors() { | |
| corridors.forEach(c => { | |
| const coords = corridorPaths[c.id]; | |
| if (!coords) return; | |
| // Create polyline | |
| viewer.entities.add({ | |
| id: c.id, | |
| name: c.name, | |
| description: `<h3>${c.name}</h3> | |
| <p><b>Risk:</b> ${c.risk} | <b>Distance:</b> ${c.distance}km | <b>Mode:</b> ${c.mode}</p> | |
| <p><b>Corridor ID:</b> ${c.id}</p> | |
| <p><b>Gap:</b> ${c.distance}km with 0% formal coverage</p> | |
| <p><b>Canoe Required:</b> ${c.mode === 'canoe' ? 'Yes' : 'No'}</p>`, | |
| polyline: { | |
| positions: Cesium.Cartesian3.fromDegreesArrayHeights(coords), | |
| width: c.width, | |
| material: new Cesium.PolylineGlowMaterialProperty({ | |
| glowPower: c.glow, | |
| color: RISK_COLORS[c.risk] | |
| }), | |
| clampToGround: false | |
| } | |
| }); | |
| }); | |
| } | |
| // Create Nodes for all corridors | |
| function createNodes() { | |
| // Node data: [lat, lng, alt, type, name, corridorId, km, country, precision] | |
| const nodes = [ | |
| // C-ET-001: Gambela → Malakal | |
| [8.25, 34.5833, 50, 'START', 'Gambela', 'C-ET-001', 0, 'ET', 'PRECISE'], | |
| [8.4008, 34.2081, 50, 'BORDER', 'Border Zone ET/SS', 'C-ET-001', 35, 'ET', 'SETTLEMENT'], | |
| [8.5677, 33.8703, 50, 'WAYPOINT', 'Waypoint C-ET-001-2', 'C-ET-001', 70, 'ET', 'SETTLEMENT'], | |
| [8.7387, 33.4757, 50, 'WAYPOINT', 'Waypoint C-ET-001-3', 'C-ET-001', 105, 'ET', 'SETTLEMENT'], | |
| [8.9070, 33.1118, 50, 'PHANTOM', 'Phantom Crossing C-ET-001', 'C-ET-001', 140, 'SS', 'INFERRED'], | |
| [9.0444, 32.7688, 50, 'WAYPOINT', 'Waypoint C-ET-001-5', 'C-ET-001', 175, 'SS', 'SETTLEMENT'], | |
| [9.2253, 32.3657, 50, 'WAYPOINT', 'Waypoint C-ET-001-6', 'C-ET-001', 210, 'SS', 'SETTLEMENT'], | |
| [9.3893, 32.0143, 50, 'WAYPOINT', 'Waypoint C-ET-001-7', 'C-ET-001', 245, 'SS', 'SETTLEMENT'], | |
| [9.5333, 31.65, 50, 'END', 'Malakal', 'C-ET-001', 280, 'SS', 'PRECISE'], | |
| // C-NG-001: Baga → Diffa | |
| [12.8167, 13.7833, 50, 'START', 'Baga', 'C-NG-001', 0, 'NG', 'PRECISE'], | |
| [12.9490, 13.4679, 50, 'BORDER', 'Border Zone NG/NE', 'C-NG-001', 37, 'NG', 'SETTLEMENT'], | |
| [13.0500, 13.2232, 50, 'PHANTOM', 'Phantom Crossing C-NG-001', 'C-NG-001', 74, 'NE', 'INFERRED'], | |
| [13.2123, 12.8940, 50, 'WAYPOINT', 'Waypoint C-NG-001-3', 'C-NG-001', 110, 'NE', 'SETTLEMENT'], | |
| [13.3167, 12.6167, 50, 'END', 'Diffa', 'C-NG-001', 147, 'NE', 'PRECISE'], | |
| // C-CD-001: Mbandaka → Impfondo | |
| [-0.0478, 18.2558, 50, 'START', 'Mbandaka', 'C-CD-001', 0, 'CD', 'PRECISE'], | |
| [0.3079, 18.2230, 50, 'BORDER', 'Border Zone CD/CG', 'C-CD-001', 38, 'CD', 'SETTLEMENT'], | |
| [0.6325, 18.1575, 50, 'PHANTOM', 'Phantom Crossing C-CD-001', 'C-CD-001', 76, 'CD', 'INFERRED'], | |
| [0.9764, 18.1304, 50, 'WAYPOINT', 'Waypoint C-CD-001-3', 'C-CD-001', 114, 'CG', 'SETTLEMENT'], | |
| [1.2734, 18.0988, 50, 'WAYPOINT', 'Waypoint C-CD-001-4', 'C-CD-001', 152, 'CG', 'SETTLEMENT'], | |
| [1.6306, 18.0594, 50, 'END', 'Impfondo', 'C-CD-001', 190, 'CG', 'PRECISE'], | |
| // C-CF-001: Birao → Nyala | |
| [10.2833, 22.7833, 50, 'START', 'Birao', 'C-CF-001', 0, 'CF', 'PRECISE'], | |
| [10.5124, 23.0229, 50, 'BORDER', 'Border Zone CF/SD', 'C-CF-001', 38, 'CF', 'SETTLEMENT'], | |
| [10.7105, 23.2891, 50, 'WAYPOINT', 'Waypoint C-CF-001-2', 'C-CF-001', 75, 'CF', 'SETTLEMENT'], | |
| [10.9526, 23.5850, 50, 'WAYPOINT', 'Waypoint C-CF-001-3', 'C-CF-001', 113, 'CF', 'SETTLEMENT'], | |
| [11.1632, 23.8500, 50, 'PHANTOM', 'Phantom Crossing C-CF-001', 'C-CF-001', 150, 'SD', 'INFERRED'], | |
| [11.3950, 24.1049, 50, 'WAYPOINT', 'Waypoint C-CF-001-5', 'C-CF-001', 188, 'SD', 'SETTLEMENT'], | |
| [11.6140, 24.3412, 50, 'WAYPOINT', 'Waypoint C-CF-001-6', 'C-CF-001', 225, 'SD', 'SETTLEMENT'], | |
| [11.8336, 24.6381, 50, 'WAYPOINT', 'Waypoint C-CF-001-7', 'C-CF-001', 263, 'SD', 'SETTLEMENT'], | |
| [12.05, 24.8833, 50, 'END', 'Nyala', 'C-CF-001', 300, 'SD', 'PRECISE'], | |
| // C-GL-001: Goma → Gisenyi | |
| [-1.6833, 29.2333, 50, 'START', 'Goma', 'C-GL-001', 0, 'CD', 'PRECISE'], | |
| [-1.6903, 29.2697, 50, 'PHANTOM', 'Phantom Crossing C-GL-001', 'C-GL-001', 2, 'RW', 'INFERRED'], | |
| [-1.7, 29.2667, 50, 'END', 'Gisenyi', 'C-GL-001', 4, 'RW', 'PRECISE'], | |
| // C-CD-002: Gbadolite → Bangui | |
| [4.2833, 21.0167, 50, 'START', 'Gbadolite', 'C-CD-002', 0, 'CD', 'PRECISE'], | |
| [4.3157, 20.7319, 50, 'BORDER', 'Border Zone CD/CF', 'C-CD-002', 35, 'CD', 'SETTLEMENT'], | |
| [4.2801, 20.3842, 50, 'WAYPOINT', 'Waypoint C-CD-002-2', 'C-CD-002', 70, 'CD', 'SETTLEMENT'], | |
| [4.3306, 20.1108, 50, 'WAYPOINT', 'Waypoint C-CD-002-3', 'C-CD-002', 105, 'CD', 'SETTLEMENT'], | |
| [4.3292, 19.7991, 50, 'PHANTOM', 'Phantom Crossing C-CD-002', 'C-CD-002', 140, 'CF', 'INFERRED'], | |
| [4.3163, 19.5079, 50, 'WAYPOINT', 'Waypoint C-CD-002-5', 'C-CD-002', 175, 'CF', 'SETTLEMENT'], | |
| [4.3542, 19.1945, 50, 'WAYPOINT', 'Waypoint C-CD-002-6', 'C-CD-002', 210, 'CF', 'SETTLEMENT'], | |
| [4.3354, 18.8895, 50, 'WAYPOINT', 'Waypoint C-CD-002-7', 'C-CD-002', 245, 'CF', 'SETTLEMENT'], | |
| [4.3667, 18.5833, 50, 'END', 'Bangui', 'C-CD-002', 280, 'CF', 'PRECISE'], | |
| // C-ET-002: Dolo Ado → Baidoa | |
| [4.1833, 42.0667, 50, 'START', 'Dolo Ado', 'C-ET-002', 0, 'ET', 'PRECISE'], | |
| [3.9934, 42.3439, 50, 'BORDER', 'Border Zone ET/SO', 'C-ET-002', 35, 'ET', 'SETTLEMENT'], | |
| [3.8294, 42.6012, 50, 'WAYPOINT', 'Waypoint C-ET-002-2', 'C-ET-002', 70, 'ET', 'SETTLEMENT'], | |
| [3.6443, 42.8797, 50, 'PHANTOM', 'Phantom Crossing C-ET-002', 'C-ET-002', 105, 'SO', 'INFERRED'], | |
| [3.4561, 43.1219, 50, 'WAYPOINT', 'Waypoint C-ET-002-4', 'C-ET-002', 140, 'SO', 'SETTLEMENT'], | |
| [3.2831, 43.3886, 50, 'WAYPOINT', 'Waypoint C-ET-002-5', 'C-ET-002', 175, 'SO', 'SETTLEMENT'], | |
| [3.1167, 43.65, 50, 'END', 'Baidoa', 'C-ET-002', 210, 'SO', 'PRECISE'], | |
| // C-SS-002: Renk → Ed Damazin | |
| [11.7667, 32.7833, 50, 'START', 'Renk', 'C-SS-002', 0, 'SS', 'PRECISE'], | |
| [11.7647, 33.0720, 50, 'BORDER', 'Border Zone SS/SD', 'C-SS-002', 35, 'SS', 'SETTLEMENT'], | |
| [11.7466, 33.3905, 50, 'PHANTOM', 'Phantom Crossing C-SS-002', 'C-SS-002', 70, 'SS', 'INFERRED'], | |
| [11.7421, 33.7326, 50, 'WAYPOINT', 'Waypoint C-SS-002-3', 'C-SS-002', 105, 'SD', 'SETTLEMENT'], | |
| [11.7580, 34.0386, 50, 'WAYPOINT', 'Waypoint C-SS-002-4', 'C-SS-002', 140, 'SD', 'SETTLEMENT'], | |
| [11.7667, 34.35, 50, 'END', 'Ed Damazin', 'C-SS-002', 175, 'SD', 'PRECISE'], | |
| // C-ET-003: Humera → Kassala | |
| [14.3, 36.6333, 50, 'START', 'Humera', 'C-ET-003', 0, 'ET', 'PRECISE'], | |
| [14.5750, 36.5867, 50, 'BORDER', 'Border Zone ET/SD', 'C-ET-003', 39, 'ET', 'SETTLEMENT'], | |
| [14.8646, 36.5130, 50, 'PHANTOM', 'Phantom Crossing C-ET-003', 'C-ET-003', 78, 'SD', 'INFERRED'], | |
| [15.1642, 36.4728, 50, 'WAYPOINT', 'Waypoint C-ET-003-3', 'C-ET-003', 116, 'SD', 'SETTLEMENT'], | |
| [15.45, 36.4, 50, 'END', 'Kassala', 'C-ET-003', 155, 'SD', 'PRECISE'], | |
| // C-NG-002: Gwoza → Mora | |
| [11.0833, 13.6833, 50, 'START', 'Gwoza', 'C-NG-002', 0, 'NG', 'PRECISE'], | |
| [11.0299, 13.8960, 50, 'PHANTOM', 'Phantom Crossing C-NG-002', 'C-NG-002', 26, 'CM', 'INFERRED'], | |
| [11.0167, 14.1333, 50, 'END', 'Mora', 'C-NG-002', 52, 'CM', 'PRECISE'], | |
| // C-SD-001: Metema → Gallabat | |
| [12.95, 36.2, 50, 'START', 'Metema', 'C-SD-001', 0, 'SD', 'PRECISE'], | |
| [12.9741, 36.1677, 50, 'PHANTOM', 'Phantom Crossing C-SD-001', 'C-SD-001', 4, 'ET', 'INFERRED'], | |
| [12.9667, 36.15, 50, 'END', 'Gallabat', 'C-SD-001', 8, 'ET', 'PRECISE'], | |
| // C-SS-003: Kapoeta → Lodwar | |
| [4.7667, 33.5833, 50, 'START', 'Kapoeta', 'C-SS-003', 0, 'SS', 'PRECISE'], | |
| [4.5274, 33.8537, 50, 'BORDER', 'Border Zone SS/KE', 'C-SS-003', 35, 'SS', 'SETTLEMENT'], | |
| [4.3103, 34.1758, 50, 'WAYPOINT', 'Waypoint C-SS-003-2', 'C-SS-003', 70, 'SS', 'SETTLEMENT'], | |
| [4.0643, 34.4249, 50, 'PHANTOM', 'Phantom Crossing C-SS-003', 'C-SS-003', 105, 'SS', 'INFERRED'], | |
| [3.8198, 34.7470, 50, 'WAYPOINT', 'Waypoint C-SS-003-4', 'C-SS-003', 140, 'KE', 'SETTLEMENT'], | |
| [3.6073, 35.0036, 50, 'WAYPOINT', 'Waypoint C-SS-003-5', 'C-SS-003', 175, 'KE', 'SETTLEMENT'], | |
| [3.3284, 35.3191, 50, 'WAYPOINT', 'Waypoint C-SS-003-6', 'C-SS-003', 210, 'KE', 'SETTLEMENT'], | |
| [3.1167, 35.6, 50, 'END', 'Lodwar', 'C-SS-003', 245, 'KE', 'PRECISE'], | |
| // C-NG-003: Banki → Amchide | |
| [11.05, 13.5833, 50, 'START', 'Banki', 'C-NG-003', 0, 'NG', 'PRECISE'], | |
| [10.8955, 13.9212, 50, 'PHANTOM', 'Phantom Crossing C-NG-003', 'C-NG-003', 37, 'CM', 'INFERRED'], | |
| [10.75, 14.2167, 50, 'END', 'Amchide', 'C-NG-003', 73, 'CM', 'PRECISE'] | |
| ]; | |
| nodes.forEach(n => { | |
| const [lat, lng, alt, type, name, cid, km, country, precision] = n; | |
| let color, pixelSize, labelText, labelOffset, labelColor; | |
| switch(type) { | |
| case 'START': | |
| color = Cesium.Color.LIME; | |
| pixelSize = 12; | |
| labelText = name; | |
| labelOffset = -14; | |
| labelColor = Cesium.Color.WHITE; | |
| break; | |
| case 'END': | |
| color = Cesium.Color.RED; | |
| pixelSize = 12; | |
| labelText = name; | |
| labelOffset = -14; | |
| labelColor = Cesium.Color.WHITE; | |
| break; | |
| case 'PHANTOM': | |
| color = Cesium.Color.GOLD; | |
| pixelSize = 14; | |
| labelText = name; | |
| labelOffset = -16; | |
| labelColor = Cesium.Color.GOLD; | |
| break; | |
| case 'BORDER': | |
| color = Cesium.Color.ORANGE; | |
| pixelSize = 6; | |
| labelText = undefined; | |
| break; | |
| default: // WAYPOINT | |
| color = Cesium.Color.WHITE.withAlpha(0.6); | |
| pixelSize = 6; | |
| labelText = undefined; | |
| } | |
| viewer.entities.add({ | |
| id: `${cid}-${type}-${km}`, | |
| name: name, | |
| description: `<b>Type:</b> ${type}<br> | |
| <b>Corridor:</b> ${cid}<br> | |
| <b>KM:</b> ${km}<br> | |
| <b>Country:</b> ${country}<br> | |
| <b>Precision:</b> ${precision}<br> | |
| ${type === 'PHANTOM' ? '<b style="color:gold">⚡ PHANTOM POE — No formal border post</b>' : ''}`, | |
| position: Cesium.Cartesian3.fromDegrees(lng, lat, alt), | |
| point: { | |
| pixelSize: pixelSize, | |
| color: color, | |
| outlineColor: Cesium.Color.BLACK, | |
| outlineWidth: type === 'PHANTOM' ? 2 : 1, | |
| scaleByDistance: type === 'PHANTOM' ? new Cesium.NearFarScalar(1000, 2.0, 500000, 0.8) : undefined | |
| }, | |
| label: labelText ? { | |
| text: labelText, | |
| font: type === 'PHANTOM' ? 'bold 13pt sans-serif' : '11pt sans-serif', | |
| style: Cesium.LabelStyle.FILL_AND_OUTLINE, | |
| outlineWidth: 2, | |
| verticalOrigin: Cesium.VerticalOrigin.BOTTOM, | |
| pixelOffset: new Cesium.Cartesian2(0, labelOffset), | |
| fillColor: labelColor, | |
| outlineColor: Cesium.Color.BLACK, | |
| distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 300000) | |
| } : undefined | |
| }); | |
| }); | |
| } | |
| // Cascade Animation System | |
| let cascadeEntities = []; | |
| let cascadeFrame = 0; | |
| let cascadePlaying = false; | |
| let cascadeCorridor = null; | |
| let cascadeInterval = null; | |
| function toggleCascadeMenu() { | |
| const menu = document.getElementById('cascade-menu'); | |
| menu.classList.toggle('hidden'); | |
| } | |
| function startCascade(corridorId) { | |
| // Hide menu | |
| document.getElementById('cascade-menu').classList.add('hidden'); | |
| // Clear previous cascade | |
| clearCascade(); | |
| // Get corridor info | |
| const corridor = corridors.find(c => c.id === corridorId); | |
| if (!corridor) return; | |
| cascadeCorridor = corridorId; | |
| cascadeFrame = 0; | |
| // Filter and sort evidence for this corridor | |
| const corridorEvidence = EVIDENCE | |
| .filter(e => e.cid === corridorId) | |
| .sort((a, b) => a.day - b.day || a.km - b.km); | |
| // If no specific evidence, generate synthetic signals | |
| const signals = corridorEvidence.length > 0 ? corridorEvidence : generateSyntheticSignals(corridorId); | |
| // Group by day | |
| const days = {}; | |
| signals.forEach(s => { | |
| if (!days[s.day]) days[s.day] = []; | |
| days[s.day].push(s); | |
| }); | |
| const sortedDays = Object.keys(days).map(Number).sort((a, b) => a - b); | |
| const totalDays = sortedDays.length; | |
| // Show info panel | |
| const info = document.getElementById('cascade-info'); | |
| info.classList.remove('hidden'); | |
| document.getElementById('cascade-corridor-name').textContent = corridor.name; | |
| document.getElementById('cascade-day').textContent = `0/${totalDays}`; | |
| document.getElementById('cascade-score').textContent = '0.000'; | |
| document.getElementById('cascade-progress').style.width = '0%'; | |
| // Fly to corridor | |
| flyToCorridor(corridorId); | |
| let cumulativeScore = 0; | |
| let phantomDetected = false; | |
| cascadePlaying = true; | |
| cascadeInterval = setInterval(() => { | |
| if (cascadeFrame >= sortedDays.length) { | |
| stopCascade(); | |
| return; | |
| } | |
| const day = sortedDays[cascadeFrame]; | |
| const daySignals = days[day]; | |
| const sources = new Set(); | |
| daySignals.forEach(sig => { | |
| sources.add(sig.src); | |
| cumulativeScore = Math.min(1, cumulativeScore + sig.score * 0.08); | |
| // Create signal marker | |
| const entity = viewer.entities.add({ | |
| position: Cesium.Cartesian3.fromDegrees(sig.lng, sig.lat, 200), | |
| point: { | |
| pixelSize: 10 + sig.score * 15, | |
| color: SOURCE_COLORS[sig.src] || Cesium.Color.WHITE, | |
| outlineColor: Cesium.Color.WHITE, | |
| outlineWidth: 2, | |
| scaleByDistance: new Cesium.NearFarScalar(500, 2.5, 200000, 0.7) | |
| }, | |
| label: { | |
| text: sig.tag, | |
| font: 'bold 12pt sans-serif', | |
| style: Cesium.LabelStyle.FILL_AND_OUTLINE, | |
| outlineWidth: 2, | |
| verticalOrigin: Cesium.VerticalOrigin.BOTTOM, | |
| pixelOffset: new Cesium.Cartesian2(0, -20), | |
| fillColor: Cesium.Color.WHITE, | |
| outlineColor: Cesium.Color.BLACK, | |
| distanceDisplayCondition: new Cesium.DistanceDisplayCondition(0, 150000), | |
| showBackground: true, | |
| backgroundColor: Cesium.Color.BLACK.withAlpha(0.7) | |
| } | |
| }); | |
| cascadeEntities.push(entity); | |
| }); | |
| // Phantom POE detection moment | |
| if (sources.size >= 2 && cumulativeScore > 0.3 && !phantomDetected) { | |
| phantomDetected = true; | |
| const pivotSig = daySignals[0]; | |
| const canvas = createPhantomPOECanvas(); | |
| const billboardOpts = canvas ? { | |
| image: canvas, | |
| width: 64, | |
| height: 64, | |
| scaleByDistance: new Cesium.NearFarScalar(500, 3, 300000, 0.5) | |
| } : undefined; | |
| const phantomEntity = viewer.entities.add({ | |
| position: Cesium.Cartesian3.fromDegrees(pivotSig.lng, pivotSig.lat, 500), | |
| billboard: billboardOpts, | |
| label: { | |
| text: '⚡ PHANTOM POE DETECTED', | |
| font: 'bold 16pt sans-serif', | |
| style: Cesium.LabelStyle.FILL_AND_OUTLINE, | |
| outlineWidth: 3, | |
| verticalOrigin: Cesium.VerticalOrigin.BOTTOM, | |
| pixelOffset: new Cesium.Cartesian2(0, -40), | |
| fillColor: Cesium.Color.GOLD, | |
| outlineColor: Cesium.Color.BLACK, | |
| showBackground: true, | |
| backgroundColor: Cesium.Color.BLACK.withAlpha(0.85) | |
| } | |
| }); | |
| cascadeEntities.push(phantomEntity); | |
| } | |
| // Update UI | |
| document.getElementById('cascade-day').textContent = `${cascadeFrame + 1}/${totalDays}`; | |
| document.getElementById('cascade-score').textContent = cumulativeScore.toFixed(3); | |
| document.getElementById('cascade-progress').style.width = `${((cascadeFrame + 1) / totalDays) * 100}%`; | |
| cascadeFrame++; | |
| }, 2000); // 2 seconds per day | |
| } | |
| function generateSyntheticSignals(cid) { | |
| // Generate synthetic signals if no real data | |
| const path = corridorPaths[cid]; | |
| const signals = []; | |
| if (!path || path.length < 3) return signals; | |
| const numPoints = Math.floor(path.length / 3); | |
| const corridor = corridors.find(c => c.id === cid); | |
| const maxDist = corridor ? corridor.distance : 280; | |
| for (let i = 0; i < 7; i++) { // 7 days | |
| const idx = Math.min(Math.floor((i / 6) * (numPoints - 1)), numPoints - 1); | |
| const lat = path[idx * 3 + 1]; | |
| const lng = path[idx * 3]; | |
| const km = Math.floor((i / 6) * maxDist); | |
| const sources = ['ACLED', 'IOM-DTM', 'DHIS2']; | |
| const types = ['CONFLICT', 'DISPLACEMENT', 'HEALTH']; | |
| if (lat && lng) { | |
| signals.push({ | |
| cid: cid, | |
| day: i, | |
| lat: lat, | |
| lng: lng, | |
| km: km, | |
| src: sources[i % 3], | |
| type: types[i % 3], | |
| tag: `${types[i % 3]}: Day ${i}`, | |
| loc: `Waypoint ${i}`, | |
| score: 0.3 + Math.random() * 0.4, | |
| precision: i === 0 ? 'PRECISE' : (i === 6 ? 'PRECISE' : 'SETTLEMENT') | |
| }); | |
| } | |
| } | |
| return signals; | |
| } | |
| function stopCascade() { | |
| cascadePlaying = false; | |
| if (cascadeInterval) clearInterval(cascadeInterval); | |
| cascadeInterval = null; | |
| } | |
| function clearCascade() { | |
| stopCascade(); | |
| cascadeEntities.forEach(e => viewer.entities.remove(e)); | |
| cascadeEntities = []; | |
| cascadeFrame = 0; | |
| document.getElementById('cascade-info').classList.add('hidden'); | |
| } | |
| function createPhantomPOECanvas() { | |
| try { | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = 64; | |
| canvas.height = 64; | |
| const ctx = canvas.getContext('2d'); | |
| if (!ctx) return null; | |
| // Gold glow | |
| const grad = ctx.createRadialGradient(32, 32, 5, 32, 32, 30); | |
| grad.addColorStop(0, 'rgba(245, 158, 11, 1)'); | |
| grad.addColorStop(0.5, 'rgba(245, 158, 11, 0.5)'); | |
| grad.addColorStop(1, 'rgba(245, 158, 11, 0)'); | |
| ctx.fillStyle = grad; | |
| ctx.fillRect(0, 0, 64, 64); | |
| // Lightning bolt | |
| ctx.fillStyle = '#FFFFFF'; | |
| ctx.font = 'bold 36px sans-serif'; | |
| ctx.textAlign = 'center'; | |
| ctx.textBaseline = 'middle'; | |
| ctx.fillText('⚡', 32, 32); | |
| return canvas; | |
| } catch (e) { | |
| console.error('Error creating canvas:', e); | |
| return null; | |
| } | |
| } | |
| // Fly to functions | |
| function flyToCorridor(id) { | |
| const coords = corridorPaths[id]; | |
| if (!coords) return; | |
| // Calculate center | |
| let sumLat = 0, sumLng = 0, count = 0; | |
| for (let i = 0; i < coords.length; i += 3) { | |
| sumLng += coords[i]; | |
| sumLat += coords[i + 1]; | |
| count++; | |
| } | |
| const centerLat = sumLat / count; | |
| const centerLng = sumLng / count; | |
| // Calculate appropriate height based on corridor length | |
| const c = corridors.find(x => x.id === id); | |
| const height = c.distance * 50; // Rough estimation | |
| viewer.camera.flyTo({ | |
| destination: Cesium.Cartesian3.fromDegrees(centerLng, centerLat, height), | |
| orientation: { heading: 0, pitch: Cesium.Math.toRadians(-55), roll: 0 }, | |
| duration: 2 | |
| }); | |
| } | |
| function resetView() { | |
| viewer.camera.flyTo({ | |
| destination: Cesium.Cartesian3.fromDegrees(25, 5, 5000000), | |
| orientation: { heading: 0, pitch: Cesium.Math.toRadians(-90), roll: 0 }, | |
| duration: 2 | |
| }); | |
| } | |
| // Initialize | |
| createCorridors(); | |
| createNodes(); | |
| // Initial view | |
| resetView(); | |
| // Click outside to close cascade menu | |
| document.addEventListener('click', (e) => { | |
| const menu = document.getElementById('cascade-menu'); | |
| const btn = e.target.closest('button'); | |
| let isToggleBtn = false; | |
| if (btn && btn.onclick && typeof btn.onclick === 'function') { | |
| const fnString = btn.onclick.toString(); | |
| isToggleBtn = fnString.includes('toggleCascadeMenu'); | |
| } | |
| if (!menu.contains(e.target) && !isToggleBtn) { | |
| menu.classList.add('hidden'); | |
| } | |
| }); | |
| // Initialize icons after a short delay to ensure DOM is ready | |
| setTimeout(() => { | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| }, 100); | |
| console.log('PHANTOM POE MAP initialized: 14 corridors, 91 nodes ready'); | |
| </script> | |
| </body> | |
| </html> |