Skip to content

UI improvements: split screen, faster profile, smoother tiles#59

Merged
mgovorcin merged 7 commits into
opera-adt:mainfrom
mgovorcin:ui-improvements
Jun 17, 2026
Merged

UI improvements: split screen, faster profile, smoother tiles#59
mgovorcin merged 7 commits into
opera-adt:mainfrom
mgovorcin:ui-improvements

Conversation

@mgovorcin

@mgovorcin mgovorcin commented May 28, 2026

Copy link
Copy Markdown
Collaborator

UI improvements: split screen, faster profile, smoother tiles

A grab-bag of frontend + backend polish on top of main, packaged together so the underlying refactors (custom Leaflet panes, two-slot tile model, shared RasterTileLayer props) only land once.

New features

  • Split-screen comparison. Toggle from the right toolbar to render a second dataset in a clipped pane with a draggable divider. A dedicated "Right Layer" sidebar section gives it its own dataset, colormap, vmin/vmax, opacity, time-step slider, and histogram. The pixel-inspector reads the correct dataset and time index based on which side of the divider the cursor is on (tooltip prefixed L:/R:).
  • Stacked basemaps. Basemap popover now exposes two slots — Layer 1 (bottom) and Layer 2 (top) — each with its own opacity slider and a Swap-order button. Lets you combine, e.g., satellite imagery underneath a labelled street map for context, with on-the-fly blending.
  • Coordinate grid. Plain / zebra-frame graticule, sitting above tiles but below markers (custom Leaflet pane at z-index 450). Lines render with a white halo so they stay visible on dark satellite basemaps.
  • Map annotations. Click the pen-to-square button to enter annotation mode, then click anywhere on the map to drop a labelled marker with custom text, color, and font size. Markers are draggable; click an existing one to edit text/color/size or delete it. Persists across map operations as part of state.annotations.
  • Pixel value on hover. Eye-dropper button on the right toolbar toggles a hover inspector that follows the cursor and reads the raster value (with dataset unit) at the mouse position from the backend. In split-screen mode it queries the correct dataset and time index per side and prefixes the tooltip L:/R:. Issue ticket: Hover to show value #35
  • Two-slot tile loading. New tile URLs load behind the active layer at opacity 0 and atomically swap on load; no more mixed-tile seam artifacts when scrubbing time / colormap / vmin-vmax. A bottom-right spinner reflects in-flight tile counts across basemaps + raster.
  • Screenshot export. Camera icon (📷) added to the chart panel, colorbar, and LOS indicator — clicking it downloads a transparent-background PNG of just that element. A black/white font toggle on the colorbar and LOS lets labels read against any background.
  • Faster profile extraction. Binned-median mode now vectorizes all per-bin pyproj.transform + xarray.sel(method='nearest') calls into a single batched read — 20–100× faster on zarr-backed datasets. The start→end gradient line was restored in binned mode (was hidden behind showLine: false). X button on the panel now closes it instead of just clearing data. Awkward shape was addressed before: "radius" round profile is strangely shaped #37
  • Pre-computed histograms. tifs-to-geozarr embeds per-time-step histogram stats (min/max/percentiles/bins) as a bowser_histogram zarr attr; the /histogram endpoint returns cached stats instantly. Falls back to a full compute for older stores.

Smaller polish

  • Reference + point-picking buttons moved from the right toolbar to the left, so the right one stays focused on visualization.
  • Toolbar background uses CSS variables → follows the dark/light theme.
  • Reference marker visibility decoupled from refEnabled (new refMarkerVisible state + toggle).
  • Complex-mode useEffect no longer overwrites the localStorage-restored vmin/vmax when switching datasets.
  • Browser-side histogram cache keyed by dataset:timeIndex (no refetch when scrubbing).
  • Toolbar-hide now also hides the Points panel (lifted toolbarsVisible to App.tsx).

Backend fixes

  • Layer-mask time-index clamping. When a mask dataset has fewer time steps than the main dataset, the tile endpoint now isels min(time_idx, size-1) (falling back to 0 when there's no time context) instead of raising IndexError.

Notes for reviewers

  • RasterTileLayer was generalized to accept pane and overrideDataset/colormap/vmin/vmax/opacity/timeIndex props so the split-screen right layer reuses the same fetch/swap pipeline. No duplicated tile logic.
  • New panes (splitLeft, splitRight, graticule) are created synchronously in the render function of their respective components — useEffect / useLayoutEffect ran after React-Leaflet's addLayer, causing getPane() is undefined crashes. This is the only reliable ordering with react-leaflet ^4.2.
  • clip-path for the split panes uses polygon() with absolute pixel values derived from the map-pane's CSS translate (updated on move/zoom/resize); percentage-based inset() was relative to the pane's full tile-buffer extent, not the viewport.

Testing

Split-screen
image

mgovorcin and others added 4 commits May 7, 2026 20:04
Frontend:
- Two-slot tile model: pending layer loads at full opacity behind active;
  atomic swap on 'load' event eliminates mixed-tile seam artifacts
- Loading spinner driven by tileloadstart/load events across all layers
  (basemaps + raster), with a ref counter for concurrent loads
- Screenshot: transparent background via CSS-variable injection on the
  live element before toPng; white/black font toggle on colorbar + LOS
- Toolbar hide now also hides the Points panel (lifted toolbarsVisible
  to App.tsx)
- Reference marker visibility decoupled from refEnabled (new
  refMarkerVisible state + TOGGLE_REF_MARKER_VISIBLE action)
- complexMode useEffect no longer overwrites localStorage-restored
  vmin/vmax when switching datasets (removed currentDataset from deps)
- Histogram: module-level browser cache keyed by dataset:timeIndex

Backend:
- tifs-to-geozarr: pre-compute per-time-step histogram stats at
  conversion time and embed as bowser_histogram zarr attr; server
  returns cached stats instantly, falls back to full compute on old stores
- Layer mask time-index clamping: isel safe_idx = min(time_idx, size-1)
  with fallback to index 0 when no time context, fixes IndexError when
  mask has fewer time steps than the main dataset

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split-screen mode lets users compare two datasets side-by-side with an
independent draggable divider, plus its own dataset/colormap/vmin-vmax/
opacity/time-step controls and histogram in a dedicated "Right Layer"
sidebar section. The pixel inspector reads the correct dataset and time
index based on which side of the divider the cursor is on.

Other UI: coordinate grid moved into a dedicated Leaflet pane so it sits
above tiles but below markers; reference and point-picking tools moved
to the left toolbar so the right one stays focused on visualization;
toolbar background uses CSS variables so it follows the dark/light theme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The binned-median path was calling pyproj.transform + xarray.sel(method=
'nearest') once per sample point, which scaled with bins x along-steps x
perpendicular-offsets and dominated extraction latency on zarr-backed
datasets. Add _read_lonlat_batch() that collapses all per-bin lookups
into a single vectorized transform and a single xarray sel, with array-
based bounds masking for off-grid points. COG mode falls back to per-
point reads (rasterio window reads are not easily batchable here).

Also restore the start->end gradient line on the median series when
sampling_interval > 0 - it was hidden behind showLine=false, so binned
profiles only showed disconnected dots without the directional cue.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mgovorcin mgovorcin requested a review from scottstanie May 28, 2026 23:48
- Export the active layer as a GeoTIFF from a new sidebar EXPORT section,
  applying the same masking shown on the map (recommended, layer, and
  custom masks). Works in both MD (GeoZarr) and COG modes via a shared
  masking helper; masked pixels become NaN nodata at native resolution.
- Add geozarr.storage_options_for() so s3:// stores open anonymously by
  default (set BOWSER_S3_ANON=0 to use the normal credential chain for
  private buckets); wire it through state and the bbox sniffer.
- ProfileChart: the close (X) button now deactivates the profile, not
  just clears it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mgovorcin mgovorcin marked this pull request as ready for review June 17, 2026 22:33
mgovorcin and others added 2 commits June 17, 2026 15:38
storage_options_for() returned {} for non-s3:// paths, and the callers
passed it as storage_options={} to xr.open_zarr / zarr.open_group. On
zarr >= 3.1 that raises TypeError("'storage_options' was provided but
unused"), breaking every local store (bowser run --stack-file cube.zarr,
local catalogs, the pip/uvx quickstart). The Docker image masked it by
pinning an older zarr.

Return None for non-s3:// URIs so callers pass nothing for local paths;
s3:// still gets {"anon": ...}. Verified both local and anonymous-s3
opens on zarr 3.1.6 / xarray 2026.4.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surfaces the BOWSER_S3_ANON setting as a CLI flag. Default (flag omitted)
leaves the env/anon-by-default behavior unchanged; --no-s3-anon signs s3://
reads with the normal AWS credential chain for private buckets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@mgovorcin mgovorcin merged commit 95f9ab4 into opera-adt:main Jun 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant