Skip to content

Leaflet ​

The @ulm/leaflet package connects a layer manager to a Leaflet map. It adds and removes Leaflet layers, shows and hides them, sets their opacity, and stacks them in the manager's order. See Installation to add it to your project.

Minimal usage ​

The simplest approach is to create each Leaflet layer yourself and keep it in layerData.leafletLayer. The adapter picks it up from there by default.

ts
import { LayerManager } from '@ulm/core';
import { LeafletLayerManagerAdapter } from '@ulm/leaflet';
import * as L from 'leaflet';

interface LayerData {
  leafletLayer: L.Layer;
}

const map = L.map('map').setView([51.505, -0.09], 13);

const manager = new LayerManager<LayerData>();
manager.setAdapter(new LeafletLayerManagerAdapter<LayerData>(map));

manager.addLayer({
  layerConfig: {
    layerId: 'osm',
    layerName: 'OpenStreetMap',
    layerType: 'layer',
    layerData: { leafletLayer: L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png') },
  },
  visible: true,
});

Showing layers with renderLayer ​

To keep plain settings in layerData, such as a URL, give the adapter a renderLayer function. It returns the Leaflet layer to show, or null to leave the layer off the map. It runs when a layer is added, and again when its layerData or timeInfo changes.

Here some layers keep a URL, and the rest keep their Leaflet layer, which the default shows:

ts
import type { LeafletLayerData } from '@ulm/leaflet';
import { defaultLeafletRenderLayer } from '@ulm/leaflet';

type LayerData = { url: string } | LeafletLayerData;

manager.setAdapter(
  new LeafletLayerManagerAdapter<LayerData>(map, {
    renderLayer(info) {
      if ('url' in info.layerData) {
        return L.tileLayer(info.layerData.url);
      }
      return defaultLeafletRenderLayer(info);
    },
  }),
);

Keeping the layer already shown ​

When it runs again, renderLayer gets the Leaflet layer already on the map as current. Return it, updated if needed, to keep its popups and handlers. Return a new layer to replace it. Here a WMS layer takes its time from the layer's timeInfo:

ts
interface LayerData {
  url: string;
  layers: string;
}

manager.setAdapter(
  new LeafletLayerManagerAdapter<LayerData>(map, {
    renderLayer(info, _map, current) {
      const time = isSingleTimeInfo(info.timeInfo) ? info.timeInfo.value.toString() : '';
      if (current instanceof L.TileLayer.WMS) {
        const params = { ...current.wmsParams, time };
        return current.setParams(params);
      }
      const options = { layers: info.layerData.layers, time };
      return L.tileLayer.wms(info.layerData.url, options).bindTooltip(info.layerName);
    },
  }),
);

Settings and data from outside the layer ​

Put anything else a layer's look depends on, such as a theme or a language, in its layerData with updateLayerData. renderLayer then runs again.

For data you have to fetch, such as the data for a new date, start the fetch in the manager's onTimeInfoChanged callback and write the result into layerData. Until it arrives, return current.

Cleaning up with disposeLayer ​

disposeLayer undoes anything you set up outside the Leaflet layer. It runs when the adapter discards a layer renderLayer returned: when the layer is removed, when renderLayer returns a different one, and when the adapter is detached. Hiding a layer doesn't count.

ts
manager.setAdapter(
  new LeafletLayerManagerAdapter<LayerData>(map, {
    renderLayer,
    disposeLayer(leafletLayer, layerId) {
      registry.delete(layerId);
    },
  }),
);

For other changes, such as opacity, use the LayerManager callbacks. See the @ulm/leaflet reference for every option.

How stacking works ​

Leaflet normally stacks layers by kind, using its built-in panes: tile layers at the bottom, then vectors, then markers. The adapter replaces this with the manager's order, so any kind of layer can sit above any other. A tile layer can be drawn above a polygon, for example, or a marker below a tile layer.

To do this, the adapter draws each layer in its own pane, named ulm-<layerId>, and sets each pane's z-index whenever the order changes. All of these panes sit inside one container pane, ulmPane, at z-index 450. That places it above Leaflet's overlay pane (400) and below its shadow, marker, tooltip and popup panes (500 to 700). Popups, tooltips and markers you add outside the manager stay on top.

When a layer is removed, its pane is removed with it. When the adapter is detached, it removes the container pane and every pane inside it.

Layers that build another layer ​

Some Leaflet layers build a second layer when they are created, and copy their pane option to it. The adapter sets pane only after renderLayer returns, so that second layer stays in Leaflet's own pane, and the manager can't stack or fade it. leaflet.wms is one of these. Give such a layer its pane when you build it, with leafletLayerPane:

ts
import { leafletLayerPane } from '@ulm/leaflet';

manager.setAdapter(
  new LeafletLayerManagerAdapter<{ url: string; wmsLayers: string }>(map, {
    renderLayer(info) {
      return L.WMS.source(info.layerData.url, {
        layers: info.layerData.wmsLayers,
        tiled: true,
        pane: leafletLayerPane(info.layerId),
      });
    },
  }),
);

Untiled leaflet.wms sources draw their images in Leaflet's overlay pane, whatever pane you give, so use tiled: true with the manager.

How opacity works ​

Opacity is applied to each layer's pane, using the layer's computed opacity. This works the same way for every kind of layer, including markers and vectors such as circles. The adapter doesn't change the layer's own style, so settings such as a polygon's fillOpacity still apply on top.

A layer at opacity 0 takes no clicks or hovers, so they reach the layers below. It stays on the map, and still counts as visible.

Things to know ​

  • Only layers are drawn. Groups have no Leaflet layer of their own, but hiding or fading a group hides or fades the layers inside it.
  • The adapter sets each layer's pane option, replacing any pane you set yourself. It also sets shadowPane on markers, and the pane of every layer inside an L.LayerGroup, such as an L.GeoJSON. A layer that copies its pane when it is built needs leafletLayerPane.
  • With preferCanvas: true, each vector layer gets its own canvas, and only the top one receives mouse events. Use Leaflet's default SVG renderer if you need to click on overlapping vector layers.
  • Reject an invalid date before calling setTimeInfo, so the map always shows the time the manager holds.

Released under the MIT License.