mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-31 07:57:28 +02:00
feat: add military bases map layer for Western Pacific
Add 18 US military bases (Japan, Guam, South Korea, Hawaii, Diego Garcia) as a toggleable map layer. Follows the existing data center layer pattern: static JSON → backend fetcher → slow-tier API → frontend GeoJSON layer. Includes red circle markers with labels, click popups showing operator and branch info, and a toggle in the left panel. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
130287bb49
commit
05de14af9d
@@ -0,0 +1,146 @@
|
||||
[
|
||||
{
|
||||
"name": "Kadena Air Base",
|
||||
"country": "Japan",
|
||||
"operator": "USAF 18th Wing",
|
||||
"branch": "air_force",
|
||||
"lat": 26.351,
|
||||
"lng": 127.767
|
||||
},
|
||||
{
|
||||
"name": "Marine Corps Air Station Futenma",
|
||||
"country": "Japan",
|
||||
"operator": "USMC",
|
||||
"branch": "marines",
|
||||
"lat": 26.274,
|
||||
"lng": 127.755
|
||||
},
|
||||
{
|
||||
"name": "Fleet Activities Yokosuka",
|
||||
"country": "Japan",
|
||||
"operator": "USN 7th Fleet",
|
||||
"branch": "navy",
|
||||
"lat": 35.283,
|
||||
"lng": 139.671
|
||||
},
|
||||
{
|
||||
"name": "Fleet Activities Sasebo",
|
||||
"country": "Japan",
|
||||
"operator": "USN",
|
||||
"branch": "navy",
|
||||
"lat": 33.159,
|
||||
"lng": 129.722
|
||||
},
|
||||
{
|
||||
"name": "Misawa Air Base",
|
||||
"country": "Japan",
|
||||
"operator": "USAF 35th Fighter Wing",
|
||||
"branch": "air_force",
|
||||
"lat": 40.703,
|
||||
"lng": 141.368
|
||||
},
|
||||
{
|
||||
"name": "MCAS Iwakuni",
|
||||
"country": "Japan",
|
||||
"operator": "USMC / USN",
|
||||
"branch": "marines",
|
||||
"lat": 34.144,
|
||||
"lng": 132.236
|
||||
},
|
||||
{
|
||||
"name": "Yokota Air Base",
|
||||
"country": "Japan",
|
||||
"operator": "USAF 374th Airlift Wing",
|
||||
"branch": "air_force",
|
||||
"lat": 35.748,
|
||||
"lng": 139.348
|
||||
},
|
||||
{
|
||||
"name": "Camp Zama",
|
||||
"country": "Japan",
|
||||
"operator": "US Army Japan",
|
||||
"branch": "army",
|
||||
"lat": 35.488,
|
||||
"lng": 139.395
|
||||
},
|
||||
{
|
||||
"name": "NAF Atsugi",
|
||||
"country": "Japan",
|
||||
"operator": "USN",
|
||||
"branch": "navy",
|
||||
"lat": 35.455,
|
||||
"lng": 139.449
|
||||
},
|
||||
{
|
||||
"name": "Camp Hansen",
|
||||
"country": "Japan",
|
||||
"operator": "USMC",
|
||||
"branch": "marines",
|
||||
"lat": 26.441,
|
||||
"lng": 127.775
|
||||
},
|
||||
{
|
||||
"name": "White Beach Naval Facility",
|
||||
"country": "Japan",
|
||||
"operator": "USN",
|
||||
"branch": "navy",
|
||||
"lat": 26.334,
|
||||
"lng": 127.897
|
||||
},
|
||||
{
|
||||
"name": "Andersen Air Force Base",
|
||||
"country": "Guam",
|
||||
"operator": "USAF 36th Wing",
|
||||
"branch": "air_force",
|
||||
"lat": 13.584,
|
||||
"lng": 144.930
|
||||
},
|
||||
{
|
||||
"name": "Naval Base Guam",
|
||||
"country": "Guam",
|
||||
"operator": "USN",
|
||||
"branch": "navy",
|
||||
"lat": 13.444,
|
||||
"lng": 144.653
|
||||
},
|
||||
{
|
||||
"name": "Osan Air Base",
|
||||
"country": "South Korea",
|
||||
"operator": "USAF 51st Fighter Wing",
|
||||
"branch": "air_force",
|
||||
"lat": 37.090,
|
||||
"lng": 127.030
|
||||
},
|
||||
{
|
||||
"name": "Camp Humphreys",
|
||||
"country": "South Korea",
|
||||
"operator": "US Army / USFK HQ",
|
||||
"branch": "army",
|
||||
"lat": 36.963,
|
||||
"lng": 127.031
|
||||
},
|
||||
{
|
||||
"name": "Kunsan Air Base",
|
||||
"country": "South Korea",
|
||||
"operator": "USAF 8th Fighter Wing",
|
||||
"branch": "air_force",
|
||||
"lat": 35.904,
|
||||
"lng": 126.616
|
||||
},
|
||||
{
|
||||
"name": "Naval Station Pearl Harbor",
|
||||
"country": "Hawaii",
|
||||
"operator": "USN / USINDOPACOM HQ",
|
||||
"branch": "navy",
|
||||
"lat": 21.345,
|
||||
"lng": -157.974
|
||||
},
|
||||
{
|
||||
"name": "Diego Garcia",
|
||||
"country": "BIOT",
|
||||
"operator": "USN / USAF",
|
||||
"branch": "navy",
|
||||
"lat": -7.316,
|
||||
"lng": 72.411
|
||||
}
|
||||
]
|
||||
@@ -305,6 +305,7 @@ async def live_data_slow(request: Request,
|
||||
"internet_outages": _f(d.get("internet_outages", [])),
|
||||
"firms_fires": _f(d.get("firms_fires", [])),
|
||||
"datacenters": _f(d.get("datacenters", [])),
|
||||
"military_bases": _f(d.get("military_bases", [])),
|
||||
"freshness": dict(source_timestamps),
|
||||
}
|
||||
bbox_tag = f"{s},{w},{n},{e}" if has_bbox else "full"
|
||||
|
||||
@@ -40,7 +40,7 @@ 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_cctv, fetch_kiwisdr,
|
||||
fetch_internet_outages, fetch_datacenters, fetch_military_bases, fetch_cctv, fetch_kiwisdr,
|
||||
)
|
||||
from services.fetchers.geo import ( # noqa: F401
|
||||
fetch_ships, fetch_airports, find_nearest_airport, cached_airports,
|
||||
@@ -85,6 +85,7 @@ def update_slow_data():
|
||||
fetch_frontlines,
|
||||
fetch_gdelt,
|
||||
fetch_datacenters,
|
||||
fetch_military_bases,
|
||||
]
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=len(slow_funcs)) as executor:
|
||||
futures = [executor.submit(func) for func in slow_funcs]
|
||||
|
||||
@@ -30,7 +30,8 @@ latest_data = {
|
||||
"space_weather": None,
|
||||
"internet_outages": [],
|
||||
"firms_fires": [],
|
||||
"datacenters": []
|
||||
"datacenters": [],
|
||||
"military_bases": []
|
||||
}
|
||||
|
||||
# Per-source freshness timestamps
|
||||
|
||||
@@ -143,6 +143,43 @@ def fetch_datacenters():
|
||||
_mark_fresh("datacenters")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Military Bases (static JSON — Western Pacific)
|
||||
# ---------------------------------------------------------------------------
|
||||
_MILITARY_BASES_PATH = Path(__file__).parent.parent.parent / "data" / "military_bases.json"
|
||||
|
||||
|
||||
def fetch_military_bases():
|
||||
"""Load static military base locations (Western Pacific focus)."""
|
||||
bases = []
|
||||
try:
|
||||
if not _MILITARY_BASES_PATH.exists():
|
||||
logger.warning(f"Military bases file not found: {_MILITARY_BASES_PATH}")
|
||||
return
|
||||
raw = json.loads(_MILITARY_BASES_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
|
||||
bases.append({
|
||||
"name": entry.get("name", "Unknown"),
|
||||
"country": entry.get("country", ""),
|
||||
"operator": entry.get("operator", ""),
|
||||
"branch": entry.get("branch", ""),
|
||||
"lat": lat, "lng": lng,
|
||||
})
|
||||
logger.info(f"Military bases: {len(bases)} locations loaded")
|
||||
except Exception as e:
|
||||
logger.error(f"Error loading military bases: {e}")
|
||||
with _data_lock:
|
||||
latest_data["military_bases"] = bases
|
||||
if bases:
|
||||
_mark_fresh("military_bases")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CCTV Cameras
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
"""Tests for military bases data and fetcher."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from services.fetchers._store import latest_data, _data_lock
|
||||
|
||||
|
||||
BASES_PATH = Path(__file__).parent.parent / "data" / "military_bases.json"
|
||||
|
||||
|
||||
class TestMilitaryBasesData:
|
||||
"""Validate the static military_bases.json file."""
|
||||
|
||||
def test_json_file_exists(self):
|
||||
assert BASES_PATH.exists()
|
||||
|
||||
def test_all_entries_have_required_fields(self):
|
||||
raw = json.loads(BASES_PATH.read_text(encoding="utf-8"))
|
||||
assert len(raw) > 0
|
||||
for entry in raw:
|
||||
assert "name" in entry and entry["name"]
|
||||
assert "country" in entry and entry["country"]
|
||||
assert "operator" in entry and entry["operator"]
|
||||
assert "branch" in entry and entry["branch"]
|
||||
assert "lat" in entry and isinstance(entry["lat"], (int, float))
|
||||
assert "lng" in entry and isinstance(entry["lng"], (int, float))
|
||||
|
||||
def test_coordinates_in_valid_range(self):
|
||||
raw = json.loads(BASES_PATH.read_text(encoding="utf-8"))
|
||||
for entry in raw:
|
||||
assert -90 <= entry["lat"] <= 90, f"{entry['name']} has invalid lat"
|
||||
assert -180 <= entry["lng"] <= 180, f"{entry['name']} has invalid lng"
|
||||
|
||||
def test_branch_values_are_known(self):
|
||||
known_branches = {"air_force", "navy", "marines", "army"}
|
||||
raw = json.loads(BASES_PATH.read_text(encoding="utf-8"))
|
||||
for entry in raw:
|
||||
assert entry["branch"] in known_branches, f"{entry['name']} has unknown branch: {entry['branch']}"
|
||||
|
||||
|
||||
class TestFetchMilitaryBases:
|
||||
"""Test the fetcher populates latest_data correctly."""
|
||||
|
||||
def test_fetch_populates_store(self):
|
||||
from services.fetchers.infrastructure import fetch_military_bases
|
||||
fetch_military_bases()
|
||||
with _data_lock:
|
||||
bases = latest_data["military_bases"]
|
||||
assert len(bases) > 0
|
||||
assert all("name" in b and "lat" in b and "lng" in b for b in bases)
|
||||
|
||||
def test_includes_key_bases(self):
|
||||
from services.fetchers.infrastructure import fetch_military_bases
|
||||
fetch_military_bases()
|
||||
with _data_lock:
|
||||
names = {b["name"] for b in latest_data["military_bases"]}
|
||||
assert "Kadena Air Base" in names
|
||||
assert "Fleet Activities Yokosuka" in names
|
||||
assert "Andersen Air Force Base" in names
|
||||
Reference in New Issue
Block a user