Skip to content

Layer time can be open-ended ("now") and granularity-aware - #337

Open
BhattaraiSijan wants to merge 11 commits into
developmentfrom
feat/open-ended-layer-time
Open

Layer time can be open-ended ("now") and granularity-aware#337
BhattaraiSijan wants to merge 11 commits into
developmentfrom
feat/open-ended-layer-time

Conversation

@BhattaraiSijan

@BhattaraiSijan BhattaraiSijan commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Closes #332 (part of epic #331)

Summary

Time-enabled layers could only declare fixed data start/end timestamps — but for growing collections the end is unknowable at authoring time, so every config went stale the day after it was written. A layer's dataStartTime/dataEndTime may now be a policy re-evaluated on demand: now, or now ± <ISO-8601 duration> (now - P1D, now + P5D). Concrete datetimes behave exactly as before.

Per veda-ui (our semantics standard), now resolves to the raw current moment — never rounded. But raw now is only half the truth for a periodic collection: a product that publishes every 7 days and started ten days ago has its newest data three days ago, not today. So a layer may also declare its cadence, time.interval (an ISO-8601 duration: P1D, P7D, P1M), and the extent's end floors to the last start-anchored stepstart + N × interval for the largest N that fits. Data exists only on the grid; the extent no longer claims otherwise. This matches how veda-ui reaches the same answer (raw open end + start-anchored periodic steps).

Core owns this vocabulary. The config keeps what the author wrote; core resolves it behind one provider, layers:getTemporalExtent, which answers { start, end } as plain ISO datetimes (or null) — for one layer by id, or for all layers keyed by UUID, in the same call shapes as layers:getCogCapabilities and layers:getBounds. It resolves at the moment of asking, so an open-ended now is always fresh — and the cadence flooring happens behind the provider too, so a plugin never sees a policy string, never sees an interval, and never imports a resolver.

Resolution degrades, never breaks: an unparseable policy or interval, a zero cadence (P0D), a missing start to anchor to — each is ignored and consumers keep their fallbacks. Calendar units step by UTC date components, not milliseconds, and start + N × interval is computed as one calendar operation, so a monthly cadence stays anchored to the start's day-of-month.

isPeriodic was dropped from an earlier revision of this PR as unused (review feedback) — correctly so: interval alone carries the fact, and its presence is what marks a layer periodic. #338's VEDA source can fill it automatically from dashboard:time_interval; authors can type it by hand today.

Commits

  1. The policy resolver — one pure function: policy string in, resolved ISO datetime out, null for anything unparseable.
  2. The Timeline consumes it — replacing its hand-parsed inline 'now' special case.
  3. Resolution moves into core — the resolver relocates to Basics/TimeControl_/layerTimePolicy.ts; Layers_ registers layers:getTemporalExtent; the Timeline reads extents instead of resolving; _shared/time/ removed. The Layers tool's "set global time to layer extent" action goes through the same path — it previously did new Date("now") and produced NaN.
  4. Periodic flooringresolveTemporalExtent floors a periodic layer's end to its last data step. Constant-time: N is seeded from a millisecond approximation, then settled by calendar math.
  5. Core wires through itlayers:getTemporalExtent and the Layers tool answer cadence-aware; the Timeline gets it with zero changes.
  6. Configure — a Data Time Interval field beside the data start/end times in the tile and vector layer forms.

Testing

  • 28 unit specs with an injected clock: all three policy forms, sub-day and calendar-unit offsets, the 7-day-cadence-ends-three-days-ago scenario, ends exactly on / off a step, month-end anchoring, end-before-start clamping, and every failure mode (P0D included) degrading to an unsnapped extent or null. Full suite green (124 files, 1790 tests); typecheck clean.
  • Verified live: an ongoing layer configured with end now draws its timeline bar reaching the current date (screenshot in Layer time can be open-ended ("now") and granularity-aware #332), while a closed-extent layer's bar correctly stops at its real end — and the now config needs no edits, ever.

Mission-level time (initialstart/initialend) keeps its existing seconds-offset syntax; unifying it with ISO durations is possible future work.

🤖 Generated with Claude Code

…uration offsets)

A layer's dataStartTime/dataEndTime may now be a policy re-evaluated at load instead of a fixed timestamp, so configs for growing collections never go stale. One pure resolver evaluates 'now' and 'now +/- <ISO duration>'; per veda-ui the result is the raw current moment, never rounded. Unparseable values resolve to null so consumers keep their fallbacks. Spec'd with an injected clock. Mechanism half of #332.
The per-layer timeline bar previously hand-parsed data times with an inline 'now' special case for the end only. It now resolves both ends through the time-policy resolver, so open-ended and offset policies render correctly on either end and reach the current moment on every load. The bus layer-config type documents the policy vocabulary plus the carried granularity/periodicity facts (interval, isPeriodic). Completes #332's visible half.
@BhattaraiSijan
BhattaraiSijan marked this pull request as ready for review August 26, 2026 20:14
The Data Start/End Time field descriptions said ISO 8601 datetime only, so an admin had no way to discover that now, or now offset by an ISO 8601 duration, is a legal value. The tile and vector layer forms both say so now. Description text only; no behaviour change. Completes #332's documentation criterion.
Comment thread src/essence/Tools/_shared/adapters/mmgisAPI.ts Outdated
Nothing in this PR writes or reads either field;
The now / now ± duration vocabulary is a core-owned meaning of dataStartTime/dataEndTime. Core resolves it at call time behind one provider; the Timeline plugin reads dates and no longer imports a resolver. The Layers tool's set-to-extent action goes through the same path, which it previously did not (a 'now' produced NaN there).
@BhattaraiSijan
BhattaraiSijan force-pushed the feat/open-ended-layer-time branch from e7760a4 to 111be6a Compare August 28, 2026 19:38
Comment thread src/essence/Tools/Layers/LayersTool.js Outdated
import F_ from '../../Basics/Formulae_/Formulae_'
import L_ from '../../Basics/Layers_/Layers_'
import Map_ from '../../Basics/Map_/Map_'
import { resolveTimePolicy } from '../../Basics/TimeControl_/layerTimePolicy'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

is this the old Layers tool?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yes its the legacy Layers tool.

Comment on lines +196 to +197
if (extent?.start != null) start = new Date(extent.start)
if (extent?.end != null) end = new Date(extent.end)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

these should be resolved based on the periodicity - not always current date

for example, if a data is updated every 7 days and it started ten days ago, the end time needs to be 3 days ago not today

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

also shouldn't this come from layers, using temporalExtentFor? since it already does the resolving, we don't need to do it in two places

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Both points addressed.

  • A layer can now declare time.interval and the extent's end snaps to the last step from the start date — so your example (7-day data, started 10 days ago) resolves to 3 days ago, not today. It's settable in Configure. No separate isPeriodic — per your earlier comment, having an interval is what makes a layer periodic. The snapping happens in core behind layers:getTemporalExtent, so the Timeline and every other consumer get it without knowing the cadence exists.

  • extents here is mmgisGetTemporalExtents() (line 180), which is the bus call to layers:getTemporalExtent, and that provider is temporalExtentFor. This line is just converting the returned ISO string to a Date for drawing.

…d-layer-time

# Conflicts:
#	src/essence/Basics/Layers_/Layers_.js
A layer may declare time.interval (an ISO-8601 duration): data exists
only at start-anchored steps, so resolveTemporalExtent answers the last
step at or before the resolved end - a 7-day cadence begun ten days ago
ended three days ago, not now.
layers:getTemporalExtent and the Layers tool's set-time-to-extent action
answer through resolveTemporalExtent, so every consumer - the Timeline
included, unchanged - sees the extent end on the last real data step.
A Data Time Interval field (ISO 8601 duration) beside the data start/end
times; the bus contract documents that the cadence is already folded
into resolved extents, so plugins never see it.
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.

Layer time can be open-ended ("now") and granularity-aware

2 participants