Skip to content

feat(netcdf): fit, colormap, and band combination for local grids - #1708

Merged
giswqs merged 7 commits into
mainfrom
feat/netcdf-emit-rendering
Aug 5, 2026
Merged

feat(netcdf): fit, colormap, and band combination for local grids#1708
giswqs merged 7 commits into
mainfrom
feat/netcdf-emit-rendering

Conversation

@giswqs

@giswqs giswqs commented Aug 5, 2026

Copy link
Copy Markdown
Member

Summary

  • Adding a local NetCDF/HDF layer now fits the camera to the data and derives robust color limits from the slice, instead of leaving the map where it was and using the renderer's stock 0-300 range. The extent is recorded on the layer so "Zoom to layer" works. Dimension names are read from NetCDF-4 dimension scales, so axes show as bands, lat, lon rather than dim_0, dim_1, dim_2.
  • Local single-band grids are colormapped in the browser and added as image overlays. This also sidesteps an upstream bug in @carbonplan/zarr-layer 0.7.0, whose shift_x uniform lookup throws on drivers that eliminate it (Mesa, so most Linux Intel/AMD machines), leaving Zarr layers permanently blank. Cloud kerchunk layers and cubes with a time axis stay on the Zarr renderer so the Time Slider can still drive them.
  • Hyperspectral cubes get an RGB band combination whose three channels are chosen by wavelength, defaulting to the bands nearest true red/green/blue, each stretched to its own 2-98 percent range with fill cells transparent.
  • The colormap picker now draws from the same catalogue as the Style panel's raster symbology (107 ramps with gradient swatches, previously 20), shared through a new useColormapRamps hook. A new NetCDF symbology section in the Style panel exposes colormap, reverse, and color limits after the fact.
  • Fixes a latent bug where sprite-sampled colormaps were returned as rgb(r, g, b) strings and parsed to black by every hex-based consumer, including the raster panel's custom-ramp seeding.
  • The Add NetCDF/HDF dialog now opens on Local file rather than the kerchunk URL.

Test plan

  • Frontend suite green (npm run test:frontend), with new cases for grid bounds, percentile color limits, RGB composition, colormapped images, dimension axes, and ramp color normalization
  • pre-commit run --files <changed> clean (oxfmt, eslint, npm build)
  • EMIT L3 observation file: variables list as (lat, lon), layer fits the map, renders with the selected ramp, "Zoom to layer" returns to the extent
  • EMIT L3 reflectance cube (1.1 GB): band picker labels wavelengths in nm, RGB composite renders as a true colour scene with transparent swath edges
  • Verified against the real GPU driver, not SwiftShader, which does not reproduce the shader-uniform failure
  • Style panel symbology: colormap change, reverse, narrowed limits, and reset all re-render the layer in place
  • Cloud kerchunk path re-checked on a machine whose driver renders Zarr layers

Summary by CodeRabbit

  • New Features

    • Added support for local and remote NetCDF/HDF datasets as colormapped overlays, RGB composites, or cloud-rendered layers.
    • Added coordinate-aware variable and dimension selection, natural-color defaults, pixel identification, and spectral profile charts.
    • Added styling controls for colormaps, reversal, editable color ranges, and reset options.
    • Expanded cloud rendering with named or custom color ramps and optional bounds.
  • Improvements

    • Improved multidimensional data handling, scaling, masking, orientation, spatial bounds, and color-ramp consistency.
    • Improved undo/redo history accounting for inline image overlays.

…rids

Adding a local NetCDF/HDF layer left the camera where it was and used the
renderer's stock 0-300 color limits, so real data such as an EMIT scene was
off screen and washed out, and "Zoom to layer" had no extent to fly to.

Local grids are now colormapped in the browser and added as image overlays,
which also sidesteps a shader-uniform crash in @carbonplan/zarr-layer that
blanks Zarr layers on Mesa drivers. Hyperspectral cubes gain an RGB band
combination picked by wavelength, and the colormap catalogue is now shared
with the Style panel's raster symbology instead of a much shorter list.
Copilot AI lite review requested due to automatic review settings August 5, 2026 03:22
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The PR adds local and remote axis-aware NetCDF rendering, RGB and colormapped image composition, editable image symbology, shared colormap loading, pixel identification, profile charts, cloud bounds metadata, and inline-image history accounting.

Changes

NetCDF rendering

Layer / File(s) Summary
Local and remote NetCDF data pipeline
packages/plugins/src/plugins/local-netcdf.ts, packages/plugins/src/index.ts, packages/plugins/package.json, tests/local-netcdf.test.ts
NetCDF readers now expose axes, selectors, bounds, color limits, grid reads, profiles, RGB composition, colormapped images, remote access, and public package exports.
Remote NetCDF worker access
apps/geolibre-desktop/src/lib/netcdf-remote-client.ts, apps/geolibre-desktop/src/workers/netcdf-remote.worker.ts, tests/netcdf-remote-client.test.ts
Direct NetCDF and HDF URLs use worker-based HTTP range requests with request tracking, cleanup, typed-array transfer, and URL validation.
Desktop NetCDF add flow
apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx, packages/core/src/store.ts, packages/plugins/src/plugins/maplibre-components.ts, apps/geolibre-desktop/src/i18n/locales/en.json
The dialog supports local files, direct NetCDF URLs, axis-aware controls, RGB band selection, colormaps, baked image overlays, and cloud layers with persisted bounds.
NetCDF image-overlay editing
apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts, apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx, apps/geolibre-desktop/src/components/panels/StylePanel.tsx
NetCDF image sources retain decoded state for re-baking, colormap reversal, color-limit validation, reset controls, and cleanup.
NetCDF pixel identification and profiles
apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts, apps/geolibre-desktop/src/lib/netcdf-profile-store.ts, apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx, apps/geolibre-desktop/src/components/layout/DesktopShell.tsx, packages/map/src/MapCanvas.tsx
Map clicks resolve NetCDF grid cells, display values and coordinates, load profiles, and render layer-scoped sampled profiles.
Shared colormap controls
apps/geolibre-desktop/src/hooks/useColormapRamps.ts, apps/geolibre-desktop/src/components/panels/RasterSymbologySection.tsx, packages/plugins/src/plugins/colormap-colors.ts, tests/colormap-colors.test.ts
Colormap loading is centralized, and sampled colors normalize to hexadecimal values.
Image history accounting
packages/core/src/history.ts, tests/undo-redo.test.ts
History sizing counts distinct inline image data URLs and ignores external image URLs.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AddNetcdfDialog
  participant RemoteNetcdfFile
  participant LocalNetcdfFile
  participant ImageOverlayLayer
  participant useNetcdfIdentify
  participant NetcdfProfileStore
  participant NetcdfProfilePanel
  User->>AddNetcdfDialog: Select source, variable, axes, and rendering mode
  AddNetcdfDialog->>RemoteNetcdfFile: Load direct URL metadata when applicable
  AddNetcdfDialog->>LocalNetcdfFile: Read grid or compose image
  LocalNetcdfFile-->>AddNetcdfDialog: Return image, bounds, and metadata
  AddNetcdfDialog->>ImageOverlayLayer: Add rendered NetCDF layer
  User->>useNetcdfIdentify: Click image cell
  useNetcdfIdentify->>NetcdfProfileStore: Store sampled profile
  NetcdfProfileStore-->>NetcdfProfilePanel: Notify profile chart
Loading

Possibly related PRs

Poem

A rabbit reads the bands with care,
And paints a glowing image there.
Grid cells answer when clicked bright,
Profiles curve across the night.
Colormaps hop from ramp to ramp.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main local NetCDF grid changes: fitting, colormap support, and RGB band combination.
Docstring Coverage ✅ Passed Docstring coverage is 95.24% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/netcdf-emit-rendering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔍 Cloudflare PR preview

Item Value
Site https://393e7f82.geolibre-preview.pages.dev
Demo app https://393e7f82.geolibre-preview.pages.dev/demo/
Commit 7e2b635

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR significantly improves how GeoLibre ingests and renders local NetCDF/HDF grids, adding robust defaults (camera fit + percentile-based color limits), a CPU-backed single-band image overlay path, and an RGB composite path for hyperspectral cubes. It also unifies colormap selection across the app by sharing a full ramp catalogue (with swatches) via a new hook, and adds a Style panel section to re-style baked NetCDF image layers after creation.

Changes:

  • Extend the local NetCDF/HDF reader to expose axis metadata, compute reliable grid bounds, and derive robust color limits (percentiles) for initial rendering.
  • Add CPU-side composition for single-band colormapped images and RGB composites, with UI support in the Add NetCDF dialog and post-add symbology controls in the Style panel.
  • Normalize sprite-sampled colormap stops to hex and share a unified ramp catalogue across raster and NetCDF pickers.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/local-netcdf.test.ts Adds unit tests for bounds, percentile clim, RGB composition, colormapped images, and axis handling.
tests/colormap-colors.test.ts Adds tests for normalizing rgb()/rgba() ramp stops to hex.
packages/plugins/src/plugins/maplibre-components.ts Extends NetCDF layer options (bounds + colormap names) and persists bounds onto the store layer record.
packages/plugins/src/plugins/local-netcdf.ts Implements bounds computation, percentile clim, RGB/colormap image composition, and axis/metadata extraction for local NetCDF/HDF.
packages/plugins/src/plugins/colormap-colors.ts Normalizes sprite-sampled colormap stops to #rrggbb and exports the helper.
packages/plugins/src/index.ts Exports the new NetCDF helpers/types and the ramp color normalizer.
packages/core/src/store.ts Extends image overlay layer creation options to carry sourceKind and merged metadata.
apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts Introduces retained in-memory grid storage + rebake helpers for NetCDF image overlays and Style-panel integration.
apps/geolibre-desktop/src/i18n/locales/en.json Adds new strings for NetCDF band-combination and symbology UI.
apps/geolibre-desktop/src/hooks/useColormapRamps.ts New hook that builds a unified, swatched colormap catalogue shared by multiple pickers.
apps/geolibre-desktop/src/components/panels/StylePanel.tsx Shows a NetCDF symbology section for baked NetCDF image layers.
apps/geolibre-desktop/src/components/panels/RasterSymbologySection.tsx Switches raster ramp picker to the shared colormap catalogue hook.
apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx New Style-panel section to re-colormap baked NetCDF image overlays and adjust limits.
apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx Adds colormap selection, RGB band-combination controls, and chooses between image vs Zarr render paths for local NetCDF.
Suppressed comments (1)

apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx:47

  • Calling setState during render (if (draftLayerId !== layer.id) { ... }) is a React anti-pattern and can trigger warnings and extra renders. Synchronize the text fields when the selected layer changes using useEffect instead, and drop the extra draftLayerId state.
  // The layer a draft belongs to; selecting a different layer must reload the
  // fields rather than keep the previous layer's numbers.
  const [draftLayerId, setDraftLayerId] = useState(layer.id);
  if (draftLayerId !== layer.id) {
    setDraftLayerId(layer.id);
    setMinText(String(applied.clim[0]));
    setMaxText(String(applied.clim[1]));
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@@ -0,0 +1,133 @@
import { useAppStore, type GeoLibreLayer } from "@geolibre/core";
import { Button, ColorRampSelect, Input, Label, Separator } from "@geolibre/ui";
import { useState } from "react";

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adopting the useEffect refactor, so no new import is needed. The render-phase setState is React's documented "adjusting state when props change" pattern (a layer switch resets the draft text fields); moving it to an effect would render one frame showing the previous layer's numbers. Leaving as is.

@giswqs giswqs Aug 5, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Disagreeing — no useEffect import is needed, so leaving this open in case you want to weigh in. This comment is about a suggested refactor of the render-phase state reset (NetcdfSymbologySection.tsx:46-57), but that pattern is intentional: comparing a draftLayerId state to the current prop and calling setState during render is React's documented way to adjust state when props change, and it is preferred over a useEffect — it re-renders before the browser paints, where an effect would flash the previous layer's values first. The reset was extended in 2533d7a (it now also clears pendingSymbology), still without an effect. useState is the only React import this file needs.

Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts Outdated
Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/panels/StylePanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • AddNetcdfDialog.tsx:144,237 — The RGB "band axis" is chosen as the first leading axis with size >= 3, without excluding axes already identified as time axes (TIME_DIMENSION_NAMES, used just below for hasTimeAxis). For the very common (time, lat, lon) shape with more than 3 time steps — including this file's own sample "air-temperature" dataset — this surfaces an "RGB composite (three bands)" control that actually combines three time steps as red/green/blue, contradicting its own help text. When both a time and a spectral axis exist, whichever comes first in dims (often time) silently wins, hiding the real band-combination feature. Medium-high confidence.
  • StylePanel.tsx:4655-4662NetcdfSymbologySection intentionally renders nothing when a baked NetCDF image layer's grid isn't in memory (e.g. after a project reload), but the "no controls" fallback message is gated only on sourceKind, not on whether the section actually rendered content. Result: the Style panel shows a blank controls area instead of the fallback text for a reloaded NetCDF image layer. Medium confidence.

Security

  • None found. Image data is encoded as a data: URL rather than fetched from an untrusted remote source in the changed paths; no injection or unsafe input handling observed.

Performance

  • No significant issues found in the changed code. Percentile sampling is capped (MAX_PERCENTILE_SAMPLES), RGB/colormap image composition decimates to maxSize (default 4096), and grid statistics are computed with bounded strides. Low-confidence note: baked images are stored as base64 PNG data URLs directly in layer/project state for persistence — reasonable given the stated constraints, but could meaningfully bloat .geolibre.json for very large single-band grids; this appears to be a deliberate, documented tradeoff rather than an oversight.

Quality

  • netcdf-image-symbology.ts:65unregisterNetcdfImageSource appears unused; the module already self-prunes via the store subscription in registerNetcdfImageSource. Low confidence, minor.
  • The extraction of useColormapRamps (deduplicating the ramp-catalogue logic previously duplicated in RasterSymbologySection.tsx) is a solid cleanup and reduces duplication as encouraged by the project's conventions.

CLAUDE.md

  • No violations found. New user-facing strings go through t()/en.json as required, no direct MapLibre mutation was introduced (layers flow through addImageOverlayLayer/updateLayer), and no relevant mirrored-constant or catalog files (whitebox menu, MAX_VECTOR_*, etc.) were touched by this change.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx`:
- Around line 352-366: Update the RGB branch in AddNetcdfDialog’s
addImageOverlayLayer call to pass an explicit non-KML sourceKind for the NetCDF
composite, matching the source-kind convention used by the single-band branch.
Keep the existing bounds and overlay behavior unchanged, and ensure the value
does not use the default "kml-ground-overlay" marker.
- Around line 149-156: Update the hasTimeAxis calculation near useImagePath to
detect a time dimension from either axes or leadingDims, using leadingDims as
the fallback when axes is empty or unavailable. Preserve the existing
case-insensitive TIME_DIMENSION_NAMES matching so time-enabled cubes continue
using the Zarr renderer.
- Around line 565-618: Update the rendering around the rgbMode branch so RGB
channel selectors are shown in addition to, not instead of, the existing
leading-dimension pickers. Exclude only the RGB axis from the regular
leadingDims picker list to avoid duplicating it, while keeping all other
dimensions editable through dimIndex for handleSubmit.
- Around line 750-762: Update wavelengthsInNanometres to recognize the missing
unit spellings: nanometre, micrometre, micrometres, microns, and micrometer.
Hoist the nanometre and micrometre unit sets to module scope so they are created
once, while preserving the existing conversion and null behavior.

In `@apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx`:
- Around line 51-57: The apply handler in NetcdfSymbologySection currently
performs bakeNetcdfImage and encodeImageOverlay synchronously, blocking controls
for large grids. Defer this work until after the interaction renders and expose
a pending state while it runs, ensuring colormap and reverse-control changes
remain responsive and updates are applied only after the bake completes.

In `@apps/geolibre-desktop/src/components/panels/StylePanel.tsx`:
- Around line 4651-4662: Update the StylePanel branch around
NetcdfSymbologySection to derive hasNetcdfSymbology from the layer source kind
and getNetcdfImageSource(layer.id), then show the no-controls message when that
value is false, including reloaded NetCDF layers without an in-memory grid. Wrap
the NetCDF content in the same ScrollArea and pe-5 clearance used by sibling
branches so the tall symbology controls can scroll without overflow.

In `@apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts`:
- Around line 160-168: Inspect pruneHistoryBySize and its temporal partialize
path for image data URLs such as those produced by encodeImageOverlay and stored
in layers. Ensure the size-pruning calculation includes these overlay strings,
then apply the existing history-size limit so repeated updateLayer symbology
changes cannot retain unbounded PNG snapshots; avoid changing image encoding
unless the pruning logic cannot account for the payload.
- Around line 44-50: Update the useAppStore subscription that prunes sources to
track the previous state.layers reference and return immediately when the
reference is unchanged. Only allocate the live layer-ID Set and remove stale
source IDs after the layers reference changes, while preserving the existing
pruning behavior.

In `@packages/plugins/src/plugins/colormap-colors.ts`:
- Around line 57-59: Update the numeric channel parsing in normalizeRampColor so
every parsed channel is validated with Number.isFinite() before rgbToHex() is
called. For malformed tokens such as "." or "1..2", return the original color
input unchanged, preserving the documented `#rrggbb` contract; add coverage for
rgb(., 0, 0) passing through unchanged.

In `@packages/plugins/src/plugins/local-netcdf.ts`:
- Around line 788-818: Update buildRgbImage to decode the NetCDF variable once
before constructing channels, then slice each requested index from that shared
decoded array instead of calling readPlane for every channel. Preserve the
existing selector, axis handling, coordinate reads, and RGB composition while
ensuring readPlane or its underlying reader/info.make path is not repeated for
the three channels.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 49c771e9-0b10-4408-b7ae-a2d722bbeaea

📥 Commits

Reviewing files that changed from the base of the PR and between 0da0215 and 0e9aaae.

📒 Files selected for processing (14)
  • apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
  • apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx
  • apps/geolibre-desktop/src/components/panels/RasterSymbologySection.tsx
  • apps/geolibre-desktop/src/components/panels/StylePanel.tsx
  • apps/geolibre-desktop/src/hooks/useColormapRamps.ts
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
  • packages/core/src/store.ts
  • packages/plugins/src/index.ts
  • packages/plugins/src/plugins/colormap-colors.ts
  • packages/plugins/src/plugins/local-netcdf.ts
  • packages/plugins/src/plugins/maplibre-components.ts
  • tests/colormap-colors.test.ts
  • tests/local-netcdf.test.ts

Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
Comment thread apps/geolibre-desktop/src/components/panels/StylePanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
Comment thread packages/plugins/src/plugins/colormap-colors.ts
Comment thread packages/plugins/src/plugins/local-netcdf.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔍 GitHub Pages PR preview

Item Value
Site Deploy failed. See the job log.
Demo app Unavailable
Commit 7e2b635

The Layers panel never offered Identify for a NetCDF layer, because the
button is gated on a native layer id these image overlays do not have, so
there was no way to read a value off the map.

A click now reads the retained grid directly, and for a cube it also walks
the band axis at that pixel to chart a spectral signature against wavelength.
Reading a profile needs the source file, so it stays open for the layer's
lifetime, capped to one cube at a time.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/panels/StylePanel.tsx`:
- Line 4661: Pass the selected layer’s id into NetcdfProfilePanel from the
StylePanel render, then update NetcdfProfilePanel to accept that layerId and
filter subscribed readings by reading.layerId. Render no profile content when no
readings match the selected layer, while preserving existing behavior for
matching readings.

In `@apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts`:
- Around line 70-78: Add English locale entries for the NetCDF identify labels
used by the popup, then use the `t` translation function in `useNetcdfIdentify`
for the `"no data"`, `"lon, lat"`, and `"row, col"` values instead of hardcoded
strings. Ensure the hook obtains `t` from `react-i18next` and preserves the
existing formatting for translated labels and coordinates.

In `@apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts`:
- Around line 123-130: Update readNetcdfProfile to catch errors thrown by
state.profile.file.readProfile and return null for failures, while preserving
the existing null result when state or profile is unavailable and the successful
read path otherwise unchanged.

In `@packages/plugins/src/plugins/local-netcdf.ts`:
- Around line 927-939: Memoize decoded NetCDF-3 variables on the Netcdf3File
instance so repeated reads reuse one decoded array per variable. Add a
fullVariable helper or equivalent cache-backed path, replace direct
getDataVariable/info.make calls in readProfile and readPlane with it, and clear
the cache in close().
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 38c87f6f-8145-47f3-b1e1-af2b6d315798

📥 Commits

Reviewing files that changed from the base of the PR and between 0e9aaae and 4b2e9f7.

📒 Files selected for processing (15)
  • apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
  • apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
  • apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
  • apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx
  • apps/geolibre-desktop/src/components/panels/StylePanel.tsx
  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
  • apps/geolibre-desktop/src/lib/netcdf-profile-store.ts
  • packages/core/src/types.ts
  • packages/map/src/MapCanvas.tsx
  • packages/plugins/src/index.ts
  • packages/plugins/src/plugins/local-netcdf.ts
  • tests/local-netcdf.test.ts
  • tests/netcdf-profile-store.test.ts

Comment thread apps/geolibre-desktop/src/components/panels/StylePanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts Outdated
Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts Outdated
Comment thread packages/plugins/src/plugins/local-netcdf.ts
Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts Outdated
Comment thread apps/geolibre-desktop/src/components/panels/StylePanel.tsx Outdated
Comment thread apps/geolibre-desktop/src/components/panels/LayerPanel.tsx Outdated
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts:101-113 — the profile-read setTimeout isn't cancelled in the effect's cleanup, so switching the identify target (or map) mid-flight can let a stale callback overwrite the spectral-profile store with data from the previous layer/pixel. Confidence: medium (narrow window, low practical impact).
  • apps/geolibre-desktop/src/components/panels/StylePanel.tsx:4657-4664 — for a NetCDF image layer whose in-memory grid was lost (e.g. after a project reload), both NetcdfSymbologySection and NetcdfProfilePanel render null, and the style.noControls fallback is also suppressed because it's gated on sourceKind !== NETCDF_IMAGE_SOURCE_KIND. Result: a completely blank Style panel with no explanation. Confidence: medium.
  • apps/geolibre-desktop/src/components/panels/LayerPanel.tsx:586hasNativeIdentifyLayers enables the Identify icon for a NetCDF image layer purely from its persisted sourceKind, even when the in-memory grid backing it (useNetcdfIdentify's states map) is gone after a reload; clicking then silently does nothing with no feedback. Confidence: medium.

Security

  • None found. The new local-file/HDF5/NetCDF-3 parsing paths, base64/data-URL image encoding, and store metadata merges didn't reveal injection, unsafe eval, or secret-handling issues.

Performance

  • No obvious inefficiencies beyond expected/inherent costs (e.g. reading a full NetCDF-3 variable into memory for readProfile, which the code comments already acknowledge as a library limitation). The percentile-clim sampling is bounded (MAX_PERCENTILE_SAMPLES), and image decimation is capped by maxSize.

Quality

  • Minor: wavelengthsInNanometres (AddNetcdfDialog.tsx) recognizes "micrometers" but not the British "micrometres" spelling — a low-impact gap in unit detection for the RGB band-combination default. Confidence: low.
  • Minor: useNetcdfIdentify's effect depends only on [activeLayerId, mapControllerRef] and reads mapControllerRef.current once; unlike useViewportHistory, it has no mapReadyGeneration-style signal to re-attach once the map initializes later. In practice this seems safe today because a NetCDF layer must already be rendered (map must exist) before Identify can target it, but it diverges from the codebase's established pattern for map-ref-consuming hooks. Confidence: low.
  • The overall implementation (CPU colormap/RGB compositing, longitude rolling, percentile clim, dimension-scale name resolution) is well factored between the two backends (HDF5 and NetCDF-3) and is backed by thorough new unit tests covering edge cases (fill values, descending axes, decimation, degenerate ranges).

CLAUDE.md

  • No violations found: no .gitignored lockfiles touched, no new external tile/map hosts requiring CSP changes, i18n strings added via t()/en.json as required, and no hand-edits to the auto-generated Whitebox catalog.

- Never pick a time axis as the RGB band axis: a (time, lat, lon) stack
  offered a "combine three bands" control that actually combined three dates.
  Prefer an axis with wavelength units, else the first non-time axis.
- Fall back to the variable's own dimension names for time detection, so a
  cube still reaches the Zarr path when listAxes throws and `axes` is empty.
- Set an explicit sourceKind on the RGB overlay; it was defaulting to
  "kml-ground-overlay", which every consumer of that marker would act on.
- Keep the other leading-dimension pickers visible in RGB mode, since
  handleSubmit still reads them and they were pinned at index 0.
- Show the "no controls" message when a NetCDF layer has no retained grid
  (after a project reload, or for an RGB composite), and wrap the branch in
  ScrollArea so a tall symbology section can scroll.
- Offer Identify only when the grid is actually readable, and mark these
  layers pixelIdentify so the tooltip matches the COG wording.
- Charge inline data URLs against the undo-history budget: it counted only
  GeoJSON features, so each re-bake pushed a multi-megabyte snapshot the
  pruner scored as free.
- Skip the layer-prune subscriber unless the layer array changed, and drop
  the subscription once nothing is registered; it was rebuilding a Set of
  every layer id on each pointer move.
- Defer the symbology re-bake so the colormap and reverse controls repaint
  before the CPU colormap and PNG encode run.
- Cover the remaining wavelength unit spellings, and reject malformed rgb()
  channels rather than emitting "#NaN...".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx (1)

35-67: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Scope deferred symbology state to its layer.

If the selected layer changes before the timer runs, pendingSymbology still applies the previous layer's colormap and limits to the new panel. An older timer can also clear pending state from a newer update. Store the target layer.id with the pending value and use a generation token before clearing it.

Proposed fix
-const [pendingSymbology, setPendingSymbology] = useState<NetcdfImageSymbology | null>(null);
-const applied = pendingSymbology ?? netcdfImageSymbology(layer, source?.dataClim ?? [0, 1]);
+const [pendingSymbology, setPendingSymbology] = useState<{
+  layerId: string;
+  value: NetcdfImageSymbology;
+} | null>(null);
+const bakeGeneration = useRef(0);
+const storedSymbology = netcdfImageSymbology(layer, source?.dataClim ?? [0, 1]);
+const applied =
+  pendingSymbology?.layerId === layer.id ? pendingSymbology.value : storedSymbology;
...
-  setPendingSymbology(next);
+  const generation = ++bakeGeneration.current;
+  setPendingSymbology({ layerId: layer.id, value: next });
   window.setTimeout(() => {
     ...
-    setPendingSymbology(null);
+    if (generation === bakeGeneration.current) setPendingSymbology(null);
   }, 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx`
around lines 35 - 67, Update the pending state and apply timer in
NetcdfSymbologySection so pendingSymbology is associated with the target
layer.id, and only use it when it matches the current layer. Add a generation
token for scheduled updates, and clear pending state only when the completing
timer is still the latest generation and targets the current layer, preventing
stale timers or layer changes from affecting newer state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx`:
- Around line 35-67: Update the pending state and apply timer in
NetcdfSymbologySection so pendingSymbology is associated with the target
layer.id, and only use it when it matches the current layer. Add a generation
token for scheduled updates, and clear pending state only when the completing
timer is still the latest generation and targets the current layer, preventing
stale timers or layer changes from affecting newer state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c1e5ff3e-e484-421f-a6b8-635a3368a15a

📥 Commits

Reviewing files that changed from the base of the PR and between 4b2e9f7 and 8dd0d60.

📒 Files selected for processing (9)
  • apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
  • apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
  • apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx
  • apps/geolibre-desktop/src/components/panels/StylePanel.tsx
  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
  • packages/core/src/history.ts
  • packages/plugins/src/plugins/colormap-colors.ts
  • tests/colormap-colors.test.ts
  • tests/undo-redo.test.ts

Comment thread packages/plugins/src/plugins/local-netcdf.ts
Comment thread packages/plugins/src/plugins/local-netcdf.ts Outdated
Comment thread apps/geolibre-desktop/src/components/panels/StylePanel.tsx
Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts Outdated
Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

All 7 inline comments posted. Now the final summary.

Code review

Bugs

  • apps/geolibre-desktop/src/components/panels/StylePanel.tsx:4666NetcdfProfilePanel is rendered with no per-layer scoping, and the profile store only replaces readings when a new click targets a different layer. Switching between two NetCDF cubes' Style panels without re-clicking the map shows the previous layer's stale spectral profile. Confidence: high.
  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts:198 (rampStops) — picking a not-yet-warmed sprite colormap silently bakes viridis while persisting the chosen name in metadata.netcdfSymbology; nothing re-bakes once the colormap warms, so the panel shows the right name while the map renders the wrong ramp until the user manually re-edits symbology. Confidence: medium.
  • packages/plugins/src/plugins/local-netcdf.ts:1724-1725 — orientation (latDescending/lonDescending) is derived from only the first/last coordinate, unlike the NaN-robust centresToEdges/gridBounds used for extent a few hundred lines away; a non-finite endpoint coordinate would silently mirror the rendered image while the reported bounds stay correct. Confidence: medium.
  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts:101-113 — the deferred spectral-profile setTimeout is never cancelled in the effect cleanup, so a stale callback can still fire and write to the profile store after the identify target has changed or unmounted. Confidence: medium.
  • apps/geolibre-desktop/src/components/panels/NetcdfSymbologySection.tsx:46-50 — the per-layer draft reset doesn't clear pendingSymbology, so a bake in flight for the previous layer can briefly leak into the newly-selected layer's controls. Confidence: low-medium.

Performance

  • packages/plugins/src/plugins/local-netcdf.ts:864-869Netcdf3File.buildRgbImage re-decodes the entire variable three times (once per RGB channel) because netcdfjs has no partial read; the HDF5 path avoids this via a genuine hyperslab read. Worth decoding once and slicing three planes instead. Confidence: medium-high.

CLAUDE.md

  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts:70-79 — the identify popup hardcodes English strings ("no data", "lon, lat", "row, col") instead of using t(), the one spot in this feature that bypasses i18n while every other new string in the PR is correctly catalogued. Confidence: high.

Quality

  • No i18n/RTL violations found elsewhere — all other new strings go through t() with matching en.json keys, and no physical ml-/pr-/left- Tailwind classes were introduced.
  • colormap-colors.ts's rgb()→hex normalization, useColormapRamps.ts memoization, maplibre-components.ts, history.ts's new byte-charging logic, and the core types.ts/store.ts/plugins/index.ts additions all checked out with no defects; test coverage for the changed math (percentiles, longitude roll, undo/redo byte-charging, colormap normalization) is solid. useNetcdfIdentify.ts itself has no dedicated test file, which is why the two issues above went unverified by CI.

- Scope the spectral profile chart to the selected layer, so selecting a
  second NetCDF layer no longer shows the first one's spectra under its
  heading.
- Catch a throw from readProfile: the caller reads it from a timer, where an
  exception would surface as an uncaught error on every click.
- Memoize the NetCDF-3 whole-variable decode, which readPlane and readProfile
  were repeating on each call, and clear it on close.
- Track and cancel the deferred profile read, so a stale one cannot overwrite
  a newer reading after the identify target changes.
- Route the identify popup labels through t().
- Drop placeholder units such as EMIT's "unitless" from the readout and the
  chart's value label.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts (1)

116-129: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Clear stale profile data when a profile read fails.

readNetcdfProfile returns null after a failed read, but this callback leaves the external store unchanged when profile is falsy. After a successful click, a later failed read leaves the previous spectrum visible while the popup shows the new pixel. Clear the reading before scheduling a new profile read and when the callback receives null.

Proposed fix
       if (!state.profile) {
         setNetcdfProfileReading(null);
         return;
       }
+      setNetcdfProfileReading(null);
       profileTimeout = window.setTimeout(() => {
         profileTimeout = null;
         const profile = readNetcdfProfile(activeLayerId, pixel.row, pixel.column);
         if (profile) {
           setNetcdfProfileReading({
             layerId: activeLayerId,
             variable: state.variable,
             units: state.units,
             lng: pixel.lng,
             lat: pixel.lat,
             profile,
           });
+        } else {
+          setNetcdfProfileReading(null);
         }
       }, 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts` around lines 116 - 129,
Update the profile-read flow in the hook containing the setNetcdfProfileReading
callback to clear the existing reading before scheduling a new read, and also
clear it when readNetcdfProfile returns null. Preserve the successful
setNetcdfProfileReading update for valid profiles while ensuring failed reads
cannot leave stale spectrum data visible.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx`:
- Around line 96-97: Update the axisLabel construction in NetcdfProfilePanel to
pass first.profile.axis.units through displayUnits before interpolating it into
the chart label. Preserve the existing label format while ensuring unitless,
“1”, and “n/a” axis units are normalized consistently with valueUnits.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts`:
- Around line 116-129: Update the profile-read flow in the hook containing the
setNetcdfProfileReading callback to clear the existing reading before scheduling
a new read, and also clear it when readNetcdfProfile returns null. Preserve the
successful setNetcdfProfileReading update for valid profiles while ensuring
failed reads cannot leave stale spectrum data visible.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 971c74f6-35eb-4649-84a1-406240657e77

📥 Commits

Reviewing files that changed from the base of the PR and between 8dd0d60 and dd4eeb6.

📒 Files selected for processing (6)
  • apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx
  • apps/geolibre-desktop/src/components/panels/StylePanel.tsx
  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
  • packages/plugins/src/plugins/local-netcdf.ts

Comment thread apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx
Comment thread apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
Comment thread apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code review

Bugs

  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts:205 (rampStops) — if the picked colormap hasn't finished async-warming yet, the bake silently substitutes viridis with no error or indication, producing a visible colormap mismatch. Medium confidence.
  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts:56 — the identify click handler is wired up in an effect that doesn't re-run when mapControllerRef.current becomes populated (only on activeLayerId/t changes), so identify could silently no-op if triggered before the map controller is ready. Low confidence — likely unreachable in normal usage.

Security

  • None found. All new code operates on locally-opened files/canvases; no injection, unsafe eval, or secret handling introduced.

Performance

  • None found. The CPU colormap/RGB compositing is bounded (percentile sampling capped at 200k strided samples, image output capped at maxSize), and the history-size accounting for inline data: URLs is O(n) per snapshot, consistent with existing patterns.

Quality

  • apps/geolibre-desktop/src/components/panels/NetcdfProfilePanel.tsx:32 — duplicates the formatValue helper in useNetcdfIdentify.ts with different thresholds/precision, risking inconsistent number formatting between the Identify popup and the Spectral Profile chart for the same value. Low-medium confidence.
  • Minor: DEFAULT_COLORMAP = "viridis" in AddNetcdfDialog.tsx and the "viridis" fallback in netcdfImageSymbology() (netcdf-image-symbology.ts) duplicate the same default in two places; not a functional issue today but could drift.

CLAUDE.md

  • No violations found. The new packages/plugins/packages/core exports, i18n strings (via t()), and RTL-safe Tailwind classes all follow the documented conventions; nothing here required updating whitebox-menu-catalog.ts, MAX_VECTOR_PMTILES_ZOOM, or the other drift-prone constants called out in CLAUDE.md, since this PR doesn't touch geolibre-wasm/maplibre-gl-vector/maplibre-gl-components versions.

The core image-compositing math (percentile clim, RGB/colormap compositing, longitude roll + decimation layout, grid-pixel lookup) was traced through carefully and is well covered by the accompanying unit tests; no correctness issues found there.

Pointing the Cloud source at a raw .nc URL failed, because it only accepted a
kerchunk manifest, so a chunked file on object storage had no way in.

A direct NetCDF/HDF URL now opens through a worker that mounts it lazily and
faults in only the byte ranges a read touches. Opening an EMIT observation
file and listing its variables costs ~12 MB of 36 MB; a band plane of the
1.1 GB reflectance cube costs ~135 MB rather than the whole file.

The worker imports the reader module directly rather than the package barrel,
which reaches maplibre-gl and other modules that touch window while evaluating
and would leave the worker never starting. It also announces readiness, so a
module that fails to load surfaces as an error instead of a dialog stuck on
"Loading...". Reads are sequential synchronous range requests with no
readahead, so they are latency-bound; that is the trade for not downloading a
cube far larger than the slice wanted.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx (2)

461-504: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the remote base name against a URL that does not parse.

isNetcdfFileUrl accepts a relative path: it catches the new URL(...) failure and falls back to string splitting. Line 468 calls new URL(dataset.url) without that guard. For such an input the constructor throws inside handleSubmit, and the user gets a TypeError message instead of a layer.

Reuse the same tolerant parse used by isNetcdfFileUrl.

🐛 Proposed fix
-        const baseName =
-          (dataset.kind === "local" ? fileName : new URL(dataset.url).pathname)
-            .split(/[\\/]/)
-            .pop() || "netcdf";
+        const remotePath = (url: string) => {
+          try {
+            return new URL(url).pathname;
+          } catch {
+            return url.split(/[?#]/)[0];
+          }
+        };
+        const baseName =
+          (dataset.kind === "local" ? fileName : remotePath(dataset.url))
+            .split(/[\\/]/)
+            .pop() || "netcdf";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx` around lines
461 - 504, Update the baseName calculation in handleSubmit to reuse the tolerant
URL/path parsing behavior from isNetcdfFileUrl instead of calling new
URL(dataset.url) unconditionally. Ensure relative remote paths fall back to
string splitting and still produce the bare filename without throwing.

505-564: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Handle a remote dataset that takes the Zarr path.

The submit branches do not cover one reachable combination: dataset.kind === "remote" with hasTimeAxis === true.

  • useImagePath is dataset !== null && !rgbMode && !hasTimeAxis, so it is false for a remote time cube.
  • rgbMode is false when the user keeps single-band mode.
  • The final branch requires dataset.kind === "local" to call buildLayerRefs.

No branch runs. handleSubmit then reaches Line 577 and closes the dialog with reset(). The user sees the dialog close and no layer is added, with no error. (time, lat, lon) is a common remote NetCDF shape, so this path is easy to hit.

Pick one behavior and implement it: bake the selected slice as an image overlay for remote cubes as well, or report an explicit error that a remote time cube is not supported yet.

🐛 Proposed fix (bake the selected slice for remote cubes)
-        } else if (dataset.kind === "local") {
+        } else if (dataset.kind === "remote") {
+          // The Zarr renderer needs a synchronous store, which a remote
+          // range-request dataset cannot provide; bake the selected slice
+          // instead of silently adding nothing.
+          throw new Error(t("addData.netcdf.errorRemoteTimeCube"));
+        } else {
           const built = dataset.file.buildLayerRefs(variable, selector);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx` around lines
505 - 564, Update handleSubmit so the reachable remote time-cube case
(dataset.kind === "remote" with hasTimeAxis === true and single-band mode) is
handled instead of falling through to reset(). Reuse the existing datasetGrid,
bakeNetcdfImage, addImageOverlayLayer, registerNetcdfLayer, and fitBounds flow
from the useImagePath branch to bake and add the selected slice for remote
datasets, while preserving the existing local buildLayerRefs path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts`:
- Around line 125-140: Update the profile-read flow around cancelProfile and
readNetcdfProfile to use an AbortController: abort the existing controller
before starting a replacement read, pass its signal through readNetcdfProfile,
state.profile.read, and the remote worker, and abort it during cleanup. Preserve
the existing cancelled-result guard while ensuring aborted requests do not
continue remote range work.

In `@apps/geolibre-desktop/src/i18n/locales/en.json`:
- Around line 701-702: Update the kerchunkUrlLabel translation to describe both
accepted kerchunk reference URLs and direct NetCDF/HDF file URLs, aligning it
with kerchunkUrlHelp.

In `@apps/geolibre-desktop/src/lib/netcdf-remote-client.ts`:
- Around line 83-109: Update the ready promise and worker.onerror flow to retain
a startup reject callback, invoke it immediately with the worker failure when
the worker fails before readiness, and clear pending requests as currently done.
Store the readiness timeout handle and cancel it when the ready promise is
resolved, while preserving normal ready-message handling and timeout rejection.

In `@packages/plugins/src/plugins/local-netcdf.ts`:
- Around line 1156-1164: Update openRemoteNetcdf to perform a single-byte HTTP
range preflight before calling Hdf5NetcdfFile.openLazy. Reject unless the
response status is 206 and its Content-Range confirms the requested byte range,
then preserve the existing Web Worker validation and only mount the remote file
after successful validation.

---

Outside diff comments:
In `@apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx`:
- Around line 461-504: Update the baseName calculation in handleSubmit to reuse
the tolerant URL/path parsing behavior from isNetcdfFileUrl instead of calling
new URL(dataset.url) unconditionally. Ensure relative remote paths fall back to
string splitting and still produce the bare filename without throwing.
- Around line 505-564: Update handleSubmit so the reachable remote time-cube
case (dataset.kind === "remote" with hasTimeAxis === true and single-band mode)
is handled instead of falling through to reset(). Reuse the existing
datasetGrid, bakeNetcdfImage, addImageOverlayLayer, registerNetcdfLayer, and
fitBounds flow from the useImagePath branch to bake and add the selected slice
for remote datasets, while preserving the existing local buildLayerRefs path.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a3d1f264-1425-40a0-934a-5e277d624450

📥 Commits

Reviewing files that changed from the base of the PR and between dd4eeb6 and 61bf6ec.

📒 Files selected for processing (10)
  • apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
  • apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
  • apps/geolibre-desktop/src/i18n/locales/en.json
  • apps/geolibre-desktop/src/lib/netcdf-image-symbology.ts
  • apps/geolibre-desktop/src/lib/netcdf-remote-client.ts
  • apps/geolibre-desktop/src/workers/netcdf-remote.worker.ts
  • packages/plugins/package.json
  • packages/plugins/src/index.ts
  • packages/plugins/src/plugins/local-netcdf.ts
  • tests/netcdf-remote-client.test.ts

Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
Comment thread apps/geolibre-desktop/src/i18n/locales/en.json Outdated
Comment thread apps/geolibre-desktop/src/lib/netcdf-remote-client.ts
Comment thread packages/plugins/src/plugins/local-netcdf.ts
Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
Comment on lines +103 to +109
worker.onerror = (event) => {
// A worker-level failure (a bad module load) never resolves the in-flight
// request, so fail them all rather than hanging the dialog.
const error = new Error(event.message || "The NetCDF reader worker failed.");
for (const entry of pending.values()) entry.reject(error);
pending.clear();
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug (medium confidence): if the worker crashes/errors after it has already opened successfully (e.g. mid-readGrid, well past the ready-timeout window), this handler rejects only the currently in-flight requests but never calls worker.terminate() — the dead worker (and its ~1GB WASM heap for a large cube) stays around until the file is later closed through normal app lifecycle (dialog reset / layer removal), which may not happen for a while.

Compounding this: send() (below, line ~111) creates a new pending entry with no timeout at all. If the worker silently stalls (not a hard crash — no onerror fires, just hangs) during a later readGrid/readProfile/listAxes call, that promise never settles, and the dialog/panel is stuck indefinitely (e.g. "Adding...") with no error surfaced to the user, since only the initial ready promise has WORKER_READY_TIMEOUT_MS.

Consider terminating the worker in onerror too, and applying a timeout to send()'s requests the same way ready has one.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half done in 7e2b635, leaving this open for the second half.

Fixed: onerror now calls worker.terminate() after rejecting pending requests, so a crashed worker no longer holds its WASM heap until the next dialog reset or layer removal. The same commit also lets onerror reject the startup wait, which previously sat out the full 20 s ready timeout because pending is empty before the first send.

Not done — needs a judgement call: a blanket send() timeout. The documented timings for this path are ~20 s for one band plane of a 1.1 GB EMIT scene and ~35 s for a whole-band-axis profile at one pixel (see the note on openRemoteNetcdf), and those are on a good connection — the reads are latency-bound with no readahead. Any timeout short enough to catch a stall would abort legitimate reads on a slow link, and one generous enough to be safe (several minutes) barely improves on the current behaviour. Distinguishing "stalled" from "slow" really wants progress reporting from the worker (bytes faulted in, say) rather than a wall-clock cap, which is more than this PR should take on. Happy to file it as a follow-up if you agree.

Comment thread apps/geolibre-desktop/src/hooks/useNetcdfIdentify.ts
@@ -176,6 +325,9 @@ async function loadH5wasm(): Promise<H5wasmModule> {
* A local HDF5/NetCDF-4 file backed by h5wasm.
*/
class Hdf5NetcdfFile implements LocalNetcdfFile {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test coverage gap (high confidence). Hdf5NetcdfFile is the largest class in this diff (~470 lines) and is the backend for this PR's headline scenario — EMIT hyperspectral cubes, NetCDF-4 groups, and the dimension-scale name resolution called out in the PR description ("Dimension names are read from NetCDF-4 dimension scales, so axes show as bands, lat, lon"). But tests/local-netcdf.test.ts only ever opens sample-nc3.nc/sample-nc3-xy.nc (classic NetCDF-3, via Netcdf3File) — every describe/it in that file goes through openLocalNetcdf(fixture("sample-nc3*.nc")). Hdf5NetcdfFile itself (dimension-scale resolution, group traversal, openLazy/remote reading) has zero direct test coverage.

Given the frontend coverage floor is a ratchet (per CLAUDE.md) and this is precisely the path the PR's own test plan exercised manually against a 1.1GB real file rather than in CI, it'd be worth adding a small .h5/.nc4 fixture and at least one Hdf5NetcdfFile-path test (dimension names, a 3-D variable listing) so this doesn't regress silently.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed and not done — flagging rather than quietly skipping, so leaving this open.

You're right that Hdf5NetcdfFile carries this PR's headline path and has no direct coverage: every case in tests/local-netcdf.test.ts goes through openLocalNetcdf(fixture("sample-nc3*.nc")), i.e. Netcdf3File. Adding an .h5 fixture plus tests for dimension-scale name resolution and group traversal is real work with its own review surface, and it is not a defect in the diff, so I've left it out of a review-response commit rather than expanding this PR by another test module.

Two things this round did add nearby, for what it's worth: six tests for the new assertByteServing range preflight, and two for the remote client's worker startup handling (tests/netcdf-remote-client.test.ts) — so the remote client half is now covered even though the h5wasm backend is not. Coverage currently sits at 82.13% lines / 84.55% branches, above the floors.

Happy to do the .h5 fixture as a follow-up PR if you want it tracked.

Comment on lines +1372 to 1387
/** Min/max of cell centres, expanded by half the mean cell size. */
function centresToEdges(values: ArrayLike<number>): [number, number] {
let min = Infinity;
let max = -Infinity;
for (let i = 0; i < values.length; i++) {
const v = Number(values[i]);
if (!Number.isFinite(v)) continue;
if (v < min) min = v;
if (v > max) max = v;
}
if (!Number.isFinite(min) || !Number.isFinite(max)) return [0, 0];
// A one-cell axis has no spacing to derive; leave it as a degenerate point
// rather than inventing a width.
const half = values.length > 1 ? (max - min) / (values.length - 1) / 2 : 0;
return [min - half, max + half];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Edge case (medium confidence): if a coordinate axis is entirely non-finite (NaN/Infinity — e.g. a corrupt file, or a grid whose lat/lon happens to be read from a bad slice), min/max stay at their Infinity/-Infinity seed values, and this falls through to return [0, 0]. gridBounds then returns effectively [0, 0, 0, 0] (clamped), silently placing the layer at Null Island instead of surfacing an error — this feeds directly into appApi.fitBounds?.(image.bounds) in AddNetcdfDialog, so the camera would fly to (0,0) with no indication anything went wrong.

The Zarr-store path (acceptCoordinate/valuesWithin) guards against this before a grid is ever used, but gridBounds/imageLayout.frame() (used by the RGB and colormapped-image paths) call centresToEdges directly with no equivalent all-finite check. Worth considering whether gridBounds should throw/return null on a fully-degenerate axis rather than silently defaulting to the origin.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this open — I'd rather you make the call than change an exported function's contract on my own read.

Having traced it: an all-non-finite axis cannot reach gridBounds from either reader. readCoordinate runs every candidate through acceptCoordinate, whose valuesWithin rejects an array containing any non-finite value, so such a file fails with NO_COORDINATES_MESSAGE before a grid is built — as you note for the Zarr path, but it is the same gate on the image path, since both go through readCoordinate. So the Null Island fitBounds needs a direct external call to gridBounds/composeColormappedImage with a fabricated axis.

That leaves a trade: making an exported function throw where it currently returns is a breaking change for any plugin calling it, to guard an input the app's own paths already reject. I've left it as-is. If you'd like it hardened anyway, throwing from centresToEdges when it finds no finite value is a two-line change and I'll push it.

For contrast, the sibling finding — a partially non-finite axis in imageLayout — I did fix (2533d7a), because there the wrong answer was a silently mirrored raster rather than an obviously-wrong extent.

Comment thread apps/geolibre-desktop/src/components/layout/AddNetcdfDialog.tsx
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

All inline comments are posted. Here's the final summary.

Code review

Bugs

  • AddNetcdfDialog.tsx:323-332,691selectVariable's staleness guard only checks the dialog-level opGen (bumped on reset/reopen), not per-call ordering; rapidly switching the selected variable can let a slower, superseded read overwrite the current variable's axes/RGB-band defaults with stale data. Medium-high confidence.
  • useNetcdfIdentify.ts:80-85 — an off-grid click for the active identify layer unconditionally clears the entire spectral-profile store, which can wipe a different layer's still-displayed chart if the user switched the identify target before landing a hit on the new layer. Medium confidence.
  • netcdf-remote-client.ts:103-109worker.onerror rejects in-flight requests but never terminates the worker, and send() has no timeout for requests issued after the initial open, so a stalled/crashed worker can hang the UI indefinitely with a leaked worker. Medium confidence.
  • local-netcdf.ts:1372-1387 (centresToEdges/gridBounds, new in this PR) — an all-non-finite coordinate axis silently produces bounds [0,0,0,0] (Null Island) instead of an error, and this feeds directly into fitBounds. Medium confidence.

Performance

  • Nothing flagged beyond the note below on test coverage; the percentile/RGB/colormap compositing math was traced and looks correct and reasonably efficient (bounded percentile sampling, decimation to maxSize, etc.).

Quality

  • NetcdfSymbologySection.tsx:58-68apply()'s deferred setTimeout write spreads layer.source/layer.metadata captured at render time rather than fresh store state; a concurrent edit to the same layer within that window would be silently reverted. Medium-low confidence.
  • local-netcdf.ts:327Hdf5NetcdfFile, the ~470-line backend for this PR's headline scenario (EMIT hyperspectral cubes, NetCDF-4 dimension-scale names), has zero direct test coverage; tests/local-netcdf.test.ts exercises only the NetCDF-3 backend via sample-nc3*.nc fixtures. High confidence — worth a small .h5 fixture given CLAUDE.md's coverage-floor ratchet.
  • AddNetcdfDialog.tsx:18 — dead import (LocalNetcdfImage, unused elsewhere in the file). High confidence, trivial.

Security

  • No new SSRF-class exposure: the direct-URL NetCDF fetch reuses the same unrestricted-HTTPS model the existing kerchunk-URL flow already had, so this isn't a new attack surface.
  • (Noted but not filed inline, since the affected recursive HDF5 group-walk predates this PR) a cycle in HDF5 soft/external links could stack-overflow the tab; now more reachable given this PR adds fetching HDF5 files from arbitrary URLs, but out of scope as a pre-existing code path this diff doesn't touch.

CLAUDE.md

  • i18n and RTL conventions were checked across all new/changed components (AddNetcdfDialog, NetcdfSymbologySection, NetcdfProfilePanel, useNetcdfIdentify) — all new user-facing strings route through t() with matching en.json keys, and no physical (ml-/left-) Tailwind utilities were introduced.
  • The one adherence gap is the test-coverage note above (Quality section), relevant to the frontend coverage-floor ratchet CLAUDE.md describes.

- Give the NetCDF identify popup MapCanvas's `geolibre-identify-popup` class.
  Without it MapLibre's own always-white `.maplibregl-popup-content` survives
  while the rows inherit the theme foreground, so the readout was white-on-white
  in dark mode (reported separately from the bot findings).
- Re-run `useNetcdfIdentify`'s effect on `mapReadyGeneration`. A ref's `.current`
  becoming non-null does not retrigger an effect, so an identify target set
  before the map finished loading never got a click handler.
- Warm a colormap before baking a NetCDF image, in both the Add dialog and the
  Style panel. Sprite ramps resolve asynchronously and nothing re-bakes when one
  lands, so picking an unwarmed ramp persisted as viridis pixels under the
  chosen name, with no self-healing path.
- Reset `pendingSymbology` when the Style panel switches NetCDF layers, and seed
  the clim fields from the new layer's own record rather than from the dropped
  pending value.
- Normalize the profile chart's axis units through `displayUnits`, so a
  coordinate variable declaring `unitless`/`1` no longer prints that in the
  axis label.
- Derive image row/column orientation from a coordinate axis' first and last
  *finite* values. A `NaN` endpoint made every comparison false, reading a
  descending axis as ascending and mirroring the raster under a `gridBounds`
  extent that still came out right. Covered by a regression test.
- Fail `openRemoteNetcdfFile`'s startup wait from `worker.onerror`, and release
  the readiness timer once ready arrives. `pending` is empty before the first
  send, so a module-load failure sat out the whole 20 s timeout.
- Preflight a one-byte range request in `openRemoteNetcdf` before mounting.
  A server that ignores `Range` answers whole, which emscripten accepts, turning
  a lazy mount into a blocking full download with no error. Gated on the 206
  status, which CORS cannot hide, with `Content-Range` checked only when exposed.
- Widen `kerchunkUrlLabel` to match the field's help text, which now documents
  direct NetCDF/HDF URLs as well as kerchunk references.
- Document why the identify readout and the profile chart format values at
  different precision, rather than merging two helpers with different jobs.
Findings from the review summary that were not attached to an inline thread.

- Order `selectVariable`'s reads per call. `opGen` only moves when the dialog
  resets or its source changes, so it cannot order two reads of the same
  dataset: switching variables while a read was outstanding let whichever
  finished last seed the axes and RGB defaults, from a variable the user had
  already moved off.
- Clear only the identify target's own profile readings on an off-grid click,
  via a new `clearNetcdfProfileReadingsForLayer`. The list holds one layer at a
  time, so clearing it wholesale took another layer's still-charted readings
  with it whenever the user switched targets before landing a hit on the new one.
- Terminate the worker in `netcdf-remote-client`'s `onerror`. Nothing can be
  read through it afterwards — every later `send` would wait on a reply that
  cannot come — and it owns a lazy-filesystem mount.
- Spread the *current* layer record in `NetcdfSymbologySection.apply`'s deferred
  write rather than the one captured at render, so a concurrent edit to the same
  layer inside that window is no longer silently reverted. Awaiting the colormap
  warm widened that window, which is what made this worth fixing now.
- Drop the dead `LocalNetcdfImage` type import.

Left `gridBounds` alone: an all-non-finite axis cannot reach it from either
reader, because `acceptCoordinate` rejects a coordinate array containing any
non-finite value (`valuesWithin`) and throws `NO_COORDINATES_MESSAGE` first. The
Null Island bounds would need a direct external call, and making an exported
function throw is the riskier change.
@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Good, the inline comment tool is loaded and ready to use once the reviews land.

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.

2 participants