Skip to content

@ulm/core ​

new LayerManager<TLayer, TGroup>(options?) ​

TLayer is the type of layerData stored on each layer. TGroup is the type stored on each group, and defaults to undefined. The manager starts as soon as it is constructed.

Options (all optional):

OptionTypeDescription
allowNestedGroupLayersbooleanAllow groups inside other groups (default false)
inspectObserver<InspectionEvent> | (event) => voidReceives XState inspection events
onLayerAdded(info) => voidA layer or group was added
onLayerRemoved(layerId) => voidA layer or group was removed, including by reset()
onVisibilityChanged(info, visible) => voidA layer or group started or stopped showing
onEnabledChanged(info, enabled) => voidA layer or group was switched on or off, including while a group above hides it
onOpacityChanged(info, computedOpacity) => voidA layer's computed opacity changed
onTimeInfoChanged(info, timeInfo) => voidA layer's time info changed
onLayerDataChanged(info) => voidA layer's layerData was replaced
onOrderChanged(layerOrder) => voidThe order changed; receives every layer ID, bottom to top, with each group followed by its children
onLayerMoved(info) => voidA layer or group was moved, raised or lowered; info.parentId is its new parent
onError(error) => voidA change was rejected, such as an unknown layer ID or an opacity outside 0 to 1; error.message says why

Methods ​

MethodDescription
addLayer(params)Add a layer. See Add parameters
addGroup(params)Add a group. See Add parameters
removeLayer(layerId)Remove a layer or an empty group
moveLayer(layerId, target)Move a layer or group. See Move target
raiseLayer(layerId)Move a layer or group one step towards the top of its parent
lowerLayer(layerId)Move a layer or group one step towards the bottom of its parent
setEnabled(layerId, enabled)Switch a layer or group on or off. Switching a layer on also switches on the groups above it
showLayer(layerId)Make a layer or group visible by switching on it and every group above it, even if it is already switched on. See Showing a layer
setOpacity(layerId, opacity)Set a layer's or group's own opacity, from 0 to 1
setTimeInfo(layerId, timeInfo)Set a layer's or group's time info
updateLayerData(layerId, layerData)Replace a layer's or group's layerData
getTree()The layer tree: every layer and group as plain data. See Layer tree
subscribe(listener)Call listener whenever the manager or a layer may have changed. Returns a function that stops the calls. Read getTree() for the result
setAdapter(adapter | null)Attach an adapter, replacing any existing one, or detach it with null
reset()Remove every layer and group, reporting each removal and the empty order
destroy()Detach the adapter and stop the manager, without reporting the removals. The instance can't be used again, and calling destroy() again does nothing

Properties ​

PropertyDescription
actorThe manager's XState actor. Experimental. See Working with XState
destroyedtrue after destroy()

Layer tree ​

getTree() returns { rootIds, layers }:

FieldDescription
rootIdsThe IDs of the top-level layers and groups, bottom first
layersEvery layer and group by ID, as layer info. A group's childIds lists its children, bottom first

The tree is read-only. The manager returns the same object until something changes, and a layer that didn't change keeps the same info object. That suits React's useSyncExternalStore, with no XState needed:

ts
function useLayerTree() {
  return useSyncExternalStore(manager.subscribe, manager.getTree);
}

Add parameters ​

addLayer(params) and addGroup(params) take:

ParamTypeDescription
layerConfigLayerConfig / LayerGroupConfigThe layer or group (see below)
visiblebooleanSwitch it on and switch on every group above it, so it shows straight away
enabledbooleanSwitch it on, without switching on the groups above it
position'top' | 'bottom'Place it at the top or bottom of its parent (default 'bottom')
indexnumberPlace it at this index in its parent's order, 0 being the bottom. An index past either end places it at the top or bottom. Takes precedence over position

With neither visible nor enabled, it is added switched off.

layerConfig fields:

FieldTypeDescription
layerIdstringUnique ID
layerNamestringDisplay name
layerType'layer' | 'layerGroup'Which kind of item this is
parentIdstring | nullThe ID of the group it belongs to. Leave it out, or use null, for the top level
layerDataTLayer / TGroupYour data for this layer or group. Optional when its type allows undefined, as for groups by default
opacitynumberIts own opacity, from 0 to 1 (default 1)
timeInfoLayerTimeInfoOptional single date or date range
listMode'show' | 'hide', plus 'hide-children' for groupsA hint for your layer list. The manager stores it but doesn't act on it (default 'show')

Move target ​

moveLayer(layerId, target) takes:

FieldTypeDescription
parentIdstring | nullThe group to move into, or null for the top level. It can be the layer's current parent
indexnumberWhere the layer ends up in its parent's order, 0 being the bottom, counted once the layer has left its old place. An index past either end places it at the top or bottom. Takes precedence over position
position'top' | 'bottom'The top or bottom of the parent (default 'bottom')

Layer info ​

Callbacks and adapters receive a ManagedLayerInfo:

FieldDescription
layerId, layerName, layerType, layerData, listMode, timeInfoFrom the layer's config and later updates
parentIdThe parent group's ID, or null
enabledWhether it is switched on
visibleWhether it is actually showing
opacityIts own opacity
computedOpacityIts opacity combined with every group above it; use this on the map
childIdsGroups only: the IDs of its children, bottom first

Adapter interface ​

Implement LayerManagerAdapter and attach it with manager.setAdapter(). Every method is optional. See Writing an adapter.

MethodCalled when
register()The adapter is attached, before it is told about existing layers
unregister()The adapter is detached, replaced, or the manager is destroyed
onLayerAdded(info)A layer or group was added, or already exists when the adapter is attached
onLayerRemoved(layerId)A layer or group was removed
onVisibilityChanged(info, visible)A layer or group started or stopped showing
onEnabledChanged(info, enabled)A layer or group was switched on or off, including while a group above hides it
onOpacityChanged(info, computedOpacity)A layer's computed opacity changed
onTimeInfoChanged(info, timeInfo)A layer's time info changed
onLayerDataChanged(info)A layer's layerData was replaced
onOrderChanged(layerOrder)The order changed, or the adapter was attached; every layer ID, bottom to top
onLayerMoved(info)A layer or group was moved, raised or lowered. Called after onOrderChanged

Emitted events ​

Experimental, for working with XState directly. The manager actor emits these events, typed as LayerManagerEmittedEvent. Listen to them with manager.actor.on(type, handler):

EventPayload
LAYER.ADDEDlayerId. A layer that starts visible follows with LAYER.VISIBILITY_CHANGED
LAYER.REMOVEDlayerId
LAYER.VISIBILITY_CHANGEDlayerId, visible
LAYER.ENABLED_CHANGEDlayerId, enabled
LAYER.OPACITY_CHANGEDlayerId, opacity, computedOpacity
LAYER.TIME_INFO_CHANGEDlayerId, timeInfo
LAYER.LAYER_DATA_CHANGEDlayerId, layerData
LAYER.ORDER_CHANGEDlayerOrder: every layer ID, bottom to top
LAYER.MOVEDlayerId, parentId. Emitted after LAYER.ORDER_CHANGED
LAYER.REJECTEDlayerId, reason

Commands ​

Experimental, for working with XState directly. Send these events to a layer or group actor, typed as LayerCommandEvent<TData>:

EventPayload
LAYER.ENABLED / LAYER.DISABLEDNone
LAYER.SHOWNone
LAYER.SET_OPACITYopacity, from 0 to 1
LAYER.SET_TIME_INFOtimeInfo
LAYER.SET_LAYER_DATAlayerData

Send these events to the manager actor, typed as LayerManagerEvent<TLayer, TGroup>:

EventPayload
LAYER.ADDparams: see Add parameters
LAYER.REMOVElayerId
LAYER.MOVElayerId, parentId, index?, position?
RESETNone

A command that cannot be carried out, such as an opacity outside 0 to 1 or an unknown layer ID, is ignored. The manager then emits LAYER.REJECTED with the reason.

For adapters that render layers ​

The Leaflet and MapLibre adapters share these types and extend the RenderAdapter class, so their options take the same shape and follow the same rules. They are not part of LayerManagerAdapter. Use them if you write an adapter of your own that shows each layer with a renderLayer function. See Writing an adapter.

TypeDescription
RenderLayer<TLayer, TMap, TRendered>(info, map, current?) => TRendered | null: returns what the adapter shows for a layer, or null to leave it off the map. Called when a layer is added, and again when its layerData or timeInfo changes, with what it returned last time as current
RenderAdapterArgs<TLayer, TOptions, TDefaultData>The adapter constructor's options argument: optional when every layer's data is TDefaultData, the shape its default renderLayer shows, and required with renderLayer otherwise, so a mismatch fails to compile
RenderAdapterOptions<TLayer, TMap, TRendered>{ renderLayer?, disposeLayer? }. disposeLayer(rendered, layerId) undoes setup for something renderLayer returned, once the adapter discards it: when its layer is removed, when renderLayer returns something different, and when the adapter is detached

RenderAdapter<TLayer, TGroup, TMap, TRendered> ​

An abstract LayerManagerAdapter that calls renderLayer and disposeLayer by the rules above. It skips groups, and keeps each layer's visibility, computed opacity and place in the order. Its constructor takes (map, { renderLayer, disposeLayer? }), with renderLayer required: pass the adapter's default when the app gives none. The map is then this.map.

A subclass implements these methods. Each layer is a RenderedLayer<TRendered>: { layerId, rendered, visible, computedOpacity }, where rendered is what renderLayer returned.

MethodCalled when
placeLayer(layer, previous?)A result is to go on the map: when a layer is added, when renderLayer returns something new, and from placeAll(). previous is the result it replaces, if any
eraseLayer(layerId, rendered, next?)A result is to come off the map: when its layer is removed, when it is replaced, and when the adapter is detached. next is the result that replaces it, if any
setLayerVisible(layer)A layer starts or stops showing
setLayerOpacity(layer)A layer's computed opacity changes
restackLayers(bottomToTop)The order changes, and after renderLayer returns something new for a layer, unless isSame says it stands in for the old result. Lists only the layers with a result
Protected memberDescription
isSame(previous, next)Override to say next stands in for previous: previous is not disposed of, and the layer is not restacked. Returns false unless overridden
renderedLayers()Every layer with a result. While eraseLayer runs it leaves out the result being erased; while placeLayer runs it includes the result being placed
placeAll()Calls placeLayer for every layer with a result, then restackLayers. For a map that lost them

A subclass that overrides unregister calls super.unregister(), which erases and disposes of every result.

Helper functions ​

isSingleTimeInfo and isRangeTimeInfo are stable. The other helpers work with the actors, and are experimental.

FunctionDescription
createLayerManagerMachine()Create the manager machine yourself, without the LayerManager class
connectAdapter(managerActor, adapter)Attach an adapter to a manager actor: replay its layers, then report every change. Returns a function that disconnects it
findManagedLayerById(layers, layerId)Find a managed item in the manager's layers, for example inside a selector
findLayerPlacement(context, layerId)Where a layer sits: { parentId, index, siblingCount }, counting from the bottom
getMoveLayerRejection(context, move)Why a move would be rejected, or undefined if it would succeed. Useful for checking a drop before making it
isLayerMachine(actor) / isLayerGroupMachine(actor)Check whether an actor is a layer or a group
isSingleTimeInfo(timeInfo) / isRangeTimeInfo(timeInfo)Check whether time info is a single date or a range

Released under the MIT License.