@anthropomorphic/delorean

中文文档
0.5.0发布于 2026/11/02MITTypeScriptES Module零依赖近 30 天下载 2199

Lightweight custom time controller library for WMS layer for Leaflet and Maplibre map frameworks.

原始使用文档
文档目录16 节
  1. Installazione
  2. Import
  3. Core
  4. Costruttore
  5. Metodi e proprietà
  6. TimesOperation
  7. Sorgente del cambio di tempo
  8. Adapter
  9. Leaflet
  10. MapLibre
  11. Controller UI
  12. GetCapabilities e formati temporali
  13. Esempi del progetto
  14. Esempi completi
  15. Esempio Leaflet
  16. Esempio MapLibre

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

FirmaDescrizione
new Delorean(defaultTimes: number[] = [], defaultCurrentTime: number = Date.now())Crea una nuova istanza del core.
ParametroTipoDefaultDescrizione
defaultTimesnumber[][]Tempi iniziali, come timestamp in millisecondi.
defaultCurrentTimenumberDate.now()Tempo iniziale da confrontare con i tempi disponibili.

Metodi e proprietà

APITipo / firmaDescrizione
timesnumber[]Tempi correntemente memorizzati.
currentTimenumber | undefinedTimestamp del tempo correntemente selezionato; undefined se non c'è una selezione attiva.
setTimes(newTimes, operation = 'intersect')(newTimes: number[], operation?: TimesOperation) => voidAggiorna i tempi e mantiene la selezione corrente solo se il tempo selezionato è ancora presente.
setCurrentTime(time)(time: number) => voidSeleziona 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) => voidSeleziona il tempo disponibile più vicino che sia minore o uguale al valore richiesto. Notifica le callback con sorgente input.
resetCurrentTime()() => voidDeseleziona il tempo corrente, interrompe la riproduzione e notifica le callback con undefined.
removeTimes(timesToRemove)(timesToRemove: number[]) => voidRimuove i tempi indicati; aggiorna o azzera la selezione se necessario.
prevTime(loop = true)(loop?: boolean) => voidPassa 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) => voidPassa al tempo successivo. Con loop: true, dall'ultimo torna al primo. Non fa nulla senza una selezione corrente o senza tempi disponibili.
firstTime()() => voidSeleziona il primo tempo disponibile; senza tempi, azzera la selezione.
lastTime()() => voidSeleziona l'ultimo tempo disponibile; senza tempi, azzera la selezione.
togglePlay(isPlaying, interval = 3000)(isPlaying: boolean, interval?: number) => voidAvvia o arresta l'avanzamento automatico. interval è espresso in millisecondi.
addTimeChangedCallback(callback)(callback: (newTime: number | undefined, source: TimeChangeSource) => void) => voidRegistra una callback per i cambi di tempo.
removeTimeChangedCallback(callback)(callback: (newTime: number | undefined, source: TimeChangeSource) => void) => voidRimuove la callback indicata.
timeChanged(newTime, source = 'default')(newTime: number | undefined, source?: TimeChangeSource) => voidInvoca tutte le callback registrate.

TimesOperation

TimesOperation è un union type, non un enum:

ValoreComportamento implementato
unionUnisce i tempi esistenti e quelli nuovi, elimina duplicati e ordina il risultato in modo crescente.
intersectAttualmente ha lo stesso comportamento di union: unisce, deduplica e ordina. Non calcola l'intersezione.
replaceSostituisce i tempi esistenti con quelli nuovi e li ordina in modo crescente. Eventuali duplicati sono mantenuti.

Nota: intersect non 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:

ValoreSignificato
defaultModifica prodotta dai normali metodi del core o dagli adapter.
inputModifica 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
layerIdNome del layer WMS.
layerLayer Leaflet sottostante.
getCapabilitiesUrlURL 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àTipoObbligatoriaDescrizione
layerIdstringsìNome del layer WMS, ad esempio workspace:layer.
tileBaseUrlstringsìEndpoint WMS usato per le richieste delle tile.
wmsOptionsleaflet.WMSOptionssìOpzioni del layer WMS di Leaflet.
getCapabilitiesUrlstringno 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
sourceIdID della raster source MapLibre.
sourceSpecifica della source raster.
layerIdNome del layer WMS e ID del layer MapLibre.
layerSpecifica del raster layer MapLibre.
getCapabilitiesUrlURL 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àTipoObbligatoriaDescrizione
sourceIdstringsìID della raster source MapLibre.
layerIdstringsìNome del layer WMS e ID del layer MapLibre.
tileBaseUrlstringsìEndpoint WMS usato per le richieste delle tile.
tileUrlParamsRecord<string, string>sìParametri della richiesta WMS. L'adapter aggiorna TIME.
tileUrlTemplateRecord<string, string>sìParametri aggiunti all'URL, inclusi placeholder come {bbox-epsg-3857}.
paintmaplibregl.RasterLayerSpecification['paint']noProprietà visuali del raster layer.
getCapabilitiesUrlstringno 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.

EventodetailDescrizione
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-playbooleanCambiamento dello stato play/pausa: true per play, false per pausa.
delorean-customtimestringEmesso 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;
}
PartElementoDescrizione
deloreandivContenitore esterno.
delorean__firstbuttonTorna al primo tempo disponibile.
delorean__previousbuttonTorna al tempo precedente.
delorean__playbuttonAlterna play e pausa.
delorean__nextbuttonPassa al tempo successivo.
delorean__lastbuttonPassa all'ultimo tempo disponibile.
delorean__datetimeinput[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 esempio P1D o PT6H).

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-leaflet
  • examples/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].

版本与发布记录

@anthropomorphic/delorean 共发布 14 个版本,首次发布于 2026/10/27;过去 12 个月发布了 14 个版本。

版本发布日期距今
0.5.0latest2026/11/025 天前
0.4.42026/11/025 天前
0.4.32026/10/299 天前
0.4.22026/10/299 天前
0.4.12026/10/299 天前
0.4.02026/10/299 天前

关于 @anthropomorphic/delorean 的常见问题

@anthropomorphic/delorean 是什么?

Lightweight custom time controller library for WMS layer for Leaflet and Maplibre map frameworks.

如何安装 @anthropomorphic/delorean?

在项目目录运行 npm install @anthropomorphic/delorean,或使用 pnpm add / yarn add / bun add。

@anthropomorphic/delorean 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@anthropomorphic/delorean 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@anthropomorphic/delorean 可以商用吗?

@anthropomorphic/delorean 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@anthropomorphic/delorean 的下载量有多少?

根据 npm 官方下载统计,@anthropomorphic/delorean 在 2026/10/06 – 2026/11/04 这 30 天内被下载 2,199 次,日均约 73 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@anthropomorphic/delorean 还在维护吗?

最新版本 0.5.0 发布于 2026/11/02(5 天前);至今共发布 14 个版本,其中过去 12 个月发布了 14 个;首次发布于 2026/10/27。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。