diff --git a/coordo-ts/src/index.ts b/coordo-ts/src/index.ts index f09e69e..f868332 100644 --- a/coordo-ts/src/index.ts +++ b/coordo-ts/src/index.ts @@ -6,6 +6,7 @@ export type { PopupOptions } from "maplibre-gl"; export { EVENTS } from "./events"; +export { getLayerSymbolId } from "./layers/symbol"; export { createMap } from "./map/map"; export type { FrictionlessField, diff --git a/coordo-ts/src/layers/symbol.ts b/coordo-ts/src/layers/symbol.ts new file mode 100644 index 0000000..e94e0e6 --- /dev/null +++ b/coordo-ts/src/layers/symbol.ts @@ -0,0 +1,88 @@ +/** + * Copyright COORDONNÉES 2025, 2026 + * SPDX-License-Identifier: MPL-2.0 + */ + +import type { Map as MapLibreMap } from "maplibre-gl"; + +export function getLayerSymbolId({ layerId }: { layerId: string }) { + return `${layerId}-symbol-layer`; +} + +function getLayerSymbolPictureId({ layerId }: { layerId: string }) { + return `${layerId}-symbol-picture-identifier`; +} + +export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { + /** + * Update the icon-image of a Layer Layout. + * To use an image from your sprite, use spriteId. + * To use an external image, use symbolUrl. + * To use an svg image, use svg. + * Use only one provider. You can provide an additional fallback image via fallbackId. + * @param params.layerId — The ID of the layer to set the layout property in. + * @param params.imageUrl — The URL of the image file. Image file must be in png, webp, or jpg format. + * @param params.spriteId — The ID of the image to load from the sprite attached to the map instance. + * @param params.iconSize — The units in factor of the original icon size + * @param params.svg — The SVG to use as picture. + * @param params.fallbackId — The ID of a picture (from a sprite or from addImage) + * to use as fallback when the main image couldn't load. + */ + async function setLayerSymbol({ + layerId, + iconSize = 1, + fallbackId, + imageUrl, + spriteId, + svg, + }: { + layerId: string; + iconSize?: number; + fallbackId?: string; + } & ( + | { imageUrl: string; spriteId?: null; svg?: null } + | { spriteId: string; imageUrl?: null; svg?: null } + | { svg: string; imageUrl?: null; spriteId?: null } + )) { + function setLayerIconImage(pictureId: string) { + const finalId = fallbackId + ? ["coalesce", ["image", pictureId], ["image", fallbackId]] + : pictureId; + map.setLayoutProperty(layerId, "icon-image", finalId); + map.setLayoutProperty(layerId, "icon-size", iconSize); + } + + if (spriteId) { + setLayerIconImage(spriteId); + return; + } + + if (imageUrl) { + const image = await map.loadImage(imageUrl); + const imageId = getLayerSymbolPictureId({ layerId }); + map.addImage(imageId, image.data); + + setLayerIconImage(imageId); + return; + } + + if (svg) { + // Ref: https://maplibre.org/maplibre-gl-js/docs/examples/display-a-remote-svg-symbol/ + const image = new Image(); + const promise = new Promise((resolve) => { + image.onload = resolve; + }); + image.src = svg; + await promise; // Wait for the image to load + const imageId = getLayerSymbolPictureId({ layerId }); + map.addImage(imageId, image); + + setLayerIconImage(imageId); + return; + } + + console.warn("[setLayerSymbol] Provide one of: spriteId, imageUrl, svg"); + } + + return setLayerSymbol; +} diff --git a/coordo-ts/src/map/map.ts b/coordo-ts/src/map/map.ts index c2ad0ee..5c81571 100644 --- a/coordo-ts/src/map/map.ts +++ b/coordo-ts/src/map/map.ts @@ -11,6 +11,7 @@ import "../index.css"; import { EVENTS } from "../events"; import { makeSetLayerFilters } from "../layers/filters"; import { makeSetLayerPopup } from "../layers/popup"; +import { makeSetLayerSymbol } from "../layers/symbol"; import { addStyleDataListener } from "./style-data"; const DEFAULT_MAP_OPTIONS: Partial = { @@ -55,10 +56,14 @@ export function createMap( return map.getCenter().toArray(); } + const addSprite = map.addSprite; + const setLayerFilters = makeSetLayerFilters({ baseUrl, map }); const setLayerPopup = makeSetLayerPopup({ map }); + const setLayerSymbol = makeSetLayerSymbol({ map }); + function addEventListener( type: T, listener: (ev: maplibregl.MapEventType[T] & Object) => void, @@ -83,6 +88,7 @@ export function createMap( return { addEventListener, + addSprite, getCenter, getLayerMetadata, getZoom, @@ -90,6 +96,7 @@ export function createMap( mapInstance: map, setLayerFilters, setLayerPopup, + setLayerSymbol, showLayer, }; }