11import { parseTimeValue , pickGranularity , type TimeGranularity } from "./time-slider-binding" ;
22
3+ export const TIME_GRANULARITIES : readonly TimeGranularity [ ] = [ "hour" , "day" , "month" , "year" ] ;
4+
35/**
46 * A layer whose time is an **internal dimension** rather than a feature property
57 * or a separate dated source: a Zarr data cube with a `time` axis, a plugin's
@@ -28,6 +30,16 @@ export interface TemporalLayerAdapter {
2830 * axis the timeline was bound to. Defaults to `"time"`.
2931 */
3032 dimension ?: string ;
33+ /**
34+ * Stepping unit for the Time Slider. When omitted, GeoLibre derives one from
35+ * the full axis span.
36+ */
37+ granularity ?: TimeGranularity ;
38+ /**
39+ * Units offered by the Time Slider's granularity controls. For example, a
40+ * daily cube can use `["day"]` to keep the track and labels at its cadence.
41+ */
42+ displayUnits ?: TimeGranularity [ ] ;
3143}
3244
3345/**
@@ -48,6 +60,8 @@ export interface SelectorTimeBinding {
4860 max : number ;
4961 /** Suggested stepping granularity derived from the axis span. */
5062 granularity : TimeGranularity ;
63+ /** Units offered by the Time Slider while this binding is active. */
64+ displayUnits ?: TimeGranularity [ ] ;
5165}
5266
5367/**
@@ -142,6 +156,7 @@ export function nearestTimeIndex(axis: readonly number[], targetMs: number): num
142156export function buildSelectorTimeBinding (
143157 dimension : string ,
144158 values : ReadonlyArray < Date | number | string > | null | undefined ,
159+ options ?: Pick < TemporalLayerAdapter , "granularity" | "displayUnits" > ,
145160) : SelectorTimeBinding | null {
146161 const axis = toEpochMsAxis ( values ) ;
147162 if ( ! axis ) return null ;
@@ -156,15 +171,42 @@ export function buildSelectorTimeBinding(
156171 // A single-slice cube still needs a non-zero span so the slider can move,
157172 // matching the vector path's treatment of a single-instant dataset.
158173 if ( max <= min ) max = min + 86_400_000 ;
174+ const granularity = options ?. granularity ?? pickGranularity ( max - min ) ;
175+ const displayUnits = options ?. displayUnits
176+ ? TIME_GRANULARITIES . filter (
177+ ( unit ) => unit === granularity || options . displayUnits ?. includes ( unit ) ,
178+ )
179+ : undefined ;
159180 return {
160181 kind : "selector" ,
161182 dimension : dimension . trim ( ) || "time" ,
162183 min,
163184 max,
164- granularity : pickGranularity ( max - min ) ,
185+ granularity,
186+ ...( displayUnits ? { displayUnits } : { } ) ,
165187 } ;
166188}
167189
190+ /**
191+ * Resolve display-unit constraints for a shared slider. Constrained selector
192+ * bindings contribute their intersection. If that intersection cannot expose
193+ * the active stepping unit, the active unit is the only safe shared control.
194+ */
195+ export function resolveSelectorDisplayUnits (
196+ bindings : readonly SelectorTimeBinding [ ] ,
197+ activeGranularity : TimeGranularity ,
198+ ) : TimeGranularity [ ] | undefined {
199+ const constrained = bindings . filter (
200+ ( binding ) : binding is SelectorTimeBinding & { displayUnits : TimeGranularity [ ] } =>
201+ Boolean ( binding . displayUnits ?. length ) ,
202+ ) ;
203+ if ( constrained . length === 0 ) return undefined ;
204+ const intersection = TIME_GRANULARITIES . filter ( ( unit ) =>
205+ constrained . every ( ( binding ) => binding . displayUnits . includes ( unit ) ) ,
206+ ) ;
207+ return intersection . includes ( activeGranularity ) ? intersection : [ activeGranularity ] ;
208+ }
209+
168210// ----- Registry --------------------------------------------------------------
169211// Adapters are transient: they belong to a *live* layer instance, so they are
170212// registered when the layer is created (natively by the Zarr flow, or by an
0 commit comments