Skip to content

A click on a raster answers with its value, like a click on a vector - #571

Draft
BhattaraiSijan wants to merge 4 commits into
developmentfrom
feat/raster-value-on-click
Draft

BhattaraiSijan wants to merge 4 commits into
developmentfrom
feat/raster-value-on-click

Conversation

@BhattaraiSijan

Copy link
Copy Markdown
Collaborator

Summary

A click on a raster layer now answers the way a click on a vector feature does. Wishlist item from the Air4US team: clicking the map on a raster should show the data value there, like the vector popup. Until now only vector layers answered a click; a raster in the browser is a stack of tiles, and the number behind the pixel is not there.

The flow is the vector flow. A click is resolved once, in core, by draw order: the topmost layer under the cursor that has something at that point answers. A vector layer answers with its picked feature, already in memory. A raster layer marked pickable answers with its value, fetched from the service that serves its tiles. Both go out as the same map:featureClick broadcast, so FeaturePopup renders a raster's value through the same heading template and property list an admin already writes for a vector, and its code does not change.

How the point URL is found

The point URL is deduced from the tile URL, and the deduction is a TiTiler-family convention, not a standard. TiTiler's factories pair /tiles/{tms}/{z}/{x}/{y} with /point/{lon},{lat}, both routes reading the same query parameters, and titiler, titiler-pgstac, titiler-multidim/xarray and titiler-cmr all build on them. So core swaps the tile path for the point path, keeps the query string as the tiles sent it (model run, lead, sel=, where=, datetime, assets, bidx), and drops the parameters that only shape a rendered tile (colormap_name, rescale, tilesize and the like). A client-side COG, drawn from a bare .tif, is asked through the layer's TiTiler /cog/point.

No OGC rule says any of this. WMS and WMTS answer a point by GetFeatureInfo on a pixel, OGC API Tiles has no point route, and other tile servers have their own or none. A tile URL of any other shape gets no point URL and the layer cannot answer a click. This is stated in the same words on toPointUrl in layerValueAt.ts, the only place the rule lives.

Verified live against every raster Air4US draws (NAQFC O₃ on titiler-multidim 0.7, TEMPO HCHO and NO₂ on titiler-multidim 0.10) and against a VEDA titiler-pgstac collection.

What's in the diff

  • Basics/Layers_/layerValueAt.ts (new, pure): toPointUrl (the deduction above), readPointValue (both answer shapes: a single dataset's values: [n], and a mosaic's [item, [values], [bands]] per item, newest first), fetchPointValue (never rejects; a failure, a stall past 10 s or an unreadable answer is null and one console warning), boundsContain.
  • Basics/Map_/clickResolver.ts (new, pure): one rule for a click. The engine's pick is the floor; the pickable rasters drawn above it are asked top-down and the first with a value there takes the click, shaped as a point feature with value, location (the clicked point as lat, lng, four decimals), asOf, band, item in its properties. Nothing there falls through to the pick. A click with no raster to ask resolves in place, so vector-only timing is unchanged. A click superseded by a newer one resolves to nothing, so a stale answer never goes out.
  • Basics/Map_/Map_.js: the map:featureClick bridge resolves through the resolver. resolveTimedTileSource is the first two steps of makeTileLayer lifted out so creation and the value lookup read the same URL; currentTileUrlFor finishes it the way the deck branch does. rasterValueAt gates on the coverage record and the layer's bounding box before asking. rasterCandidatesAbove reads draw order, visibility, pickable and the raster type.
  • MapEngines/Adapters/DeckGLHelpers.ts: an empty pick now carries where it landed, which the resolver needs.
  • Core says when it is fetching. rasterValueAt emits layers:valueFetchStart { layerName, latlng } before the request and layers:valueFetchEnd { layerName, latlng, value, failed } when it settles, once per request that actually goes out. Both are listed in the Event Bus API doc. A point query takes two to seven seconds on Air4US, so the deadline is 20 s.
  • Tools/FeaturePopup/FeaturePopupTool.js: listens to layers:valueFetchStart and, for a layer that opted in, shows a card reading "Reading value…" at the point so the click visibly landed. A value then arrives as a feature and replaces it. A null answer is said, not swallowed: on layers:valueFetchEnd the card becomes "No data at this point." under the layer's heading, or "The value could not be read." when the request failed, and the empty click that follows leaves that card up; the next click elsewhere takes it down. One raster asked after another in the same click finds the card already up. The feature click handling is unchanged apart from that one keep.
  • Tools/FeaturePopup/config.json: the description says a pickable tile layer answers with value, location, asOf, band, item, and shows the reading card while the value is fetched.

Opt-in is pickable: true on a tile layer, a core field with a default of off, so no layer fires a request unasked. Core does not read the plugin's featurePopup block to decide.

Testing

  • tests/unit/layerValueAt.spec.js (new): the rewrite on every shape in use (multidim, pgstac collection with @1x, pgstac item with .png, /cog/tiles with .webp, bare .tif with and without a base), refusal of OSM, WMS and a non-matching path, parameter stripping, both answer shapes, null, the asOf read, a failed, unreadable, throwing and stalled fetch, bounds.
  • tests/unit/clickResolver.spec.js (new): in-place result with nothing to ask, no position means no ask, raster above the pick takes it, top-down order and early stop, fall-through to the pick, superseded click.
  • tests/unit/deckGLHelpers.spec.js: an empty pick carries its position.
  • tests/unit/FeaturePopupTool.spec.js: the plugin renders a resolved raster answer with the configured label; the reading card shows for an opted-in raster, not for one that did not opt in, once for two rasters in one click, is replaced by the value, emits no action when replaced, and never shows on Leaflet; a null answer turns it into the no-data card, a failed one into the could-not-read card, the empty click of the same spot leaves it up and the next click elsewhere takes it down; a null answer for a spot no longer shown is ignored, and a second raster is read after the first said nothing.
  • npx vitest run tests/unit: all green (see the run in the PR checks). npx tsc --noEmit clean.
  • Live, Air4US, still to do: NAQFC on, click over CONUS → "Reading value…" at the point at once, then the ozone card in its place; change run or valid time → the value changes; the request is /point/{lon},{lat} with the tiles' sel= and no colormap_name. TEMPO NO₂ inside its window → value with the observed time; the three where= filters go percent-encoded. Both on → the topmost answers; reorder and the other does. Pacific → "No data at this point." under the layer's heading, staying up until the next click. Click an AQS station drawn above NAQFC → the station card. Two quick clicks → only the last card.

Not in this PR

  • Configure fields for the switch and the popup block on tile layers: next PR.
  • The Air4US config export with the three rasters opted in (air4us-v48-pick-value.config.json): not committed, imported into the dashboard.
  • Disasters layers: out of scope this round.

🤖 Generated with Claude Code

This branch has not been deployed

No deployments
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