mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-28 19:51:57 +02:00
Merge pull request #87 from adust09/feat/power-plants-layer
feat: add power plants layer (WRI Global Power Plant Database)
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", [])),
|
"firms_fires": _f(d.get("firms_fires", [])),
|
||||||
"datacenters": _f(d.get("datacenters", [])),
|
"datacenters": _f(d.get("datacenters", [])),
|
||||||
"military_bases": _f(d.get("military_bases", [])),
|
"military_bases": _f(d.get("military_bases", [])),
|
||||||
|
"power_plants": _f(d.get("power_plants", [])),
|
||||||
"freshness": dict(source_timestamps),
|
"freshness": dict(source_timestamps),
|
||||||
}
|
}
|
||||||
bbox_tag = f"{s},{w},{n},{e}" if has_bbox else "full"
|
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,
|
fetch_earthquakes, fetch_firms_fires, fetch_space_weather, fetch_weather,
|
||||||
)
|
)
|
||||||
from services.fetchers.infrastructure import ( # noqa: F401
|
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
|
from services.fetchers.geo import ( # noqa: F401
|
||||||
fetch_ships, fetch_airports, find_nearest_airport, cached_airports,
|
fetch_ships, fetch_airports, find_nearest_airport, cached_airports,
|
||||||
@@ -86,6 +87,7 @@ def update_slow_data():
|
|||||||
fetch_gdelt,
|
fetch_gdelt,
|
||||||
fetch_datacenters,
|
fetch_datacenters,
|
||||||
fetch_military_bases,
|
fetch_military_bases,
|
||||||
|
fetch_power_plants,
|
||||||
]
|
]
|
||||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(slow_funcs)) as executor:
|
with concurrent.futures.ThreadPoolExecutor(max_workers=len(slow_funcs)) as executor:
|
||||||
futures = [executor.submit(func) for func in slow_funcs]
|
futures = [executor.submit(func) for func in slow_funcs]
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ latest_data = {
|
|||||||
"internet_outages": [],
|
"internet_outages": [],
|
||||||
"firms_fires": [],
|
"firms_fires": [],
|
||||||
"datacenters": [],
|
"datacenters": [],
|
||||||
"military_bases": []
|
"military_bases": [],
|
||||||
|
"power_plants": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# Per-source freshness timestamps
|
# Per-source freshness timestamps
|
||||||
|
|||||||
@@ -180,6 +180,44 @@ def fetch_military_bases():
|
|||||||
_mark_fresh("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
|
# CCTV Cameras
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -163,6 +163,7 @@ export default function Dashboard() {
|
|||||||
internet_outages: false,
|
internet_outages: false,
|
||||||
datacenters: false,
|
datacenters: false,
|
||||||
military_bases: false,
|
military_bases: false,
|
||||||
|
power_plants: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// NASA GIBS satellite imagery state
|
// NASA GIBS satellite imagery state
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
svgTanker, svgRecon, svgPlanePink, svgPlaneAlertRed, svgPlaneDarkBlue,
|
svgTanker, svgRecon, svgPlanePink, svgPlaneAlertRed, svgPlaneDarkBlue,
|
||||||
svgPlaneWhiteAlert, svgHeliPink, svgHeliAlertRed, svgHeliDarkBlue,
|
svgPlaneWhiteAlert, svgHeliPink, svgHeliAlertRed, svgHeliDarkBlue,
|
||||||
svgHeliBlue, svgHeliLime, svgHeliWhiteAlert, svgPlaneBlack, svgHeliBlack,
|
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,
|
svgShipBlue, svgShipWhite, svgShipPink, svgCarrier, svgCctv, svgWarning, svgThreat,
|
||||||
svgTriangleYellow, svgTriangleRed,
|
svgTriangleYellow, svgTriangleRed,
|
||||||
svgFireYellow, svgFireOrange, svgFireRed, svgFireDarkRed,
|
svgFireYellow, svgFireOrange, svgFireRed, svgFireDarkRed,
|
||||||
@@ -50,7 +50,7 @@ import { useClusterLabels } from "@/components/map/hooks/useClusterLabels";
|
|||||||
import { spreadAlertItems } from "@/utils/alertSpread";
|
import { spreadAlertItems } from "@/utils/alertSpread";
|
||||||
import {
|
import {
|
||||||
buildEarthquakesGeoJSON, buildJammingGeoJSON, buildCctvGeoJSON, buildKiwisdrGeoJSON,
|
buildEarthquakesGeoJSON, buildJammingGeoJSON, buildCctvGeoJSON, buildKiwisdrGeoJSON,
|
||||||
buildFirmsGeoJSON, buildInternetOutagesGeoJSON, buildDataCentersGeoJSON, buildMilitaryBasesGeoJSON,
|
buildFirmsGeoJSON, buildInternetOutagesGeoJSON, buildDataCentersGeoJSON, buildPowerPlantsGeoJSON, buildMilitaryBasesGeoJSON,
|
||||||
buildGdeltGeoJSON, buildLiveuaGeoJSON, buildFrontlineGeoJSON,
|
buildGdeltGeoJSON, buildLiveuaGeoJSON, buildFrontlineGeoJSON,
|
||||||
buildFlightLayerGeoJSON, buildUavGeoJSON,
|
buildFlightLayerGeoJSON, buildUavGeoJSON,
|
||||||
buildSatellitesGeoJSON, buildShipsGeoJSON, buildCarriersGeoJSON,
|
buildSatellitesGeoJSON, buildShipsGeoJSON, buildCarriersGeoJSON,
|
||||||
@@ -222,6 +222,10 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
|||||||
activeLayers.datacenters ? buildDataCentersGeoJSON(data?.datacenters) : null,
|
activeLayers.datacenters ? buildDataCentersGeoJSON(data?.datacenters) : null,
|
||||||
[activeLayers.datacenters, data?.datacenters]);
|
[activeLayers.datacenters, data?.datacenters]);
|
||||||
|
|
||||||
|
const powerPlantsGeoJSON = useMemo(() =>
|
||||||
|
activeLayers.power_plants ? buildPowerPlantsGeoJSON(data?.power_plants) : null,
|
||||||
|
[activeLayers.power_plants, data?.power_plants]);
|
||||||
|
|
||||||
const militaryBasesGeoJSON = useMemo(() =>
|
const militaryBasesGeoJSON = useMemo(() =>
|
||||||
activeLayers.military_bases ? buildMilitaryBasesGeoJSON(data?.military_bases) : null,
|
activeLayers.military_bases ? buildMilitaryBasesGeoJSON(data?.military_bases) : null,
|
||||||
[activeLayers.military_bases, data?.military_bases]);
|
[activeLayers.military_bases, data?.military_bases]);
|
||||||
@@ -353,6 +357,8 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
|||||||
loadImg('fire-cluster-xl', svgFireClusterXL);
|
loadImg('fire-cluster-xl', svgFireClusterXL);
|
||||||
// Data center icon
|
// Data center icon
|
||||||
loadImg('datacenter', svgDataCenter);
|
loadImg('datacenter', svgDataCenter);
|
||||||
|
// Power plant icon
|
||||||
|
loadImg('power-plant', svgPowerPlant);
|
||||||
// Satellite mission-type icons
|
// Satellite mission-type icons
|
||||||
loadImg('sat-mil', makeSatSvg('#ff3333'));
|
loadImg('sat-mil', makeSatSvg('#ff3333'));
|
||||||
loadImg('sat-sar', makeSatSvg('#00e5ff'));
|
loadImg('sat-sar', makeSatSvg('#00e5ff'));
|
||||||
@@ -592,6 +598,7 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
|||||||
kiwisdrGeoJSON && 'kiwisdr-layer',
|
kiwisdrGeoJSON && 'kiwisdr-layer',
|
||||||
internetOutagesGeoJSON && 'internet-outages-layer',
|
internetOutagesGeoJSON && 'internet-outages-layer',
|
||||||
dataCentersGeoJSON && 'datacenters-layer',
|
dataCentersGeoJSON && 'datacenters-layer',
|
||||||
|
powerPlantsGeoJSON && 'power-plants-layer',
|
||||||
militaryBasesGeoJSON && 'military-bases-layer',
|
militaryBasesGeoJSON && 'military-bases-layer',
|
||||||
firmsGeoJSON && 'firms-viirs-layer'
|
firmsGeoJSON && 'firms-viirs-layer'
|
||||||
].filter(Boolean) as string[];
|
].filter(Boolean) as string[];
|
||||||
@@ -1468,6 +1475,61 @@ const MaplibreViewer = ({ data, activeLayers, onEntityClick, flyToLocation, sele
|
|||||||
</Source>
|
</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 */}
|
{/* Military Base positions */}
|
||||||
{militaryBasesGeoJSON && (
|
{militaryBasesGeoJSON && (
|
||||||
<Source id="military-bases" type="geojson" data={militaryBasesGeoJSON as any}>
|
<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' && (() => {
|
{selectedEntity?.type === 'military_base' && (() => {
|
||||||
const base = data?.military_bases?.find((_: any, i: number) => `milbase-${i}` === selectedEntity.id);
|
const base = data?.military_bases?.find((_: any, i: number) => `milbase-${i}` === selectedEntity.id);
|
||||||
if (!base) return null;
|
if (!base) return null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import React, { useState, useEffect, useRef, useMemo } from "react";
|
import React, { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
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 packageJson from "../../package.json";
|
||||||
import { useTheme } from "@/lib/ThemeContext";
|
import { useTheme } from "@/lib/ThemeContext";
|
||||||
|
|
||||||
@@ -41,6 +41,7 @@ const FRESHNESS_MAP: Record<string, string> = {
|
|||||||
firms: "firms_fires",
|
firms: "firms_fires",
|
||||||
internet_outages: "internet_outages",
|
internet_outages: "internet_outages",
|
||||||
datacenters: "datacenters",
|
datacenters: "datacenters",
|
||||||
|
power_plants: "power_plants",
|
||||||
};
|
};
|
||||||
|
|
||||||
// POTUS fleet ICAO hex codes for client-side filtering
|
// 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: "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: "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: "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: "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 },
|
{ 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.
|
// 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.
|
// 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 { classifyAircraft } from "@/utils/aircraftClassification";
|
||||||
import { MISSION_COLORS, MISSION_ICON_MAP } from "@/components/map/icons/SatelliteIcons";
|
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 ─────────────────────────────────────────────────────────
|
// ─── Military Bases ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Classify base alignment: red = adversary, blue = US/allied, green = ROC
|
// 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 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 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 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 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 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>`)}`;
|
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;
|
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 {
|
export interface MilitaryBase {
|
||||||
name: string;
|
name: string;
|
||||||
country: string;
|
country: string;
|
||||||
@@ -422,6 +432,7 @@ export interface DashboardData {
|
|||||||
firms_fires?: FireHotspot[];
|
firms_fires?: FireHotspot[];
|
||||||
datacenters?: DataCenter[];
|
datacenters?: DataCenter[];
|
||||||
military_bases?: MilitaryBase[];
|
military_bases?: MilitaryBase[];
|
||||||
|
power_plants?: PowerPlant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── COMPONENT PROPS ────────────────────────────────────────────────────────
|
// ─── COMPONENT PROPS ────────────────────────────────────────────────────────
|
||||||
@@ -451,6 +462,7 @@ export interface ActiveLayers {
|
|||||||
internet_outages: boolean;
|
internet_outages: boolean;
|
||||||
datacenters: boolean;
|
datacenters: boolean;
|
||||||
military_bases: boolean;
|
military_bases: boolean;
|
||||||
|
power_plants: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectedEntity {
|
export interface SelectedEntity {
|
||||||
|
|||||||
Reference in New Issue
Block a user