Skip to content

Layer Manager: flag a layer that has no data at the current time #410

Description

@sandesh-sp

Layer Manager: flag a layer that has no data at the current time

Parent: #409

Motivation

Once the core stops requesting a time-enabled layer outside its declared data coverage (#409), a layer that is switched on can be absent from the map for a good reason. Without a signal, that looks exactly like a broken layer: the checkbox says on, the map shows nothing, and the user has no way to tell "no data on this day" from "the tiles failed". The layer needs to explain itself.

How it should work

A suppressed layer's row in the Layer Manager stays exactly as it is — no dimming, checkbox untouched, every other control in place — plus a small calendar warning icon beside the layer's name. It appears only on time-enabled layers, and only while the core is suppressing that layer.

Hovering or focusing the icon opens a popover that says No data at this time, the instant being asked for, and the coverage in words:

  • continuous: "Data available 2020-01-01 to 2020-03-01", or with an open bound "Data available from 2020-01-01" / "Data available until 2020-03-01"
  • sparse, one entry, named as precisely as it was listed: "Data available in 2020", "in March 2020", "on 2020-03-04", or "on 2020-03-04 at 14:30 UTC"
  • sparse, several entries: "Data available for 12 listed periods, March 2020 to 2020-11-30", with the first and last each named at their own precision

Listed entries can be a year, a month, a day or an hour, and the core says which for each one, so the popover never guesses an entry's precision from how long its span is. All times are UTC. The instant is read when the popover opens, so it names the time being asked for now rather than the time of the last change.

The panel is a consumer. It reads the coverage record the core serves over the message bus and words it itself; it computes no coverage and reaches into no core state for it. The portable half of the plugin stays free of the host: it is handed what it renders, and never asks the bus itself.

Done when

  • Stepping the timeline into a gap in a sparse layer's dates shows the icon on that layer's row; stepping back out hides it.
  • Opening the panel after a layer was already suppressed shows the icon immediately, without waiting for the next time step.
  • The popover carries the three lines above with the right instant and the right wording for continuous, open-bound, several-entry sparse, and single-entry sparse coverage.
  • A single listed entry is named at its own precision for each of year, month, day and hour; an hour entry written to the minute is named by the time as listed.
  • The icon appears only on time-enabled layers, and a layer that declares no coverage never shows it.
  • Refreshing the list — toggling, reordering, or adding layers — never leaves a stray or duplicate popover behind.
  • Closing the panel removes its popovers and its bus subscription.
  • A test asserts that nothing under the tools directory imports the core coverage module or reads the core registry directly.

Out of scope

Draft implementation plan — written as of 93c37cc on 2026-09-11. Rough guide; re-verify against latest code.

Depends on #409 (PR #421, branch feature/layer-data-coverage-request-gating); stack on it until it merges. Tasks 8–10 of the local plan docs/superpowers/plans/2026-09-09-layer-data-coverage-request-gating.md were written for the older Layers tool and are superseded by this section.

Current behavior

src/essence/Tools/LayerManager/ is a React plugin split in two: lib/ is the portable, host-free half (components, utilities, its own types.ts), and adapters/ plus MMGISLayerManagerAdapter.tsx talk to core over the bus. __tests__/libBoundary.spec.js fails any lib/ file that imports outside lib/, names an MMGIS module, or mentions mmgisAPI / mmgisglobal. Tests run under vitest.

The adapter builds rows in getVisibleLayersWithLegends (from layers:getAllConfigs and friends, keyed by layer UUID), refreshes on bus events via useMMGISEvent, and waits for layers:getAll before the first refresh. Each row is lib/geo/LayerLegend/LayerLegend.tsx; its popovers are FloatingPopovers portaled per row and controlled by isOpen. The row has no warning icon of any kind today. LayerManagerTool.destroy unmounts the React root, which releases every useMMGISEvent subscription.

The bus surface already exists on the #409 branch: layers:getDataCoverage (one layer by UUID or name, or all keyed by UUID) and layers:dataCoverageChanged, with typed wrappers mmgisGetDataCoverage, mmgisGetLayerDataCoverage, mmgisOnDataCoverageChanged and the LayerDataCoverage / CoverageSpan types in src/essence/Tools/_shared/adapters/mmgisAPI.ts.

Where the change lands & rough plan

  1. A pure wording function in lib/ (e.g. lib/utils/dataCoverageWording.ts) — record in, { title, instant, coverage } out — with a spec. Because lib/ cannot import the shared adapter types, declare the structural coverage shape it needs in lib/types.ts. Sparse wording switches on each span's unit and formats its at at that precision (YYYY, MMMM YYYY, YYYY-MM-DD, YYYY-MM-DD HH:mm UTC); continuous wording uses start/end, treating ±Infinity as open.
  2. lib/: Layer gains an optional coverage field; LayerLegend renders a calendar warning icon beside the title while it says out-of-range, with a hover/focus FloatingPopover. The instant is fetched on open through a prop callback (same shape as canZoomToLayer), so lib/ stays host-free. Styles in lib/styles/components-geo/layer-legend.scss.
  3. Adapter: fetch the bulk coverage map with the row data so an already-suppressed layer shows on first render; subscribe to layers:dataCoverageChanged with a stable handler and patch that layer's row; supply the on-open callback via mmgisGetLayerDataCoverage. Show the icon only where the record says outOfDataRange on a time-enabled layer.
  4. tests/unit/dataCoverageBoundary.spec.js (or alongside libBoundary.spec.js): walks src/essence/Tools/ and fails on any import of layerDataCoverage or any read of layers.dataCoverage.

⚠️ Gotcha: the coverage record's requestedWindow is stored on every time step but the change event fires only when the verdict or coverage changes, so a popover that cached the event payload would name a stale instant. Read the record on show.

⚠️ Gotcha: lib/ must not reach the bus or import the shared mmgisAPI types — libBoundary.spec.js enforces it. Anything the row needs from core arrives as a prop or a callback.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions