From 5652a45140652bbac1bf3e89b1b74d181fcce7ac Mon Sep 17 00:00:00 2001 From: David Bretaud Date: Mon, 13 Apr 2026 22:06:47 +0200 Subject: [PATCH 1/3] feat(symbol): Allow to set custom symbol --- coordo-ts/src/index.ts | 1 + coordo-ts/src/layers/symbol.ts | 63 ++++++++++++++++++++++++++++++++++ coordo-ts/src/map/map.ts | 7 ++++ 3 files changed, 71 insertions(+) create mode 100644 coordo-ts/src/layers/symbol.ts 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..34fdbf5 --- /dev/null +++ b/coordo-ts/src/layers/symbol.ts @@ -0,0 +1,63 @@ +/** + * 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 external image, use symbolUrl. + * To use an image from your sprite, use spriteId + * @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.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 setLayerSymbolViaProperty({ + layerId, + iconSize = 1, + fallbackId, + imageUrl, + spriteId, + }: { + layerId: string; + iconSize?: number; + fallbackId?: string; + } & ( + | { imageUrl: string; spriteId?: null } + | { spriteId: string; imageUrl?: null } + )) { + let pictureId: string = ""; + + if (spriteId) { + pictureId = spriteId; + } + + if (imageUrl) { + const image = await map.loadImage(imageUrl); + const imageId = getLayerSymbolPictureId({ layerId }); + map.addImage(imageId, image.data); + pictureId = imageId; + } + + const finalId = fallbackId + ? ["coalesce", ["image", pictureId], ["image", fallbackId]] + : pictureId; + map.setLayoutProperty(layerId, "icon-image", finalId); + map.setLayoutProperty(layerId, "icon-size", iconSize); + } + + return setLayerSymbolViaProperty; +} 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, }; } From 99355f721b6042a35f82de45181b35624e75501e Mon Sep 17 00:00:00 2001 From: David Bretaud Date: Fri, 17 Apr 2026 09:48:20 +0200 Subject: [PATCH 2/3] feat(symbol): Allow to provide svg --- coordo-ts/src/layers/symbol.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/coordo-ts/src/layers/symbol.ts b/coordo-ts/src/layers/symbol.ts index 34fdbf5..c2ef5ab 100644 --- a/coordo-ts/src/layers/symbol.ts +++ b/coordo-ts/src/layers/symbol.ts @@ -22,6 +22,7 @@ export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { * @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. */ @@ -31,13 +32,15 @@ export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { fallbackId, imageUrl, spriteId, + svg, }: { layerId: string; iconSize?: number; fallbackId?: string; } & ( - | { imageUrl: string; spriteId?: null } - | { spriteId: string; imageUrl?: null } + | { imageUrl: string; spriteId?: null; svg?: null } + | { spriteId: string; imageUrl?: null; svg?: null } + | { svg: string; imageUrl?: null; spriteId?: null } )) { let pictureId: string = ""; @@ -52,6 +55,19 @@ export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { pictureId = imageId; } + 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); + pictureId = imageId; + } + const finalId = fallbackId ? ["coalesce", ["image", pictureId], ["image", fallbackId]] : pictureId; From a24b643c6ee1fa829e30737eaa999c1960936569 Mon Sep 17 00:00:00 2001 From: David Bretaud Date: Sun, 19 Apr 2026 11:45:19 +0200 Subject: [PATCH 3/3] fix(symbol): Add priority in executions --- coordo-ts/src/layers/symbol.ts | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/coordo-ts/src/layers/symbol.ts b/coordo-ts/src/layers/symbol.ts index c2ef5ab..e94e0e6 100644 --- a/coordo-ts/src/layers/symbol.ts +++ b/coordo-ts/src/layers/symbol.ts @@ -16,8 +16,10 @@ function getLayerSymbolPictureId({ layerId }: { layerId: string }) { 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 image from your sprite, use spriteId + * 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. @@ -26,7 +28,7 @@ export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { * @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 setLayerSymbolViaProperty({ + async function setLayerSymbol({ layerId, iconSize = 1, fallbackId, @@ -42,17 +44,26 @@ export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { | { spriteId: string; imageUrl?: null; svg?: null } | { svg: string; imageUrl?: null; spriteId?: null } )) { - let pictureId: string = ""; + 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) { - pictureId = spriteId; + setLayerIconImage(spriteId); + return; } if (imageUrl) { const image = await map.loadImage(imageUrl); const imageId = getLayerSymbolPictureId({ layerId }); map.addImage(imageId, image.data); - pictureId = imageId; + + setLayerIconImage(imageId); + return; } if (svg) { @@ -65,15 +76,13 @@ export function makeSetLayerSymbol({ map }: { map: MapLibreMap }) { await promise; // Wait for the image to load const imageId = getLayerSymbolPictureId({ layerId }); map.addImage(imageId, image); - pictureId = imageId; + + setLayerIconImage(imageId); + return; } - const finalId = fallbackId - ? ["coalesce", ["image", pictureId], ["image", fallbackId]] - : pictureId; - map.setLayoutProperty(layerId, "icon-image", finalId); - map.setLayoutProperty(layerId, "icon-size", iconSize); + console.warn("[setLayerSymbol] Provide one of: spriteId, imageUrl, svg"); } - return setLayerSymbolViaProperty; + return setLayerSymbol; }