From 0bece7dae63eab7227fc3b43abeec408e35ed270 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 2 Jan 2026 00:17:07 +0000 Subject: [PATCH] Add global infrastructure support with region tracking - Add region field to InfrastructureAsset dataclass - New /api/infrastructure endpoint returns all global assets with region stats - Keep /api/infrastructure/baltic for backwards compatibility - Update frontend to show region count and region in popups - Add oil pipeline color to legend --- infra_analysis.py | 20 +++++++++++++++----- server.py | 27 ++++++++++++++++++++++----- static/index.html | 19 ++++++++++++++----- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/infra_analysis.py b/infra_analysis.py index b089c57..4d8064d 100644 --- a/infra_analysis.py +++ b/infra_analysis.py @@ -84,6 +84,7 @@ class InfrastructureAsset: operator: Optional[str] = None capacity: Optional[str] = None # e.g., "1.2 Tbps" or "650 MW" notes: str = "" + region: Optional[str] = None # e.g., "baltic_sea", "trans_pacific" def get_nearest_point(self, lat: float, lon: float) -> Tuple[float, float, float]: """ @@ -175,7 +176,8 @@ def load_infrastructure_from_json(filepath: str = None) -> List['InfrastructureA protection_radius_nm=cable.get('protection_radius_nm', 3.0), operator=cable.get('operator'), capacity=cable.get('capacity'), - notes=cable.get('notes', '') + notes=cable.get('notes', ''), + region=cable.get('region', 'unknown') )) # Load pipelines @@ -191,7 +193,8 @@ def load_infrastructure_from_json(filepath: str = None) -> List['InfrastructureA protection_radius_nm=pipeline.get('protection_radius_nm', 5.0), operator=pipeline.get('operator'), capacity=pipeline.get('capacity'), - notes=pipeline.get('notes', '') + notes=pipeline.get('notes', ''), + region=pipeline.get('region', 'unknown') )) # Load wind farms (as point assets) @@ -205,7 +208,8 @@ def load_infrastructure_from_json(filepath: str = None) -> List['InfrastructureA protection_radius_nm=farm.get('protection_radius_nm', 2.0), operator=farm.get('operator'), capacity=f"{farm.get('capacity_mw', 0)} MW ({farm.get('turbines', 0)} turbines)", - notes=farm.get('notes', '') + notes=farm.get('notes', ''), + region=farm.get('region', 'unknown') )) print(f"Loaded {len(assets)} infrastructure assets from {filepath}") @@ -952,7 +956,12 @@ def _parse_timestamp(ts) -> datetime: # ============================================================================= def get_baltic_infrastructure() -> List[dict]: - """Get list of all infrastructure for map display.""" + """Get list of all infrastructure for map display (legacy name, returns global).""" + return get_global_infrastructure() + + +def get_global_infrastructure() -> List[dict]: + """Get list of all global infrastructure for map display.""" return [ { "name": asset.name, @@ -963,7 +972,8 @@ def get_baltic_infrastructure() -> List[dict]: "protection_radius_nm": asset.protection_radius_nm, "operator": asset.operator, "capacity": asset.capacity, - "notes": asset.notes + "notes": asset.notes, + "region": asset.region } for asset in get_all_infrastructure() ] diff --git a/server.py b/server.py index 188ee4f..426a0e9 100644 --- a/server.py +++ b/server.py @@ -108,8 +108,9 @@ # Import infrastructure threat analysis module try: from infra_analysis import ( - get_baltic_infrastructure, analyze_vessel_for_incident, - analyze_infrastructure_incident, BALTIC_INFRASTRUCTURE + get_baltic_infrastructure, get_global_infrastructure, + analyze_vessel_for_incident, analyze_infrastructure_incident, + BALTIC_INFRASTRUCTURE ) INFRA_ANALYSIS_AVAILABLE = True except ImportError: @@ -1838,15 +1839,31 @@ def do_GET(self): # ========== Infrastructure Analysis Endpoints ========== + elif path == '/api/infrastructure' or path == '/api/infrastructure/all': + # Get all global undersea infrastructure for map overlay + if not INFRA_ANALYSIS_AVAILABLE: + return self.send_json({'error': 'Infrastructure analysis module not available'}, 500) + infra = get_global_infrastructure() + # Group by region for stats + regions = {} + for item in infra: + r = item.get('region', 'unknown') + regions[r] = regions.get(r, 0) + 1 + return self.send_json({ + 'infrastructure': infra, + 'count': len(infra), + 'regions': regions + }, cache_seconds=3600) # Cache for 1 hour + elif path == '/api/infrastructure/baltic': - # Get Baltic Sea undersea infrastructure for map overlay + # Legacy endpoint - now returns all infrastructure if not INFRA_ANALYSIS_AVAILABLE: return self.send_json({'error': 'Infrastructure analysis module not available'}, 500) - infra = get_baltic_infrastructure() + infra = get_global_infrastructure() return self.send_json({ 'infrastructure': infra, 'count': len(infra), - 'region': 'Baltic Sea' + 'region': 'Global' # Updated from 'Baltic Sea' }, cache_seconds=3600) # Cache for 1 hour elif path.startswith('/api/vessels/') and path.endswith('/infra-analysis'): diff --git a/static/index.html b/static/index.html index 5c5e360..cd2635d 100644 --- a/static/index.html +++ b/static/index.html @@ -750,12 +750,16 @@
Gas Pipeline +
+
+ Oil Pipeline +
Protection Zone
- 0 assets loaded
+ 0 assets • 0 regions
Click cable for details
@@ -3358,19 +3362,22 @@ } // ========== Infrastructure Analysis ========== - // Baltic Sea undersea cables and pipelines overlay + // Global undersea cables and pipelines overlay async function loadBalticInfrastructure() { try { - const result = await api('/infrastructure/baltic'); + const result = await api('/infrastructure'); balticInfrastructure = result.infrastructure || []; renderInfrastructure(); - console.log(`Loaded ${balticInfrastructure.length} infrastructure assets`); + console.log(`Loaded ${balticInfrastructure.length} infrastructure assets from ${Object.keys(result.regions || {}).length} regions`); - // Update legend count and show it + // Update legend counts const countEl = document.getElementById('infra-count'); if (countEl) countEl.textContent = balticInfrastructure.length; + const regionsEl = document.getElementById('infra-regions'); + if (regionsEl) regionsEl.textContent = Object.keys(result.regions || {}).length; + const legend = document.getElementById('infra-legend'); if (legend && showInfrastructure && balticInfrastructure.length > 0) { legend.style.display = 'block'; @@ -3411,10 +3418,12 @@ opacity: 0.7, dashArray: asset.type.includes('cable') ? null : '10, 5' }); + const regionName = (asset.region || 'unknown').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); line.bindPopup(`

${asset.name}

${asset.type.replace('_', ' ').toUpperCase()}
+
Region: ${regionName}
${asset.operator ? `
Operator: ${asset.operator}
` : ''} ${asset.capacity ? `
Capacity: ${asset.capacity}
` : ''} ${asset.notes ? `
${asset.notes}
` : ''}