# openEO explorer in the browser

`openeo.html` runs editable openEO Python in Pyodide, compiles the resulting graph with the
shared Rust adapter in WASM, executes it in the Folia worker, and renders the result with
`folia-render`. Raster values, labeled tensors, and vector tables use the same execution path;
the browser does not call a gateway to perform the computation.

## Start

From `products/sdk/folia-render-web`, with Rust, `wasm-pack`, Node, and Python 3 installed:

```sh
npm run demo:openeo:prepare
npm run build:wasm:openeo
npm run demo:serve
```

Open <http://127.0.0.1:8788/folia-render-web/demo/openeo.html>. Choose an example and press
**Run Python** (or Ctrl/Cmd Enter). `demo/build-openeo-wasm.mjs` is the build wrapper used by
`build:wasm:openeo`; it finds a clang with a `wasm32` target and passes it to `wasm-pack`. If
automatic discovery fails, set `CC_wasm32_unknown_unknown` and
`AR_wasm32_unknown_unknown` to LLVM's `clang` and `llvm-ar`.

The companion [compatibility report](./openeo-conformance.html) compares the stable standard
catalog with the general adapter, admitted hosted profile, and live deployment. It also reports
the latest retained official-suite measurement separately from hosted-profile fixtures. Its
counts are generated from the governed registries, crosswalk, suite reports, and support matrix.

The compiler-enabled `browser-vectors` build is opt-in. The final combined WASM is
25,535,916 bytes after standard spatial filtering; the previous measured artifact was
25,499,551 bytes (36,365 additional bytes). The optional local GeoArrow bundle is 1,138,526 bytes. The lean
renderer build remains available through `build:wasm`.

## Examples

| Example | What it does | Returned value |
| --- | --- | --- |
| Vegetation / NDVI | Loads Sentinel-2 red/NIR bands and reduces time | Scalar grid |
| True color | Loads red, green, and blue bands and reduces time | Three-band Tensor |
| Band cube | Keeps red/NIR and the acquisition coordinate | Time × band Tensor |
| NDVI threshold | Masks values below the typed threshold | Scalar grid |
| Copernicus DEM | Loads the static `COPERNICUS_30` elevation band | Scalar 30 m grid |
| HRRR isotherms | Interpolates catalog-native HRRR PRS temperature/height profiles onto catalog-native GLO-30 terrain, then traces governed isolines | Vector table rendered as MapLibre lines |
| HRRR orography | Loads HGT through the same catalog-native hourly Zarr path and traces elevation contours | Vector table rendered as MapLibre lines |
| Overture roads | Filters, clips, and measures transportation segments | Feature table rendered with GeoArrow |
| Road total | Runs the same road graph and explicitly aggregates `length_m` | Summary table |

The first four examples preserve the existing Sentinel-2 fixture and live behavior. The DEM and
road examples have live browser and independent numerical evidence recorded in
`.tickets/impl/openeo-vector-dem/tracks/verification.md`.

The HRRR temperature example is the browser counterpart to
`demos/wasatch-temperature-downscale`: it loads eleven matching HRRR pressure-level temperature
and height planes plus Copernicus DEM GLO-30, interpolates the profile to terrain height with
`folia_profile_interp`, and feeds that raster to the governed contour operation. The Python and
CLI forms author catalog identities and logical coordinates; neither contains an hourly store
URL, physical array path, or copied HRRR projection.
The adjacent orography example exercises a second physical Zarr array (`HGT`) through the same
generic per-hour source adapter. Both contour views use a registry palette and native MapLibre
line layers for display, so the governed vector result shares the basemap canvas instead of a
second WebGL canvas obscuring it.

The editor uses ordinary `DataCube` and `openeo.processes` calls. The final value is assigned to
`result`. UI values enter a fresh Python namespace as `parameters`, so they are not inserted into
source text. A run captures one immutable parameter snapshot, and the receipt contains the exact
Python and graph that ran. Panning the map does not execute; use **Use map bounds** or type an AOI,
then press **Run Python**.

The **Data source** selector chooses the live registry profile or `?source=fixture`. Fixture mode
is deterministic and refuses locations or dates outside its captured data. Live failures never
fall back to the fixture. The optional street basemap is context only and has separate OpenStreetMap
attribution.

## The new live profiles

The DEM profile uses the checked-in Copernicus DEM registry records and the Microsoft Planetary
Computer STAC catalog. Asset URLs use short-lived SAS tokens, and the source record declares the
CORS route needed by the browser worker. The product is one static snapshot at
`2021-04-22T00:00:00Z`; the default openEO interval is the half-open UTC day beginning at that
instant. A user-supplied interval must contain the publication instant. The fixed example AOIs are
Naples `[14.20,40.80,14.21,40.81]` and Sacramento `[-121.495,38.575,-121.49,38.58]`.

`openeo-dem.py` calls `load_collection('COPERNICUS_30', ...)` with `bands=['data']`, reduces the
single `t` dimension with `mean`, and declares `GTiff` for scalar display. It does not embed a
scene, asset URL, elevation value, or fixture fallback. Both fixed AOIs passed independent source-window verification: 1,369 Naples and 361
Sacramento cells matched exactly, including the returned grid. Each run read under 5 MiB.

The road profile reads the pinned Overture transportation release `2026-08-19.0` from its static
STAC catalog. `openeo-roads.py` supplies the AOI as an inline rectangular `load_geojson` mask and
an explicit `folia_vector_clip`, then applies the typed road predicates and spherical geodesic
length. `openeo-road-total.py` uses the explicit `folia_table_aggregate` extension to compute the
summary in the engine. The browser does not sum delivered feature rows in JavaScript. The frozen SF case returned 36 independently matched feature IDs and clipped geometries;
its engine total is 18,632.85230313573 metres (within 0.01 m of the independent oracle).
A separate Sacramento AOI returned six features and passed clipping and map/table selection
checks. Both use live source discovery.

## What runs where

The execution boundary is:

```text
Python in Pyodide
  → openEO process graph
  → shared Rust adapter
  → WASM engine and worker
  → Raster / Tensor (FTN1) or Table (FTB1)
  → folia-render / GeoArrow display and picking
```

Raster and Tensor output keeps the existing value contracts. A vector `Data::Table` crosses as
canonical FTB1 bytes with descriptors and batch boundaries; Rust decodes the table, and the
browser-owned GeoArrow path constructs the rendered layers. The table view keeps feature order,
nullable rows, geometry metadata, and property types. `load_geojson` follows the pinned proposal
shape. `folia_vector_filter_attributes`, `folia_vector_clip`, `folia_vector_length`, and
`folia_table_aggregate` are explicitly namespaced Folia extensions. Their presence does not claim
standard openEO conformance or an experimental L2B process; unsupported L2B work remains a
separate gap.

## Source and number behavior

The live profile passes raw registry dataset, distribution, and source documents through the
execution receipt. Sentinel-2 reads Earth Search COG ranges and preserves its raw digital numbers.
The DEM profile uses Earth Search's anonymous Copernicus GLO-30 COGs and preserves their EPSG:4326
30 m source metadata. Because the GLO-30 bucket omits browser CORS headers, the demo server exposes
only its exact immutable tile paths through a same-origin range proxy; catalog and asset identity
remain the registry/STAC values. Returned grid metadata, rather than the requested rectangle alone,
defines the actual raster bounds and padding.

Arrow and FTB1 preserve signed and unsigned 64-bit integers. Before a table view crosses into
JavaScript, `Int64` and `UInt64` values are displayed as decimal strings so they cannot be rounded
by a JSON/JavaScript number conversion. The RFC 7946 GeoJSON writer retains JSON number tokens for
numeric properties; clients that need exact wide-integer values should use the FTB1 table and its
descriptors. Mixed or precision-sensitive numeric columns are represented as exact decimal text.

## Limits and what they mean

The browser validates authored requests and delivered outputs against these bounds:

- Python source: 256 KiB; serialized graph: 2 MiB; captured STAC request: 16 KiB.
- Live output values: 32 MiB; fixture output values: 1 MiB. Descriptor metadata is separate.
- FTB1 table: 32 MiB, at most 10,000 rows, 128 columns, 1,000,000 cells, 10,000 batches, and
  1,000,000 geometry coordinates.
- DEM: one load, one `data` band, one candidate tile, 30 m minimum resolution, 8 Mi source-grid
  pixels, a 128 MiB decoded/source estimate, and a 32 MiB output.
- Roads: an 8 km maximum AOI side, at most 512 catalog items and 16 MiB of catalog metadata,
  plus the table limits above.
- HRRR terrain profile: one analysis hour, one PRS `TMP` variable, one PRS `HGT` variable,
  2–24 matching pressure levels, one scalar GLO-30 target, a 30 km maximum AOI side, and at most
  24 explicit contour levels. The local demo server provides a same-origin, read-only range
  proxy because the anonymous upstream HRRR bucket does not offer a reliable browser CORS
  contract.
- Sentinel-2: one load, four bands, eight preflight candidates, a 14-day interval, and the
  existing 10 km per-side allowance including the grid-snap margin.

With the openEO WASM/runtime assets prepared and the demo server running, exercise the live HRRR
path with:

```sh
FOLIA_OPENEO_HRRR_BASE_URL=http://127.0.0.1:8788/folia-render-web/demo/openeo.html \
  node test/browser_openeo_hrrr.mjs
```

The limits are browser admission, decoded-value, and transfer limits. They are not a hard total
process-memory or network-wire cap: the synchronous WASM transport can buffer a provider response
before checking its Range result, and a provider that ignores Range may return a whole object.
Receipts report actual source reads and bytes. The DEM profile's 128 MiB source figure is an
estimate, not a wire guarantee. A display view can also refuse with a named size limit while its
canonical FTB1 value and descriptor remain usable to a caller that supports a non-table view.

## Verification

Deterministic tests run without a catalog or asset request:

```sh
npm test
```

Live browser verification is opt-in and needs a served demo plus the built WASM artifact:

```sh
OPENEO_LIVE_RUN=1 OPENEO_LIVE_BASE_URL=http://127.0.0.1:8788 npm run test:openeo:live
```

The live browser gate checks profile selection, requests, source identities, limits, rendering,
and refusal paths. It is not the numerical oracle. The separate
`test/openeo_live_oracle.py` compares populated raster values, missing-data positions, CRS, and
grid alignment against independently decoded source windows. DEM and road evidence uses
`test/openeo_vector_dem_oracle.py`, an offline verifier over captured response bodies and
the retained independent road handoff. Run it with `--help` for explicit evidence arguments.

The vector/DEM live driver is `test/browser_openeo_vector_dem.mjs`; it requires an explicit
opt-in and enforces a cumulative 512 MiB capture budget. List its fixed cases with
`npm run test:openeo:vector-dem -- --list`; pass `--run --case roadsSF` for a live check.

The fully offline Table/locked-replay proof uses an independently minted native lock:

```sh
FOLIA_OPENEO_TABLE_ORACLE_DIR=/tmp/folia-openeo-table cargo test --manifest-path ../Cargo.toml -p folia-openeo --test browser_vector_runtime
FOLIA_OPENEO_TABLE_ORACLE_DIR=/tmp/folia-openeo-table npm run test:openeo:table
```

The standard `filter_vector` browser parity proof uses only inline GeoJSON and the pinned
independent GEOS/spec receipts (including two documented RelateNG adjudications). Point both commands at the same receipt directory and a loopback preview:

```sh
FOLIA_OPENEO_SPATIAL_ORACLE_DIR=/tmp/folia-openeo-spatial \
  cargo test --manifest-path ../Cargo.toml -p folia-openeo --test vector_spatial_conformance
FOLIA_OPENEO_SPATIAL_ORACLE_DIR=/tmp/folia-openeo-spatial \
  FOLIA_OPENEO_SPATIAL_BASE_URL=http://127.0.0.1:18791 npm run test:openeo:spatial
```

The browser test requires a loopback preview and Playwright/Chromium; set
`FOLIA_OPENEO_TABLE_BASE_URL`, `OPENEO_PLAYWRIGHT_NODE_MODULES` and `FOLIA_BROWSER`
when using paths outside the repository defaults. The task also verified map-bounds execution,
source-change refusal, existing Sentinel-2 fixture parity, and the focused web unit tests.

## Boundaries

`save_result(format="GTiff")` declares the terminal format used by the graph; the browser returns
the bounded value and display view and does not provide a downloadable GeoTIFF. This demo is not
an HTTP openEO backend, XYZ/WMTS service, production authentication layer, or job scheduler. It
does not add computation to a gateway, and it does not silently substitute a fixture, another
provider, an unbounded catalog, or an unsupported process.
