Delorean
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 per i controlli temporali.
Installazione
Installa Delorean e il framework cartografico utilizzato dall'applicazione:
npm install @anthropomorphic/delorean leaflet
# oppure
npm install @anthropomorphic/delorean maplibre-gl
Leaflet e MapLibre GL JS sono peer dependency opzionali: installa quello richiesto dall'adapter che utilizzi. Le richieste GetCapabilities vengono effettuate dal browser, quindi il server WMS deve consentire richieste cross-origin (CORS).
Import
Il package espone entry point distinti e non esporta un entry point principale:
import { Delorean } from '@anthropomorphic/delorean/core';
import { DeloreanController } from '@anthropomorphic/delorean/ui';
import { LeafletRasterTimeLayer } from '@anthropomorphic/delorean/leaflet';
import { MaplibreglRasterTimeLayer } from '@anthropomorphic/delorean/maplibre';
Importa solo gli entry point necessari. Importare @anthropomorphic/delorean/ui registra il custom element <delorean-controller>.
Core
La classe Delorean gestisce timestamp Unix espressi in millisecondi. Può essere usata indipendentemente da Leaflet, MapLibre e dal controller UI.
import { Delorean } from '@anthropomorphic/delorean/core';
const times = [
Date.parse('2024-01-01T00:00:00Z'),
Date.parse('2024-01-02T00:00:00Z')
];
const delorean = new Delorean(times);
delorean.addTimeChangedCallback((time, source) => {
console.log(time === undefined ? undefined : new Date(time).toISOString(), source);
});
delorean.nextTime();
Costruttore
| Firma | Descrizione |
|---|---|
new Delorean(defaultTimes: number[] = [], defaultCurrentTime: number = Date.now()) | Crea una nuova istanza del core. |
| Parametro | Tipo | Default | Descrizione |
|---|---|---|---|
defaultTimes | number[] | [] | Tempi iniziali, come timestamp in millisecondi. |
defaultCurrentTime | number | Date.now() | Tempo iniziale da confrontare con i tempi disponibili. |
Metodi e proprietà
| API | Tipo / firma | Descrizione |
|---|---|---|
times | number[] | Tempi correntemente memorizzati. |
currentTime | number | undefined | Timestamp del tempo correntemente selezionato; undefined se non c'è una selezione attiva. |
setTimes(newTimes, operation = 'intersect') | (newTimes: number[], operation?: TimesOperation) => void | Aggiorna i tempi e mantiene la selezione corrente solo se il tempo selezionato è ancora presente. |
setCurrentTime(time) | (time: number) => void | Seleziona il tempo disponibile precedente o uguale al timestamp richiesto; se il timestamp precede il primo disponibile, seleziona il primo. Notifica le callback con sorgente default. |
closestTime(time) | (time: number | undefined) => void | Seleziona il tempo disponibile più vicino che sia minore o uguale al valore richiesto. Notifica le callback con sorgente input. |
resetCurrentTime() | () => void | Deseleziona il tempo corrente, interrompe la riproduzione e notifica le callback con undefined. |
removeTimes(timesToRemove) | (timesToRemove: number[]) => void | Rimuove i tempi indicati; aggiorna o azzera la selezione se necessario. |
prevTime(loop = true) | (loop?: boolean) => void | Passa al tempo precedente. Con loop: true, dal primo torna all'ultimo. Non fa nulla senza una selezione corrente o senza tempi disponibili. |
nextTime(loop = true) | (loop?: boolean) => void | Passa al tempo successivo. Con loop: true, dall'ultimo torna al primo. Non fa nulla senza una selezione corrente o senza tempi disponibili. |
firstTime() | () => void | Seleziona il primo tempo disponibile; senza tempi, azzera la selezione. |
lastTime() | () => void | Seleziona l'ultimo tempo disponibile; senza tempi, azzera la selezione. |
togglePlay(isPlaying, interval = 3000) | (isPlaying: boolean, interval?: number) => void | Avvia o arresta l'avanzamento automatico. interval è espresso in millisecondi. |
addTimeChangedCallback(callback) | (callback: (newTime: number | undefined, source: TimeChangeSource) => void) => void | Registra una callback per i cambi di tempo. |
removeTimeChangedCallback(callback) | (callback: (newTime: number | undefined, source: TimeChangeSource) => void) => void | Rimuove la callback indicata. |
timeChanged(newTime, source = 'default') | (newTime: number | undefined, source?: TimeChangeSource) => void | Invoca tutte le callback registrate. |
TimesOperation
TimesOperation è un union type, non un enum:
| Valore | Comportamento implementato |
|---|---|
union | Unisce i tempi esistenti e quelli nuovi, elimina duplicati e ordina il risultato in modo crescente. |
intersect | Attualmente ha lo stesso comportamento di union: unisce, deduplica e ordina. Non calcola l'intersezione. |
replace | Sostituisce i tempi esistenti con quelli nuovi e li ordina in modo crescente. Eventuali duplicati sono mantenuti. |
Nota:
intersectnon ha ancora il comportamento matematico suggerito dal nome. Non fare affidamento su un'intersezione effettiva finché questa operazione non viene corretta nella libreria.
Il costruttore ordina i tempi iniziali in ordine crescente. Se l'istanza non contiene tempi, currentTime è undefined. Quando setTimes() rimuove il tempo corrente, la selezione viene azzerata; quando il tempo è ancora presente, rimane selezionato.
Sorgente del cambio di tempo
Il tipo TimeChangeSource indica l'origine della modifica:
| Valore | Significato |
|---|---|
default | Modifica prodotta dai normali metodi del core o dagli adapter. |
input | Modifica prodotta da closestTime(), tipicamente in risposta all'input custom del controller. |
Questo permette di non riscrivere il valore digitato nell'input quando il core seleziona il tempo disponibile precedente. Esempio:
delorean.addTimeChangedCallback((time, source) => {
if (source === 'input') return;
controller.time = time;
});
Adapter
Gli adapter collegano un'istanza Delorean a un layer raster WMS del framework cartografico corrispondente. Caricano i tempi dalla dimensione time del documento WMS GetCapabilities e aggiornano il layer quando cambia il tempo del core.
Leaflet
LeafletRasterTimeLayer crea un WMS TileLayer Leaflet.
import * as leaflet from 'leaflet';
import { Delorean } from '@anthropomorphic/delorean/core';
import { LeafletRasterTimeLayer } from '@anthropomorphic/delorean/leaflet';
const map = leaflet.map('map').setView([39.67209208942453, -101.03346117851018], 5);
const delorean = new Delorean();
const timeLayer = new LeafletRasterTimeLayer(delorean, {
layerId: 'nexrad-n0r-wmst',
tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
wmsOptions: {
layers: 'nexrad-n0r-wmst',
format: 'image/png',
styles: '',
transparent: true,
version: '1.3.0'
},
getCapabilitiesUrl:
'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
});
await timeLayer.addTo(map);
// Quando il layer non serve più:
timeLayer.removeFrom(map);
Firma del costruttore: new LeafletRasterTimeLayer(delorean, options).
| Metodo / proprietà | Descrizione |
|---|---|
layerId | Nome del layer WMS. |
layer | Layer Leaflet sottostante. |
getCapabilitiesUrl | URL GetCapabilities opzionale nel tipo; necessario per caricare i tempi. |
addTo(map, defaultTime?, operation = 'intersect') | Carica i tempi, aggiunge il layer alla mappa e registra la callback. Usa defaultTime se passato, altrimenti il valore predefinito GetCapabilities se definito, e infine l'ultimo tempo disponibile. |
removeFrom(map) | Rimuove il layer, i suoi tempi dal core e la callback di aggiornamento. |
loadTimes() | Restituisce una Promise<LayerTimeInfo> con tempo predefinito e tempi discreti. |
setTime(time) | Aggiorna il parametro TIME del layer. Se time è undefined, usa l'ultimo tempo del core; se non ci sono tempi disponibili, non modifica il layer. |
LeafletRasterTimeLayerOptions
| Proprietà | Tipo | Obbligatoria | Descrizione |
|---|---|---|---|
layerId | string | sì | Nome del layer WMS, ad esempio workspace:layer. |
tileBaseUrl | string | sì | Endpoint WMS usato per le richieste delle tile. |
wmsOptions | leaflet.WMSOptions | sì | Opzioni del layer WMS di Leaflet. |
getCapabilitiesUrl | string | no nel tipo, sì per addTo() | URL del documento GetCapabilities da cui leggere i tempi. |
MapLibre
MaplibreglRasterTimeLayer crea una raster source e un raster layer MapLibre. I parametri WMS e i valori di tileUrlParams sono stringhe; tileUrlTemplate contiene parametri di URL e placeholder di tile, ad esempio il bbox MapLibre.
import * as maplibregl from 'maplibre-gl';
import { Delorean } from '@anthropomorphic/delorean/core';
import { MaplibreglRasterTimeLayer } from '@anthropomorphic/delorean/maplibre';
const map = new maplibregl.Map({
container: 'map',
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [-101.03346117851018, 39.67209208942453],
zoom: 5
});
const delorean = new Delorean();
const timeLayer = new MaplibreglRasterTimeLayer(delorean, {
sourceId: 'wms-test-source',
layerId: 'nexrad-n0r-wmst',
tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
tileUrlParams: {
service: 'WMS',
layers: 'nexrad-n0r-wmst',
styles: '',
transparent: 'true',
format: 'image/png',
version: '1.3.0',
request: 'GetMap',
crs: 'EPSG:3857',
width: '256',
height: '256'
},
tileUrlTemplate: {
bbox: '{bbox-epsg-3857}'
},
getCapabilitiesUrl:
'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
});
await timeLayer.addTo(map);
// Quando il layer non serve più:
timeLayer.removeFrom(map);
Firma del costruttore: new MaplibreglRasterTimeLayer(delorean, options).
| Metodo / proprietà | Descrizione |
|---|---|
sourceId | ID della raster source MapLibre. |
source | Specifica della source raster. |
layerId | Nome del layer WMS e ID del layer MapLibre. |
layer | Specifica del raster layer MapLibre. |
getCapabilitiesUrl | URL GetCapabilities opzionale nel tipo; necessario per caricare i tempi. |
addTo(map, defaultTime?, operation = 'intersect') | Carica i tempi, aggiunge source e layer e registra la callback. Usa defaultTime se passato, altrimenti il valore predefinito GetCapabilities se definito, e infine l'ultimo tempo disponibile. |
removeFrom(map) | Rimuove layer e source, i suoi tempi dal core e la callback di aggiornamento. |
loadTimes() | Restituisce una Promise<LayerTimeInfo> con tempo predefinito e tempi discreti. |
setTime(map, time) | Aggiorna l'URL delle tile con il parametro TIME. Se time è undefined, usa l'ultimo tempo del core; se non ci sono tempi disponibili, non modifica il layer. |
MaplibreglRasterTimeLayerOptions
| Proprietà | Tipo | Obbligatoria | Descrizione |
|---|---|---|---|
sourceId | string | sì | ID della raster source MapLibre. |
layerId | string | sì | Nome del layer WMS e ID del layer MapLibre. |
tileBaseUrl | string | sì | Endpoint WMS usato per le richieste delle tile. |
tileUrlParams | Record<string, string> | sì | Parametri della richiesta WMS. L'adapter aggiorna TIME. |
tileUrlTemplate | Record<string, string> | sì | Parametri aggiunti all'URL, inclusi placeholder come {bbox-epsg-3857}. |
paint | maplibregl.RasterLayerSpecification['paint'] | no | Proprietà visuali del raster layer. |
getCapabilitiesUrl | string | no nel tipo, sì per addTo() | URL del documento GetCapabilities da cui leggere i tempi. |
Controller UI
La libreria fornisce il web component <delorean-controller>. L'input temporale è modificabile per default. Aggiungi l'attributo booleano readonly per renderlo non modificabile:
<!-- Input modificabile -->
<delorean-controller></delorean-controller>
<!-- Input in sola lettura -->
<delorean-controller readonly></delorean-controller>
L'attributo può anche essere aggiunto o rimosso dinamicamente; il componente aggiorna lo stato readonly dell'input interno:
controller.setAttribute('readonly', '');
controller.removeAttribute('readonly');
Importare @anthropomorphic/delorean/ui registra il custom element:
import { DeloreanController } from '@anthropomorphic/delorean/ui';
const controller = document.querySelector('delorean-controller') as DeloreanController;
controller.time = Date.now();
Il setter time accetta un timestamp in millisecondi oppure undefined; quest'ultimo svuota il campo.
Il controller emette eventi DOM con bubbles: true e composed: true, ascoltabili dagli elementi antenati anche attraversando uno shadow DOM.
| Evento | detail | Descrizione |
|---|---|---|
delorean-firsttime | — | Richiesta di selezionare il primo tempo. |
delorean-prevtime | — | Richiesta di selezionare il tempo precedente. |
delorean-nexttime | — | Richiesta di selezionare il tempo successivo. |
delorean-lasttime | — | Richiesta di selezionare l'ultimo tempo. |
delorean-play | boolean | Cambiamento dello stato play/pausa: true per play, false per pausa. |
delorean-customtime | string | Emesso quando il campo data perde il focus; contiene il valore datetime-local inserito dall'utente. |
Per collegare un tempo digitato al core, converti il valore locale in timestamp e usa closestTime(). Il valore di datetime-local non include un fuso orario: new Date(value) lo interpreta nel fuso locale del browser. Il metodo sceglie il tempo disponibile precedente o uguale al valore digitato. Il callback riceve source: 'input' se viene selezionato un tempo; ignorando questa sorgente nell'aggiornamento del controller si mantiene visibile il valore digitato, mentre gli adapter possono aggiornare i layer al tempo disponibile selezionato.
this.addEventListener('delorean-customtime', (event: Event) => {
const value = (event as CustomEvent<string>).detail;
const timestamp = value ? new Date(value).getTime() : undefined;
delorean.closestTime(timestamp);
});
delorean.addTimeChangedCallback((time, source) => {
if (source === 'input') return;
controller.time = time;
});
Se il valore inserito precede il primo tempo disponibile, closestTime() azzera la selezione; in questo caso il callback riceve undefined con sorgente default. Se è successivo all'ultimo, seleziona l'ultimo tempo disponibile. Passando undefined (ad esempio per un campo vuoto) si azzera la selezione. delorean-customtime viene emesso al blur, non a ogni modifica del campo.
Le parti del web component sono personalizzabili dall'esterno con lo pseudo-elemento CSS ::part():
delorean-controller::part(delorean) {
flex-direction: column;
}
delorean-controller::part(delorean__first) {
background-color: red;
}
| Part | Elemento | Descrizione |
|---|---|---|
delorean | div | Contenitore esterno. |
delorean__first | button | Torna al primo tempo disponibile. |
delorean__previous | button | Torna al tempo precedente. |
delorean__play | button | Alterna play e pausa. |
delorean__next | button | Passa al tempo successivo. |
delorean__last | button | Passa all'ultimo tempo disponibile. |
delorean__datetime | input[type="datetime-local"] | Campo temporale; modificabile salvo presenza dell'attributo readonly. |
GetCapabilities e formati temporali
Gli adapter richiedono il documento WMS GetCapabilities tramite fetch, individuano il layer corrispondente a layerId e leggono una dimensione time (Dimension o Extent). Il browser deve poter accedere all'endpoint tramite CORS.
Il parser gestisce:
- valori temporali discreti separati da virgola;
- un singolo intervallo nel formato
inizio/fine/periodo, con periodi espressi in giorni, ore, minuti e secondi (ad esempioP1DoPT6H).
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 viene espansa come una serie completa di intervalli.
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().
Esempi del progetto
Il repository contiene due demo eseguibili:
examples/delorean-leafletexamples/delorean-maplibre
Per provare le demo con il codice presente nel repository, crea prima il tarball locale dalla cartella principale:
npm pack
npm pack esegue anche la build e genera un file .tgz (ad esempio anthropomorphic-delorean-0.4.0.tgz). Poi, dalla cartella della demo, installa il tarball generato e avviala:
cd examples/delorean-leaflet
npm install ../../anthropomorphic-delorean-0.4.0.tgz
npm run dev
Sostituisci il nome del file .tgz con quello effettivamente generato da npm pack. Per la demo MapLibre, usa examples/delorean-maplibre al posto della cartella Leaflet. Questo flusso testa localmente la versione del repository, comprese le modifiche non ancora pubblicate; per verificare una release pubblicata, installa invece il package con npm install @anthropomorphic/delorean.
Esempi completi
Gli esempi seguenti sono basati sulle demo del repository. Usano il package @anthropomorphic/delorean, i nuovi eventi e il layer WMS NEXRAD delle demo. Per testarli con il codice locale, crea e installa il tarball come descritto nella sezione precedente; in un'applicazione si installa normalmente il package pubblicato con npm.
Esempio Leaflet
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>delorean-leaflet</title>
</head>
<body>
<div id="app">
<button type="button" id="add-layer" class="add-layer-btn">ADD WMS LAYER</button>
<button type="button" id="remove-layer" class="remove-layer-btn">REMOVE WMS LAYER</button>
<app-leaflet id="maplibre"></app-leaflet>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
/* style.css */
html,
body {
margin: 0;
padding: 0;
}
app-leaflet {
display: block;
height: 100dvh;
}
.add-layer-btn,
.remove-layer-btn {
position: fixed;
z-index: 999;
left: 16px;
}
.add-layer-btn { top: 16px; }
.remove-layer-btn { top: 48px; }
// main.ts
import './style.css';
import './leaflet';
import type { Leaflet } from './leaflet';
const leaflet = document.querySelector('#maplibre') as Leaflet;
const addBtn = document.querySelector('#add-layer') as HTMLButtonElement;
const removeBtn = document.querySelector('#remove-layer') as HTMLButtonElement;
addBtn.onclick = () => leaflet.addWmsTimeLayer({
layerId: 'nexrad-n0r-wmst',
tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
wmsOptions: {
layers: 'nexrad-n0r-wmst',
format: 'image/png',
styles: '',
transparent: true,
version: '1.3.0'
},
getCapabilitiesUrl:
'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
});
removeBtn.onclick = () => leaflet.removeWmsTimeLayer('nexrad-n0r-wmst');
// leaflet.ts
import * as leaflet from 'leaflet';
import style from 'leaflet/dist/leaflet.css?raw';
import { Delorean } from '@anthropomorphic/delorean/core';
import {
LeafletRasterTimeLayer,
type LeafletRasterTimeLayerOptions
} from '@anthropomorphic/delorean/leaflet';
import { DeloreanController } from '@anthropomorphic/delorean/ui';
export class Leaflet extends HTMLElement {
public shadowRoot: ShadowRoot;
#map?: leaflet.Map;
#delorean?: Delorean;
#deloreanController?: DeloreanController;
#layers: LeafletRasterTimeLayer[] = [];
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(this.#html().content.cloneNode(true));
this.shadowRoot.appendChild(this.#css().cloneNode(true));
const leafletStylesheet = new CSSStyleSheet();
leafletStylesheet.replaceSync(style);
this.shadowRoot.adoptedStyleSheets.push(leafletStylesheet);
this.#deloreanController = this.shadowRoot.querySelector('#delorean') as DeloreanController;
}
#html(): HTMLTemplateElement {
const template = document.createElement('template');
template.innerHTML = `
<div id="leaflet" class="leaflet"></div>
<delorean-controller id="delorean" class="delorean"></delorean-controller>
`;
return template;
}
#css(): HTMLStyleElement {
const style = document.createElement('style');
style.innerHTML = `
.leaflet { height: 100%; }
.delorean {
position: fixed;
z-index: 999;
bottom: 40px;
left: 40px;
}
`;
return style;
}
public connectedCallback(): void {
this.#init();
this.#setup();
}
#init(): void {
const container = this.shadowRoot.querySelector('#leaflet');
if (!(container instanceof HTMLElement)) return;
this.#map = leaflet.map(container, { zoomControl: false })
.setView([39.67209208942453, -101.03346117851018], 5)
.addLayer(leaflet.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png'));
this.#delorean = new Delorean();
this.#delorean.addTimeChangedCallback((newTime, source) => {
if (source === 'input') return;
if (!this.#map || !this.#deloreanController) return;
this.#deloreanController.time = newTime;
});
}
#setup(): void {
this.addEventListener('delorean-firsttime', () => this.#delorean?.firstTime());
this.addEventListener('delorean-prevtime', () => this.#delorean?.prevTime());
this.addEventListener('delorean-nexttime', () => this.#delorean?.nextTime());
this.addEventListener('delorean-lasttime', () => this.#delorean?.lastTime());
this.addEventListener('delorean-play', (event: Event) => {
this.#delorean?.togglePlay((event as CustomEvent).detail, 3000);
});
this.addEventListener('delorean-customtime', (event: Event) => {
const value = (event as CustomEvent<string>).detail;
const time = value ? new Date(value).getTime() : undefined;
this.#delorean?.closestTime(time);
});
}
public async addWmsTimeLayer(options: LeafletRasterTimeLayerOptions): Promise<void> {
if (!this.#map || !this.#delorean) return;
const wmsTimeLayer = new LeafletRasterTimeLayer(this.#delorean, options);
await wmsTimeLayer.addTo(this.#map, 1787196600000, 'intersect');
this.#layers.push(wmsTimeLayer);
}
public removeWmsTimeLayer(layerId: string): void {
const foundLayer = this.#layers.find((layer) => layer.layerId === layerId);
if (!this.#map || !foundLayer) return;
foundLayer.removeFrom(this.#map);
this.#layers = this.#layers.filter((layer) => layer !== foundLayer);
}
}
customElements.define('app-leaflet', Leaflet);
Esempio MapLibre
<!-- index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>delorean-maplibre</title>
</head>
<body>
<div id="app">
<button type="button" id="add-layer" class="add-layer-btn">ADD WMS LAYER</button>
<button type="button" id="remove-layer" class="remove-layer-btn">REMOVE WMS LAYER</button>
<app-maplibre id="maplibre"></app-maplibre>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
/* style.css */
html,
body {
margin: 0;
padding: 0;
}
app-maplibre {
display: block;
height: 100dvh;
}
.add-layer-btn,
.remove-layer-btn {
position: fixed;
z-index: 999;
left: 16px;
}
.add-layer-btn { top: 16px; }
.remove-layer-btn { top: 48px; }
// main.ts
import './style.css';
import './maplibre';
import type { Maplibre } from './maplibre';
const maplibre = document.querySelector('#maplibre') as Maplibre;
const addBtn = document.querySelector('#add-layer') as HTMLButtonElement;
const removeBtn = document.querySelector('#remove-layer') as HTMLButtonElement;
addBtn.onclick = () => maplibre.addWmsTimeLayer({
sourceId: 'wms-test-source',
layerId: 'nexrad-n0r-wmst',
tileBaseUrl: 'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi',
tileUrlParams: {
service: 'WMS',
layers: 'nexrad-n0r-wmst',
styles: '',
transparent: 'true',
format: 'image/png',
version: '1.3.0',
request: 'GetMap',
crs: 'EPSG:3857',
width: '256',
height: '256'
},
tileUrlTemplate: {
bbox: '{bbox-epsg-3857}'
},
getCapabilitiesUrl:
'https://mesonet.agron.iastate.edu/cgi-bin/wms/nexrad/n0r-t.cgi?SERVICE=WMS&REQUEST=GetCapabilities&VERSION=1.3.0'
});
removeBtn.onclick = () => maplibre.removeWmsTimeLayer('nexrad-n0r-wmst');
// maplibre.ts
import * as maplibregl from 'maplibre-gl';
import style from 'maplibre-gl/dist/maplibre-gl.css?raw';
import workerUrl from 'maplibre-gl/dist/maplibre-gl-worker.mjs?worker&url';
import { Delorean } from '@anthropomorphic/delorean/core';
import {
MaplibreglRasterTimeLayer,
type MaplibreglRasterTimeLayerOptions
} from '@anthropomorphic/delorean/maplibre';
import { DeloreanController } from '@anthropomorphic/delorean/ui';
maplibregl.setWorkerUrl(workerUrl);
export class Maplibre extends HTMLElement {
public shadowRoot: ShadowRoot;
#map?: maplibregl.Map;
#delorean?: Delorean;
#deloreanController?: DeloreanController;
#layers: MaplibreglRasterTimeLayer[] = [];
constructor() {
super();
this.shadowRoot = this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(this.#html().content.cloneNode(true));
this.shadowRoot.appendChild(this.#css().cloneNode(true));
const maplibreStylesheet = new CSSStyleSheet();
maplibreStylesheet.replaceSync(style);
this.shadowRoot.adoptedStyleSheets.push(maplibreStylesheet);
this.#deloreanController = this.shadowRoot.querySelector('#delorean') as DeloreanController;
}
#html(): HTMLTemplateElement {
const template = document.createElement('template');
template.innerHTML = `
<div id="maplibre" class="maplibre"></div>
<delorean-controller id="delorean" class="delorean"></delorean-controller>
`;
return template;
}
#css(): HTMLStyleElement {
const style = document.createElement('style');
style.innerHTML = `
.maplibre { height: 100%; }
.delorean {
position: fixed;
z-index: 999;
bottom: 40px;
left: 40px;
}
`;
return style;
}
public connectedCallback(): void {
this.#init();
this.#setup();
}
#init(): void {
const container = this.shadowRoot.querySelector('#maplibre');
if (!(container instanceof HTMLElement)) return;
this.#map = new maplibregl.Map({
container,
style: 'https://tiles.openfreemap.org/styles/liberty',
center: [-101.03346117851018, 39.67209208942453],
zoom: 5
});
this.#delorean = new Delorean();
this.#delorean.addTimeChangedCallback((newTime, source) => {
if (source === 'input') return;
if (!this.#map || !this.#deloreanController) return;
this.#deloreanController.time = newTime;
});
}
#setup(): void {
this.addEventListener('delorean-firsttime', () => this.#delorean?.firstTime());
this.addEventListener('delorean-prevtime', () => this.#delorean?.prevTime());
this.addEventListener('delorean-nexttime', () => this.#delorean?.nextTime());
this.addEventListener('delorean-lasttime', () => this.#delorean?.lastTime());
this.addEventListener('delorean-play', (event: Event) => {
this.#delorean?.togglePlay((event as CustomEvent).detail, 1000);
});
this.addEventListener('delorean-customtime', (event: Event) => {
const value = (event as CustomEvent<string>).detail;
const time = value ? new Date(value).getTime() : undefined;
this.#delorean?.closestTime(time);
});
}
public async addWmsTimeLayer(options: MaplibreglRasterTimeLayerOptions): Promise<void> {
if (!this.#map || !this.#delorean) return;
const wmsTimeLayer = new MaplibreglRasterTimeLayer(this.#delorean, options);
await wmsTimeLayer.addTo(this.#map);
this.#layers.push(wmsTimeLayer);
}
public removeWmsTimeLayer(layerId: string): void {
const foundLayer = this.#layers.find((layer) => layer.layerId === layerId);
if (!this.#map || !foundLayer) return;
foundLayer.removeFrom(this.#map);
this.#layers = this.#layers.filter((layer) => layer !== foundLayer);
}
}
customElements.define('app-maplibre', Maplibre);
La demo Leaflet imposta un timestamp iniziale esplicito in addTo(). La demo MapLibre non lo passa e usa quindi il default WMS, se presente, oppure l'ultimo tempo disponibile. In entrambi gli esempi il controller è modificabile per default; aggiungi readonly al tag <delorean-controller> se vuoi impedire l'inserimento custom.
Gli esempi usano le coordinate iniziali delle demo: Leaflet riceve [latitudine, longitudine] e MapLibre [longitudine, latitudine].