mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-07-21 11:11:13 +02:00
8cddf6794d
New features:
- NASA GIBS (MODIS Terra) daily satellite imagery with 30-day time slider
- Esri World Imagery high-res satellite layer (sub-meter, zoom 18+)
- KiwiSDR SDR receivers on map with embedded radio tuner
- Sentinel-2 intel card — right-click for recent satellite photo popup
- LOCATE bar — search by coordinates or place name (Nominatim geocoding)
- SATELLITE style preset in bottom bar cycling
- v0.4 changelog modal on first launch
Fixes:
- Satellite imagery renders below data icons (imagery-ceiling anchor)
- Sentinel-2 opens full-res PNG directly (not STAC catalog JSON)
- Light/dark theme: UI stays dark, only map basemap changes
Security:
- Removed test files with hardcoded API keys from tracking
- Removed .git_backup directory from tracking
- Updated .gitignore to exclude test files, dev scripts, cache files
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Former-commit-id: e89e992293
82 lines
3.1 KiB
Python
82 lines
3.1 KiB
Python
"""
|
|
Sentinel-2 satellite imagery search via Microsoft Planetary Computer STAC API.
|
|
Free, keyless search for metadata + thumbnails. Used in the right-click dossier.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import datetime, timedelta
|
|
from cachetools import TTLCache
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Cache by rounded lat/lon (0.02° grid ~= 2km), TTL 1 hour
|
|
_sentinel_cache = TTLCache(maxsize=200, ttl=3600)
|
|
|
|
|
|
def search_sentinel2_scene(lat: float, lng: float) -> dict:
|
|
"""Search for the latest Sentinel-2 L2A scene covering a point."""
|
|
cache_key = f"{round(lat, 2)}_{round(lng, 2)}"
|
|
if cache_key in _sentinel_cache:
|
|
return _sentinel_cache[cache_key]
|
|
|
|
try:
|
|
from pystac_client import Client
|
|
|
|
catalog = Client.open("https://planetarycomputer.microsoft.com/api/stac/v1")
|
|
end = datetime.utcnow()
|
|
start = end - timedelta(days=30)
|
|
|
|
search = catalog.search(
|
|
collections=["sentinel-2-l2a"],
|
|
intersects={"type": "Point", "coordinates": [lng, lat]},
|
|
datetime=f"{start.isoformat()}Z/{end.isoformat()}Z",
|
|
sortby=[{"field": "datetime", "direction": "desc"}],
|
|
max_items=3,
|
|
query={"eo:cloud_cover": {"lt": 30}},
|
|
)
|
|
|
|
items = list(search.items())
|
|
if not items:
|
|
result = {"found": False, "message": "No clear scenes in last 30 days"}
|
|
_sentinel_cache[cache_key] = result
|
|
return result
|
|
|
|
item = items[0]
|
|
# Try to sign item first for Azure blob URLs
|
|
try:
|
|
import planetary_computer
|
|
item = planetary_computer.sign_item(item)
|
|
except ImportError:
|
|
pass # planetary_computer not installed, try unsigned URLs
|
|
except Exception as e:
|
|
logger.warning(f"Sentinel-2 signing failed: {e}")
|
|
|
|
# Get the rendered_preview (full-res PNG) and thumbnail separately
|
|
rendered = item.assets.get("rendered_preview")
|
|
thumbnail = item.assets.get("thumbnail")
|
|
|
|
# Full-res image URL — what opens when user clicks
|
|
fullres_url = rendered.href if rendered else (thumbnail.href if thumbnail else None)
|
|
# Thumbnail URL — what shows in the popup card
|
|
thumb_url = thumbnail.href if thumbnail else (rendered.href if rendered else None)
|
|
|
|
result = {
|
|
"found": True,
|
|
"scene_id": item.id,
|
|
"datetime": item.datetime.isoformat() if item.datetime else None,
|
|
"cloud_cover": item.properties.get("eo:cloud_cover"),
|
|
"thumbnail_url": thumb_url,
|
|
"fullres_url": fullres_url,
|
|
"bbox": list(item.bbox) if item.bbox else None,
|
|
"platform": item.properties.get("platform", "Sentinel-2"),
|
|
}
|
|
_sentinel_cache[cache_key] = result
|
|
return result
|
|
|
|
except ImportError:
|
|
logger.warning("pystac-client not installed — Sentinel-2 search unavailable")
|
|
return {"found": False, "error": "pystac-client not installed"}
|
|
except Exception as e:
|
|
logger.error(f"Sentinel-2 search failed for ({lat}, {lng}): {e}")
|
|
return {"found": False, "error": str(e)}
|