Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>RAD-X v10.0 • Fused Intelligence Briefing</title> | |
<!-- Mapbox CSS --> | |
<link href="https://api.mapbox.com/mapbox-gl-js/v3.4.0/mapbox-gl.css" rel="stylesheet" /> | |
<!-- Tailwind CSS --> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<!-- Font Awesome for icons --> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<script> | |
tailwind.config = { | |
theme: { | |
extend: { | |
colors: { | |
'radx-blue': '#00b3ff', | |
'radx-dark': '#0b0f14', | |
'radx-panel': 'rgba(15,20,25,0.92)', | |
'radx-highlight': '#7ee0ff', | |
'radx-danger': '#ff4d4d', | |
'radx-warning': '#ffa500', | |
'radx-success': '#00ffa3' | |
} | |
} | |
} | |
} | |
</script> | |
<style> | |
:root { | |
--ticker-height: 54px; | |
} | |
body { | |
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
background-color: #000; | |
color: #e7f3ff; | |
overflow: hidden; | |
} | |
#map { | |
position: fixed; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: var(--ticker-height); | |
z-index: 1; | |
} | |
#chat-panel { | |
position: fixed; | |
top: 16px; | |
bottom: calc(var(--ticker-height) + 16px); | |
right: 16px; | |
width: 380px; | |
display: flex; | |
flex-direction: column; | |
z-index: 80; | |
background: rgba(15, 20, 25, 0.92); | |
border: 1px solid rgba(0, 179, 255, 0.3); | |
border-radius: 12px; | |
padding: 14px; | |
backdrop-filter: blur(10px); | |
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); | |
transition: transform 0.35s ease, opacity 0.25s ease; | |
} | |
#chat-panel.minimized { | |
transform: translateX(calc(100% + 16px)); | |
} | |
#chat-header { | |
display: flex; | |
align-items: center; | |
justify-content: space-between; | |
gap: 8px; | |
margin-bottom: 8px; | |
} | |
#chat-header h2 { | |
margin: 0; | |
font-size: 16px; | |
color: #7ee0ff; | |
letter-spacing: 0.04em; | |
} | |
.chat-btn { | |
cursor: pointer; | |
border: 1px solid rgba(0, 179, 255, 0.35); | |
background: transparent; | |
color: #9ee7ff; | |
font-size: 12px; | |
border-radius: 999px; | |
padding: 6px 10px; | |
} | |
#chat-log { | |
flex: 1; | |
overflow-y: auto; | |
padding-right: 6px; | |
scrollbar-width: thin; | |
scrollbar-color: rgba(0, 179, 255, 0.3) transparent; | |
} | |
#chat-log::-webkit-scrollbar { | |
width: 6px; | |
} | |
#chat-log::-webkit-scrollbar-track { | |
background: transparent; | |
} | |
#chat-log::-webkit-scrollbar-thumb { | |
background-color: rgba(0, 179, 255, 0.3); | |
border-radius: 3px; | |
} | |
.msg { | |
font-size: 13px; | |
line-height: 1.5; | |
margin: 8px 0; | |
color: #ced8e4; | |
} | |
.msg.user { | |
color: #7ee0ff; | |
} | |
.msg.sys { | |
color: #a3e635; | |
} | |
#chat-form { | |
display: flex; | |
gap: 8px; | |
margin-top: 8px; | |
} | |
#chat-input { | |
flex: 1; | |
padding: 0.7rem 0.9rem; | |
color: #fff; | |
background: rgba(0, 0, 0, 0.35); | |
border: 1px solid rgba(0, 179, 255, 0.45); | |
border-radius: 8px; | |
outline: none; | |
transition: border 0.2s, box-shadow 0.2s; | |
} | |
#chat-input:focus { | |
border-color: #00b3ff; | |
box-shadow: 0 0 8px rgba(0, 179, 255, 0.3); | |
} | |
#chat-send { | |
padding: 0.7rem 0.9rem; | |
color: #001018; | |
background: #7ee0ff; | |
border: none; | |
border-radius: 8px; | |
cursor: pointer; | |
font-weight: 600; | |
} | |
#story-panel { | |
position: fixed; | |
top: 16px; | |
left: 16px; | |
width: 420px; | |
max-height: calc(100vh - var(--ticker-height) - 32px); | |
overflow-y: auto; | |
z-index: 70; | |
background: #0b0f14; | |
border: 1px solid rgba(0, 179, 255, 0.18); | |
border-radius: 12px; | |
transition: transform 0.35s ease, opacity 0.25s ease; | |
opacity: 0; | |
transform: translateX(-450px); | |
pointer-events: none; | |
} | |
#story-panel.visible { | |
opacity: 1; | |
transform: translateX(0); | |
pointer-events: all; | |
} | |
#story-header { | |
position: sticky; | |
top: 0; | |
background: #0b0f14; | |
z-index: 1; | |
padding: 12px 16px; | |
border-bottom: 1px solid rgba(0, 179, 255, 0.12); | |
display: flex; | |
justify-content: space-between; | |
align-items: center; | |
} | |
#story-header h3 { | |
margin: 0; | |
color: #7ee0ff; | |
font-size: 14px; | |
letter-spacing: 0.08em; | |
text-transform: uppercase; | |
} | |
#story-close { | |
cursor: pointer; | |
background: transparent; | |
border: 1px solid rgba(0, 179, 255, 0.35); | |
color: #9ee7ff; | |
padding: 6px 10px; | |
border-radius: 999px; | |
font-size: 12px; | |
} | |
.chapter { | |
padding: 20px 22px; | |
border-bottom: 1px solid rgba(0, 179, 255, 0.12); | |
opacity: 0.3; | |
transition: opacity 0.25s ease; | |
} | |
.chapter.active { | |
opacity: 1; | |
} | |
.chapter h4 { | |
margin: 0 0 6px 0; | |
color: #f9b4b4; | |
letter-spacing: 0.03em; | |
} | |
.chapter p { | |
margin: 0; | |
color: #c6d3e0; | |
font-size: 13px; | |
line-height: 1.5; | |
} | |
.hud-panel { | |
background: rgba(15, 20, 25, 0.92); | |
border: 1px solid rgba(0, 179, 255, 0.3); | |
border-radius: 12px; | |
padding: 14px; | |
backdrop-filter: blur(8px); | |
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5); | |
} | |
#ticker { | |
position: fixed; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
height: var(--ticker-height); | |
display: flex; | |
align-items: center; | |
gap: 12px; | |
background: rgba(10, 12, 16, 0.96); | |
border-top: 1px solid rgba(0, 179, 255, 0.25); | |
z-index: 50; | |
overflow: hidden; | |
pointer-events: none; | |
} | |
#ticker .label { | |
color: #00b3ff; | |
font-size: 12px; | |
letter-spacing: 0.12em; | |
text-transform: uppercase; | |
padding: 0 14px; | |
border-right: 1px solid rgba(0, 179, 255, 0.2); | |
} | |
#ticker .marquee { | |
position: relative; | |
white-space: nowrap; | |
will-change: transform; | |
} | |
.tick-item { | |
display: inline-flex; | |
gap: 8px; | |
align-items: center; | |
margin-right: 36px; | |
color: #c9d6df; | |
font-size: 13px; | |
} | |
.badge { | |
font-size: 11px; | |
padding: 2px 6px; | |
border-radius: 6px; | |
background: rgba(0, 179, 255, 0.12); | |
border: 1px solid rgba(0, 179, 255, 0.35); | |
color: #9ee7ff; | |
} | |
#train-panel { | |
position: fixed; | |
inset: 0; | |
z-index: 90; | |
display: none; | |
align-items: center; | |
justify-content: center; | |
background: rgba(0, 0, 0, 0.66); | |
backdrop-filter: blur(4px); | |
} | |
#train-card { | |
width: min(900px, 92vw); | |
max-height: 88vh; | |
overflow: hidden; | |
background: rgba(15, 20, 25, 0.97); | |
border: 1px solid rgba(0, 179, 255, 0.3); | |
border-radius: 14px; | |
box-shadow: 0 12px 48px rgba(0, 0, 0, 0.6); | |
display: flex; | |
flex-direction: column; | |
} | |
#tooltip { | |
position: fixed; | |
transform: translate(-50%, -110%); | |
pointer-events: none; | |
display: none; | |
z-index: 60; | |
min-width: 320px; | |
} | |
#tooltip-title { | |
color: #ff9999; | |
font-weight: 700; | |
font-size: 14px; | |
letter-spacing: 0.08em; | |
text-transform: uppercase; | |
} | |
#tooltip-info { | |
font-size: 13px; | |
margin-top: 6px; | |
color: #cfe2f0; | |
white-space: pre-line; | |
} | |
#tooltip-coords { | |
margin-top: 8px; | |
padding-top: 6px; | |
border-top: 1px solid rgba(0, 179, 255, 0.15); | |
font-size: 12px; | |
color: #89a; | |
} | |
#tooltip .row { | |
margin-top: 8px; | |
} | |
#tooltip .pill { | |
display: inline-block; | |
padding: 2px 6px; | |
border: 1px solid rgba(0, 179, 255, 0.35); | |
border-radius: 999px; | |
font-size: 11px; | |
color: #9ee7ff; | |
margin-right: 6px; | |
margin-top: 4px; | |
} | |
#tooltip .kv { | |
font-size: 12px; | |
color: #cfe2f0; | |
} | |
#tooltip .meter-s { | |
height: 6px; | |
background: #06202a; | |
border-radius: 999px; | |
overflow: hidden; | |
margin-top: 6px; | |
} | |
#tooltip .meter-s > span { | |
display: block; | |
height: 100%; | |
background: linear-gradient(90deg, #ff4d4d, #ffa500, #00ffa3); | |
width: 0; | |
} | |
.status-indicator { | |
display: inline-block; | |
width: 10px; | |
height: 10px; | |
border-radius: 50%; | |
margin-right: 6px; | |
} | |
.status-active { | |
background-color: #00ffa3; | |
box-shadow: 0 0 5px #00ffa3; | |
} | |
.status-warning { | |
background-color: #ffa500; | |
box-shadow: 0 0 5px #ffa500; | |
} | |
.status-critical { | |
background-color: #ff4d4d; | |
box-shadow: 0 0 5px #ff4d4d; | |
} | |
.map-overlay { | |
position: absolute; | |
top: 20px; | |
left: 20px; | |
z-index: 20; | |
background: rgba(15, 20, 25, 0.8); | |
border: 1px solid rgba(0, 179, 255, 0.3); | |
border-radius: 8px; | |
padding: 12px; | |
color: white; | |
} | |
.legend-item { | |
display: flex; | |
align-items: center; | |
margin-bottom: 8px; | |
} | |
.legend-color { | |
width: 16px; | |
height: 16px; | |
border-radius: 50%; | |
margin-right: 8px; | |
} | |
.hotspot-marker { | |
width: 20px; | |
height: 20px; | |
background-color: #e00; | |
border-radius: 50%; | |
box-shadow: 0 0 10px #f00; | |
border: 2px solid #fff; | |
position: relative; | |
animation: pulse 2s infinite; | |
z-index: 10; | |
} | |
@keyframes pulse { | |
0% { transform: scale(1); opacity: 1; } | |
50% { transform: scale(1.2); opacity: 0.7; } | |
100% { transform: scale(1); opacity: 1; } | |
} | |
.pulse-circle { | |
position: absolute; | |
top: 50%; | |
left: 50%; | |
transform: translate(-50%, -50%); | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: rgba(255, 0, 0, 0.5); | |
animation: pulseRing 2s infinite; | |
} | |
@keyframes pulseRing { | |
0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0.8; } | |
100% { transform: translate(-50%, -50%) scale(2); opacity: 0; } | |
} | |
</style> | |
</head> | |
<body> | |
<!-- Map container --> | |
<div id="map"></div> | |
<!-- Map overlay legend --> | |
<div class="map-overlay hud-panel"> | |
<h3 class="text-sm font-bold text-radx-highlight mb-2">INTELLIGENCE OVERLAY</h3> | |
<div class="legend-item"> | |
<div class="legend-color bg-radx-danger"></div> | |
<span class="text-xs">Contamination Source</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color bg-radx-warning"></div> | |
<span class="text-xs">Infrastructure Weakness</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color bg-yellow-300"></div> | |
<span class="text-xs">Informal Vendor Hub</span> | |
</div> | |
<div class="legend-item"> | |
<div class="legend-color bg-radx-danger" style="opacity: 0.3;"></div> | |
<span class="text-xs">High-Risk Zone</span> | |
</div> | |
</div> | |
<!-- Story panel --> | |
<div id="story-panel"> | |
<div id="story-header"> | |
<h3>CHOLERA BRIEFING - ROOT CAUSE</h3> | |
<button id="story-close" class="story-close-btn"> | |
<i class="fas fa-times"></i> | |
</button> | |
</div> | |
<div id="story-body"> | |
<div class="chapter active"> | |
<h4>Nairobi Cholera Outbreak Analysis</h4> | |
<p>This briefing examines the primary factors contributing to the cholera outbreak in Nairobi, focusing on contamination sources, infrastructure vulnerabilities, and high-risk zones.</p> | |
</div> | |
<div class="chapter"> | |
<h4>Mathare River Junction</h4> | |
<p>A leaky sewer-water junction in Mathare Valley has been identified as a high-probability contamination source. Heavy rainfall during recent months has exacerbated this issue.</p> | |
</div> | |
<div class="chapter"> | |
<h4>Aged Pipeline Corridor</h4> | |
<p>A structurally compromised pipeline segment crossing untreated drainage poses a risk of infiltration in the informal settlements near Kibera.</p> | |
</div> | |
<div class="chapter"> | |
<h4>Kibera Borehole Cluster</h4> | |
<p>A high-density vendor area with decentralized quality control systems has been identified as vulnerable to single-source outbreaks.</p> | |
</div> | |
<div class="chapter"> | |
<h4>Mathare-Mukuru Corridor</h4> | |
<p>Persistent infrastructure gaps in this area create a permanent high-risk environment for waterborne diseases.</p> | |
</div> | |
</div> | |
</div> | |
<!-- Chat panel --> | |
<div id="chat-panel"> | |
<div id="chat-header"> | |
<h2>RAD-X <span class="text-xs">v10.0</span></h2> | |
<button class="chat-btn" id="chat-toggle"> | |
<i class="fas fa-minus"></i> | |
</button> | |
</div> | |
<div id="chat-log"> | |
<div class="msg sys">RAD-X Matrix initialized. Ready for intelligence briefing.</div> | |
<div class="msg sys">Issue your target query. I will animate, explain, and simulate in 3D.</div> | |
<div class="msg user">> Trace cholera origin in Kibera, Nairobi</div> | |
<div class="msg sys">Analyzing cholera outbreak patterns in Kibera zone. Primary contamination sources identified...</div> | |
</div> | |
<form id="chat-form"> | |
<input type="text" id="chat-input" placeholder="Enter query..."> | |
<button type="submit" id="chat-send"> | |
<i class="fas fa-paper-plane"></i> | |
</button> | |
</form> | |
</div> | |
<!-- Ticker --> | |
<div id="ticker"> | |
<div class="label">INTELLIGENCE FEED</div> | |
<div class="marquee"> | |
<div class="tick-item"> | |
<span class="badge">CHOLERA</span> | |
<span>Kibera: 142 cases reported in last 24h</span> | |
</div> | |
<div class="tick-item"> | |
<span class="badge">OUTBREAK</span> | |
<span>Mathare: Contamination source confirmed</span> | |
</div> | |
<div class="tick-item"> | |
<span class="badge">INFRASTRUCTURE</span> | |
<span>Pipeline repair in progress near Kibera</span> | |
</div> | |
<div class="tick-item"> | |
<span class="badge">PREVENTIVE</span> | |
<span>Water treatment units deployed in Mukuru</span> | |
</div> | |
</div> | |
</div> | |
<!-- Tooltip --> | |
<div id="tooltip" class="hud-panel"> | |
<div id="tooltip-title">Mathare River Junction</div> | |
<div id="tooltip-info">Leaky sewer-water junction identified as a high-probability contamination source.</div> | |
<div id="tooltip-coords">36.8565, -1.2820</div> | |
<div class="row"> | |
<div class="kv">Category: <span class="text-radx-highlight">Contamination Source</span></div> | |
</div> | |
<div class="row"> | |
<div class="kv">Severity:</div> | |
<div class="meter-s"> | |
<span style="width: 90%"></span> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="kv">Confidence:</div> | |
<div class="meter-s"> | |
<span style="width: 85%"></span> | |
</div> | |
</div> | |
<div class="row"> | |
<div class="kv">Contributors:</div> | |
<span class="pill">sewer leak</span> | |
<span class="pill">heavy rainfall</span> | |
</div> | |
</div> | |
<!-- Mapbox GL JS --> | |
<script src="https://api.mapbox.com/mapbox-gl-js/v3.14.0/mapbox-gl.js"></script> | |
<!-- Turf.js for geospatial calculations --> | |
<script src='https://npmcdn.com/@turf/turf/turf.min.js'></script> | |
<!-- Speech Synthesis --> | |
<script> | |
// Speech synthesis implementation | |
const speechSynthesis = { | |
supported: typeof window !== "undefined" && "speechSynthesis" in window, | |
voices: [], | |
selectedVoice: null, | |
isSpeaking: false, | |
populateVoiceList() { | |
if (!this.supported) return; | |
const availableVoices = window.speechSynthesis.getVoices(); | |
if (availableVoices.length > 0) { | |
this.voices = availableVoices; | |
// Voice selection logic | |
const voicePriority = [ | |
(v) => v.lang === "en-ZA" && /male/i.test(v.name), | |
(v) => v.lang === "en-NG" && /male/i.test(v.name), | |
(v) => v.lang.startsWith("en-") && /male/i.test(v.name), | |
(v) => /male/i.test(v.name) && v.default, | |
(v) => v.default, | |
]; | |
let foundVoice = null; | |
for (const check of voicePriority) { | |
const voice = availableVoices.find(check); | |
if (voice) { | |
foundVoice = voice; | |
break; | |
} | |
} | |
this.selectedVoice = foundVoice || availableVoices[0] || null; | |
} | |
}, | |
speak(text) { | |
if (!this.supported || !text || !this.selectedVoice) return; | |
if (window.speechSynthesis.speaking) { | |
window.speechSynthesis.cancel(); | |
} | |
const utterance = new SpeechSynthesisUtterance(text); | |
utterance.voice = this.selectedVoice; | |
utterance.pitch = 0.9; | |
utterance.rate = 1.05; | |
utterance.onstart = () => this.isSpeaking = true; | |
utterance.onend = () => this.isSpeaking = false; | |
utterance.onerror = (e) => { | |
console.error("Speech synthesis error:", e); | |
this.isSpeaking = false; | |
}; | |
window.speechSynthesis.speak(utterance); | |
}, | |
cancel() { | |
if (!this.supported) return; | |
window.speechSynthesis.cancel(); | |
this.isSpeaking = false; | |
} | |
}; | |
// Initialize speech synthesis | |
if (speechSynthesis.supported) { | |
speechSynthesis.populateVoiceList(); | |
if (window.speechSynthesis.onvoiceschanged !== undefined) { | |
window.speechSynthesis.onvoiceschanged = speechSynthesis.populateVoiceList; | |
} | |
} | |
</script> | |
<script> | |
// Mapbox access token - use your own token here | |
mapboxgl.accessToken = 'pk.eyJ1IjoiYWthbmltbzEiLCJhIjoiY2x4czNxbjU2MWM2eTJqc2gwNGIwaWhkMSJ9.jSwZdyaPa1dOHepNU5P71g'; | |
// Initialize map | |
const map = new mapboxgl.Map({ | |
container: 'map', | |
antialias: true, | |
attributionControl: false, | |
style: 'mapbox://styles/mapbox/satellite-streets-v12', | |
center: [18.4, 8.8], // Africa view | |
zoom: 2.5, | |
pitch: 0, | |
bearing: 0, | |
antialias: true | |
}); | |
// Ensure map container has proper dimensions | |
document.getElementById('map').style.width = '100%'; | |
document.getElementById('map').style.height = '100vh'; | |
// Add terrain and fog | |
map.on('load', () => { | |
// Add terrain | |
map.addSource('mapbox-dem', { | |
type: 'raster-dem', | |
url: 'mapbox://mapbox.mapbox-terrain-dem-v1', | |
tileSize: 512, | |
maxzoom: 14 | |
}); | |
map.setTerrain({ 'source': 'mapbox-dem', 'exaggeration': 1.6 }); | |
// Add fog for better 3D effect | |
map.setFog({ | |
'range': [0.5, 10], | |
'color': '#0b0f14', | |
'horizon-blend': 0.03 | |
}); | |
// Add sky layer for realistic atmosphere | |
map.addLayer({ | |
'id': 'sky', | |
'type': 'sky', | |
'paint': { | |
'sky-type': 'atmosphere', | |
'sky-atmosphere-sun': [0.0, 0.0], | |
'sky-atmosphere-sun-intensity': 15 | |
} | |
}); | |
// Initialize live data sources | |
const diseaseDataSources = { | |
cholera: 'https://api.example.com/feed/cholera', | |
mpox: 'https://api.example.com/feed/mpox', | |
malaria: 'https://api.example.com/feed/malaria', | |
dengue: 'https://api.example.com/feed/dengue', | |
lassa: 'https://api.example.com/feed/lassa' | |
}; | |
// Fetch and update disease data | |
async function fetchDiseaseData(disease) { | |
try { | |
const response = await fetch(diseaseDataSources[disease]); | |
const data = await response.json(); | |
return data; | |
} catch (error) { | |
console.error(`Error fetching ${disease} data:`, error); | |
return null; | |
} | |
} | |
// Add live intelligence data source | |
map.addSource('live_disease_data', { | |
'type': 'geojson', | |
'data': { type: 'FeatureCollection', features: [] } | |
}); | |
======= | |
{ | |
"type": "Feature", | |
"geometry": { | |
"type": "LineString", | |
"coordinates": [ | |
[36.8400, -1.3120], | |
[36.8445, -1.3100], | |
[36.8490, -1.3080] | |
] | |
}, | |
"properties": { | |
"id": "I1", | |
"category": "Infrastructure Weakness", | |
"title": "Aged Pipeline Corridor", | |
"description": "Structurally compromised pipeline segment crossing untreated drainage.", | |
"severity": 0.6, | |
"confidence": 0.9 | |
} | |
}, | |
{ | |
"type": "Feature", | |
"geometry": { | |
"type": "Point", | |
"coordinates": [36.8260, -1.3175] | |
}, | |
"properties": { | |
"id": "V1", | |
"category": "Informal Vendor Hub", | |
"title": "Kibera Borehole Cluster", | |
"description": "High-density vendor area with decentralized quality control.", | |
"severity": 0.7, | |
"confidence": 0.8 | |
} | |
}, | |
{ | |
"type": "Feature", | |
"geometry": { | |
"type": "Polygon", | |
"coordinates": [[ | |
[36.8520, -1.2600], | |
[36.8580, -1.2830], | |
[36.8600, -1.3000], | |
[36.8550, -1.3170], | |
[36.8300, -1.3200], | |
[36.8350, -1.2800], | |
[36.8450, -1.2650], | |
[36.8520, -1.2600] | |
]] | |
}, | |
"properties": { | |
"id": "HN2", | |
"category": "Zone of Historical Neglect", | |
"title": "Mathare-Mukuru Corridor", | |
"description": "Persistent infrastructure gaps create a permanent high-risk environment.", | |
"severity": 0.8, | |
"confidence": 0.95 | |
} | |
} | |
] | |
}; | |
map.addSource('radx_data', { | |
'type': 'geojson', | |
'data': intelligenceData | |
}); | |
// Add layers | |
map.addLayer({ | |
'id': 'zones-fill', | |
'type': 'fill', | |
'source': 'radx_data', | |
'filter': ['==', ['geometry-type'], 'Polygon'], | |
'paint': { | |
'fill-color': '#ff4d4d', | |
'fill-opacity': 0.15 | |
} | |
}); | |
map.addLayer({ | |
'id': 'zones-outline', | |
'type': 'line', | |
'source': 'radx_data', | |
'filter': ['==', ['geometry-type'], 'Polygon'], | |
'paint': { | |
'line-color': '#ff4d4d', | |
'line-width': 1, | |
'line-dasharray': [2, 1] | |
} | |
}); | |
map.addLayer({ | |
'id': 'infra-lines', | |
'type': 'line', | |
'source': 'radx_data', | |
'filter': ['==', ['geometry-type'], 'LineString'], | |
'paint': { | |
'line-color': '#ffa500', | |
'line-width': 2.5, | |
'line-dasharray': [1, 1.5] | |
} | |
}); | |
map.addLayer({ | |
'id': 'cause-points', | |
'type': 'circle', | |
'source': 'radx_data', | |
'filter': ['==', ['geometry-type'], 'Point'], | |
'paint': { | |
'circle-radius': 7, | |
'circle-stroke-width': 1.5, | |
'circle-stroke-color': '#ffffff', | |
'circle-color': [ | |
'match', | |
['get', 'category'], | |
'Contamination Source', '#ff4d4d', | |
'Informal Vendor Hub', '#ffff66', | |
'#cccccc' | |
] | |
} | |
}); | |
// Add individual layers for each feature to enable highlighting | |
intelligenceData.features.forEach(feature => { | |
const id = feature.properties.id; | |
if (feature.geometry.type === 'Point') { | |
map.addLayer({ | |
'id': `cause-points-${id}`, | |
'type': 'circle', | |
'source': 'radx_data', | |
'filter': ['==', ['get', 'id'], id], | |
'paint': { | |
'circle-radius': 7, | |
'circle-stroke-width': 1.5, | |
'circle-stroke-color': '#ffffff', | |
'circle-color': [ | |
'match', | |
['get', 'category'], | |
'Contamination Source', '#ff4d4d', | |
'Informal Vendor Hub', '#ffff66', | |
'#cccccc' | |
] | |
} | |
}); | |
} | |
}); | |
// Add markers for high-risk points | |
intelligenceData.features | |
.filter(f => f.geometry.type === 'Point') | |
.forEach(feature => { | |
const el = document.createElement('div'); | |
el.className = 'hotspot-marker'; | |
el.innerHTML = '<div class="pulse-circle"></div>'; | |
new mapboxgl.Marker(el) | |
.setLngLat(feature.geometry.coordinates) | |
.setPopup( | |
new mapboxgl.Popup({ offset: 25 }) // add popups | |
.setHTML( | |
`<h3>${feature.properties.title}</h3><p>${feature.properties.description}</p>` | |
) | |
) | |
.addTo(map); | |
}); | |
}); | |
// Tooltip functionality | |
const tooltip = document.getElementById('tooltip'); | |
let hoverFeatureData = null; | |
let tooltipLngLat = null; | |
const layers = ['zones-fill', 'infra-lines', 'cause-points']; | |
layers.forEach(layerId => { | |
map.on('mousemove', layerId, (e) => { | |
const f = (e.features || [])[0]; | |
if (!f) return; | |
map.getCanvas().style.cursor = 'pointer'; | |
tooltipLngLat = e.lngLat; | |
hoverFeatureData = f.properties; | |
showTooltip(); | |
}); | |
map.on('mouseleave', layerId, () => { | |
map.getCanvas().style.cursor = ''; | |
hoverFeatureData = null; | |
tooltip.style.display = 'none'; | |
}); | |
}); | |
map.on('move', showTooltip); | |
function showTooltip() { | |
if (!hoverFeatureData || !tooltipLngLat) return; | |
tooltip.style.display = 'block'; | |
tooltip.style.left = `${tooltipLngLat.x}px`; | |
tooltip.style.top = `${tooltipLngLat.y}px`; | |
document.getElementById('tooltip-title').textContent = hoverFeatureData.title; | |
document.getElementById('tooltip-info').textContent = hoverFeatureData.description; | |
document.getElementById('tooltip-coords').textContent = | |
`${tooltipLngLat.lng.toFixed(4)}, ${tooltipLngLat.lat.toFixed(4)}`; | |
} | |
// Chat panel functionality | |
const chatPanel = document.getElementById('chat-panel'); | |
const chatToggle = document.getElementById('chat-toggle'); | |
const chatLog = document.getElementById('chat-log'); | |
const chatForm = document.getElementById('chat-form'); | |
const chatInput = document.getElementById('chat-input'); | |
const chatSend = document.getElementById('chat-send'); | |
chatToggle.addEventListener('click', () => { | |
chatPanel.classList.toggle('minimized'); | |
chatToggle.innerHTML = chatPanel.classList.contains('minimized') ? | |
'<i class="fas fa-plus"></i>' : '<i class="fas fa-minus"></i>'; | |
}); | |
chatForm.addEventListener('submit', async (e) => { | |
e.preventDefault(); | |
const message = chatInput.value.trim(); | |
if (!message) return; | |
// Add user message | |
addMessage(message, 'user'); | |
chatInput.value = ''; | |
// Detect disease from query | |
const detectedDisease = detectDiseaseFromQuery(message); | |
if (detectedDisease) { | |
addMessage(`Fetching live ${detectedDisease} outbreak data...`, 'sys'); | |
try { | |
const diseaseData = await fetchDiseaseData(detectedDisease); | |
if (diseaseData) { | |
// Update map with live data | |
map.getSource('live_disease_data').setData(diseaseData); | |
// Build dynamic story from live data | |
buildStoryFromLiveData(detectedDisease, diseaseData); | |
addMessage(`Live ${detectedDisease} data loaded. ${diseaseData.features.length} hotspots detected.`, 'sys'); | |
} else { | |
addMessage(`Could not fetch live ${detectedDisease} data. Trying cached information...`, 'sys'); | |
} | |
} catch (error) { | |
addMessage(`Error processing ${detectedDisease} query: ${error.message}`, 'sys'); | |
} | |
} else { | |
addMessage('Please specify a disease (cholera, mpox, malaria, dengue, lassa) and location.', 'sys'); | |
} | |
}); | |
function detectDiseaseFromQuery(query) { | |
const diseases = ['cholera', 'mpox', 'malaria', 'dengue', 'lassa']; | |
const queryLower = query.toLowerCase(); | |
return diseases.find(d => queryLower.includes(d)); | |
} | |
function buildStoryFromLiveData(disease, geojsonData) { | |
const storyPanel = document.getElementById('story-panel'); | |
const storyBody = document.getElementById('story-body'); | |
storyBody.innerHTML = ''; | |
storyPanel.style.display = 'block'; | |
// Create chapters from live data features | |
geojsonData.features.forEach((feature, index) => { | |
const chapter = document.createElement('div'); | |
chapter.className = 'chapter' + (index === 0 ? ' active' : ''); | |
chapter.id = `chapter-${feature.id}`; | |
const h4 = document.createElement('h4'); | |
h4.textContent = feature.properties.title || `${disease} Hotspot ${index + 1}`; | |
chapter.appendChild(h4); | |
const p = document.createElement('p'); | |
p.textContent = feature.properties.description || `Location identified as ${disease} risk area.`; | |
chapter.appendChild(p); | |
storyBody.appendChild(chapter); | |
// Add marker for this feature | |
const [lon, lat] = feature.geometry.coordinates; | |
const el = document.createElement('div'); | |
el.className = 'hotspot-marker'; | |
el.style.backgroundColor = getDiseaseColor(disease); | |
new mapboxgl.Marker(el) | |
.setLngLat([lon, lat]) | |
.setPopup(new mapboxgl.Popup({ offset: 25 }) | |
.setHTML(`<h3>${feature.properties.title}</h3><p>${feature.properties.description}</p>`)) | |
.addTo(map); | |
if (index === 0) { | |
map.flyTo({ | |
center: [lon, lat], | |
zoom: 12, | |
pitch: 45, | |
bearing: 0 | |
}); | |
} | |
}); | |
// Set up intersection observer for chapters | |
const observer = new IntersectionObserver((entries) => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
const chapterId = entry.target.id.replace('chapter-', ''); | |
const feature = geojsonData.features.find(f => f.id === chapterId); | |
if (feature) { | |
document.querySelectorAll('.chapter').forEach(c => c.classList.remove('active')); | |
entry.target.classList.add('active'); | |
const [lon, lat] = feature.geometry.coordinates; | |
map.flyTo({ | |
center: [lon, lat], | |
zoom: 12, | |
pitch: 45, | |
bearing: 0 | |
}); | |
} | |
} | |
}); | |
}, { threshold: 0.7 }); | |
document.querySelectorAll('.chapter').forEach(chapter => { | |
observer.observe(chapter); | |
}); | |
} | |
function getDiseaseColor(disease) { | |
switch(disease) { | |
case 'cholera': return '#ff0033'; | |
case 'mpox': return '#ffa500'; | |
case 'malaria': return '#00ff88'; | |
case 'dengue': return '#bb66ff'; | |
case 'lassa': return '#a020f0'; | |
default: return '#ffa500'; | |
} | |
} | |
======= | |
function addMessage(text, type) { | |
const msgEl = document.createElement('div'); | |
msgEl.className = `msg ${type}`; | |
msgEl.textContent = text === 'user' ? `> ${text}` : text; | |
chatLog.appendChild(msgEl); | |
chatLog.scrollTop = chatLog.scrollHeight; | |
} | |
// Story builder function | |
function buildStory(disease, keys) { | |
const storyPanel = document.getElementById('story-panel'); | |
const storyBody = document.getElementById('story-body'); | |
storyBody.innerHTML = ''; | |
storyPanel.style.display = 'block'; | |
const knowledge = { | |
cholera: { | |
kibera: { | |
key: 'kibera', | |
place: 'Nairobi', | |
center: [36.7876, -1.3146], | |
title: 'Kibera — River Intake', | |
info: 'ROOT CAUSE: Sanitation + contaminated tributaries.\nTurbidity ~180 NTU; residual chlorine ~0.05 mg/L.' | |
}, | |
mathare: { | |
key: 'mathare', | |
place: 'Nairobi', | |
center: [36.8706, -1.2600], | |
title: 'Mathare Valley', | |
info: 'ROOT CAUSE: Drainage overflow → wells.\nSpike after rainfall; inspect upstream drains.' | |
} | |
}, | |
mpox: { | |
gombe: { | |
key: 'gombe', | |
place: 'Kinshasa', | |
center: [15.31, -4.322], | |
title: 'Kinshasa — Gombe Zone', | |
info: 'Syndromic cluster flagged; confirm lab & contact tracing routes.' | |
} | |
} | |
}; | |
const dict = knowledge[disease] || {}; | |
const toActivate = []; | |
keys.forEach((k, i) => { | |
const cfg = dict[k]; | |
if (!cfg) return; | |
const sec = document.createElement('div'); | |
sec.className = 'chapter' + (i === 0 ? ' active' : ''); | |
sec.id = `chap-${cfg.key}`; | |
const h4 = document.createElement('h4'); | |
h4.textContent = cfg.title; | |
sec.appendChild(h4); | |
const p = document.createElement('p'); | |
p.textContent = cfg.info; | |
sec.appendChild(p); | |
storyBody.appendChild(sec); | |
toActivate.push(cfg); | |
}); | |
toActivate.forEach((cfg, i) => { | |
const [lon, lat] = cfg.center; | |
const el = document.createElement('div'); | |
el.className = 'hotspot-marker'; | |
new mapboxgl.Marker(el) | |
.setLngLat([lon, lat]) | |
.setPopup(new mapboxgl.Popup({ offset: 25 }) | |
.setHTML(`<h3>${cfg.title}</h3><p>${cfg.info}</p>`)) | |
.addTo(map); | |
if (i === 0) { | |
map.flyTo({ | |
center: [lon, lat], | |
zoom: 14, | |
pitch: 50, | |
bearing: -20, | |
speed: 1.4, | |
curve: 1.2 | |
}); | |
} | |
}); | |
const observer = new IntersectionObserver((entries) => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
const id = entry.target.id.replace('chap-', ''); | |
const cfg = Object.values(dict).find(c => c.key === id); | |
if (!cfg) return; | |
document.querySelectorAll('.chapter').forEach(s => s.classList.remove('active')); | |
entry.target.classList.add('active'); | |
map.flyTo({ | |
center: cfg.center, | |
zoom: 14, | |
pitch: 50, | |
bearing: -20, | |
speed: 1.4, | |
curve: 1.2 | |
}); | |
} | |
}); | |
}, { root: storyPanel, threshold: 0.7 }); | |
document.querySelectorAll('.chapter').forEach(sec => observer.observe(sec)); | |
} | |
// Story panel functionality | |
const storyPanel = document.getElementById('story-panel'); | |
const storyClose = document.getElementById('story-close'); | |
const chapters = document.querySelectorAll('.chapter'); | |
// Show story panel on load | |
setTimeout(() => { | |
storyPanel.classList.add('visible'); | |
}, 1000); | |
storyClose.addEventListener('click', () => { | |
storyPanel.classList.remove('visible'); | |
}); | |
// Intersection observer for story chapters | |
const observer = new IntersectionObserver((entries) => { | |
entries.forEach(entry => { | |
if (entry.isIntersecting) { | |
entry.target.classList.add('active'); | |
} else { | |
entry.target.classList.remove('active'); | |
} | |
}); | |
}, { threshold: 0.7 }); | |
chapters.forEach(chapter => { | |
observer.observe(chapter); | |
}); | |
// Live ticker with disease updates | |
async function updateLiveTicker() { | |
try { | |
const response = await fetch('https://api.example.com/live-updates'); | |
const updates = await response.json(); | |
const tickerMarquee = document.querySelector('#ticker .marquee'); | |
tickerMarquee.innerHTML = ''; | |
updates.forEach(update => { | |
const item = document.createElement('div'); | |
item.className = 'tick-item'; | |
const badge = document.createElement('span'); | |
badge.className = 'badge'; | |
badge.textContent = update.disease.toUpperCase(); | |
badge.style.backgroundColor = getDiseaseColor(update.disease); | |
const text = document.createElement('span'); | |
text.textContent = `${update.location}: ${update.message}`; | |
item.appendChild(badge); | |
item.appendChild(text); | |
tickerMarquee.appendChild(item); | |
// Clone for infinite scroll | |
const clone = item.cloneNode(true); | |
tickerMarquee.appendChild(clone); | |
}); | |
// Animate ticker | |
let position = 0; | |
function animate() { | |
position--; | |
if (position < -tickerMarquee.scrollWidth / 2) { | |
position = 0; | |
} | |
tickerMarquee.style.transform = `translateX(${position}px)`; | |
requestAnimationFrame(animate); | |
} | |
animate(); | |
} catch (error) { | |
console.error('Error fetching live updates:', error); | |
} | |
// Refresh every 5 minutes | |
setTimeout(updateLiveTicker, 300000); | |
} | |
// Initial ticker load | |
updateLiveTicker(); | |
======= | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MoShow/new-dynamo" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
</html> |