@anthropomorphic/delorean 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Anthropomorphic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,679 @@
1
- # Temporary Holding Version
1
+ # Delorean
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Delorean è una libreria per gestire sequenze temporali di layer raster WMS con Leaflet e MapLibre GL JS. Include un core indipendente dal framework, due adapter per i layer WMS e un web component opzionale per i controlli temporali.
4
+
5
+ ## Installazione
6
+
7
+ Installa Delorean e il framework cartografico utilizzato dall'applicazione:
8
+
9
+ ```sh
10
+ npm install delorean leaflet
11
+ # oppure
12
+ npm install delorean maplibre-gl
13
+ ```
14
+
15
+ Leaflet e MapLibre GL JS sono peer dependency opzionali: è sufficiente installare quello richiesto dall'adapter utilizzato. Le richieste GetCapabilities vengono effettuate dal browser, quindi il server WMS deve consentire le richieste cross-origin (CORS).
16
+
17
+ ## Import
18
+
19
+ Il package espone entry point distinti e non esporta un entry point principale `delorean`:
20
+
21
+ ```ts
22
+ import { Delorean } from 'delorean/core';
23
+ import { DeloreanController } from 'delorean/ui';
24
+ import { LeafletRasterTimeLayer } from 'delorean/leaflet';
25
+ import { MaplibreglRasterTimeLayer } from 'delorean/maplibre';
26
+ ```
27
+
28
+ Importa solo gli entry point necessari. Importare `delorean/ui` registra il custom element `<delorean-controller>`.
29
+
30
+ ## Core
31
+
32
+ La classe `Delorean` gestisce timestamp Unix espressi in millisecondi. Può essere usata indipendentemente da Leaflet, MapLibre e dal controller UI.
33
+
34
+ ```ts
35
+ import { Delorean } from 'delorean/core';
36
+
37
+ const times = [
38
+ Date.parse('2024-01-01T00:00:00Z'),
39
+ Date.parse('2024-01-02T00:00:00Z')
40
+ ];
41
+
42
+ const delorean = new Delorean(times);
43
+ delorean.addTimeChangedCallback((time) => {
44
+ console.log(new Date(time).toISOString());
45
+ });
46
+
47
+ delorean.nextTime();
48
+ ```
49
+
50
+ ### Costruttore
51
+
52
+ | Firma | Descrizione |
53
+ | --- | --- |
54
+ | `new Delorean(defaultTimes: number[] = [], defaultCurrentTime: number = Date.now())` | Crea una nuova istanza del core. |
55
+
56
+ | Parametro | Tipo | Default | Descrizione |
57
+ | --- | --- | --- | --- |
58
+ | `defaultTimes` | `number[]` | `[]` | Tempi iniziali, come timestamp in millisecondi. |
59
+ | `defaultCurrentTime` | `number` | `Date.now()` | Tempo iniziale da confrontare con i tempi disponibili. |
60
+
61
+ ### Metodi e proprietà
62
+
63
+ | API | Tipo / firma | Descrizione |
64
+ | --- | --- | --- |
65
+ | `times` | `number[]` | Tempi correntemente memorizzati. |
66
+ | `currentTime` | `number` | Timestamp del tempo correntemente selezionato. |
67
+ | `setTimes(newTimes, operation = 'intersect')` | `(newTimes: number[], operation?: TimesOperation) => void` | Aggiorna i tempi secondo l'operazione scelta. |
68
+ | `setCurrentTime(time)` | `(time: number) => void` | Seleziona l'ultimo tempo disponibile minore o uguale al valore richiesto; se il valore precede il primo tempo, seleziona il primo. Notifica le callback. |
69
+ | `removeTimes(timesToRemove)` | `(timesToRemove: number[]) => void` | Rimuove i tempi indicati. Non notifica le callback né corregge l'indice corrente. |
70
+ | `prevTime(loop = true)` | `(loop?: boolean) => void` | Passa al tempo precedente. Con `loop: true`, dal primo torna all'ultimo. |
71
+ | `nextTime(loop = true)` | `(loop?: boolean) => void` | Passa al tempo successivo. Con `loop: true`, dall'ultimo torna al primo. |
72
+ | `firstTime()` | `() => void` | Seleziona il primo tempo disponibile e notifica le callback. |
73
+ | `lastTime()` | `() => void` | Seleziona l'ultimo tempo disponibile e notifica le callback. |
74
+ | `togglePlay(isPlaying, interval = 3000)` | `(isPlaying: boolean, interval?: number) => void` | Avvia o arresta l'avanzamento automatico. `interval` è espresso in millisecondi. |
75
+ | `addTimeChangedCallback(callback)` | `(callback: (newTime: number) => void) => void` | Registra una callback per i cambi di tempo. |
76
+ | `removeTimeChangedCallback(callback)` | `(callback: (newTime: number) => void) => void` | Rimuove la callback indicata. |
77
+ | `timeChanged(newTime)` | `(newTime: number) => void` | Invoca tutte le callback registrate. |
78
+
79
+ ### `TimesOperation`
80
+
81
+ `TimesOperation` è un union type, non un enum:
82
+
83
+ | Valore | Comportamento implementato |
84
+ | --- | --- |
85
+ | `union` | Concatena i tempi esistenti e quelli nuovi e li ordina. I duplicati sono mantenuti. |
86
+ | `intersect` | Ordina i tempi combinati ed elimina i duplicati. **Non calcola l'intersezione**, nonostante il nome. |
87
+ | `replace` | Sostituisce i tempi esistenti con quelli nuovi, senza ordinarli. |
88
+
89
+ > **Nota:** `intersect` attualmente si comporta come un'unione con deduplicazione. Non fare affidamento su un'intersezione matematica finché il comportamento non viene corretto nella libreria.
90
+
91
+ Il costruttore ordina i tempi iniziali in ordine crescente. Per utilizzare la navigazione, inizializza il core con almeno un tempo; con un array vuoto, le proprietà e i metodi di navigazione non hanno un valore temporale valido. Dopo `setTimes()` l'indice corrente non viene ricalcolato automaticamente.
92
+
93
+ ## Adapter
94
+
95
+ Gli adapter collegano un'istanza `Delorean` a un layer raster WMS del framework cartografico corrispondente. Entrambi leggono i tempi dalla dimensione `time` del documento WMS GetCapabilities e aggiornano il parametro temporale del layer quando cambia il tempo del core.
96
+
97
+ ### Leaflet
98
+
99
+ `LeafletRasterTimeLayer` crea un WMS `TileLayer` Leaflet.
100
+
101
+ ```ts
102
+ import * as leaflet from 'leaflet';
103
+ import { Delorean } from 'delorean/core';
104
+ import { LeafletRasterTimeLayer } from 'delorean/leaflet';
105
+
106
+ const map = leaflet.map('map').setView([44.4, 8.9], 8);
107
+ const delorean = new Delorean();
108
+
109
+ const timeLayer = new LeafletRasterTimeLayer(delorean, {
110
+ layerId: 'nexrad-n0r-wmst',
111
+ tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
112
+ wmsOptions: {
113
+ layers: 'nexrad-n0r-wmst',
114
+ format: 'image/png',
115
+ styles: '',
116
+ transparent: true,
117
+ version: '1.3.0'
118
+ },
119
+ getCapabilitiesUrl:
120
+ 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
121
+ });
122
+
123
+ await timeLayer.addTo(map);
124
+ // Quando il layer non serve più:
125
+ timeLayer.removeFrom(map);
126
+ ```
127
+
128
+ Firma del costruttore: `new LeafletRasterTimeLayer(delorean, options)`.
129
+
130
+ | Metodo / proprietà | Descrizione |
131
+ | --- | --- |
132
+ | `layerId` | Nome del layer WMS. |
133
+ | `layer` | Layer Leaflet sottostante. |
134
+ | `getCapabilitiesUrl` | URL GetCapabilities opzionale nel tipo; necessario per caricare i tempi. |
135
+ | `addTo(map, defaultTime?, operation = 'intersect')` | Carica i tempi, aggiunge il layer alla mappa e registra la callback temporale. Se `defaultTime` non è specificato, usa il default GetCapabilities se disponibile, altrimenti seleziona l'ultimo tempo. |
136
+ | `removeFrom(map)` | Rimuove il layer, i suoi tempi dal core e la callback di aggiornamento. |
137
+ | `loadTimes()` | Restituisce una `Promise<LayerTimeInfo>` con il tempo predefinito e i tempi discreti. |
138
+ | `setTime(time)` | Aggiorna il parametro `TIME` del layer. Restituisce una Promise. |
139
+
140
+ #### `LeafletRasterTimeLayerOptions`
141
+
142
+ | Proprietà | Tipo | Obbligatoria | Descrizione |
143
+ | --- | --- | --- | --- |
144
+ | `layerId` | `string` | sì | Nome del layer WMS, ad esempio `workspace:layer`. |
145
+ | `tileBaseUrl` | `string` | sì | Endpoint WMS usato per le richieste delle tile. |
146
+ | `wmsOptions` | `leaflet.WMSOptions` | sì | Opzioni del layer WMS di Leaflet. |
147
+ | `getCapabilitiesUrl` | `string` | no nel tipo, sì per `addTo()` | URL del documento GetCapabilities da cui leggere i tempi. |
148
+
149
+ ### MapLibre
150
+
151
+ `MaplibreglRasterTimeLayer` crea una raster source e un raster layer MapLibre. I parametri WMS e i valori passati a `tileUrlParams` sono stringhe; `tileUrlTemplate` contiene parametri di URL e placeholder di tile, ad esempio il bbox MapLibre.
152
+
153
+ ```ts
154
+ import * as maplibregl from 'maplibre-gl';
155
+ import { Delorean } from 'delorean/core';
156
+ import { MaplibreglRasterTimeLayer } from 'delorean/maplibre';
157
+
158
+ const map = new maplibregl.Map({
159
+ container: 'map',
160
+ style: 'https://tiles.openfreemap.org/styles/liberty',
161
+ center: [8.9, 44.4],
162
+ zoom: 8
163
+ });
164
+ const delorean = new Delorean();
165
+
166
+ const timeLayer = new MaplibreglRasterTimeLayer(delorean, {
167
+ sourceId: 'wms-source',
168
+ layerId: 'nexrad-n0r-wmst',
169
+ tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
170
+ tileUrlParams: {
171
+ service: 'WMS',
172
+ layers: 'nexrad-n0r-wmst',
173
+ styles: '',
174
+ transparent: 'true',
175
+ format: 'image/png',
176
+ version: '1.3.0',
177
+ request: 'GetMap',
178
+ crs: 'EPSG:3857',
179
+ width: '256',
180
+ height: '256'
181
+ },
182
+ tileUrlTemplate: {
183
+ bbox: '{bbox-epsg-3857}'
184
+ },
185
+ getCapabilitiesUrl:
186
+ 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
187
+ });
188
+
189
+ await timeLayer.addTo(map);
190
+ // Quando il layer non serve più:
191
+ timeLayer.removeFrom(map);
192
+ ```
193
+
194
+ Firma del costruttore: `new MaplibreglRasterTimeLayer(delorean, options)`.
195
+
196
+ | Metodo / proprietà | Descrizione |
197
+ | --- | --- |
198
+ | `sourceId` | ID della raster source MapLibre. |
199
+ | `source` | Specifica della source raster. |
200
+ | `layerId` | Nome del layer WMS e ID del layer MapLibre. |
201
+ | `layer` | Specifica del raster layer MapLibre. |
202
+ | `getCapabilitiesUrl` | URL GetCapabilities opzionale nel tipo; necessario per caricare i tempi. |
203
+ | `addTo(map, defaultTime?, operation = 'intersect')` | Carica i tempi, aggiunge source e layer e registra la callback temporale. Se `defaultTime` non è specificato, usa il default GetCapabilities se disponibile, altrimenti seleziona l'ultimo tempo. |
204
+ | `removeFrom(map)` | Rimuove layer e source, i tempi dal core e la callback di aggiornamento. |
205
+ | `loadTimes()` | Restituisce una `Promise<LayerTimeInfo>` con il tempo predefinito e i tempi discreti. |
206
+ | `setTime(map, time)` | Aggiorna l'URL delle tile con il parametro `TIME`. Restituisce una Promise. |
207
+
208
+ #### `MaplibreglRasterTimeLayerOptions`
209
+
210
+ | Proprietà | Tipo | Obbligatoria | Descrizione |
211
+ | --- | --- | --- | --- |
212
+ | `sourceId` | `string` | sì | ID della raster source MapLibre. |
213
+ | `layerId` | `string` | sì | Nome del layer WMS e ID del layer MapLibre. |
214
+ | `tileBaseUrl` | `string` | sì | Endpoint WMS usato per le richieste delle tile. |
215
+ | `tileUrlParams` | `Record<string, string>` | sì | Parametri della richiesta WMS. L'adapter aggiorna `TIME`. |
216
+ | `tileUrlTemplate` | `Record<string, string>` | sì | Parametri aggiunti all'URL, inclusi i placeholder di tile MapLibre come `{bbox-epsg-3857}`. |
217
+ | `paint` | `maplibregl.RasterLayerSpecification['paint']` | no | Proprietà visuali del raster layer. |
218
+ | `getCapabilitiesUrl` | `string` | no nel tipo, sì per `addTo()` | URL del documento GetCapabilities da cui leggere i tempi. |
219
+
220
+ ## Controller UI
221
+
222
+ La libreria fornisce il web component `<delorean-controller>`, utilizzabile insieme alla classe `Delorean`. Importare `delorean/ui` registra il custom element:
223
+
224
+ ```ts
225
+ import { DeloreanController } from 'delorean/ui';
226
+ ```
227
+
228
+ ```html
229
+ <delorean-controller></delorean-controller>
230
+ ```
231
+
232
+ Il controller emette eventi DOM con `bubbles: true` e `composed: true`; possono quindi essere ascoltati dagli elementi antenati anche attraversando uno shadow DOM. Gli eventi di navigazione non hanno `detail`. `delorean-play` invia in `event.detail` un booleano: `true` per play e `false` per pausa.
233
+
234
+ | Evento | `detail` | Descrizione |
235
+ | --- | --- | --- |
236
+ | `delorean-firsttime` | — | Richiesta di selezionare il primo tempo. |
237
+ | `delorean-prevtime` | — | Richiesta di selezionare il tempo precedente. |
238
+ | `delorean-nexttime` | — | Richiesta di selezionare il tempo successivo. |
239
+ | `delorean-lasttime` | — | Richiesta di selezionare l'ultimo tempo. |
240
+ | `delorean-play` | `boolean` | Cambiamento dello stato play/pausa. |
241
+
242
+ Il setter `time` aggiorna il valore visualizzato nell'input. Accetta un timestamp in millisecondi:
243
+
244
+ ```ts
245
+ const controller = document.querySelector('delorean-controller') as DeloreanController;
246
+ controller.time = Date.now();
247
+ ```
248
+
249
+ Le parti del web component sono personalizzabili dall'esterno con lo pseudo-elemento CSS `::part()`:
250
+
251
+ ```css
252
+ delorean-controller::part(delorean) {
253
+ flex-direction: column;
254
+ }
255
+
256
+ delorean-controller::part(delorean__first) {
257
+ background-color: red;
258
+ }
259
+ ```
260
+
261
+ | Part | Elemento | Descrizione |
262
+ | --- | --- | --- |
263
+ | `delorean` | `div` | Contenitore esterno. |
264
+ | `delorean__first` | `button` | Torna al primo tempo disponibile. |
265
+ | `delorean__previous` | `button` | Torna al tempo precedente. |
266
+ | `delorean__play` | `button` | Alterna play e pausa. |
267
+ | `delorean__next` | `button` | Passa al tempo successivo. |
268
+ | `delorean__last` | `button` | Passa all'ultimo tempo disponibile. |
269
+ | `delorean__datetime` | `input[type="datetime-local"]` | Campo in sola lettura che mostra il tempo assegnato alla proprietà `time`. |
270
+
271
+ ## GetCapabilities e formati temporali
272
+
273
+ Gli adapter richiedono il documento WMS GetCapabilities tramite `fetch`, individuano il layer corrispondente a `layerId` e leggono una dimensione `time` (elemento `Dimension` o `Extent`). Il browser deve poter accedere all'endpoint tramite CORS.
274
+
275
+ Il parser gestisce:
276
+
277
+ - valori temporali discreti separati da virgola;
278
+ - un singolo intervallo nel formato `inizio/fine/periodo`, con periodi espressi in giorni, ore, minuti e secondi (ad esempio `P1D` o `PT6H`).
279
+
280
+ Gli intervalli senza periodo, con periodo non supportato o con fine precedente all'inizio producono una lista di tempi vuota. La combinazione di più intervalli e valori discreti nella stessa dimensione non è gestita come una serie completa di intervalli.
281
+
282
+ Gli adapter cercano il layer per nome esatto. Se il layer non esiste nel documento o non espone una dimensione temporale, il caricamento fallisce con un errore. `getCapabilitiesUrl` è opzionale nelle interfacce TypeScript, ma deve essere fornito per usare `loadTimes()` o `addTo()`.
283
+
284
+ ## Esempi del progetto
285
+
286
+ Il repository contiene due demo eseguibili:
287
+
288
+ - `examples/delorean-leaflet`
289
+ - `examples/delorean-maplibre`
290
+
291
+ Per avviare una demo:
292
+
293
+ ```sh
294
+ cd examples/delorean-leaflet
295
+ npm install
296
+ npm run dev
297
+ ```
298
+
299
+ Per la demo MapLibre, usa `examples/delorean-maplibre` al posto di `examples/delorean-leaflet`.
300
+
301
+ ## Esempi completi
302
+
303
+ Gli esempi completi sotto seguono l'organizzazione e le opzioni delle demo presenti nel repository. Le dipendenze `delorean` sono installate dai pacchetti locali (`file:../../delorean-0.1.0.tgz`) nei manifest di esempio; in un progetto applicativo installa la versione pubblicata del package.
304
+
305
+ ### Esempio Leaflet
306
+
307
+ ```html
308
+ <!-- index.html -->
309
+ <html>
310
+ <head>
311
+ <title>Delorean Leaflet example</title>
312
+ </head>
313
+ <body>
314
+ <button type="button" id="add-layer" class="add-layer-btn">ADD WMS LAYER</button>
315
+ <button type="button" id="remove-layer" class="remove-layer-btn">REMOVE WMS LAYER</button>
316
+ <app-leaflet id="maplibre"></app-leaflet>
317
+ <script type="module" src="/src/main.ts"></script>
318
+ </body>
319
+ </html>
320
+ ```
321
+
322
+ ```css
323
+ /* style.css */
324
+ html,
325
+ body {
326
+ margin: 0;
327
+ padding: 0;
328
+ }
329
+
330
+ app-leaflet {
331
+ display: block;
332
+ height: 100dvh;
333
+ }
334
+
335
+ .add-layer-btn,
336
+ .remove-layer-btn {
337
+ position: fixed;
338
+ z-index: 999;
339
+ left: 16px;
340
+ }
341
+
342
+ .add-layer-btn {
343
+ top: 16px;
344
+ }
345
+
346
+ .remove-layer-btn {
347
+ top: 48px;
348
+ }
349
+ ```
350
+
351
+ ```ts
352
+ // main.ts
353
+ import './style.css';
354
+ import './leaflet';
355
+ import type { Leaflet } from './leaflet';
356
+
357
+ main();
358
+
359
+ function main() {
360
+ const leaflet = document.querySelector('#maplibre') as Leaflet;
361
+ const addBtn = document.querySelector('#add-layer') as HTMLButtonElement;
362
+ const removeBtn = document.querySelector('#remove-layer') as HTMLButtonElement;
363
+
364
+ addBtn.onclick = () => leaflet.addWmsTimeLayer({
365
+ layerId: 'nexrad-n0r-wmst',
366
+ tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
367
+ wmsOptions: {
368
+ layers: 'nexrad-n0r-wmst',
369
+ format: 'image/png',
370
+ styles: '',
371
+ transparent: true,
372
+ version: '1.3.0'
373
+ },
374
+ getCapabilitiesUrl:
375
+ 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
376
+ });
377
+
378
+ removeBtn.onclick = () => leaflet.removeWmsTimeLayer('nexrad-n0r-wmst');
379
+ }
380
+ ```
381
+
382
+ ```ts
383
+ // leaflet.ts
384
+ import * as leaflet from 'leaflet';
385
+ import style from 'leaflet/dist/leaflet.css?raw';
386
+ import { Delorean } from 'delorean/core';
387
+ import { LeafletRasterTimeLayer, type LeafletRasterTimeLayerOptions } from 'delorean/leaflet';
388
+ import { DeloreanController } from 'delorean/ui';
389
+
390
+ export class Leaflet extends HTMLElement {
391
+ public shadowRoot: ShadowRoot;
392
+
393
+ #map?: leaflet.Map;
394
+ #delorean?: Delorean;
395
+ #deloreanController?: DeloreanController;
396
+ #layers: LeafletRasterTimeLayer[] = [];
397
+
398
+ constructor() {
399
+ super();
400
+ this.shadowRoot = this.attachShadow({ mode: 'open' });
401
+ this.shadowRoot.appendChild(this.#html().content.cloneNode(true));
402
+ this.shadowRoot.appendChild(this.#css().cloneNode(true));
403
+
404
+ const leafletStylesheet = new CSSStyleSheet();
405
+ leafletStylesheet.replaceSync(style);
406
+ this.shadowRoot.adoptedStyleSheets.push(leafletStylesheet);
407
+ this.#deloreanController = this.shadowRoot.querySelector('#delorean') as DeloreanController;
408
+ }
409
+
410
+ #html(): HTMLTemplateElement {
411
+ const template = document.createElement('template');
412
+ template.innerHTML = `
413
+ <div id="leaflet" class="leaflet"></div>
414
+ <delorean-controller id="delorean" class="delorean"></delorean-controller>
415
+ `;
416
+ return template;
417
+ }
418
+
419
+ #css(): HTMLStyleElement {
420
+ const style = document.createElement('style');
421
+ style.innerHTML = `
422
+ .leaflet {
423
+ height: 100%;
424
+ }
425
+
426
+ .delorean {
427
+ position: fixed;
428
+ z-index: 999;
429
+ bottom: 40px;
430
+ left: 40px;
431
+ }
432
+ `;
433
+ return style;
434
+ }
435
+
436
+ public connectedCallback(): void {
437
+ this.#init();
438
+ this.#setup();
439
+ }
440
+
441
+ #init(): void {
442
+ const container = this.shadowRoot.querySelector('#leaflet');
443
+ if (!(container instanceof HTMLElement)) return;
444
+
445
+ this.#map = leaflet.map(container, { zoomControl: false })
446
+ .setView([44.40370599796929, 8.928584505583844], 9)
447
+ .addLayer(leaflet.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png'));
448
+
449
+ this.#delorean = new Delorean();
450
+ this.#delorean.addTimeChangedCallback((newTime: number) => {
451
+ if (!this.#map || !this.#deloreanController) return;
452
+ this.#deloreanController.time = newTime;
453
+ });
454
+ }
455
+
456
+ #setup(): void {
457
+ this.addEventListener('delorean-firsttime', () => this.#delorean?.firstTime());
458
+ this.addEventListener('delorean-prevtime', () => this.#delorean?.prevTime());
459
+ this.addEventListener('delorean-nexttime', () => this.#delorean?.nextTime());
460
+ this.addEventListener('delorean-lasttime', () => this.#delorean?.lastTime());
461
+ this.addEventListener('delorean-play', (event: Event) => {
462
+ this.#delorean?.togglePlay((event as CustomEvent).detail, 3000);
463
+ });
464
+ }
465
+
466
+ public async addWmsTimeLayer(options: LeafletRasterTimeLayerOptions): Promise<void> {
467
+ if (!this.#map || !this.#delorean) return;
468
+ const wmsTimeLayer = new LeafletRasterTimeLayer(this.#delorean, options);
469
+ await wmsTimeLayer.addTo(this.#map, 1787196600000, 'intersect');
470
+ this.#layers.push(wmsTimeLayer);
471
+ }
472
+
473
+ public removeWmsTimeLayer(layerId: string): void {
474
+ const foundLayer = this.#layers.find((layer) => layer.layerId === layerId);
475
+ if (!this.#map || !foundLayer) return;
476
+ foundLayer.removeFrom(this.#map);
477
+ this.#layers = this.#layers.filter((layer) => layer !== foundLayer);
478
+ }
479
+ }
480
+
481
+ customElements.define('app-leaflet', Leaflet);
482
+ ```
483
+
484
+ ### Esempio MapLibre
485
+
486
+ ```html
487
+ <!-- index.html -->
488
+ <html>
489
+ <head>
490
+ <title>Delorean MapLibre example</title>
491
+ </head>
492
+ <body>
493
+ <button type="button" id="add-layer" class="add-layer-btn">ADD WMS LAYER</button>
494
+ <button type="button" id="remove-layer" class="remove-layer-btn">REMOVE WMS LAYER</button>
495
+ <app-maplibre id="maplibre"></app-maplibre>
496
+ <script type="module" src="/src/main.ts"></script>
497
+ </body>
498
+ </html>
499
+ ```
500
+
501
+ ```css
502
+ /* style.css */
503
+ html,
504
+ body {
505
+ margin: 0;
506
+ padding: 0;
507
+ }
508
+
509
+ app-maplibre {
510
+ display: block;
511
+ height: 100dvh;
512
+ }
513
+
514
+ .add-layer-btn,
515
+ .remove-layer-btn {
516
+ position: fixed;
517
+ z-index: 999;
518
+ left: 16px;
519
+ }
520
+
521
+ .add-layer-btn {
522
+ top: 16px;
523
+ }
524
+
525
+ .remove-layer-btn {
526
+ top: 48px;
527
+ }
528
+ ```
529
+
530
+ ```ts
531
+ // main.ts
532
+ import './style.css';
533
+ import './maplibre';
534
+ import type { Maplibre } from './maplibre';
535
+
536
+ main();
537
+
538
+ function main() {
539
+ const maplibre = document.querySelector('#maplibre') as Maplibre;
540
+ const addBtn = document.querySelector('#add-layer') as HTMLButtonElement;
541
+ const removeBtn = document.querySelector('#remove-layer') as HTMLButtonElement;
542
+
543
+ addBtn.onclick = () => maplibre.addWmsTimeLayer({
544
+ sourceId: 'wms-test-source',
545
+ layerId: 'nexrad-n0r-wmst',
546
+ tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
547
+ tileUrlParams: {
548
+ service: 'WMS',
549
+ layers: 'nexrad-n0r-wmst',
550
+ styles: '',
551
+ transparent: 'true',
552
+ format: 'image/png',
553
+ version: '1.3.0',
554
+ request: 'GetMap',
555
+ crs: 'EPSG:3857',
556
+ width: '256',
557
+ height: '256'
558
+ },
559
+ tileUrlTemplate: {
560
+ bbox: '{bbox-epsg-3857}'
561
+ },
562
+ getCapabilitiesUrl:
563
+ 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
564
+ });
565
+
566
+ removeBtn.onclick = () => maplibre.removeWmsTimeLayer('nexrad-n0r-wmst');
567
+ }
568
+ ```
569
+
570
+ ```ts
571
+ // maplibre.ts
572
+ import * as maplibregl from 'maplibre-gl';
573
+ import style from 'maplibre-gl/dist/maplibre-gl.css?raw';
574
+ import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
575
+ import { Delorean } from 'delorean/core';
576
+ import { MaplibreglRasterTimeLayer, type MaplibreglRasterTimeLayerOptions } from 'delorean/maplibre';
577
+ import { DeloreanController } from 'delorean/ui';
578
+
579
+ maplibregl.setWorkerUrl(workerUrl);
580
+
581
+ export class Maplibre extends HTMLElement {
582
+ public shadowRoot: ShadowRoot;
583
+
584
+ #map?: maplibregl.Map;
585
+ #delorean?: Delorean;
586
+ #deloreanController?: DeloreanController;
587
+ #layers: MaplibreglRasterTimeLayer[] = [];
588
+
589
+ constructor() {
590
+ super();
591
+ this.shadowRoot = this.attachShadow({ mode: 'open' });
592
+ this.shadowRoot.appendChild(this.#html().content.cloneNode(true));
593
+ this.shadowRoot.appendChild(this.#css().cloneNode(true));
594
+
595
+ const maplibreStylesheet = new CSSStyleSheet();
596
+ maplibreStylesheet.replaceSync(style);
597
+ this.shadowRoot.adoptedStyleSheets.push(maplibreStylesheet);
598
+ this.#deloreanController = this.shadowRoot.querySelector('#delorean') as DeloreanController;
599
+ }
600
+
601
+ #html(): HTMLTemplateElement {
602
+ const template = document.createElement('template');
603
+ template.innerHTML = `
604
+ <div id="maplibre" class="maplibre"></div>
605
+ <delorean-controller id="delorean" class="delorean"></delorean-controller>
606
+ `;
607
+ return template;
608
+ }
609
+
610
+ #css(): HTMLStyleElement {
611
+ const style = document.createElement('style');
612
+ style.innerHTML = `
613
+ .maplibre {
614
+ height: 100%;
615
+ }
616
+
617
+ .delorean {
618
+ position: fixed;
619
+ z-index: 999;
620
+ bottom: 40px;
621
+ left: 40px;
622
+ }
623
+ `;
624
+ return style;
625
+ }
626
+
627
+ public connectedCallback(): void {
628
+ this.#init();
629
+ this.#setup();
630
+ }
631
+
632
+ #init(): void {
633
+ const container = this.shadowRoot.querySelector('#maplibre');
634
+ if (!(container instanceof HTMLElement)) return;
635
+
636
+ this.#map = new maplibregl.Map({
637
+ container,
638
+ style: 'https://tiles.openfreemap.org/styles/liberty',
639
+ center: [8.928584505583844, 44.40370599796929],
640
+ zoom: 9
641
+ });
642
+
643
+ this.#delorean = new Delorean();
644
+ this.#delorean.addTimeChangedCallback((newTime: number) => {
645
+ if (!this.#map || !this.#deloreanController) return;
646
+ this.#deloreanController.time = newTime;
647
+ });
648
+ }
649
+
650
+ #setup(): void {
651
+ this.addEventListener('delorean-firsttime', () => this.#delorean?.firstTime());
652
+ this.addEventListener('delorean-prevtime', () => this.#delorean?.prevTime());
653
+ this.addEventListener('delorean-nexttime', () => this.#delorean?.nextTime());
654
+ this.addEventListener('delorean-lasttime', () => this.#delorean?.lastTime());
655
+ this.addEventListener('delorean-play', (event: Event) => {
656
+ this.#delorean?.togglePlay((event as CustomEvent).detail, 1000);
657
+ });
658
+ }
659
+
660
+ public async addWmsTimeLayer(options: MaplibreglRasterTimeLayerOptions): Promise<void> {
661
+ if (!this.#map || !this.#delorean) return;
662
+ const wmsTimeLayer = new MaplibreglRasterTimeLayer(this.#delorean, options);
663
+ await wmsTimeLayer.addTo(this.#map);
664
+ this.#layers.push(wmsTimeLayer);
665
+ }
666
+
667
+ public removeWmsTimeLayer(layerId: string): void {
668
+ const foundLayer = this.#layers.find((layer) => layer.layerId === layerId);
669
+ if (!this.#map || !foundLayer) return;
670
+ foundLayer.removeFrom(this.#map);
671
+ this.#layers = this.#layers.filter((layer) => layer !== foundLayer);
672
+ }
673
+ }
674
+
675
+ customElements.define('app-maplibre', Maplibre);
676
+ ```
677
+
678
+ L'esempio Leaflet passa un timestamp iniziale esplicito a `addTo()`. La demo MapLibre non lo passa e quindi usa il tempo predefinito restituito dal GetCapabilities, se presente, oppure l'ultimo tempo disponibile. Sostituisci endpoint, nome del layer e parametri WMS con quelli del servizio che vuoi utilizzare.
679
+ # ett-delorean-timecontroller-typescript
@@ -0,0 +1,89 @@
1
+ var d = (r) => {
2
+ throw TypeError(r);
3
+ };
4
+ var T = (r, t, e) => t.has(r) || d("Cannot " + e);
5
+ var i = (r, t, e) => (T(r, t, "read from private field"), e ? e.call(r) : t.get(r)), g = (r, t, e) => t.has(r) ? d("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(r) : t.set(r, e), n = (r, t, e, a) => (T(r, t, "write to private field"), a ? a.call(r, e) : t.set(r, e), e), f = (r, t, e) => (T(r, t, "access private method"), e);
6
+ var u = (r, t, e, a) => ({
7
+ set _(m) {
8
+ n(r, t, m, e);
9
+ },
10
+ get _() {
11
+ return i(r, t, a);
12
+ }
13
+ });
14
+ import { a as C } from "../delorean.utils-DOgTNSYn.js";
15
+ import { g as y } from "../delorean.utils-DOgTNSYn.js";
16
+ var h, s, l, o, c, b, k;
17
+ class x {
18
+ constructor(t = [], e = (/* @__PURE__ */ new Date()).getTime()) {
19
+ g(this, c);
20
+ g(this, h);
21
+ g(this, s);
22
+ g(this, l, []);
23
+ g(this, o, !1);
24
+ n(this, h, [...t].toSorted((a, m) => a - m)), n(this, s, C(e, t));
25
+ }
26
+ get times() {
27
+ return i(this, h);
28
+ }
29
+ get currentTime() {
30
+ return i(this, h)[i(this, s)];
31
+ }
32
+ setTimes(t, e = "intersect") {
33
+ switch (e) {
34
+ case "union":
35
+ n(this, h, [...i(this, h), ...t].toSorted((a, m) => a - m));
36
+ break;
37
+ case "intersect":
38
+ n(this, h, [...new Set([...i(this, h), ...t].toSorted((a, m) => a - m))]);
39
+ break;
40
+ default:
41
+ n(this, h, [...t]);
42
+ break;
43
+ }
44
+ }
45
+ setCurrentTime(t) {
46
+ n(this, s, C(t, i(this, h))), this.timeChanged(this.currentTime);
47
+ }
48
+ removeTimes(t) {
49
+ const e = new Set(t);
50
+ n(this, h, i(this, h).filter(
51
+ (a) => !e.has(a)
52
+ ));
53
+ }
54
+ prevTime(t = !0) {
55
+ u(this, s)._--, t ? (i(this, s) < 0 && n(this, s, i(this, h).length - 1), i(this, s) > i(this, h).length - 1 && n(this, s, 0)) : (i(this, s) < 0 && n(this, s, 0), i(this, s) > i(this, h).length - 1 && n(this, s, i(this, h).length - 1)), this.timeChanged(this.currentTime);
56
+ }
57
+ nextTime(t = !0) {
58
+ u(this, s)._++, t ? (i(this, s) < 0 && n(this, s, i(this, h).length - 1), i(this, s) > i(this, h).length - 1 && n(this, s, 0)) : (i(this, s) < 0 && n(this, s, 0), i(this, s) > i(this, h).length - 1 && n(this, s, i(this, h).length - 1)), this.timeChanged(this.currentTime);
59
+ }
60
+ firstTime() {
61
+ n(this, s, 0), this.timeChanged(this.currentTime);
62
+ }
63
+ lastTime() {
64
+ n(this, s, i(this, h).length - 1), this.timeChanged(this.currentTime);
65
+ }
66
+ togglePlay(t, e = 3e3) {
67
+ n(this, o, t), f(this, c, b).call(this, () => this.nextTime(), e);
68
+ }
69
+ addTimeChangedCallback(t) {
70
+ i(this, l).push(t);
71
+ }
72
+ removeTimeChangedCallback(t) {
73
+ n(this, l, i(this, l).filter((e) => e !== t));
74
+ }
75
+ timeChanged(t) {
76
+ i(this, l).forEach((e) => e(t));
77
+ }
78
+ }
79
+ h = new WeakMap(), s = new WeakMap(), l = new WeakMap(), o = new WeakMap(), c = new WeakSet(), b = async function(t, e) {
80
+ for (; i(this, o) && (await t(), !!i(this, o)); )
81
+ await f(this, c, k).call(this, e);
82
+ }, k = function(t) {
83
+ return new Promise((e) => setTimeout(e, t));
84
+ };
85
+ export {
86
+ x as Delorean,
87
+ C as getCurrentTimeIndex,
88
+ y as getLayerTimes
89
+ };
@@ -0,0 +1,2 @@
1
+ export * from './leaflet';
2
+ export * from './maplibre';
@@ -0,0 +1,2 @@
1
+ export * from './leaflet.adapter';
2
+ export * from './leaflet.options';
@@ -0,0 +1,14 @@
1
+ import * as leaflet from 'leaflet';
2
+ import type { Delorean, LayerTimeInfo, TimesOperation } from '../../core';
3
+ import type { LeafletRasterTimeLayerOptions } from './leaflet.options';
4
+ export declare class LeafletRasterTimeLayer {
5
+ #private;
6
+ readonly layerId: string;
7
+ readonly layer: leaflet.Layer;
8
+ readonly getCapabilitiesUrl?: string;
9
+ constructor(delorean: Delorean, options: LeafletRasterTimeLayerOptions);
10
+ addTo(map: leaflet.Map, defaultTime?: number, operation?: TimesOperation): Promise<this>;
11
+ removeFrom(map: leaflet.Map): this;
12
+ loadTimes(): Promise<LayerTimeInfo>;
13
+ setTime(time: number): Promise<this>;
14
+ }
@@ -0,0 +1,7 @@
1
+ import * as leaflet from 'leaflet';
2
+ export interface LeafletRasterTimeLayerOptions {
3
+ layerId: string;
4
+ tileBaseUrl: string;
5
+ wmsOptions: leaflet.WMSOptions;
6
+ getCapabilitiesUrl?: string;
7
+ }
@@ -0,0 +1,2 @@
1
+ export * from './maplibregl.adapter';
2
+ export * from './maplibregl.options';
@@ -0,0 +1,16 @@
1
+ import * as maplibregl from 'maplibre-gl';
2
+ import type { Delorean, LayerTimeInfo, TimesOperation } from '../../core';
3
+ import type { MaplibreglRasterTimeLayerOptions } from './maplibregl.options';
4
+ export declare class MaplibreglRasterTimeLayer {
5
+ #private;
6
+ readonly sourceId: string;
7
+ readonly source: maplibregl.RasterSourceSpecification;
8
+ readonly layerId: string;
9
+ readonly layer: maplibregl.RasterLayerSpecification;
10
+ readonly getCapabilitiesUrl?: string;
11
+ constructor(delorean: Delorean, options: MaplibreglRasterTimeLayerOptions);
12
+ addTo(map: maplibregl.Map, defaultTime?: number, operation?: TimesOperation): Promise<this>;
13
+ removeFrom(map: maplibregl.Map): this;
14
+ loadTimes(): Promise<LayerTimeInfo>;
15
+ setTime(map: maplibregl.Map, time: number): Promise<this>;
16
+ }
@@ -0,0 +1,10 @@
1
+ import * as maplibregl from 'maplibre-gl';
2
+ export interface MaplibreglRasterTimeLayerOptions {
3
+ sourceId: string;
4
+ layerId: string;
5
+ tileBaseUrl: string;
6
+ tileUrlParams: Record<string, string>;
7
+ tileUrlTemplate: Record<string, string>;
8
+ paint?: maplibregl.RasterLayerSpecification['paint'];
9
+ getCapabilitiesUrl?: string;
10
+ }
@@ -0,0 +1,18 @@
1
+ import type { TimesOperation } from './delorean.types';
2
+ export declare class Delorean {
3
+ #private;
4
+ constructor(defaultTimes?: number[], defaultCurrentTime?: number);
5
+ get times(): number[];
6
+ get currentTime(): number;
7
+ setTimes(newTimes: number[], operation?: TimesOperation): void;
8
+ setCurrentTime(time: number): void;
9
+ removeTimes(timesToRemove: number[]): void;
10
+ prevTime(loop?: boolean): void;
11
+ nextTime(loop?: boolean): void;
12
+ firstTime(): void;
13
+ lastTime(): void;
14
+ togglePlay(isPlaying: boolean, interval?: number): void;
15
+ addTimeChangedCallback(callback: (newTime: number) => void): void;
16
+ removeTimeChangedCallback(callback: (newTime: number) => void): void;
17
+ timeChanged(newTime: number): void;
18
+ }
@@ -0,0 +1,5 @@
1
+ export type TimesOperation = 'union' | 'intersect' | 'replace';
2
+ export interface LayerTimeInfo {
3
+ defaultValue?: number;
4
+ discreteValues?: number[];
5
+ }
@@ -0,0 +1,10 @@
1
+ import type { LayerTimeInfo } from './delorean.types';
2
+ export declare function getCurrentTimeIndex(time: number, times: number[]): number;
3
+ /**
4
+ * Funzione end-to-end: dato l'URL base del GeoServer e l'id/nome del layer,
5
+ * restituisce le informazioni temporali del layer.
6
+ *
7
+ * @param geoserverBaseUrl es. "https://mio-server/geoserver/wms"
8
+ * @param layerId es. "mio_workspace:mio_layer"
9
+ */
10
+ export declare function getLayerTimes(geoserverBaseUrl: string, layerId: string): Promise<LayerTimeInfo>;
@@ -0,0 +1,3 @@
1
+ export * from './delorean.store';
2
+ export * from './delorean.types';
3
+ export * from './delorean.utils';
@@ -0,0 +1,7 @@
1
+ export declare class DeloreanController extends HTMLElement {
2
+ #private;
3
+ shadowRoot: ShadowRoot;
4
+ constructor();
5
+ connectedCallback(): void;
6
+ set time(value: number);
7
+ }
@@ -0,0 +1 @@
1
+ export * from './delorean.component';
@@ -0,0 +1,91 @@
1
+ function y(r, n) {
2
+ let t = -1;
3
+ if (r > n[n.length - 1]) return n.length - 1;
4
+ for (let e = 0; e < n.length; e++) {
5
+ const a = n[e];
6
+ if (r < a) {
7
+ t = e - 1;
8
+ break;
9
+ }
10
+ if (r === a) {
11
+ t = e;
12
+ break;
13
+ }
14
+ }
15
+ return t === -1 ? 0 : t;
16
+ }
17
+ async function p(r, n = "1.3.0") {
18
+ const t = new URL(r);
19
+ t.searchParams.set("SERVICE", "WMS"), t.searchParams.set("REQUEST", "GetCapabilities"), t.searchParams.set("VERSION", n);
20
+ const e = await fetch(t.toString());
21
+ if (!e.ok)
22
+ throw new Error(
23
+ `GetCapabilities fallita: HTTP ${e.status} ${e.statusText}`
24
+ );
25
+ return e.text();
26
+ }
27
+ function E(r) {
28
+ const t = new DOMParser().parseFromString(r, "application/xml"), e = t.getElementsByTagName("parsererror")[0];
29
+ if (e)
30
+ throw new Error("Errore nel parsing dell'XML: " + e.textContent);
31
+ return t;
32
+ }
33
+ function w(r, n) {
34
+ var e;
35
+ const t = Array.from(r.getElementsByTagName("Layer"));
36
+ for (const a of t) {
37
+ const s = Array.from(a.children).find(
38
+ (i) => i.tagName === "Name" || i.tagName.endsWith(":Name")
39
+ );
40
+ if (s && ((e = s.textContent) == null ? void 0 : e.trim()) === n)
41
+ return a;
42
+ }
43
+ return null;
44
+ }
45
+ function h(r) {
46
+ return Array.from(
47
+ r.getElementsByTagName("Dimension")
48
+ ).concat(Array.from(r.getElementsByTagName("Extent"))).find(
49
+ (t) => {
50
+ var e;
51
+ return ((e = t.getAttribute("name")) == null ? void 0 : e.toLowerCase()) === "time";
52
+ }
53
+ ) ?? null;
54
+ }
55
+ function T(r) {
56
+ const n = r.trim();
57
+ if (n.includes("/")) {
58
+ const [t, e, a] = n.split("/");
59
+ if (!t || !e || !a) return [];
60
+ const s = new Date(t).getTime(), i = new Date(e).getTime(), o = a.match(
61
+ /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/
62
+ );
63
+ if (!Number.isFinite(s) || !Number.isFinite(i) || !o)
64
+ return [];
65
+ const [, c = "0", m = "0", d = "0", g = "0"] = o, u = Number(c) * 864e5 + Number(m) * 36e5 + Number(d) * 6e4 + Number(g) * 1e3;
66
+ if (u <= 0 || i < s) return [];
67
+ const f = [];
68
+ for (let l = s; l <= i; l += u)
69
+ f.push(l);
70
+ return f;
71
+ }
72
+ return n.split(",").map((t) => new Date(t.trim()).getTime()).filter(Number.isFinite);
73
+ }
74
+ async function N(r, n) {
75
+ var m;
76
+ const t = await p(r), e = E(t), a = w(e.documentElement, n);
77
+ if (!a)
78
+ throw new Error(`Layer "${n}" non trovato nel GetCapabilities`);
79
+ const s = h(a);
80
+ if (!s)
81
+ throw new Error(`Il layer "${n}" non espone una dimensione "time"`);
82
+ const i = ((m = s.textContent) == null ? void 0 : m.trim()) ?? "", o = s.hasAttribute("default") ? new Date(s.getAttribute("default")).getTime() : void 0, c = T(i);
83
+ return {
84
+ defaultValue: o,
85
+ discreteValues: c
86
+ };
87
+ }
88
+ export {
89
+ y as a,
90
+ N as g
91
+ };
@@ -0,0 +1,48 @@
1
+ var f = Object.defineProperty;
2
+ var g = (t) => {
3
+ throw TypeError(t);
4
+ };
5
+ var u = (t, e, i) => e in t ? f(t, e, { enumerable: !0, configurable: !0, writable: !0, value: i }) : t[e] = i;
6
+ var c = (t, e, i) => u(t, typeof e != "symbol" ? e + "" : e, i), w = (t, e, i) => e.has(t) || g("Cannot " + i);
7
+ var s = (t, e, i) => (w(t, e, "read from private field"), i ? i.call(t) : e.get(t)), h = (t, e, i) => e.has(t) ? g("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(t) : e.set(t, i), m = (t, e, i, l) => (w(t, e, "write to private field"), l ? l.call(t, i) : e.set(t, i), i);
8
+ import * as b from "leaflet";
9
+ import { g as p } from "../delorean.utils-DOgTNSYn.js";
10
+ var a, o, d, n, r;
11
+ class L {
12
+ constructor(e, i) {
13
+ c(this, "layerId");
14
+ c(this, "layer");
15
+ c(this, "getCapabilitiesUrl");
16
+ h(this, a);
17
+ h(this, o);
18
+ h(this, d);
19
+ h(this, n);
20
+ h(this, r);
21
+ m(this, a, e), this.layerId = i.layerId, this.getCapabilitiesUrl = i.getCapabilitiesUrl, m(this, o, i.tileBaseUrl), m(this, d, i.wmsOptions), this.layer = b.tileLayer.wms(s(this, o), s(this, d));
22
+ }
23
+ async addTo(e, i, l = "intersect") {
24
+ const y = await this.loadTimes();
25
+ e.addLayer(this.layer), m(this, n, y.discreteValues), s(this, a).setTimes(s(this, n) ?? [], l), m(this, r, (T) => this.setTime(T)), s(this, a).addTimeChangedCallback(s(this, r));
26
+ const C = i ?? y.defaultValue;
27
+ return C ? s(this, a).setCurrentTime(C) : s(this, a).lastTime(), await new Promise((T) => this.layer.once("load", T)), this;
28
+ }
29
+ removeFrom(e) {
30
+ return e.removeLayer(this.layer), s(this, a).removeTimes(s(this, n) ?? []), s(this, r) && s(this, a).removeTimeChangedCallback(s(this, r)), this;
31
+ }
32
+ loadTimes() {
33
+ if (!this.getCapabilitiesUrl) throw new Error("getCapabilitiesUrl undefined");
34
+ return p(this.getCapabilitiesUrl, this.layerId);
35
+ }
36
+ async setTime(e) {
37
+ const i = this.layer, l = new Promise((y) => {
38
+ i.once("load", () => y());
39
+ });
40
+ return i.setParams({
41
+ time: new Date(e).toISOString()
42
+ }), await l, this;
43
+ }
44
+ }
45
+ a = new WeakMap(), o = new WeakMap(), d = new WeakMap(), n = new WeakMap(), r = new WeakMap();
46
+ export {
47
+ L as LeafletRasterTimeLayer
48
+ };
@@ -0,0 +1,66 @@
1
+ var $ = Object.defineProperty;
2
+ var b = (i) => {
3
+ throw TypeError(i);
4
+ };
5
+ var L = (i, e, t) => e in i ? $(i, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : i[e] = t;
6
+ var n = (i, e, t) => L(i, typeof e != "symbol" ? e + "" : e, t), C = (i, e, t) => e.has(i) || b("Cannot " + t);
7
+ var r = (i, e, t) => (C(i, e, "read from private field"), t ? t.call(i) : e.get(i)), l = (i, e, t) => e.has(i) ? b("Cannot add the same private member more than once") : e instanceof WeakSet ? e.add(i) : e.set(i, t), o = (i, e, t, d) => (C(i, e, "write to private field"), d ? d.call(i, t) : e.set(i, t), t), y = (i, e, t) => (C(i, e, "access private method"), t);
8
+ import * as p from "maplibre-gl";
9
+ import { g as v } from "../delorean.utils-DOgTNSYn.js";
10
+ var s, I, u, T, m, c, a, w, S, U;
11
+ class R {
12
+ constructor(e, t) {
13
+ l(this, a);
14
+ n(this, "sourceId");
15
+ n(this, "source");
16
+ n(this, "layerId");
17
+ n(this, "layer");
18
+ n(this, "getCapabilitiesUrl");
19
+ l(this, s);
20
+ l(this, I);
21
+ l(this, u);
22
+ l(this, T);
23
+ l(this, m);
24
+ l(this, c);
25
+ o(this, s, e), this.sourceId = t.sourceId, this.layerId = t.layerId, this.getCapabilitiesUrl = t.getCapabilitiesUrl, o(this, I, t.tileBaseUrl), o(this, u, new URLSearchParams(t.tileUrlParams)), o(this, T, t.tileUrlTemplate), this.source = y(this, a, w).call(this), this.layer = y(this, a, S).call(this, t.paint);
26
+ }
27
+ async addTo(e, t, d = "intersect") {
28
+ const h = await this.loadTimes();
29
+ e.addSource(this.sourceId, this.source), e.addLayer(this.layer), o(this, m, h.discreteValues), r(this, s).setTimes(r(this, m) ?? [], d), o(this, c, (f) => this.setTime(e, f)), r(this, s).addTimeChangedCallback(r(this, c));
30
+ const g = t ?? h.defaultValue;
31
+ return g ? r(this, s).setCurrentTime(g) : r(this, s).lastTime(), await new Promise((f) => e.once("idle", f)), this;
32
+ }
33
+ removeFrom(e) {
34
+ return e.getLayer(this.layerId) && e.removeLayer(this.layerId), e.getSource(this.sourceId) && e.removeSource(this.sourceId), r(this, s).removeTimes(r(this, m) ?? []), r(this, c) && r(this, s).removeTimeChangedCallback(r(this, c)), this;
35
+ }
36
+ loadTimes() {
37
+ if (!this.getCapabilitiesUrl) throw new Error("getCapabilitiesUrl undefined");
38
+ return v(this.getCapabilitiesUrl, this.layerId);
39
+ }
40
+ async setTime(e, t) {
41
+ if (!e) throw new Error(`Cannot set time: layer "${this.layerId}" is not attached to a map.`);
42
+ const d = new Promise((g) => {
43
+ e.once("idle", () => g());
44
+ }), h = e.getSource(this.sourceId);
45
+ if (!h || h.type !== "raster") throw new Error(`Raster source "${this.sourceId}" not found.`);
46
+ return r(this, u).set("TIME", `${new Date(t).toISOString()}`), h instanceof p.RasterTileSource && h.setTiles([y(this, a, U).call(this)]), await d, this;
47
+ }
48
+ }
49
+ s = new WeakMap(), I = new WeakMap(), u = new WeakMap(), T = new WeakMap(), m = new WeakMap(), c = new WeakMap(), a = new WeakSet(), w = function() {
50
+ return {
51
+ type: "raster",
52
+ tiles: [y(this, a, U).call(this)]
53
+ };
54
+ }, S = function(e) {
55
+ return {
56
+ id: this.layerId,
57
+ source: this.sourceId,
58
+ type: "raster",
59
+ paint: e ?? {}
60
+ };
61
+ }, U = function() {
62
+ return `${r(this, I)}?${r(this, u).toString()}&${Object.entries(r(this, T)).map(([e, t]) => `${e}=${t}`).join("&")}`;
63
+ };
64
+ export {
65
+ R as MaplibreglRasterTimeLayer
66
+ };
@@ -0,0 +1,129 @@
1
+ var q = Object.defineProperty;
2
+ var _ = (e) => {
3
+ throw TypeError(e);
4
+ };
5
+ var A = (e, s, t) => s in e ? q(e, s, { enumerable: !0, configurable: !0, writable: !0, value: t }) : e[s] = t;
6
+ var k = (e, s, t) => A(e, typeof s != "symbol" ? s + "" : s, t), y = (e, s, t) => s.has(e) || _("Cannot " + t);
7
+ var n = (e, s, t) => (y(e, s, "read from private field"), t ? t.call(e) : s.get(e)), a = (e, s, t) => s.has(e) ? _("Cannot add the same private member more than once") : s instanceof WeakSet ? s.add(e) : s.set(e, t), l = (e, s, t, i) => (y(e, s, "write to private field"), i ? i.call(e, t) : s.set(e, t), t), h = (e, s, t) => (y(e, s, "access private method"), t);
8
+ var r, p, c, m, g, u, d, w, b, o, f, B, M, x, D, F, C, E, Z, R;
9
+ class L extends HTMLElement {
10
+ constructor() {
11
+ super();
12
+ a(this, o);
13
+ k(this, "shadowRoot");
14
+ a(this, r, !1);
15
+ a(this, p);
16
+ a(this, c);
17
+ a(this, m);
18
+ a(this, g);
19
+ a(this, u);
20
+ a(this, d);
21
+ a(this, w, "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M560-200v-560h160v560H560Zm-320%200v-560h160v560H240Z%22%2F%3E%3C%2Fsvg%3E");
22
+ a(this, b, "data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M320-200v-560l440%20280-440%20280Z%22%2F%3E%3C%2Fsvg%3E");
23
+ a(this, x, () => {
24
+ this.dispatchEvent(
25
+ new CustomEvent("delorean-firsttime", { bubbles: !0, composed: !0 })
26
+ );
27
+ });
28
+ a(this, D, () => {
29
+ this.dispatchEvent(
30
+ new CustomEvent("delorean-prevtime", { bubbles: !0, composed: !0 })
31
+ );
32
+ });
33
+ a(this, F, () => {
34
+ this.dispatchEvent(
35
+ new CustomEvent("delorean-nexttime", { bubbles: !0, composed: !0 })
36
+ );
37
+ });
38
+ a(this, C, () => {
39
+ this.dispatchEvent(
40
+ new CustomEvent("delorean-lasttime", { bubbles: !0, composed: !0 })
41
+ );
42
+ });
43
+ a(this, E, () => {
44
+ l(this, r, !n(this, r)), h(this, o, Z).call(this, n(this, r)), this.dispatchEvent(
45
+ new CustomEvent("delorean-play", { bubbles: !0, composed: !0, detail: n(this, r) })
46
+ );
47
+ });
48
+ this.shadowRoot = this.attachShadow({ mode: "open" }), this.shadowRoot.appendChild(h(this, o, f).call(this).content.cloneNode(!0)), this.shadowRoot.appendChild(h(this, o, B).call(this).cloneNode(!0)), l(this, p, this.shadowRoot.querySelector("#datetime")), l(this, c, this.shadowRoot.querySelector("#first")), l(this, m, this.shadowRoot.querySelector("#prev")), l(this, g, this.shadowRoot.querySelector("#next")), l(this, u, this.shadowRoot.querySelector("#last")), l(this, d, this.shadowRoot.querySelector("#play"));
49
+ }
50
+ connectedCallback() {
51
+ h(this, o, M).call(this);
52
+ }
53
+ set time(t) {
54
+ n(this, p).value = h(this, o, R).call(this, t);
55
+ }
56
+ }
57
+ r = new WeakMap(), p = new WeakMap(), c = new WeakMap(), m = new WeakMap(), g = new WeakMap(), u = new WeakMap(), d = new WeakMap(), w = new WeakMap(), b = new WeakMap(), o = new WeakSet(), f = function() {
58
+ const t = document.createElement("template");
59
+ return t.innerHTML = `
60
+ <div part="delorean" class="delorean">
61
+ <button type="button" part="delorean__first" id="first" class="delorean-btn">
62
+ <span class="delorean-btn__icon" style="mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M220-240v-480h80v480h-80Zm520%200L380-480l360-240v480Z%22%2F%3E%3C%2Fsvg%3E')"></span>
63
+ </button>
64
+ <button type="button" part="delorean__previous" id="prev" class="delorean-btn">
65
+ <span class="delorean-btn__icon" style="mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M860-240%20500-480l360-240v480Zm-400%200L100-480l360-240v480Z%22%2F%3E%3C%2Fsvg%3E')"></span>
66
+ </button>
67
+ <button type="button" part="delorean__play" id="play" class="delorean-btn">
68
+ <span class="delorean-btn__icon" style="mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M320-200v-560l440%20280-440%20280Z%22%2F%3E%3C%2Fsvg%3E')"></span>
69
+ </button>
70
+ <button type="button" part="delorean__next" id="next" class="delorean-btn">
71
+ <span class="delorean-btn__icon" style="mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M100-240v-480l360%20240-360%20240Zm400%200v-480l360%20240-360%20240Z%22%2F%3E%3C%2Fsvg%3E')"></span>
72
+ </button>
73
+ <button type="button" part="delorean__last" id="last" class="delorean-btn">
74
+ <span class="delorean-btn__icon" style="mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224px%22%20viewBox%3D%220%20-960%20960%20960%22%20width%3D%2224px%22%20fill%3D%22%23000000%22%3E%3Cpath%20d%3D%22M660-240v-480h80v480h-80Zm-440%200v-480l360%20240-360%20240Z%22%2F%3E%3C%2Fsvg%3E')"></span>
75
+ </button>
76
+ <input type="datetime-local" part="delorean__datetime" id="datetime" class="delorean__datetime" readonly />
77
+ </div>
78
+ `, t;
79
+ }, B = function() {
80
+ const t = document.createElement("style");
81
+ return t.innerHTML = `
82
+ .delorean {
83
+ display: flex;
84
+ align-items: center;
85
+ }
86
+
87
+ .delorean-btn {
88
+ width: 24px;
89
+ height: 24px;
90
+ margin: 0;
91
+ padding: 0;
92
+ }
93
+
94
+ .delorean-btn,
95
+ .delorean-btn__icon {
96
+ display: flex;
97
+ justify-content: center;
98
+ align-items: center;
99
+ }
100
+
101
+ .delorean-btn__icon {
102
+ background-color: black;
103
+ mask-repeat: no-repeat;
104
+ mask-position: center;
105
+ mask-size: contain;
106
+ height: 100%;
107
+ width: 100%;
108
+ }
109
+
110
+ .delorean__datetime {
111
+ height: 24px;
112
+ }
113
+ `, t;
114
+ }, M = function() {
115
+ n(this, c).onclick = n(this, x), n(this, m).onclick = n(this, D), n(this, g).onclick = n(this, F), n(this, u).onclick = n(this, C), n(this, d).onclick = n(this, E);
116
+ }, x = new WeakMap(), D = new WeakMap(), F = new WeakMap(), C = new WeakMap(), E = new WeakMap(), Z = function(t) {
117
+ const i = n(this, d).querySelector("span.delorean-btn__icon");
118
+ i && (i.style.maskImage = `url('${t ? n(this, w) : n(this, b)}')`);
119
+ }, R = function(t) {
120
+ const i = new Date(t), v = (S) => S.toString().padStart(2, "0");
121
+ return [
122
+ `${i.getFullYear()}-${v(i.getMonth() + 1)}-${v(i.getDate())}`,
123
+ `${v(i.getHours())}:${v(i.getMinutes())}`
124
+ ].join("T");
125
+ };
126
+ customElements.define("delorean-controller", L);
127
+ export {
128
+ L as DeloreanController
129
+ };
package/package.json CHANGED
@@ -1,6 +1,56 @@
1
1
  {
2
2
  "name": "@anthropomorphic/delorean",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "Lightweight custom time controller library for WMS layer for Leaflet and Maplibre map frameworks.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "dist",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "exports": {
13
+ "./core": {
14
+ "types": "./dist/delorean/core/index.d.ts",
15
+ "import": "./dist/core/index.js"
16
+ },
17
+ "./ui": {
18
+ "types": "./dist/delorean/ui/index.d.ts",
19
+ "import": "./dist/ui/index.js"
20
+ },
21
+ "./leaflet": {
22
+ "types": "./dist/delorean/adapters/leaflet/index.d.ts",
23
+ "import": "./dist/leaflet/index.js"
24
+ },
25
+ "./maplibre": {
26
+ "types": "./dist/delorean/adapters/maplibre/index.d.ts",
27
+ "import": "./dist/maplibre/index.js"
28
+ }
29
+ },
30
+ "scripts": {
31
+ "build": "vite build && tsc -p tsconfig.build.json",
32
+ "prepack": "npm run build"
33
+ },
34
+ "peerDependencies": {
35
+ "leaflet": "*",
36
+ "maplibre-gl": "*"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "leaflet": {
40
+ "optional": true
41
+ },
42
+ "maplibre-gl": {
43
+ "optional": true
44
+ }
45
+ },
46
+ "devDependencies": {
47
+ "@types/leaflet": "^1.9.22",
48
+ "leaflet": "^1.9.4",
49
+ "maplibre-gl": "^6.16.0",
50
+ "typescript": "^5.0.0",
51
+ "vite": "^6.0.0"
52
+ },
53
+ "sideEffects": [
54
+ "./dist/ui/index.js"
55
+ ]
6
56
  }