mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-05-09 02:35:37 +02:00
feat: add power plants layer with WRI Global Power Plant Database
Map ~35,000 power generation facilities from 164 countries using the WRI Global Power Plant Database (CC BY 4.0). Follows the existing datacenter layer pattern with clustered icon symbols, amber color scheme, and click popups showing fuel type, capacity, and operator. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -306,6 +306,7 @@ async def live_data_slow(request: Request,
|
||||
"firms_fires": _f(d.get("firms_fires", [])),
|
||||
"datacenters": _f(d.get("datacenters", [])),
|
||||
"military_bases": _f(d.get("military_bases", [])),
|
||||
"power_plants": _f(d.get("power_plants", [])),
|
||||
"freshness": dict(source_timestamps),
|
||||
}
|
||||
bbox_tag = f"{s},{w},{n},{e}" if has_bbox else "full"
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Download WRI Global Power Plant Database CSV and convert to compact JSON.
|
||||
|
||||
Usage:
|
||||
python backend/scripts/convert_power_plants.py
|
||||
|
||||
Output:
|
||||
backend/data/power_plants.json
|
||||
"""
|
||||
import csv
|
||||
import json
|
||||
import io
|
||||
import zipfile
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
# WRI Global Power Plant Database v1.3.0 (GitHub release)
|
||||
CSV_URL = "https://raw.githubusercontent.com/wri/global-power-plant-database/master/output_database/global_power_plant_database.csv"
|
||||
OUT_PATH = Path(__file__).parent.parent / "data" / "power_plants.json"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
print(f"Downloading WRI Global Power Plant Database from GitHub...")
|
||||
req = urllib.request.Request(CSV_URL, headers={"User-Agent": "ShadowBroker-OSINT/1.0"})
|
||||
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||
raw = resp.read().decode("utf-8")
|
||||
|
||||
reader = csv.DictReader(io.StringIO(raw))
|
||||
plants: list[dict] = []
|
||||
skipped = 0
|
||||
for row in reader:
|
||||
try:
|
||||
lat = float(row["latitude"])
|
||||
lng = float(row["longitude"])
|
||||
except (ValueError, KeyError):
|
||||
skipped += 1
|
||||
continue
|
||||
if not (-90 <= lat <= 90 and -180 <= lng <= 180):
|
||||
skipped += 1
|
||||
continue
|
||||
capacity_raw = row.get("capacity_mw", "")
|
||||
capacity_mw = float(capacity_raw) if capacity_raw else None
|
||||
plants.append({
|
||||
"name": row.get("name", "Unknown"),
|
||||
"country": row.get("country_long", ""),
|
||||
"fuel_type": row.get("primary_fuel", "Unknown"),
|
||||
"capacity_mw": capacity_mw,
|
||||
"owner": row.get("owner", ""),
|
||||
"lat": round(lat, 5),
|
||||
"lng": round(lng, 5),
|
||||
})
|
||||
|
||||
OUT_PATH.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUT_PATH.write_text(json.dumps(plants, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
||||
print(f"Wrote {len(plants)} power plants to {OUT_PATH} (skipped {skipped})")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -40,7 +40,8 @@ from services.fetchers.earth_observation import ( # noqa: F401
|
||||
fetch_earthquakes, fetch_firms_fires, fetch_space_weather, fetch_weather,
|
||||
)
|
||||
from services.fetchers.infrastructure import ( # noqa: F401
|
||||
fetch_internet_outages, fetch_datacenters, fetch_military_bases, fetch_cctv, fetch_kiwisdr,
|
||||
fetch_internet_outages, fetch_datacenters, fetch_military_bases, fetch_power_plants,
|
||||
fetch_cctv, fetch_kiwisdr,
|
||||
)
|
||||
from services.fetchers.geo import ( # noqa: F401
|
||||
fetch_ships, fetch_airports, find_nearest_airport, cached_airports,
|
||||
@@ -86,6 +87,7 @@ def update_slow_data():
|
||||
fetch_gdelt,
|
||||
fetch_datacenters,
|
||||
fetch_military_bases,
|
||||
fetch_power_plants,
|
||||
]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(slow_funcs)) as executor:
|
||||
futures = [executor.submit(func) for func in slow_funcs]
|
||||
|
||||
@@ -31,7 +31,8 @@ latest_data = {
|
||||
"internet_outages": [],
|
||||
"firms_fires": [],
|
||||
"datacenters": [],
|
||||
"military_bases": []
|
||||
"military_bases": [],
|
||||
"power_plants": []
|
||||
}
|
||||
|
||||
# Per-source freshness timestamps
|
||||
|
||||
@@ -180,6 +180,44 @@ def fetch_military_bases():
|
||||
_mark_fresh("military_bases")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Power Plants (WRI Global Power Plant Database)
|
||||
# ---------------------------------------------------------------------------
|
||||
_POWER_PLANTS_PATH = Path(__file__).parent.parent.parent / "data" / "power_plants.json"
|
||||
|
||||
|
||||
def fetch_power_plants():
|
||||
"""Load WRI Global Power Plant Database (~35K facilities)."""
|
||||
plants = []
|
||||
try:
|
||||
if not _POWER_PLANTS_PATH.exists():
|
||||
logger.warning(f"Power plants file not found: {_POWER_PLANTS_PATH}")
|
||||
return
|
||||
raw = json.loads(_POWER_PLANTS_PATH.read_text(encoding="utf-8"))
|
||||
for entry in raw:
|
||||
lat = entry.get("lat")
|
||||
lng = entry.get("lng")
|
||||
if lat is None or lng is None:
|
||||
continue
|
||||
if not (-90 <= lat <= 90 and -180 <= lng <= 180):
|
||||
continue
|
||||
plants.append({
|
||||
"name": entry.get("name", "Unknown"),
|
||||
"country": entry.get("country", ""),
|
||||
"fuel_type": entry.get("fuel_type", "Unknown"),
|
||||
"capacity_mw": entry.get("capacity_mw"),
|
||||
"owner": entry.get("owner", ""),
|
||||
"lat": lat, "lng": lng,
|
||||
})
|
||||
logger.info(f"Power plants: {len(plants)} facilities loaded")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading power plants: {e}")
|
||||
with _data_lock:
|
||||
latest_data["power_plants"] = plants
|
||||
if plants:
|
||||
_mark_fresh("power_plants")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CCTV Cameras
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -163,6 +163,7 @@ export default function Dashboard() {
|
||||
internet_outages: false,
|
||||
datacenters: false,
|
||||
military_bases: false,
|
||||
power_plants: false,
|
||||
});
|
||||
|
||||
// NASA GIBS satellite imagery state
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
svgTanker, svgRecon, svgPlanePink, svgPlaneAlertRed, svgPlaneDarkBlue,
|
||||
svgPlaneWhiteAlert, svgHeliPink, svgHeliAlertRed, svgHeliDarkBlue,
|
||||
svgHeliBlue, svgHeliLime, svgHeliWhiteAlert, svgPlaneBlack, svgHeliBlack,
|
||||
svgDrone, svgDataCenter, svgRadioTower, svgShipGray, svgShipRed, svgShipYellow,
|
||||
svgDrone, svgDataCenter, svgPowerPlant, svgRadioTower, svgShipGray, svgShipRed, svgShipYellow,
|
||||
svgShipBlue, svgShipWhite, svgShipPink, svgCarrier, svgCctv, svgWarning, svgThreat,
|
||||
svgTriangleYellow, svgTriangleRed,
|
||||
svgFireYellow, svgFireOrange, svgFireRed, svgFireDarkRed,
|
||||
@@ -50,7 +50,7 @@ import { useClusterLabels } from "@/components/map/hooks/useClusterLabels";
|
||||
import { spreadAlertItems } from "@/utils/alertSpread";
|
||||
import {
|
||||
buildEarthquakesGeoJSON, buildJammingGeoJSON, buildCctvGeoJSON, buildKiwisdrGeoJSON,
|
||||
buildFirmsGeoJSON, buildInternetOutagesGeoJSON, buildDataCentersGeoJSON, buildMilitaryBasesGeoJSON,
|
||||
buildFirmsGeoJSON, buildInternetOutagesGeoJSON, buildDataCentersGeoJSON, buildPowerPlantsGeoJSON, buildMilitaryBasesGeoJSON,
|
||||
buildGdeltGeoJSON, buildLiveuaGeoJSON, buildFrontlineGeoJSON,
|
||||
buildFlightLayerGeoJSON, buildUavGeoJSON,
|
||||
buildSatellitesGeoJSON, buildShipsGeoJSON, buildCarriersGeoJSON,
|
||||
@@ -222,6 +222,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
activeLayers.datacenters ? buildDataCentersGeoJSON(data?.datacenters) : null,
|
||||
[activeLayers.datacenters, data?.datacenters]);
|
||||
|
||||
const powerPlantsGeoJSON = useMemo(() =>
|
||||
activeLayers.power_plants ? buildPowerPlantsGeoJSON(data?.power_plants) : null,
|
||||
[activeLayers.power_plants, data?.power_plants]);
|
||||
|
||||
const militaryBasesGeoJSON = useMemo(() =>
|
||||
activeLayers.military_bases ? buildMilitaryBasesGeoJSON(data?.military_bases) : null,
|
||||
[activeLayers.military_bases, data?.military_bases]);
|
||||
@@ -353,6 +357,8 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
loadImg('fire-cluster-xl', svgFireClusterXL);
|
||||
// Data center icon
|
||||
loadImg('datacenter', svgDataCenter);
|
||||
// Power plant icon
|
||||
loadImg('power-plant', svgPowerPlant);
|
||||
// Satellite mission-type icons
|
||||
loadImg('sat-mil', makeSatSvg('#ff3333'));
|
||||
loadImg('sat-sar', makeSatSvg('#00e5ff'));
|
||||
@@ -592,6 +598,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
kiwisdrGeoJSON && 'kiwisdr-layer',
|
||||
internetOutagesGeoJSON && 'internet-outages-layer',
|
||||
dataCentersGeoJSON && 'datacenters-layer',
|
||||
powerPlantsGeoJSON && 'power-plants-layer',
|
||||
militaryBasesGeoJSON && 'military-bases-layer',
|
||||
firmsGeoJSON && 'firms-viirs-layer'
|
||||
].filter(Boolean) as string[];
|
||||
@@ -1468,6 +1475,61 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* Power Plant positions */}
|
||||
{powerPlantsGeoJSON && (
|
||||
<Source id="power-plants" type="geojson" data={powerPlantsGeoJSON as any} cluster={true} clusterRadius={30} clusterMaxZoom={8}>
|
||||
{/* Cluster circles */}
|
||||
<Layer
|
||||
id="power-plants-clusters"
|
||||
type="circle"
|
||||
filter={['has', 'point_count']}
|
||||
paint={{
|
||||
'circle-color': '#92400e',
|
||||
'circle-radius': ['step', ['get', 'point_count'], 12, 10, 16, 50, 20],
|
||||
'circle-opacity': 0.7,
|
||||
'circle-stroke-width': 1,
|
||||
'circle-stroke-color': '#f59e0b',
|
||||
}}
|
||||
/>
|
||||
<Layer
|
||||
id="power-plants-cluster-count"
|
||||
type="symbol"
|
||||
filter={['has', 'point_count']}
|
||||
layout={{
|
||||
'text-field': '{point_count_abbreviated}',
|
||||
'text-font': ['Noto Sans Bold'],
|
||||
'text-size': 10,
|
||||
'text-allow-overlap': true,
|
||||
}}
|
||||
paint={{
|
||||
'text-color': '#fde68a',
|
||||
}}
|
||||
/>
|
||||
{/* Individual power plant icons */}
|
||||
<Layer
|
||||
id="power-plants-layer"
|
||||
type="symbol"
|
||||
filter={['!', ['has', 'point_count']]}
|
||||
layout={{
|
||||
'icon-image': 'power-plant',
|
||||
'icon-size': ['interpolate', ['linear'], ['zoom'], 2, 0.5, 6, 0.7, 10, 1.0],
|
||||
'icon-allow-overlap': true,
|
||||
'text-field': ['step', ['zoom'], '', 6, ['get', 'name']],
|
||||
'text-font': ['Noto Sans Regular'],
|
||||
'text-size': 9,
|
||||
'text-offset': [0, 1.2],
|
||||
'text-anchor': 'top',
|
||||
'text-allow-overlap': false,
|
||||
}}
|
||||
paint={{
|
||||
'text-color': '#fbbf24',
|
||||
'text-halo-color': 'rgba(0,0,0,0.9)',
|
||||
'text-halo-width': 1,
|
||||
}}
|
||||
/>
|
||||
</Source>
|
||||
)}
|
||||
|
||||
{/* Military Base positions */}
|
||||
{militaryBasesGeoJSON && (
|
||||
<Source id="military-bases" type="geojson" data={militaryBasesGeoJSON as any}>
|
||||
@@ -1885,6 +1947,52 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
||||
);
|
||||
})()}
|
||||
|
||||
{/* Power Plant click popup */}
|
||||
{selectedEntity?.type === 'power_plant' && (() => {
|
||||
const pp = data?.power_plants?.find((_: any, i: number) => `pp-${i}` === selectedEntity.id);
|
||||
if (!pp) return null;
|
||||
return (
|
||||
<Popup
|
||||
longitude={pp.lng}
|
||||
latitude={pp.lat}
|
||||
closeButton={false}
|
||||
closeOnClick={false}
|
||||
onClose={() => onEntityClick?.(null)}
|
||||
className="threat-popup"
|
||||
maxWidth="280px"
|
||||
>
|
||||
<div className="map-popup bg-[#1a0f00] border border-amber-400/40 text-[#fde68a] min-w-[200px]">
|
||||
<div className="map-popup-title text-amber-400 border-b border-amber-400/20 pb-1">
|
||||
{pp.name}
|
||||
</div>
|
||||
{pp.fuel_type && (
|
||||
<div className="map-popup-row">
|
||||
Fuel: <span className="text-[#fbbf24]">{pp.fuel_type}</span>
|
||||
</div>
|
||||
)}
|
||||
{pp.capacity_mw != null && (
|
||||
<div className="map-popup-row">
|
||||
Capacity: <span className="text-white">{pp.capacity_mw.toLocaleString()} MW</span>
|
||||
</div>
|
||||
)}
|
||||
{pp.owner && (
|
||||
<div className="map-popup-row">
|
||||
Operator: <span className="text-white">{pp.owner}</span>
|
||||
</div>
|
||||
)}
|
||||
{pp.country && (
|
||||
<div className="map-popup-row">
|
||||
Country: <span className="text-white">{pp.country}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-1.5 text-[9px] text-amber-600 tracking-wider">
|
||||
POWER PLANT
|
||||
</div>
|
||||
</div>
|
||||
</Popup>
|
||||
);
|
||||
})()}
|
||||
|
||||
{selectedEntity?.type === 'military_base' && (() => {
|
||||
const base = data?.military_bases?.find((_: any, i: number) => `milbase-${i}` === selectedEntity.id);
|
||||
if (!base) return null;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Plane, AlertTriangle, Activity, Satellite, Cctv, ChevronDown, ChevronUp, Ship, Eye, Anchor, Settings, Sun, Moon, BookOpen, Radio, Play, Pause, Globe, Flame, Wifi, Server, Shield, ToggleLeft, ToggleRight, Palette } from "lucide-react";
|
||||
import { Plane, AlertTriangle, Activity, Satellite, Cctv, ChevronDown, ChevronUp, Ship, Eye, Anchor, Settings, Sun, Moon, BookOpen, Radio, Play, Pause, Globe, Flame, Wifi, Server, Shield, Zap, ToggleLeft, ToggleRight, Palette } from "lucide-react";
|
||||
import packageJson from "../../package.json";
|
||||
import { useTheme } from "@/lib/ThemeContext";
|
||||
|
||||
@@ -41,6 +41,7 @@ const FRESHNESS_MAP: Record<string, string> = {
|
||||
firms: "firms_fires",
|
||||
internet_outages: "internet_outages",
|
||||
datacenters: "datacenters",
|
||||
power_plants: "power_plants",
|
||||
};
|
||||
|
||||
// POTUS fleet ICAO hex codes for client-side filtering
|
||||
@@ -147,6 +148,7 @@ const WorldviewLeftPanel = React.memo(function WorldviewLeftPanel({ data, active
|
||||
{ id: "firms", name: "Fire Hotspots (24h)", source: "NASA FIRMS VIIRS", count: data?.firms_fires?.length || 0, icon: Flame },
|
||||
{ id: "internet_outages", name: "Internet Outages", source: "IODA / Georgia Tech", count: data?.internet_outages?.length || 0, icon: Wifi },
|
||||
{ id: "datacenters", name: "Data Centers", source: "DC Map (GitHub)", count: data?.datacenters?.length || 0, icon: Server },
|
||||
{ id: "power_plants", name: "Power Plants", source: "WRI (Static)", count: data?.power_plants?.length || 0, icon: Zap },
|
||||
{ id: "military_bases", name: "Military Bases", source: "OSINT (Static)", count: data?.military_bases?.length || 0, icon: Shield },
|
||||
{ id: "day_night", name: "Day / Night Cycle", source: "Solar Calc", count: null, icon: Sun },
|
||||
];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Extracted from MaplibreViewer to reduce component size and enable unit testing.
|
||||
// Each function takes data arrays + optional helpers and returns a GeoJSON FeatureCollection or null.
|
||||
|
||||
import type { Earthquake, GPSJammingZone, FireHotspot, InternetOutage, DataCenter, MilitaryBase, GDELTIncident, LiveUAmapIncident, CCTVCamera, KiwiSDR, FrontlineGeoJSON, UAV, Satellite, Ship, ActiveLayers } from "@/types/dashboard";
|
||||
import type { Earthquake, GPSJammingZone, FireHotspot, InternetOutage, DataCenter, MilitaryBase, PowerPlant, GDELTIncident, LiveUAmapIncident, CCTVCamera, KiwiSDR, FrontlineGeoJSON, UAV, Satellite, Ship, ActiveLayers } from "@/types/dashboard";
|
||||
import { classifyAircraft } from "@/utils/aircraftClassification";
|
||||
import { MISSION_COLORS, MISSION_ICON_MAP } from "@/components/map/icons/SatelliteIcons";
|
||||
|
||||
@@ -195,6 +195,28 @@ export function buildDataCentersGeoJSON(datacenters?: DataCenter[]): FC {
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Power Plants ──────────────────────────────────────────────────────────
|
||||
|
||||
export function buildPowerPlantsGeoJSON(plants?: PowerPlant[]): FC {
|
||||
if (!plants?.length) return null;
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: plants.map((p, i) => ({
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
id: `pp-${i}`,
|
||||
type: 'power_plant',
|
||||
name: p.name || 'Unknown',
|
||||
country: p.country || '',
|
||||
fuel_type: p.fuel_type || 'Unknown',
|
||||
capacity_mw: p.capacity_mw ?? 0,
|
||||
owner: p.owner || '',
|
||||
},
|
||||
geometry: { type: 'Point' as const, coordinates: [p.lng, p.lat] }
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Military Bases ─────────────────────────────────────────────────────────
|
||||
|
||||
// Classify base alignment: red = adversary, blue = US/allied, green = ROC
|
||||
|
||||
@@ -25,6 +25,7 @@ export const svgHeliWhiteAlert = `data:image/svg+xml;utf8,${encodeURIComponent(`
|
||||
export const svgPlaneBlack = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#222" stroke="#444"><path d="M21 16v-2l-8-5V3.5c0-.83-.67-1.5-1.5-1.5S10 2.67 10 3.5V9l-8 5v2l8-2.5V19l-2 1.5V22l3.5-1 3.5 1v-1.5L13 19v-5.5l8 2.5z" /></svg>`)}`;
|
||||
export const svgHeliBlack = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="#222" stroke="#444"><path d="M10 6L10 14L8 16L8 18L10 17L12 22L14 17L16 18L16 16L14 14L14 6C14 4 13 2 12 2C11 2 10 4 10 6Z"/><circle cx="12" cy="12" r="8" fill="none" stroke="#444" stroke-dasharray="2 2" stroke-width="1"/></svg>`)}`;
|
||||
export const svgDrone = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="orange" stroke="black"><path d="M12 2L15 8H9L12 2Z" /><rect x="8" y="8" width="8" height="2" /><path d="M4 10L10 14H14L20 10V12L14 16H10L4 12V10Z" /><circle cx="12" cy="14" r="2" fill="red"/></svg>`)}`;
|
||||
export const svgPowerPlant = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="1.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="#78350f" stroke="#f59e0b" stroke-linejoin="round" stroke-linecap="round"/></svg>`)}`;
|
||||
export const svgDataCenter = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#a78bfa" stroke-width="1.5"><rect x="3" y="3" width="18" height="6" rx="1" fill="#2e1065"/><rect x="3" y="11" width="18" height="6" rx="1" fill="#2e1065"/><circle cx="7" cy="6" r="1" fill="#a78bfa"/><circle cx="7" cy="14" r="1" fill="#a78bfa"/><line x1="11" y1="6" x2="17" y2="6" stroke="#a78bfa" stroke-width="1"/><line x1="11" y1="14" x2="17" y2="14" stroke="#a78bfa" stroke-width="1"/><line x1="12" y1="19" x2="12" y2="22" stroke="#a78bfa" stroke-width="1.5"/></svg>`)}`;
|
||||
export const svgShipGray = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="12" height="24" viewBox="0 0 24 24" fill="none"><path d="M6 20 L6 8 L12 2 L18 8 L18 20 C18 22 6 22 6 20 Z" fill="gray" stroke="#000" stroke-width="1"/><polygon points="12,6 16,16 8,16" fill="#fff" stroke="#000" stroke-width="1"/></svg>`)}`;
|
||||
export const svgShipRed = `data:image/svg+xml;utf8,${encodeURIComponent(`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="32" viewBox="0 0 24 24" fill="none"><path d="M6 22 L6 6 L12 2 L18 6 L18 22 Z" fill="#ff2222" stroke="#000" stroke-width="1"/><rect x="8" y="15" width="8" height="4" fill="#880000" stroke="#000" stroke-width="1"/><rect x="8" y="7" width="8" height="6" fill="#444" stroke="#000" stroke-width="1"/></svg>`)}`;
|
||||
|
||||
@@ -232,6 +232,16 @@ export interface DataCenter {
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface PowerPlant {
|
||||
name: string;
|
||||
country: string;
|
||||
fuel_type: string;
|
||||
capacity_mw: number | null;
|
||||
owner: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
}
|
||||
|
||||
export interface MilitaryBase {
|
||||
name: string;
|
||||
country: string;
|
||||
@@ -422,6 +432,7 @@ export interface DashboardData {
|
||||
firms_fires?: FireHotspot[];
|
||||
datacenters?: DataCenter[];
|
||||
military_bases?: MilitaryBase[];
|
||||
power_plants?: PowerPlant[];
|
||||
}
|
||||
|
||||
// ─── COMPONENT PROPS ────────────────────────────────────────────────────────
|
||||
@@ -451,6 +462,7 @@ export interface ActiveLayers {
|
||||
internet_outages: boolean;
|
||||
datacenters: boolean;
|
||||
military_bases: boolean;
|
||||
power_plants: boolean;
|
||||
}
|
||||
|
||||
export interface SelectedEntity {
|
||||
|
||||
Reference in New Issue
Block a user