Skip to content

Layers and groups ​

This page covers how to build up the layer structure of your map: adding and removing layers and groups, nesting groups, and attaching your own data and time information to each layer.

The examples assume a layer manager has already been created, where each layer carries a short note as its data:

ts
import { LayerManager } from '@ulm/core';

interface LayerData {
  note: string;
}

const manager = new LayerManager<LayerData>();

Adding and removing ​

Adding a layer ​

Add a layer with addLayer, giving it a unique layerId, a display name and your own layerData. A parentId of null places it at the top level.

ts
manager.addLayer({
  layerConfig: {
    layerId: 'coastline',
    layerName: 'Coastline',
    layerType: 'layer',
    parentId: null,
    layerData: { note: 'High resolution coastline' },
  },
  visible: true,
});

visible: true switches the layer on so it shows straight away. Leave it out to add the layer switched off. See Visibility and opacity for the difference between switching a layer on and it being visible.

Adding a group ​

Groups are added in the same way with addGroup, using a layerType of 'layerGroup'.

ts
manager.addGroup({
  layerConfig: {
    layerId: 'ocean',
    layerName: 'Ocean',
    layerType: 'layerGroup',
    parentId: null,
    layerData: undefined,
  },
  visible: true,
});

Adding a layer to a group ​

To put a layer inside a group, set its parentId to the group's layerId. The group needs to exist first.

ts
manager.addLayer({
  layerConfig: {
    layerId: 'sea-ice',
    layerName: 'Sea ice',
    layerType: 'layer',
    parentId: 'ocean',
    layerData: { note: 'Daily sea ice extent' },
  },
  visible: true,
});

Choosing where it goes ​

New layers and groups go to the bottom of their parent by default. Use position to add one at the top or bottom, or index to add it at an exact place in its parent's order, counting from 0 at the bottom:

ts
// on top of everything else at the top level
manager.addLayer({
  layerConfig: {
    layerId: 'labels',
    layerName: 'Labels',
    layerType: 'layer',
    parentId: null,
    layerData: { note: 'Place names' },
  },
  position: 'top',
});

// second from the bottom of the ocean group
manager.addLayer({
  layerConfig: {
    layerId: 'bathymetry',
    layerName: 'Bathymetry',
    layerType: 'layer',
    parentId: 'ocean',
    layerData: { note: 'Sea floor depth' },
  },
  index: 1,
});

If you give both, index wins. An index outside the current order is ignored and position is used instead. See Ordering and moving for moving layers once they are added.

Removing a layer or group ​

Remove a layer or a group by its layerId:

ts
manager.removeLayer('coastline');

A group can only be removed once it is empty, so remove the layers inside it first.

Nested groups ​

By default, groups can only sit at the top level, which keeps the layer list to a simple, single level. To allow groups inside other groups, turn on allowNestedGroupLayers when creating the manager:

ts
const manager = new LayerManager<LayerData>({ allowNestedGroupLayers: true });

manager.addGroup({
  layerConfig: {
    layerId: 'forecasts',
    layerName: 'Forecasts',
    layerType: 'layerGroup',
    parentId: 'ocean',
    layerData: undefined,
  },
});

Your own data ​

Each layer can carry any data you like in layerData, and the manager passes it back to you wherever that layer appears. You set its type with the first type parameter of LayerManager. Groups can carry their own data too, using the second type parameter, which is undefined by default.

You can read the data back in any callback, and replace it with updateLayerData:

ts
const manager = new LayerManager<LayerData>({
  onLayerAdded(info) {
    console.log(info.layerName, info.layerData);
  },
});

manager.updateLayerData('sea-ice', { note: 'Updated daily at 06:00' });

This is a good place for anything the rest of your application needs to know about a layer, such as a map library layer, a legend or an attribution.

Time info ​

Layers can optionally carry time information in timeInfo, either a single point in time or a range. precision says whether the value is a date or a date and time.

Dates are Temporal values, and each one can be any of these:

TypeUse it forExample
Temporal.PlainDateA calendar dateTemporal.PlainDate.from('2026-06-01')
Temporal.PlainDateTimeA date and time, with no time zoneTemporal.PlainDateTime.from('2026-06-01T06:00')
Temporal.ZonedDateTimeA date and time in a time zoneTemporal.ZonedDateTime.from('2026-06-01T06:00[UTC]')

Not every browser supports Temporal yet, so import it from temporal-polyfill. See Dates for time info to install it.

ts
import { Temporal } from 'temporal-polyfill';

manager.addLayer({
  layerConfig: {
    layerId: 'winter-ice',
    layerName: 'Winter sea ice',
    layerType: 'layer',
    parentId: 'ocean',
    layerData: { note: 'Winter average' },
    timeInfo: {
      type: 'range',
      precision: 'date',
      start: Temporal.PlainDate.from('2026-06-01'),
      end: Temporal.PlainDate.from('2026-08-31'),
    },
  },
});

Change it later with setTimeInfo:

ts
manager.setTimeInfo('sea-ice', {
  type: 'single',
  precision: 'date',
  value: Temporal.PlainDate.from('2026-10-01'),
});

The manager stores the time information and reports changes through onTimeInfoChanged. What you do with it, such as showing dates in your layer list or driving a time slider, is up to you.

TIP

You only need the polyfill until every browser you support has Temporal built in. The manager accepts built-in and polyfilled dates alike, so your layer code stays the same when you drop it.

Listening for changes ​

To keep your layer list and the rest of your UI up to date, pass callbacks when creating the manager. Each one is called after the change has been made:

ts
const manager = new LayerManager<LayerData>({
  onLayerAdded(info) {
    console.log(`added ${info.layerName}`);
  },
  onLayerRemoved(layerId) {
    console.log(`removed ${layerId}`);
  },
  onLayerDataChanged(info) {
    console.log(`${info.layerName} data is now`, info.layerData);
  },
  onTimeInfoChanged(info, timeInfo) {
    console.log(`${info.layerName} now has ${timeInfo.type} time info`);
  },
});

Adding the sea ice layer, updating its data, setting a single date and then removing it prints:

added Sea ice
Sea ice data is now { note: 'Updated daily at 06:00' }
Sea ice now has single time info
removed sea-ice

Rejections and errors ​

If a change can't be made, the manager leaves the layer structure as it was and reports why through the onError callback:

ts
const manager = new LayerManager<LayerData>({
  onError(error) {
    console.warn(error.message);
  },
});

An add is rejected when the layerId is already in use, the parent group doesn't exist, or a group is added inside a group without allowNestedGroupLayers. A remove is rejected when the layer doesn't exist or the group still has layers in it. For example, removing the ocean group above while it still holds sea-ice logs:

Error

Layer group ocean has children. Layer not removed.

Released under the MIT License.