Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

v0 — AOI preview tool (shipped)

v0 — AOI preview tool

Given an AOI, a date range, and a list of sensors, return the intersecting STAC items with their footprints, timestamps, scene-wide cloud cover, and preview thumbnail URLs — before downloading anything.

Status: shipped. The code referenced by this doc is the contents of this PR (projects/satellite_viewer/). This page exists so the staging arc starts from the thing already built, not from a blank slate.

Goal

A user-facing tool that answers “what scenes are available here?” without paying the bandwidth bill of opening any of them. It’s the upstream-of-download step: inspect first, choose, then commit to a download (or a geocatalog.from_stac_items(...) ingest, or a v2.5 clear-fraction request).

Question answered

“Across these sensors, between these dates, which scenes touched my AOI, when, and how cloudy were they at the scene level?”

This is scene-level metadata only. v2.5 is the same input shape but the output is the pixel-level truthful clear-fraction for that AOI — see v2_5_pixel_level.md.

Scope

Polar-orbiting / tasked sensors only, all via Microsoft Planetary Computer STAC (anonymous read, no auth):

keydescription
sentinel-2-l2aSentinel-2 L2A surface reflectance (10-60 m, ~5 d revisit)
sentinel-1-grdSentinel-1 C-band SAR GRD (10 m, all-weather)
landsat-c2-l2Landsat C2 L2 surface reflectance (30 m, ~16 d revisit)
modis-09a1MODIS Terra/Aqua 8-day surface reflectance (500 m)
emit-l2a-rflEMIT L2A imaging spectrometer reflectance (60 m, tasked)

Geostationary platforms (GOES, Himawari, Meteosat) are deliberately out of scope — their ~5-15 minute cadence makes “is there imagery over my AOI?” trivially yes inside any scan sector.

Algorithm

def search(sensor, aoi, t0, t1, *, cloud_lt=None, max_items=100):
    cfg = SENSORS[sensor]
    client = pystac_client.Client.open(
        cfg.stac_endpoint,
        modifier=planetary_computer.sign_inplace if cfg.requires_pc_signing else None,
    )
    items = client.search(
        collections=[cfg.collection_id],
        intersects=aoi.__geo_interface__,
        datetime=f"{t0.isoformat()}/{t1.isoformat()}",
        query={cfg.cloud_field: {"lt": cloud_lt}} if cloud_lt is not None else None,
        max_items=max_items,
    ).items()
    return gpd.GeoDataFrame.from_records(
        {
            "id": it.id,
            "datetime": ...,
            "geometry": shape(it.geometry),
            "sensor": cfg.name,
            "cloud_cover": it.properties.get(cfg.cloud_field),
            "preview_url": it.assets[cfg.preview_asset].href,
        }
        for it in items
    )

One STAC call per sensor; signed rendered_preview hrefs let the UI display thumbnails without fetching pixel data.

Architecture

projects/satellite_viewer/
├── src/satellite_viewer/
│   ├── __init__.py          # exports search, SENSORS
│   ├── sensors.py           # the SENSORS registry (5 entries)
│   └── search.py            # the search() function above
├── apps/
│   ├── panel_app.py         # Panel subapp (leafmap + holoviews + tabulator)
│   └── streamlit_app.py     # Streamlit subapp (folium + altair)
├── notebooks/
│   └── viewer.py            # jupytext py:percent notebook (ipywidgets + leafmap + matplotlib)
└── tests/
    └── test_sensors.py      # offline registry sanity checks (4 tests)

Repos & how they slot in:

Output

A single GeoDataFrame, schema stable across sensors:

columns:
  id                   : string         # STAC item id
  datetime             : datetime[UTC]  # acquisition timestamp
  geometry             : Polygon        # scene footprint (WGS84)
  sensor               : string         # registry key
  cloud_cover          : float | NaN    # eo:cloud_cover (scene-wide)
  preview_url          : string | None  # signed rendered_preview href
crs:  EPSG:4326

UI integration

Three subapps share this DataFrame as the data layer:

  1. Panel (apps/panel_app.py) — sidebar widgets, leafmap map, holoviews timeline, FlexBox thumbnail strip, tabulator results.
  2. Streamlit (apps/streamlit_app.py) — sidebar widgets, folium map with Draw plugin, altair timeline, st.columns thumbnail grid, st.dataframe results.
  3. Jupyter (notebooks/viewer.py) — ipywidgets controls, leafmap map with DrawControl, matplotlib timeline, ipywidgets.Image grid.

All three call satellite_viewer.search(...) for discovery; they only differ in the presentation layer.

Compute budget

Per request:

Risks & open questions

Acceptance

Where v0 ends, the climatology starts

v0 returns which scenes intersect my AOI. Three follow-on questions are answered by the climatology stages:

The v2.5 AOI subapp will reuse satellite_viewer.search verbatim for its discovery step — that’s the v0 → v2.5 contract.

Out of scope