From 8089db6105a38d5e57ba91b04a1cc376f041598e Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 30 Apr 2026 12:59:29 +0100 Subject: [PATCH 01/11] IM-246 WIP --- .../src/adapters/maplibre/layerBuilders.js | 4 +- .../adapters/maplibre/maplibreLayerAdapter.js | 6 +- .../src/adapters/maplibre/symbolImages.js | 2 +- providers/maplibre/src/maplibreProvider.js | 2 +- providers/maplibre/src/utils/symbolImages.js | 34 ----- src/services/symbolRegistry.js | 127 ++++++++++++++++++ src/utils/symbolUtils.js | 20 --- 7 files changed, 134 insertions(+), 61 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js b/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js index 71efa378..06542261 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +++ b/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js @@ -2,7 +2,7 @@ import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.j import { hasPattern } from './patternImages.js' import { mergeSublayer } from '../../utils/mergeSublayer.js' import { getSourceId, getLayerIds, getSublayerLayerIds, isDynamicSource, MAX_TILE_ZOOM } from './layerIds.js' -import { hasSymbol, getSymbolDef, getSymbolAnchor, anchorToMaplibre, getSymbolImageId } from './symbolImages.js' +import { hasSymbol, getSymbolDef, getSymbolAnchor, anchorToMaplibre } from './symbolImages.js' // ─── Source ─────────────────────────────────────────────────────────────────── @@ -83,7 +83,7 @@ export const addSymbolLayer = (map, dataset, layerId, sourceId, sourceLayer, vis if (!layerId || map.getLayer(layerId)) { return } const symbolDef = getSymbolDef(dataset, symbolRegistry) if (!symbolDef) { return } - const imageId = getSymbolImageId(dataset, mapStyle, symbolRegistry, false, pixelRatio) + const imageId = symbolRegistry.getSymbolImageId(dataset, mapStyle, false, pixelRatio) if (!imageId) { return } const anchor = getSymbolAnchor(dataset, symbolDef) map.addLayer({ diff --git a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js index 6e36acbb..769db79d 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +++ b/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js @@ -2,7 +2,7 @@ import { applyExclusionFilter } from '../../utils/filters.js' import { getSourceId, getLayerIds, getSublayerLayerIds, getAllLayerIds } from './layerIds.js' import { addDatasetLayers, addSublayerLayers } from './layerBuilders.js' import { getPatternConfigs, hasPattern } from './patternImages.js' -import { getSymbolConfigs, getSymbolImageId } from './symbolImages.js' +import { getSymbolConfigs } from './symbolImages.js' import { mergeSublayer } from '../../utils/mergeSublayer.js' import { scaleFactor } from '../../../../../../src/config/appConfig.js' @@ -127,7 +127,7 @@ export default class MaplibreLayerAdapter { datasets.forEach(dataset => { getAllLayerIds(dataset).forEach(layerId => { if (!this._symbolLayerIds.has(layerId) || !this._map.getLayer(layerId)) { return } - const imageId = getSymbolImageId(dataset, mapStyle, this._symbolRegistry, false, pixelRatio) + const imageId = this._symbolRegistry.getSymbolImageId(dataset, mapStyle, false, pixelRatio) if (imageId) { this._map.setLayoutProperty(layerId, 'icon-image', imageId) } @@ -145,7 +145,7 @@ export default class MaplibreLayerAdapter { const merged = mergeSublayer(dataset, sublayer) const { symbolLayerId, fillLayerId } = getSublayerLayerIds(dataset.id, sublayer.id) if (this._map.getLayer(symbolLayerId)) { - const imageId = getSymbolImageId(merged, mapStyle, this._symbolRegistry, false, pixelRatio) + const imageId = this._symbolRegistry.getSymbolImageId(merged, mapStyle, false, pixelRatio) if (imageId) { this._map.setLayoutProperty(symbolLayerId, 'icon-image', imageId) } diff --git a/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js b/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js index f0ffe90b..3288e020 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +++ b/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js @@ -7,7 +7,7 @@ export { hasSymbol, getSymbolDef, getSymbolStyleColors, getSymbolViewBox, getSym // Re-export MapLibre-specific symbol utilities from the provider. // This is the single cross-boundary import in the adapter; in a separate-package // setup this would be: '@interactive-map/maplibre-provider/utils/symbolImages' -export { anchorToMaplibre, getSymbolImageId } from '../../../../../../providers/maplibre/src/utils/symbolImages.js' +export { anchorToMaplibre } from '../../../../../../providers/maplibre/src/utils/symbolImages.js' /** * Returns a flat list of datasets and merged sublayers that require symbol images. diff --git a/providers/maplibre/src/maplibreProvider.js b/providers/maplibre/src/maplibreProvider.js index d2995f41..f8aa8fb4 100755 --- a/providers/maplibre/src/maplibreProvider.js +++ b/providers/maplibre/src/maplibreProvider.js @@ -318,7 +318,7 @@ export default class MapLibreProvider { */ async addSymbolsToMap (symbolConfigs, mapStyle, symbolRegistry) { const pixelRatio = (this.map.getPixelRatio() || 1) * (scaleFactor[this.mapSize] || 1) - return addSymbolsToMap(this.map, symbolConfigs, mapStyle, symbolRegistry, pixelRatio) + return symbolRegistry.addSymbolsToMap(this.map, symbolConfigs, mapStyle, pixelRatio) } /** diff --git a/providers/maplibre/src/utils/symbolImages.js b/providers/maplibre/src/utils/symbolImages.js index 2d9e8357..a92723b4 100644 --- a/providers/maplibre/src/utils/symbolImages.js +++ b/providers/maplibre/src/utils/symbolImages.js @@ -3,15 +3,6 @@ import { rasteriseToImageData } from './rasteriseToImageData.js' const ANCHOR_LOW = 0.25 const ANCHOR_HIGH = 0.75 -const HASH_BASE = 36 - -const hashString = (str) => { - let hash = 0 - for (const ch of str) { - hash = Math.trunc(((hash << 5) - hash) + ch.codePointAt(0)) - } - return Math.abs(hash).toString(HASH_BASE) -} // ─── MapLibre-specific anchor conversion ────────────────────────────────────── @@ -48,31 +39,6 @@ export const anchorToMaplibre = ([ax, ay]) => { return (y + (x && y ? '-' : '') + x) || 'center' } -// ─── Image IDs ──────────────────────────────────────────────────────────────── - -/** - * Returns a deterministic image ID for a symbol in normal or selected state. - * Based on the hash of the fully resolved SVG content and the pixel ratio. - * - * @param {Object} dataset - * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) - * @param {Object} symbolRegistry - * @param {boolean} [selected=false] - * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor - * @returns {string|null} - */ -export const getSymbolImageId = (dataset, mapStyle, symbolRegistry, selected = false, pixelRatio = 2) => { - const symbolDef = getSymbolDef(dataset, symbolRegistry) - if (!symbolDef) { - return null - } - const styleColors = getSymbolStyleColors(dataset) - const resolved = selected - ? symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle) - : symbolRegistry.resolve(symbolDef, styleColors, mapStyle) - return `symbol-${selected ? 'sel-' : ''}${hashString(resolved)}-${pixelRatio}x` -} - // ─── Rasterisation ──────────────────────────────────────────────────────────── // Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols. diff --git a/src/services/symbolRegistry.js b/src/services/symbolRegistry.js index 39f57d06..411d7a45 100644 --- a/src/services/symbolRegistry.js +++ b/src/services/symbolRegistry.js @@ -1,10 +1,24 @@ import { getValueForStyle } from '../utils/getValueForStyle.js' import { symbolDefaults, pin, circle, square, graphics } from '../config/symbolConfig.js' +import { getSymbolStyleColors, getSymbolViewBox } from '../utils/symbolUtils.js' import { SCHEME_COLORS } from '../config/mapTheme.js' +import { rasteriseToImageData } from './rasteriseToImageData.js' const symbols = new Map() +// Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols. +const imageDataCache = new Map() let _constructorDefaults = {} +const HASH_BASE = 36 + +const hashString = (str) => { + let hash = 0 + for (const ch of str) { + hash = Math.trunc(((hash << 5) - hash) + ch.codePointAt(0)) + } + return Math.abs(hash).toString(HASH_BASE) +} + // Keys that are structural — not token values for SVG substitution const STRUCTURAL = new Set(['id', 'svg', 'viewBox', 'anchor', 'symbol', 'symbolSvgContent']) @@ -105,6 +119,119 @@ export const symbolRegistry = { const colors = resolveValues(symbolDef, styleColors || {}, mapStyle) if (!symbolDef) { return '' } return resolveLayer(symbolDef.svg, colors) + }, + + // ─── Image IDs ──────────────────────────────────────────────────────────────── + + /** + * Returns a deterministic image ID for a symbol in normal or selected state. + * Based on the hash of the fully resolved SVG content and the pixel ratio. + * + * @param {Object} dataset + * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) + * @param {boolean} [selected=false] + * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor + * @returns {string|null} + */ + getSymbolImageId (dataset, mapStyle, selected = false, pixelRatio = 2) { + const symbolDef = this.getSymbolDef(dataset) + if (!symbolDef) { + return null + } + const styleColors = getSymbolStyleColors(dataset) + const resolved = selected + ? this.resolveSelected(symbolDef, styleColors, mapStyle) + : this.resolve(symbolDef, styleColors, mapStyle) + return `symbol-${selected ? 'sel-' : ''}${hashString(resolved)}-${pixelRatio}x` + }, + + /** + * Resolves the symbolDef for a dataset's symbol config. + * + * dataset.symbol is a string symbol ID (e.g. 'pin'). + * dataset.symbolSvgContent is inline SVG content for a custom symbol. + * + * @param {Object} dataset + * @returns {Object|undefined} + */ + getSymbolDef (dataset) { + if (dataset.symbolSvgContent) { + return { svg: dataset.symbolSvgContent } + } + if (dataset.symbol) { + return this.get(dataset.symbol) + } + return undefined + }, + + /** + * Register normal and selected symbol images for the given pre-resolved symbol configs. + * Skips images that are already registered (safe to call on style change). + * Updates `map._symbolImageMap` with normal→selected image ID pairs. + * + * Callers are responsible for resolving sublayers before calling this function + * (see `getSymbolConfigs` in the datasets plugin adapter). + * + * @param {Object} map - MapLibre map instance + * @param {Object[]} styleArray - Flat list of datasets/merged-sublayers that have a symbol config + * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) + * @param {Object} symbolRegistry + * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor (computed by caller) + * @returns {Promise} + */ + async addSymbolsToMap (map, styleArray, mapStyle, pixelRatio = 2) { + if (!styleArray.length) { + return + } + + // Reset the normal→selected image ID lookup so stale entries don't persist after a style change + map._symbolImageMap = {} + + await Promise.all(styleArray.flatMap(config => { + const normalId = this.getSymbolImageId(config, mapStyle, false, pixelRatio) + const selectedId = this.getSymbolImageId(config, mapStyle, true, pixelRatio) + if (normalId && selectedId) { + map._symbolImageMap[normalId] = selectedId + } + return [false, true].map(async (selected) => { + const imageId = selected ? selectedId : normalId + if (!imageId || map.hasImage(imageId)) { + return + } + const result = await this.rasteriseSymbolImage(config, mapStyle, selected, pixelRatio) + if (result && !map.hasImage(result.imageId)) { + map.addImage(result.imageId, result.imageData, { pixelRatio }) + } + }) + })) + }, + + // ─── Rasterisation ──────────────────────────────────────────────────────────── + + async rasteriseSymbolImage (dataset, mapStyle, selected, pixelRatio) { + const symbolDef = this.getSymbolDef(dataset) + if (!symbolDef) { + return null + } + const styleColors = getSymbolStyleColors(dataset) + const resolvedContent = selected + ? symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle) + : symbolRegistry.resolve(symbolDef, styleColors, mapStyle) + + const imageId = `symbol-${selected ? 'sel-' : ''}${hashString(resolvedContent)}-${pixelRatio}x` + + let imageData = imageDataCache.get(imageId) + if (!imageData) { + const viewBox = getSymbolViewBox(dataset, symbolDef) + const [,, width, height] = viewBox.split(' ').map(Number) + // Render at pixelRatio× to keep icons crisp at the current device DPI and map size. + // MapLibre receives the matching pixelRatio so the image displays at its original logical size. + const svgString = `${resolvedContent}` + imageData = await rasteriseToImageData(svgString, width * pixelRatio, height * pixelRatio) + imageDataCache.set(imageId, imageData) + } + + return { imageId, imageData } } } diff --git a/src/utils/symbolUtils.js b/src/utils/symbolUtils.js index a3e4de0b..2b561292 100644 --- a/src/utils/symbolUtils.js +++ b/src/utils/symbolUtils.js @@ -31,26 +31,6 @@ export const isStandaloneLabel = (marker) => { return marker.symbol === null || marker.symbolSvgContent === null } -/** - * Resolves the symbolDef for a dataset's symbol config. - * - * dataset.symbol is a string symbol ID (e.g. 'pin'). - * dataset.symbolSvgContent is inline SVG content for a custom symbol. - * - * @param {Object} dataset - * @param {Object} symbolRegistry - * @returns {Object|undefined} - */ -export const getSymbolDef = (dataset, symbolRegistry) => { - if (dataset.symbolSvgContent) { - return { svg: dataset.symbolSvgContent } - } - if (dataset.symbol) { - return symbolRegistry.get(dataset.symbol) - } - return undefined -} - /** * Extracts token overrides from a dataset's flat symbol style props. * Strips the 'symbol' prefix to produce internal token names (e.g. symbolBackgroundColor → backgroundColor). From e195ac08b0cc20b7cd49617284a3ccd3308869fd Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 30 Apr 2026 13:02:50 +0100 Subject: [PATCH 02/11] IM-246 wip 2 --- .../src/adapters/maplibre/layerBuilders.js | 4 +- providers/maplibre/src/maplibreProvider.js | 1 - providers/maplibre/src/utils/symbolImages.js | 76 ------------------- src/services/symbolRegistry.js | 5 +- 4 files changed, 4 insertions(+), 82 deletions(-) diff --git a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js b/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js index 06542261..e59d3a21 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +++ b/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js @@ -2,7 +2,7 @@ import { getValueForStyle } from '../../../../../../src/utils/getValueForStyle.j import { hasPattern } from './patternImages.js' import { mergeSublayer } from '../../utils/mergeSublayer.js' import { getSourceId, getLayerIds, getSublayerLayerIds, isDynamicSource, MAX_TILE_ZOOM } from './layerIds.js' -import { hasSymbol, getSymbolDef, getSymbolAnchor, anchorToMaplibre } from './symbolImages.js' +import { hasSymbol, getSymbolAnchor, anchorToMaplibre } from './symbolImages.js' // ─── Source ─────────────────────────────────────────────────────────────────── @@ -81,7 +81,7 @@ export const addStrokeLayer = (map, config, layerId, sourceId, sourceLayer, visi export const addSymbolLayer = (map, dataset, layerId, sourceId, sourceLayer, visibility, { mapStyle, symbolRegistry, pixelRatio }) => { if (!layerId || map.getLayer(layerId)) { return } - const symbolDef = getSymbolDef(dataset, symbolRegistry) + const symbolDef = symbolRegistry.getSymbolDef(dataset) if (!symbolDef) { return } const imageId = symbolRegistry.getSymbolImageId(dataset, mapStyle, false, pixelRatio) if (!imageId) { return } diff --git a/providers/maplibre/src/maplibreProvider.js b/providers/maplibre/src/maplibreProvider.js index f8aa8fb4..041a1e45 100755 --- a/providers/maplibre/src/maplibreProvider.js +++ b/providers/maplibre/src/maplibreProvider.js @@ -13,7 +13,6 @@ import { createMapLabelNavigator } from './utils/labels.js' import { updateHighlightedFeatures } from './utils/highlightFeatures.js' import { queryFeatures } from './utils/queryFeatures.js' import { setupHoverCursor } from './utils/hoverCursor.js' -import { addSymbolsToMap } from './utils/symbolImages.js' import { addPatternsToMap } from './utils/patternImages.js' /** diff --git a/providers/maplibre/src/utils/symbolImages.js b/providers/maplibre/src/utils/symbolImages.js index a92723b4..a9c2e653 100644 --- a/providers/maplibre/src/utils/symbolImages.js +++ b/providers/maplibre/src/utils/symbolImages.js @@ -1,6 +1,3 @@ -import { getSymbolDef, getSymbolStyleColors, getSymbolViewBox } from '../../../../src/utils/symbolUtils.js' -import { rasteriseToImageData } from './rasteriseToImageData.js' - const ANCHOR_LOW = 0.25 const ANCHOR_HIGH = 0.75 @@ -38,76 +35,3 @@ export const anchorToMaplibre = ([ax, ay]) => { const y = yAnchor(ay) return (y + (x && y ? '-' : '') + x) || 'center' } - -// ─── Rasterisation ──────────────────────────────────────────────────────────── - -// Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols. -const imageDataCache = new Map() - -const rasteriseSymbolImage = async (dataset, mapStyle, symbolRegistry, selected, pixelRatio) => { - const symbolDef = getSymbolDef(dataset, symbolRegistry) - if (!symbolDef) { - return null - } - const styleColors = getSymbolStyleColors(dataset) - const resolvedContent = selected - ? symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle) - : symbolRegistry.resolve(symbolDef, styleColors, mapStyle) - - const imageId = `symbol-${selected ? 'sel-' : ''}${hashString(resolvedContent)}-${pixelRatio}x` - - let imageData = imageDataCache.get(imageId) - if (!imageData) { - const viewBox = getSymbolViewBox(dataset, symbolDef) - const [,, width, height] = viewBox.split(' ').map(Number) - // Render at pixelRatio× to keep icons crisp at the current device DPI and map size. - // MapLibre receives the matching pixelRatio so the image displays at its original logical size. - const svgString = `${resolvedContent}` - imageData = await rasteriseToImageData(svgString, width * pixelRatio, height * pixelRatio) - imageDataCache.set(imageId, imageData) - } - - return { imageId, imageData } -} - -/** - * Register normal and selected symbol images for the given pre-resolved symbol configs. - * Skips images that are already registered (safe to call on style change). - * Updates `map._symbolImageMap` with normal→selected image ID pairs. - * - * Callers are responsible for resolving sublayers before calling this function - * (see `getSymbolConfigs` in the datasets plugin adapter). - * - * @param {Object} map - MapLibre map instance - * @param {Object[]} styleArray - Flat list of datasets/merged-sublayers that have a symbol config - * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) - * @param {Object} symbolRegistry - * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor (computed by caller) - * @returns {Promise} - */ -export const addSymbolsToMap = async (map, styleArray, mapStyle, symbolRegistry, pixelRatio = 2) => { - if (!styleArray.length) { - return - } - - // Reset the normal→selected image ID lookup so stale entries don't persist after a style change - map._symbolImageMap = {} - - await Promise.all(styleArray.flatMap(config => { - const normalId = getSymbolImageId(config, mapStyle, symbolRegistry, false, pixelRatio) - const selectedId = getSymbolImageId(config, mapStyle, symbolRegistry, true, pixelRatio) - if (normalId && selectedId) { - map._symbolImageMap[normalId] = selectedId - } - return [false, true].map(async (selected) => { - const imageId = selected ? selectedId : normalId - if (!imageId || map.hasImage(imageId)) { - return - } - const result = await rasteriseSymbolImage(config, mapStyle, symbolRegistry, selected, pixelRatio) - if (result && !map.hasImage(result.imageId)) { - map.addImage(result.imageId, result.imageData, { pixelRatio }) - } - }) - })) -} diff --git a/src/services/symbolRegistry.js b/src/services/symbolRegistry.js index 411d7a45..2d29d4e4 100644 --- a/src/services/symbolRegistry.js +++ b/src/services/symbolRegistry.js @@ -175,7 +175,6 @@ export const symbolRegistry = { * @param {Object} map - MapLibre map instance * @param {Object[]} styleArray - Flat list of datasets/merged-sublayers that have a symbol config * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) - * @param {Object} symbolRegistry * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor (computed by caller) * @returns {Promise} */ @@ -215,8 +214,8 @@ export const symbolRegistry = { } const styleColors = getSymbolStyleColors(dataset) const resolvedContent = selected - ? symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle) - : symbolRegistry.resolve(symbolDef, styleColors, mapStyle) + ? this.resolveSelected(symbolDef, styleColors, mapStyle) + : this.resolve(symbolDef, styleColors, mapStyle) const imageId = `symbol-${selected ? 'sel-' : ''}${hashString(resolvedContent)}-${pixelRatio}x` From b17e442683f97ebbad40207dc0c10ba4ac2d4460 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Thu, 30 Apr 2026 15:56:34 +0100 Subject: [PATCH 03/11] IM-246 moved symbolRegistry helpers into symbolRegistry --- .../beta/datasets/src/components/KeySvg.jsx | 4 +- providers/maplibre/src/utils/symbolImages.js | 125 ------------------ src/services/symbolRegistry.js | 90 ++++++++----- 3 files changed, 59 insertions(+), 160 deletions(-) diff --git a/plugins/beta/datasets/src/components/KeySvg.jsx b/plugins/beta/datasets/src/components/KeySvg.jsx index 70fd3eb7..3a46fe7f 100644 --- a/plugins/beta/datasets/src/components/KeySvg.jsx +++ b/plugins/beta/datasets/src/components/KeySvg.jsx @@ -1,4 +1,4 @@ -import { hasSymbol, getSymbolDef } from '../../../../../src/utils/symbolUtils.js' +import { hasSymbol } from '../../../../../src/utils/symbolUtils.js' import { hasPattern } from '../../../../../src/utils/patternUtils.js' import { KeySvgPattern } from './KeySvgPattern.jsx' import { KeySvgSymbol } from './KeySvgSymbol.jsx' @@ -7,7 +7,7 @@ import { KeySvgRect } from './KeySvgRect.jsx' export const KeySvg = (props) => { const { symbolRegistry } = props - const symbolDef = hasSymbol(props) && getSymbolDef(props, symbolRegistry) + const symbolDef = hasSymbol(props) && symbolRegistry.getSymbolDef(props) if (symbolDef) { return } diff --git a/providers/maplibre/src/utils/symbolImages.js b/providers/maplibre/src/utils/symbolImages.js index 037f4204..a9c2e653 100644 --- a/providers/maplibre/src/utils/symbolImages.js +++ b/providers/maplibre/src/utils/symbolImages.js @@ -35,128 +35,3 @@ export const anchorToMaplibre = ([ax, ay]) => { const y = yAnchor(ay) return (y + (x && y ? '-' : '') + x) || 'center' } - -// ─── Image IDs ──────────────────────────────────────────────────────────────── - -/** - * Returns a deterministic image ID for a symbol in normal or selected state. - * Based on the hash of the fully resolved SVG content and the pixel ratio. - * - * @param {Object} dataset - * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) - * @param {Object} symbolRegistry - * @param {boolean} [selected=false] - * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor - * @returns {string|null} - */ -export const getSymbolImageId = (dataset, mapStyle, symbolRegistry, active = false, pixelRatio = 2) => { - const symbolDef = getSymbolDef(dataset, symbolRegistry) - if (!symbolDef) { - return null - } - const styleColors = getSymbolStyleColors(dataset) - const resolved = active - ? symbolRegistry.resolveActive(symbolDef, styleColors, mapStyle) - : symbolRegistry.resolve(symbolDef, styleColors, mapStyle) - return `symbol-${active ? 'act-' : ''}${hashString(resolved)}-${pixelRatio}x` -} - -// ─── Rasterisation ──────────────────────────────────────────────────────────── - -// Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols. -const imageDataCache = new Map() - -/** - * Rasterise one variant of a symbol to ImageData for use as a MapLibre image. - * Results are cached by imageId so identical symbols are only rendered once. - * - * @param {Object} dataset - Dataset or marker config with symbol properties - * @param {Object} mapStyle - Current map style config - * @param {Object} symbolRegistry - * @param {'normal'|'active'|'selected'} variant - * - `'normal'` — no rings (default display) - * - `'active'` — both rings (keyboard cursor, yellow + black) - * - `'selected'` — selected ring only (black) - * @param {number} pixelRatio - Device pixel ratio × map size scale factor - * @returns {Promise<{imageId: string, imageData: ImageData}|null>} - */ -const rasteriseSymbolImage = async (dataset, mapStyle, symbolRegistry, variant, pixelRatio) => { - const symbolDef = getSymbolDef(dataset, symbolRegistry) - if (!symbolDef) { - return null - } - const styleColors = getSymbolStyleColors(dataset) - let resolvedContent, prefix - if (variant === 'active') { - resolvedContent = symbolRegistry.resolveActive(symbolDef, styleColors, mapStyle) - prefix = 'act-' - } else if (variant === 'selected') { - resolvedContent = symbolRegistry.resolveSelected(symbolDef, styleColors, mapStyle) - prefix = 'sel-' - } else { - resolvedContent = symbolRegistry.resolve(symbolDef, styleColors, mapStyle) - prefix = '' - } - - const imageId = `symbol-${prefix}${hashString(resolvedContent)}-${pixelRatio}x` - - let imageData = imageDataCache.get(imageId) - if (!imageData) { - const viewBox = getSymbolViewBox(dataset, symbolDef) - const [,, width, height] = viewBox.split(' ').map(Number) - // Render at pixelRatio× to keep icons crisp at the current device DPI and map size. - // MapLibre receives the matching pixelRatio so the image displays at its original logical size. - const svgString = `${resolvedContent}` - imageData = await rasteriseToImageData(svgString, width * pixelRatio, height * pixelRatio) - imageDataCache.set(imageId, imageData) - } - - return { imageId, imageData } -} - -/** - * Register normal, active (both rings) and selected (black ring) symbol images. - * Skips images that are already registered (safe to call on style change). - * Updates `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap` (normal→selected). - * - * Callers are responsible for resolving sublayers before calling this function - * (see `getSymbolConfigs` in the datasets plugin adapter). - * - * @param {Object} map - MapLibre map instance - * @param {Object[]} styleArray - Flat list of datasets/merged-sublayers that have a symbol config - * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) - * @param {Object} symbolRegistry - * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor (computed by caller) - * @returns {Promise} - */ -export const addSymbolsToMap = async (map, styleArray, mapStyle, symbolRegistry, pixelRatio = 2) => { - if (!styleArray.length) { - return - } - - map._activeSymbolImageMap = {} - map._selectedSymbolImageMap = {} - - await Promise.all(styleArray.flatMap(config => { - const normalId = getSymbolImageId(config, mapStyle, symbolRegistry, false, pixelRatio) - const activeId = getSymbolImageId(config, mapStyle, symbolRegistry, true, pixelRatio) - if (normalId && activeId) { - map._activeSymbolImageMap[normalId] = activeId - } - return ['normal', 'active', 'selected'].map(async (variant) => { - const imageId = variant === 'active' ? activeId : normalId - if (variant !== 'selected' && (!imageId || map.hasImage(imageId))) { - return - } - const result = await rasteriseSymbolImage(config, mapStyle, symbolRegistry, variant, pixelRatio) - if (result) { - if (variant === 'selected' && normalId) { - map._selectedSymbolImageMap[normalId] = result.imageId - } - if (!map.hasImage(result.imageId)) { - map.addImage(result.imageId, result.imageData, { pixelRatio }) - } - } - }) - })) -} diff --git a/src/services/symbolRegistry.js b/src/services/symbolRegistry.js index 3ed1a6fc..e7111eac 100644 --- a/src/services/symbolRegistry.js +++ b/src/services/symbolRegistry.js @@ -141,25 +141,25 @@ export const symbolRegistry = { // ─── Image IDs ──────────────────────────────────────────────────────────────── /** - * Returns a deterministic image ID for a symbol in normal or selected state. - * Based on the hash of the fully resolved SVG content and the pixel ratio. - * - * @param {Object} dataset - * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) - * @param {boolean} [selected=false] - * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor - * @returns {string|null} - */ - getSymbolImageId (dataset, mapStyle, selected = false, pixelRatio = 2) { + * Returns a deterministic image ID for a symbol in normal or selected state. + * Based on the hash of the fully resolved SVG content and the pixel ratio. + * + * @param {Object} dataset + * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) + * @param {boolean} [selected=false] + * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor + * @returns {string|null} + */ + getSymbolImageId (dataset, mapStyle, active = false, pixelRatio = 2) { const symbolDef = this.getSymbolDef(dataset) if (!symbolDef) { return null } const styleColors = getSymbolStyleColors(dataset) - const resolved = selected - ? this.resolveSelected(symbolDef, styleColors, mapStyle) + const resolved = active + ? this.resolveActive(symbolDef, styleColors, mapStyle) : this.resolve(symbolDef, styleColors, mapStyle) - return `symbol-${selected ? 'sel-' : ''}${hashString(resolved)}-${pixelRatio}x` + return `symbol-${active ? 'act-' : ''}${hashString(resolved)}-${pixelRatio}x` }, /** @@ -182,9 +182,9 @@ export const symbolRegistry = { }, /** - * Register normal and selected symbol images for the given pre-resolved symbol configs. + * Register normal, active (both rings) and selected (black ring) symbol images. * Skips images that are already registered (safe to call on style change). - * Updates `map._symbolImageMap` with normal→selected image ID pairs. + * Updates `map._activeSymbolImageMap` (normal→active) and `map._selectedSymbolImageMap` (normal→selected). * * Callers are responsible for resolving sublayers before calling this function * (see `getSymbolConfigs` in the datasets plugin adapter). @@ -200,41 +200,65 @@ export const symbolRegistry = { return } - // Reset the normal→selected image ID lookup so stale entries don't persist after a style change - map._symbolImageMap = {} + map._activeSymbolImageMap = {} + map._selectedSymbolImageMap = {} await Promise.all(styleArray.flatMap(config => { const normalId = this.getSymbolImageId(config, mapStyle, false, pixelRatio) - const selectedId = this.getSymbolImageId(config, mapStyle, true, pixelRatio) - if (normalId && selectedId) { - map._symbolImageMap[normalId] = selectedId + const activeId = this.getSymbolImageId(config, mapStyle, true, pixelRatio) + if (normalId && activeId) { + map._activeSymbolImageMap[normalId] = activeId } - return [false, true].map(async (selected) => { - const imageId = selected ? selectedId : normalId - if (!imageId || map.hasImage(imageId)) { + return ['normal', 'active', 'selected'].map(async (variant) => { + const imageId = variant === 'active' ? activeId : normalId + if (variant !== 'selected' && (!imageId || map.hasImage(imageId))) { return } - const result = await this.rasteriseSymbolImage(config, mapStyle, selected, pixelRatio) - if (result && !map.hasImage(result.imageId)) { - map.addImage(result.imageId, result.imageData, { pixelRatio }) + const result = await this.rasteriseSymbolImage(config, mapStyle, variant, pixelRatio) + if (result) { + if (variant === 'selected' && normalId) { + map._selectedSymbolImageMap[normalId] = result.imageId + } + if (!map.hasImage(result.imageId)) { + map.addImage(result.imageId, result.imageData, { pixelRatio }) + } } }) })) }, - // ─── Rasterisation ──────────────────────────────────────────────────────────── - - async rasteriseSymbolImage (dataset, mapStyle, selected, pixelRatio) { + /** + * Rasterise one variant of a symbol to ImageData for use as a MapLibre image. + * Results are cached by imageId so identical symbols are only rendered once. + * + * @param {Object} dataset - Dataset or marker config with symbol properties + * @param {Object} mapStyle - Current map style config + * @param {'normal'|'active'|'selected'} variant + * - `'normal'` — no rings (default display) + * - `'active'` — both rings (keyboard cursor, yellow + black) + * - `'selected'` — selected ring only (black) + * @param {number} pixelRatio - Device pixel ratio × map size scale factor + * @returns {Promise<{imageId: string, imageData: ImageData}|null>} + */ + async rasteriseSymbolImage (dataset, mapStyle, variant, pixelRatio) { const symbolDef = this.getSymbolDef(dataset) if (!symbolDef) { return null } const styleColors = getSymbolStyleColors(dataset) - const resolvedContent = selected - ? this.resolveSelected(symbolDef, styleColors, mapStyle) - : this.resolve(symbolDef, styleColors, mapStyle) + let resolvedContent, prefix + if (variant === 'active') { + resolvedContent = this.resolveActive(symbolDef, styleColors, mapStyle) + prefix = 'act-' + } else if (variant === 'selected') { + resolvedContent = this.resolveSelected(symbolDef, styleColors, mapStyle) + prefix = 'sel-' + } else { + resolvedContent = this.resolve(symbolDef, styleColors, mapStyle) + prefix = '' + } - const imageId = `symbol-${selected ? 'sel-' : ''}${hashString(resolvedContent)}-${pixelRatio}x` + const imageId = `symbol-${prefix}${hashString(resolvedContent)}-${pixelRatio}x` let imageData = imageDataCache.get(imageId) if (!imageData) { From c28270374f2b6d8e16fcb016804380ef2b8d94ce Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 08:45:36 +0100 Subject: [PATCH 04/11] IM-246 fixed rasteriseToImageData import --- src/services/symbolRegistry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/symbolRegistry.js b/src/services/symbolRegistry.js index e7111eac..a3c70b0b 100644 --- a/src/services/symbolRegistry.js +++ b/src/services/symbolRegistry.js @@ -2,7 +2,7 @@ import { getValueForStyle } from '../utils/getValueForStyle.js' import { symbolDefaults, pin, circle, square, graphics } from '../config/symbolConfig.js' import { getSymbolStyleColors, getSymbolViewBox } from '../utils/symbolUtils.js' import { THEME_COLORS } from '../config/mapTheme.js' -import { rasteriseToImageData } from './rasteriseToImageData.js' +import { rasteriseToImageData } from '../../providers/maplibre/src/utils/rasteriseToImageData.js' const symbols = new Map() // Module-level cache: imageId → ImageData. Avoids re-rasterising identical symbols. From 3a2c46910802e234a0439bae09dc73b30e795635 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 09:04:59 +0100 Subject: [PATCH 05/11] IM-246 moved symbolUtils tests to symbolRegistry.test.js --- src/services/symbolRegistry.js | 32 +++++++++++++++++++++-------- src/services/symbolRegistry.test.js | 31 +++++++++++++++++++++++++++- src/utils/symbolUtils.test.js | 31 ---------------------------- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/services/symbolRegistry.js b/src/services/symbolRegistry.js index a3c70b0b..f2883e01 100644 --- a/src/services/symbolRegistry.js +++ b/src/services/symbolRegistry.js @@ -89,6 +89,22 @@ export const symbolRegistry = { return [...symbols.values()] }, + /** + * Clears all registered symbols (including built-ins). Mainly for testing purposes. + */ + clear () { + symbols.clear() + }, + + /** + * Clears all registered symbols (including built-ins). Mainly for testing purposes. + */ + initialise () { + this.register(pin) + this.register(circle) + this.register(square) + }, + /** * Resolve a symbol's SVG string for normal (unselected, inactive) rendering. * Both selectedColor and activeColor are set to 'none' — all rings hidden. @@ -168,15 +184,15 @@ export const symbolRegistry = { * dataset.symbol is a string symbol ID (e.g. 'pin'). * dataset.symbolSvgContent is inline SVG content for a custom symbol. * - * @param {Object} dataset + * @param {Object} style * @returns {Object|undefined} */ - getSymbolDef (dataset) { - if (dataset.symbolSvgContent) { - return { svg: dataset.symbolSvgContent } + getSymbolDef (style) { + if (style.symbolSvgContent) { + return { svg: style.symbolSvgContent } } - if (dataset.symbol) { - return this.get(dataset.symbol) + if (style.symbol) { + return this.get(style.symbol) } return undefined }, @@ -275,6 +291,4 @@ export const symbolRegistry = { } } -symbolRegistry.register(pin) -symbolRegistry.register(circle) -symbolRegistry.register(square) +symbolRegistry.initialise() diff --git a/src/services/symbolRegistry.test.js b/src/services/symbolRegistry.test.js index 46d14579..4602c0e0 100644 --- a/src/services/symbolRegistry.test.js +++ b/src/services/symbolRegistry.test.js @@ -1,5 +1,5 @@ import { symbolRegistry } from './symbolRegistry.js' -import { symbolDefaults } from '../config/symbolConfig.js' +import { symbolDefaults, pin } from '../config/symbolConfig.js' import { THEME_COLORS } from '../config/mapTheme.js' import { getValueForStyle } from '../utils/getValueForStyle.js' @@ -9,6 +9,8 @@ const COLOR_OVERRIDE = '#ff0000' const FILL_OVERRIDE = `fill="${COLOR_OVERRIDE}"` beforeEach(() => { + symbolRegistry.clear() + symbolRegistry.initialise() symbolRegistry.setDefaults({}) }) @@ -283,3 +285,30 @@ describe('symbolRegistry — graphic token', () => { expect(resolved).toContain('translate(22, 19) scale(0.8) translate(-8, -8)') }) }) + +// ─── getSymbolDef ───────────────────────────────────────────────────────────── + +describe('getSymbolDef', () => { + it('returns undefined when dataset has no symbol', () => { + expect(symbolRegistry.getSymbolDef({})).toBeUndefined() + }) + + it('looks up string symbol id in the registry', () => { + expect(symbolRegistry.getSymbolDef({ symbol: 'pin' })).toBe(pin) + }) + + it('returns undefined for an unregistered string symbol', () => { + expect(symbolRegistry.getSymbolDef({ symbol: 'missing' })).toBeUndefined() + }) + + it('returns inline def from symbolSvgContent with svg key', () => { + const dataset = { symbolSvgContent: '', symbolViewBox: '0 0 10 10' } + const result = symbolRegistry.getSymbolDef(dataset) + expect(result.svg).toBe('') + }) + + it('symbolSvgContent takes precedence over symbol id', () => { + const result = symbolRegistry.getSymbolDef({ symbol: 'pin', symbolSvgContent: '' }) + expect(result.svg).toBe('') + }) +}) diff --git a/src/utils/symbolUtils.test.js b/src/utils/symbolUtils.test.js index 6207749e..398abb60 100644 --- a/src/utils/symbolUtils.test.js +++ b/src/utils/symbolUtils.test.js @@ -60,37 +60,6 @@ describe('isStandaloneLabel', () => { }) }) -// ─── getSymbolDef ───────────────────────────────────────────────────────────── - -describe('getSymbolDef', () => { - it('returns undefined when dataset has no symbol', () => { - expect(getSymbolDef({}, mockRegistry())).toBeUndefined() - }) - - it('looks up string symbol id in the registry', () => { - const pinDef = { id: 'pin', svg: '' } - const registry = mockRegistry({ pin: pinDef }) - expect(getSymbolDef({ symbol: 'pin' }, registry)).toBe(pinDef) - }) - - it('returns undefined for an unregistered string symbol', () => { - expect(getSymbolDef({ symbol: 'missing' }, mockRegistry())).toBeUndefined() - }) - - it('returns inline def from symbolSvgContent with svg key', () => { - const dataset = { symbolSvgContent: '', symbolViewBox: '0 0 10 10' } - const result = getSymbolDef(dataset, mockRegistry()) - expect(result.svg).toBe('') - }) - - it('symbolSvgContent takes precedence over symbol id', () => { - const pinDef = { id: 'pin', svg: '' } - const registry = mockRegistry({ pin: pinDef }) - const result = getSymbolDef({ symbol: 'pin', symbolSvgContent: '' }, registry) - expect(result.svg).toBe('') - }) -}) - // ─── getSymbolStyleColors ───────────────────────────────────────────────────── describe('getSymbolStyleColors', () => { From 6de05f9e5589a8dd331044296e911dd6012a91cd Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 09:06:35 +0100 Subject: [PATCH 06/11] IM-246 changed dataset to style in symbolRegistry.js --- src/services/symbolRegistry.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/services/symbolRegistry.js b/src/services/symbolRegistry.js index f2883e01..01b6b533 100644 --- a/src/services/symbolRegistry.js +++ b/src/services/symbolRegistry.js @@ -160,18 +160,18 @@ export const symbolRegistry = { * Returns a deterministic image ID for a symbol in normal or selected state. * Based on the hash of the fully resolved SVG content and the pixel ratio. * - * @param {Object} dataset + * @param {Object} style * @param {Object} mapStyle - Current map style config (provides id, selectedColor, haloColor) * @param {boolean} [selected=false] * @param {number} [pixelRatio=2] - Device pixel ratio × map size scale factor * @returns {string|null} */ - getSymbolImageId (dataset, mapStyle, active = false, pixelRatio = 2) { - const symbolDef = this.getSymbolDef(dataset) + getSymbolImageId (style, mapStyle, active = false, pixelRatio = 2) { + const symbolDef = this.getSymbolDef(style) if (!symbolDef) { return null } - const styleColors = getSymbolStyleColors(dataset) + const styleColors = getSymbolStyleColors(style) const resolved = active ? this.resolveActive(symbolDef, styleColors, mapStyle) : this.resolve(symbolDef, styleColors, mapStyle) @@ -179,10 +179,10 @@ export const symbolRegistry = { }, /** - * Resolves the symbolDef for a dataset's symbol config. + * Resolves the symbolDef for a style's symbol config. * - * dataset.symbol is a string symbol ID (e.g. 'pin'). - * dataset.symbolSvgContent is inline SVG content for a custom symbol. + * style.symbol is a string symbol ID (e.g. 'pin'). + * style.symbolSvgContent is inline SVG content for a custom symbol. * * @param {Object} style * @returns {Object|undefined} @@ -247,7 +247,7 @@ export const symbolRegistry = { * Rasterise one variant of a symbol to ImageData for use as a MapLibre image. * Results are cached by imageId so identical symbols are only rendered once. * - * @param {Object} dataset - Dataset or marker config with symbol properties + * @param {Object} style - Dataset or marker config with symbol properties * @param {Object} mapStyle - Current map style config * @param {'normal'|'active'|'selected'} variant * - `'normal'` — no rings (default display) @@ -256,12 +256,12 @@ export const symbolRegistry = { * @param {number} pixelRatio - Device pixel ratio × map size scale factor * @returns {Promise<{imageId: string, imageData: ImageData}|null>} */ - async rasteriseSymbolImage (dataset, mapStyle, variant, pixelRatio) { - const symbolDef = this.getSymbolDef(dataset) + async rasteriseSymbolImage (style, mapStyle, variant, pixelRatio) { + const symbolDef = this.getSymbolDef(style) if (!symbolDef) { return null } - const styleColors = getSymbolStyleColors(dataset) + const styleColors = getSymbolStyleColors(style) let resolvedContent, prefix if (variant === 'active') { resolvedContent = this.resolveActive(symbolDef, styleColors, mapStyle) @@ -278,7 +278,7 @@ export const symbolRegistry = { let imageData = imageDataCache.get(imageId) if (!imageData) { - const viewBox = getSymbolViewBox(dataset, symbolDef) + const viewBox = getSymbolViewBox(style, symbolDef) const [,, width, height] = viewBox.split(' ').map(Number) // Render at pixelRatio× to keep icons crisp at the current device DPI and map size. // MapLibre receives the matching pixelRatio so the image displays at its original logical size. From f7456c48e0c1c2e7b497afb7b355d9412ea3045f Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 09:37:09 +0100 Subject: [PATCH 07/11] IM-246 maplibreProvider.test.js verifies actual symbolRegistry use --- .../maplibre/src/maplibreProvider.test.js | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/providers/maplibre/src/maplibreProvider.test.js b/providers/maplibre/src/maplibreProvider.test.js index 9e06f8ce..3f98e897 100644 --- a/providers/maplibre/src/maplibreProvider.test.js +++ b/providers/maplibre/src/maplibreProvider.test.js @@ -4,9 +4,10 @@ import { attachAppEvents } from './appEvents.js' import { createMapLabelNavigator } from './utils/labels.js' import { updateHighlightedFeatures } from './utils/highlightFeatures.js' import { queryFeatures } from './utils/queryFeatures.js' -import { addSymbolsToMap } from './utils/symbolImages.js' import { addPatternsToMap } from './utils/patternImages.js' import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds, isGeometryObscured } from './utils/spatial.js' +import { symbolRegistry } from '../../../src/services/symbolRegistry.js' +const addSymbolsToMapSpy = jest.spyOn(symbolRegistry, 'addSymbolsToMap').mockImplementation(() => Promise.resolve()) jest.mock('./defaults.js', () => ({ DEFAULTS: { animationDuration: 400, coordinatePrecision: 7 }, @@ -35,7 +36,6 @@ jest.mock('./utils/labels.js', () => ({ })) jest.mock('./utils/highlightFeatures.js', () => ({ updateHighlightedFeatures: jest.fn(() => []) })) jest.mock('./utils/queryFeatures.js', () => ({ queryFeatures: jest.fn(() => []) })) -jest.mock('./utils/symbolImages.js', () => ({ addSymbolsToMap: jest.fn(() => Promise.resolve()) })) jest.mock('./utils/patternImages.js', () => ({ addPatternsToMap: jest.fn(() => Promise.resolve()) })) describe('MapLibreProvider', () => { @@ -273,9 +273,8 @@ describe('MapLibreProvider', () => { await doInitMap(p) const configs = [{ symbol: 'pin' }] const mapStyle = { id: 'test', selectedColor: '#0b0c0c' } - const registry = {} - await p.addSymbolsToMap(configs, mapStyle, registry) - expect(addSymbolsToMap).toHaveBeenCalledWith(map, configs, mapStyle, registry, expect.any(Number)) + await p.addSymbolsToMap(configs, mapStyle, symbolRegistry) + expect(addSymbolsToMapSpy).toHaveBeenCalledWith(map, configs, mapStyle, expect.any(Number)) }) test('addSymbolsToMap computes pixelRatio from getPixelRatio and mapSize scale factor', async () => { @@ -283,9 +282,8 @@ describe('MapLibreProvider', () => { await doInitMap(p) map.getPixelRatio.mockReturnValue(2) p.mapSize = 'medium' // scaleFactor['medium'] = 1.5 - const registry = {} - await p.addSymbolsToMap([], { id: 'test' }, registry) - expect(addSymbolsToMap).toHaveBeenCalledWith(map, [], { id: 'test' }, registry, 3) // 2 * 1.5 + await p.addSymbolsToMap([], { id: 'test' }, symbolRegistry) + expect(addSymbolsToMapSpy).toHaveBeenCalledWith(map, [], { id: 'test' }, 3) }) test('addSymbolsToMap falls back to pixelRatio 1 when getPixelRatio returns 0', async () => { @@ -293,9 +291,8 @@ describe('MapLibreProvider', () => { await doInitMap(p) map.getPixelRatio.mockReturnValue(0) p.mapSize = 'small' // scaleFactor['small'] = 1 - const registry = {} - await p.addSymbolsToMap([], { id: 'test' }, registry) - expect(addSymbolsToMap).toHaveBeenCalledWith(map, [], { id: 'test' }, registry, 1) // (0 || 1) * 1 + await p.addSymbolsToMap([], { id: 'test' }, symbolRegistry) + expect(addSymbolsToMapSpy).toHaveBeenCalledWith(map, [], { id: 'test' }, 1) }) test('addPatternsToMap delegates to utility with map instance and pixelRatio', async () => { From c394f50fda37c736afc36270accfde211b650573 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 10:33:27 +0100 Subject: [PATCH 08/11] IM-246 moved symbolImages tests to symbolRegistry.test.js --- .../maplibre/src/utils/symbolImages.test.js | 200 +----------------- src/services/symbolRegistry.test.js | 197 +++++++++++++++++ 2 files changed, 198 insertions(+), 199 deletions(-) diff --git a/providers/maplibre/src/utils/symbolImages.test.js b/providers/maplibre/src/utils/symbolImages.test.js index c3da60e4..d51d7762 100644 --- a/providers/maplibre/src/utils/symbolImages.test.js +++ b/providers/maplibre/src/utils/symbolImages.test.js @@ -1,30 +1,6 @@ -import { anchorToMaplibre, getSymbolImageId, addSymbolsToMap } from './symbolImages.js' +import { anchorToMaplibre } from './symbolImages.js' import { symbolRegistry } from '../../../../src/services/symbolRegistry.js' -const STYLE_ID = 'test' -const mapStyle = { id: STYLE_ID } - -beforeAll(() => { - globalThis.URL.createObjectURL = jest.fn(() => 'blob:mock') - globalThis.URL.revokeObjectURL = jest.fn() - - HTMLCanvasElement.prototype.getContext = jest.fn(() => ({ - drawImage: jest.fn(), - getImageData: jest.fn((_x, _y, w, h) => ({ width: w, height: h })) - })) - - globalThis.Image = class { - constructor (w, h) { - this.width = w - this.height = h - this._src = '' - } - - get src () { return this._src } - set src (val) { this._src = val; this.onload?.() } - } -}) - beforeEach(() => { symbolRegistry.setDefaults({}) }) @@ -86,177 +62,3 @@ describe('anchorToMaplibre', () => { expect(anchorToMaplibre([0.5, 0.75])).toBe('bottom') // NOSONAR S109 — ANCHOR_HIGH boundary }) }) - -// ─── getSymbolImageId ───────────────────────────────────────────────────────── - -describe('getSymbolImageId', () => { - it('returns null when dataset has no symbol', () => { - expect(getSymbolImageId({}, mapStyle, symbolRegistry)).toBeNull() - }) - - it('returns null for an unregistered symbol id', () => { - expect(getSymbolImageId({ symbol: 'does-not-exist' }, mapStyle, symbolRegistry)).toBeNull() - }) - - it('returns a string prefixed symbol- for normal state', () => { - const id = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry) - expect(typeof id).toBe('string') - expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/) - }) - - it('returns a string prefixed symbol-act- for active state', () => { - const id = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, true) - expect(typeof id).toBe('string') - expect(id).toMatch(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/) - }) - - it('normal and active ids differ for the same dataset', () => { - const normalId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, false) - const activeId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, true) - expect(normalId).not.toBe(activeId) - }) - - it('same dataset and style always produces the same id', () => { - const id1 = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry) - const id2 = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry) - expect(id1).toBe(id2) - }) - - it('different symbols produce different ids', () => { - const pinId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry) - const circleId = getSymbolImageId({ symbol: 'circle' }, mapStyle, symbolRegistry) - expect(pinId).not.toBe(circleId) - }) - - it('different backgrounds produce different ids', () => { - const redId = getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#ff0000' }, mapStyle, symbolRegistry) - const blueId = getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#0000ff' }, mapStyle, symbolRegistry) - expect(redId).not.toBe(blueId) - }) - - it('resolves inline symbolSvgContent', () => { - const dataset = { - symbolSvgContent: '', - symbolViewBox: '0 0 38 38', - symbolAnchor: [0.5, 0.5] - } - const id = getSymbolImageId(dataset, mapStyle, symbolRegistry) - expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/) - }) -}) - -// ─── addSymbolsToMap ────────────────────────────────────────────────────────── - -const makeMap = (existingIds = []) => ({ - _activeSymbolImageMap: {}, - _selectedSymbolImageMap: {}, - hasImage: jest.fn((id) => existingIds.includes(id)), - addImage: jest.fn() -}) - -describe('addSymbolsToMap — registration', () => { - it('returns early and does not touch map for empty configs', async () => { - const map = makeMap() - await addSymbolsToMap(map, [], mapStyle, symbolRegistry) - expect(map.hasImage).not.toHaveBeenCalled() - expect(map.addImage).not.toHaveBeenCalled() - }) - - it('resets _activeSymbolImageMap and _selectedSymbolImageMap before processing', async () => { - const map = makeMap() - map._activeSymbolImageMap = { stale: 'entry' } - map._selectedSymbolImageMap = { stale: 'entry' } - await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry) - expect(map._activeSymbolImageMap).not.toHaveProperty('stale') - expect(map._selectedSymbolImageMap).not.toHaveProperty('stale') - }) - - it('calls addImage for normal, active and selected variants', async () => { - const map = makeMap() - await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry) - expect(map.addImage).toHaveBeenCalledTimes(3) // NOSONAR S109 — normal, active, selected - expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 }) - expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 }) - expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 }) - }) - - it('populates _activeSymbolImageMap and _selectedSymbolImageMap with normal → variant id pairs', async () => { - const map = makeMap() - await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry) - const normalId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, false) - const activeId = getSymbolImageId({ symbol: 'pin' }, mapStyle, symbolRegistry, true) - const selectedId = map._selectedSymbolImageMap[normalId] - expect(map._activeSymbolImageMap[normalId]).toBe(activeId) - expect(selectedId).toMatch(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/) - }) - - it('skips addImage when all three variant images are already registered', async () => { - // Run once to discover the selected image ID (not derivable without rasterising) - const setupMap = makeMap() - await addSymbolsToMap(setupMap, [{ symbol: 'circle' }], mapStyle, symbolRegistry) - const normalId = getSymbolImageId({ symbol: 'circle' }, mapStyle, symbolRegistry, false) - const activeId = getSymbolImageId({ symbol: 'circle' }, mapStyle, symbolRegistry, true) - const selectedId = setupMap._selectedSymbolImageMap[normalId] - - const map = makeMap([normalId, activeId, selectedId]) - await addSymbolsToMap(map, [{ symbol: 'circle' }], mapStyle, symbolRegistry) - expect(map.addImage).not.toHaveBeenCalled() - }) - - it('processes multiple configs independently', async () => { - const map = makeMap() - await addSymbolsToMap(map, [{ symbol: 'pin' }, { symbol: 'circle' }], mapStyle, symbolRegistry) - expect(map.addImage).toHaveBeenCalledTimes(6) // NOSONAR S109 — 2 configs × 3 variants each - expect(Object.keys(map._activeSymbolImageMap)).toHaveLength(2) - expect(Object.keys(map._selectedSymbolImageMap)).toHaveLength(2) - }) -}) - -describe('addSymbolsToMap — null results and caching', () => { - it('does not call addImage when rasteriseSymbolImage returns null', async () => { - // getSymbolImageId (called twice — normal + active) needs a real symbolDef to produce imageIds, - // but rasteriseSymbolImage must get undefined from getSymbolDef so it returns null. - // The registry.get call order: [1] getSymbolImageId normal, [2] getSymbolImageId active, - // [3] rasteriseSymbolImage normal, [4] rasteriseSymbolImage active, [5] rasteriseSymbolImage selected. - const pinDef = symbolRegistry.get('pin') - const getSpy = jest.spyOn(symbolRegistry, 'get') - .mockReturnValueOnce(pinDef) - .mockReturnValueOnce(pinDef) - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(undefined) - .mockReturnValueOnce(undefined) - const map = makeMap() - await addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle, symbolRegistry) - expect(map.addImage).not.toHaveBeenCalled() - getSpy.mockRestore() - }) - - it('skips config when symbolDef cannot be resolved', async () => { - const map = makeMap() - await addSymbolsToMap(map, [{ symbol: 'no-such-symbol' }], mapStyle, symbolRegistry) - expect(map.addImage).not.toHaveBeenCalled() - expect(map._activeSymbolImageMap).toEqual({}) - expect(map._selectedSymbolImageMap).toEqual({}) - }) - - it('reuses cached imageData when called again with the same pixelRatio', async () => { - // Use an unusual ratio so this test owns its cache entries - const uniqueRatio = 7 - - const map1 = makeMap() - const getContextCallsBefore = HTMLCanvasElement.prototype.getContext.mock.calls.length - await addSymbolsToMap(map1, [{ symbol: 'pin' }], mapStyle, symbolRegistry, uniqueRatio) - const getContextCallsAfterFirst = HTMLCanvasElement.prototype.getContext.mock.calls.length - // Rasterisation ran — canvas was used - expect(getContextCallsAfterFirst).toBeGreaterThan(getContextCallsBefore) - - // Second call with a fresh map (hasImage → false) but same ratio → cache hit - const map2 = makeMap() - await addSymbolsToMap(map2, [{ symbol: 'pin' }], mapStyle, symbolRegistry, uniqueRatio) - const getContextCallsAfterSecond = HTMLCanvasElement.prototype.getContext.mock.calls.length - // No new canvas — rasterisation was skipped via cache - expect(getContextCallsAfterSecond).toBe(getContextCallsAfterFirst) - // addImage still called because map2 has no pre-registered images - expect(map2.addImage).toHaveBeenCalledTimes(3) // NOSONAR S109 — normal, active, selected - }) -}) diff --git a/src/services/symbolRegistry.test.js b/src/services/symbolRegistry.test.js index 4602c0e0..08efa7ec 100644 --- a/src/services/symbolRegistry.test.js +++ b/src/services/symbolRegistry.test.js @@ -8,6 +8,27 @@ const mapStyle = { id: STYLE_ID } const COLOR_OVERRIDE = '#ff0000' const FILL_OVERRIDE = `fill="${COLOR_OVERRIDE}"` +beforeAll(() => { + globalThis.URL.createObjectURL = jest.fn(() => 'blob:mock') + globalThis.URL.revokeObjectURL = jest.fn() + + HTMLCanvasElement.prototype.getContext = jest.fn(() => ({ + drawImage: jest.fn(), + getImageData: jest.fn((_x, _y, w, h) => ({ width: w, height: h })) + })) + + globalThis.Image = class { + constructor (w, h) { + this.width = w + this.height = h + this._src = '' + } + + get src () { return this._src } + set src (val) { this._src = val; this.onload?.() } + } +}) + beforeEach(() => { symbolRegistry.clear() symbolRegistry.initialise() @@ -312,3 +333,179 @@ describe('getSymbolDef', () => { expect(result.svg).toBe('') }) }) + +// ─── getSymbolImageId ───────────────────────────────────────────────────────── + +describe('getSymbolImageId', () => { + it('returns null when dataset has no symbol', () => { + expect(symbolRegistry.getSymbolImageId({}, mapStyle)).toBeNull() + }) + + it('returns null for an unregistered symbol id', () => { + expect(symbolRegistry.getSymbolImageId({ symbol: 'does-not-exist' }, mapStyle)).toBeNull() + }) + + it('returns a string prefixed symbol- for normal state', () => { + const id = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle) + expect(typeof id).toBe('string') + expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/) + }) + + it('returns a string prefixed symbol-act- for active state', () => { + const id = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true) + expect(typeof id).toBe('string') + expect(id).toMatch(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/) + }) + + it('normal and active ids differ for the same dataset', () => { + const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false) + const activeId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true) + expect(normalId).not.toBe(activeId) + }) + + it('same dataset and style always produces the same id', () => { + const id1 = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle) + const id2 = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle) + expect(id1).toBe(id2) + }) + + it('different symbols produce different ids', () => { + const pinId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle) + const circleId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle) + expect(pinId).not.toBe(circleId) + }) + + it('different backgrounds produce different ids', () => { + const redId = symbolRegistry.getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#ff0000' }, mapStyle) + const blueId = symbolRegistry.getSymbolImageId({ symbol: 'pin', symbolBackgroundColor: '#0000ff' }, mapStyle) + expect(redId).not.toBe(blueId) + }) + + it('resolves inline symbolSvgContent', () => { + const dataset = { + symbolSvgContent: '', + symbolViewBox: '0 0 38 38', + symbolAnchor: [0.5, 0.5] + } + const id = symbolRegistry.getSymbolImageId(dataset, mapStyle) + expect(id).toMatch(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/) + }) +}) + +describe('addSymbolsToMap', () => { + const makeMap = (existingIds = []) => ({ + _activeSymbolImageMap: {}, + _selectedSymbolImageMap: {}, + hasImage: jest.fn((id) => existingIds.includes(id)), + addImage: jest.fn() + }) + + describe('registration', () => { + it('returns early and does not touch map for empty configs', async () => { + const map = makeMap() + await symbolRegistry.addSymbolsToMap(map, [], mapStyle) + expect(map.hasImage).not.toHaveBeenCalled() + expect(map.addImage).not.toHaveBeenCalled() + }) + + it('resets _activeSymbolImageMap and _selectedSymbolImageMap before processing', async () => { + const map = makeMap() + map._activeSymbolImageMap = { stale: 'entry' } + map._selectedSymbolImageMap = { stale: 'entry' } + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle) + expect(map._activeSymbolImageMap).not.toHaveProperty('stale') + expect(map._selectedSymbolImageMap).not.toHaveProperty('stale') + }) + + it('calls addImage for normal, active and selected variants', async () => { + const map = makeMap() + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle) + expect(map.addImage).toHaveBeenCalledTimes(3) // NOSONAR S109 — normal, active, selected + expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 }) + expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-act-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 }) + expect(map.addImage).toHaveBeenCalledWith(expect.stringMatching(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/), expect.any(Object), { pixelRatio: 2 }) + }) + + it('populates _activeSymbolImageMap and _selectedSymbolImageMap with normal → variant id pairs', async () => { + const map = makeMap() + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle) + const normalId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, false) + const activeId = symbolRegistry.getSymbolImageId({ symbol: 'pin' }, mapStyle, true) + const selectedId = map._selectedSymbolImageMap[normalId] + expect(map._activeSymbolImageMap[normalId]).toBe(activeId) + expect(selectedId).toMatch(/^symbol-sel-[a-z0-9]+-\d+(\.\d+)?x$/) + }) + + it('skips addImage when all three variant images are already registered', async () => { + // Run once to discover the selected image ID (not derivable without rasterising) + const setupMap = makeMap() + await symbolRegistry.addSymbolsToMap(setupMap, [{ symbol: 'circle' }], mapStyle) + const normalId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle, false) + const activeId = symbolRegistry.getSymbolImageId({ symbol: 'circle' }, mapStyle, true) + const selectedId = setupMap._selectedSymbolImageMap[normalId] + + const map = makeMap([normalId, activeId, selectedId]) + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'circle' }], mapStyle) + expect(map.addImage).not.toHaveBeenCalled() + }) + + it('processes multiple configs independently', async () => { + const map = makeMap() + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'pin' }, { symbol: 'circle' }], mapStyle, symbolRegistry) + expect(map.addImage).toHaveBeenCalledTimes(6) // NOSONAR S109 — 2 configs × 3 variants each + expect(Object.keys(map._activeSymbolImageMap)).toHaveLength(2) + expect(Object.keys(map._selectedSymbolImageMap)).toHaveLength(2) + }) + }) + + describe('null results and caching', () => { + it('does not call addImage when rasteriseSymbolImage returns null', async () => { + // getSymbolImageId (called twice — normal + active) needs a real symbolDef to produce imageIds, + // but rasteriseSymbolImage must get undefined from getSymbolDef so it returns null. + // The registry.get call order: [1] getSymbolImageId normal, [2] getSymbolImageId active, + // [3] rasteriseSymbolImage normal, [4] rasteriseSymbolImage active, [5] rasteriseSymbolImage selected. + const pinDef = symbolRegistry.get('pin') + const getSpy = jest.spyOn(symbolRegistry, 'get') + .mockReturnValueOnce(pinDef) + .mockReturnValueOnce(pinDef) + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(undefined) + .mockReturnValueOnce(undefined) + const map = makeMap() + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'pin' }], mapStyle) + expect(map.addImage).not.toHaveBeenCalled() + getSpy.mockRestore() + }) + + it('skips config when symbolDef cannot be resolved', async () => { + const map = makeMap() + await symbolRegistry.addSymbolsToMap(map, [{ symbol: 'no-such-symbol' }], mapStyle) + expect(map.addImage).not.toHaveBeenCalled() + expect(map._activeSymbolImageMap).toEqual({}) + expect(map._selectedSymbolImageMap).toEqual({}) + }) + + it('reuses cached imageData when called again with the same pixelRatio', async () => { + // Use an unusual ratio so this test owns its cache entries + const uniqueRatio = 7 + symbolRegistry.clear() + symbolRegistry.initialise() + + const map1 = makeMap() + const getContextCallsBefore = HTMLCanvasElement.prototype.getContext.mock.calls.length + await symbolRegistry.addSymbolsToMap(map1, [{ symbol: 'pin' }], mapStyle, uniqueRatio) + const getContextCallsAfterFirst = HTMLCanvasElement.prototype.getContext.mock.calls.length + // Rasterisation ran — canvas was used + expect(getContextCallsAfterFirst).toBeGreaterThan(getContextCallsBefore) + + // Second call with a fresh map (hasImage → false) but same ratio → cache hit + const map2 = makeMap() + await symbolRegistry.addSymbolsToMap(map2, [{ symbol: 'pin' }], mapStyle, uniqueRatio) + const getContextCallsAfterSecond = HTMLCanvasElement.prototype.getContext.mock.calls.length + // No new canvas — rasterisation was skipped via cache + expect(getContextCallsAfterSecond).toBe(getContextCallsAfterFirst) + // addImage still called because map2 has no pre-registered images + expect(map2.addImage).toHaveBeenCalledTimes(3) // NOSONAR S109 — normal, active, selected + }) + }) +}) From 7cd135989ff13871e8ec7f89ad1244f234c127b0 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 10:35:24 +0100 Subject: [PATCH 09/11] IM-246 lint fixes --- src/utils/symbolUtils.test.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/utils/symbolUtils.test.js b/src/utils/symbolUtils.test.js index 398abb60..f5d49538 100644 --- a/src/utils/symbolUtils.test.js +++ b/src/utils/symbolUtils.test.js @@ -1,16 +1,11 @@ import { hasSymbol, isStandaloneLabel, - getSymbolDef, getSymbolStyleColors, getSymbolViewBox, getSymbolAnchor } from './symbolUtils.js' -const mockRegistry = (defs = {}) => ({ - get: jest.fn((id) => defs[id]) -}) - // ─── hasSymbol ──────────────────────────────────────────────────────────────── describe('hasSymbol', () => { From d10c6894d1ccb2edacd2a6a1ca018efefc42ae46 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 10:47:32 +0100 Subject: [PATCH 10/11] IM-246 gixed unused export and demo map centres on Appleby --- demo/js/index.js | 21 +++++++++++++++++-- .../src/adapters/maplibre/symbolImages.js | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/demo/js/index.js b/demo/js/index.js index 846216a5..c6b0a53f 100755 --- a/demo/js/index.js +++ b/demo/js/index.js @@ -26,7 +26,24 @@ import searchPlugin from '/plugins/search/src/index.js' import createInteractPlugin from '/plugins/interact/src/index.js' import createFramePlugin from '/plugins/beta/frame/src/index.js' -const pointData = {type: 'FeatureCollection',features: [{type: 'Feature',properties: { category:'prehistoric', name: 'Prehistoric feature' }, geometry: { coordinates: [-2.4558622,54.5617135], type: 'Point' }},{ type: 'Feature', properties: { category: 'roman', name: 'Roman feature' }, geometry: { coordinates: [-2.439823,54.5525437], type: 'Point' }},{ type: 'Feature', properties: { category:'medieval', name: 'Medieval feature' }, geometry: { coordinates: [-2.4481939,54.5575261], type: 'Point'} }]} +const pointData = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: { category:'prehistoric', name: 'Prehistoric feature' }, + geometry: { coordinates: [-2.4558622,54.5617135], type: 'Point' } + }, + { + type: 'Feature', + properties: { category: 'roman', name: 'Roman feature' }, + geometry: { coordinates: [-2.439823,54.5525437], type: 'Point' } + }, + { + type: 'Feature', + properties: { category:'medieval', name: 'Medieval feature' }, + geometry: { coordinates: [-2.4481939,54.5575261], type: 'Point'} + }] +} const interactPlugin = createInteractPlugin({ layers: [{ @@ -257,7 +274,7 @@ const interactiveMap = new InteractiveMap('map', { maxZoom: 20, autoColorScheme: true, // center: [-2.938769, 54.893806], - bounds: [-2.989707, 54.864555, -2.878635, 54.937635], + bounds: [-2.450804, 54.5599279, -2.403804, 54.6199279], containerHeight: '650px', transformRequest: transformTileRequest, readMapText: true, diff --git a/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js b/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js index 3288e020..bb6ebc9a 100644 --- a/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +++ b/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js @@ -2,7 +2,7 @@ import { hasSymbol } from '../../../../../../src/utils/symbolUtils.js' import { mergeSublayer } from '../../utils/mergeSublayer.js' -export { hasSymbol, getSymbolDef, getSymbolStyleColors, getSymbolViewBox, getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js' +export { hasSymbol, getSymbolStyleColors, getSymbolViewBox, getSymbolAnchor } from '../../../../../../src/utils/symbolUtils.js' // Re-export MapLibre-specific symbol utilities from the provider. // This is the single cross-boundary import in the adapter; in a separate-package From d9b7d7bd98c90a16cb8ecfca0efdd3fab596ccc1 Mon Sep 17 00:00:00 2001 From: Mark Fee Date: Fri, 1 May 2026 10:56:54 +0100 Subject: [PATCH 11/11] IM-246 enabed historic and roman in demo interact --- demo/js/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/demo/js/index.js b/demo/js/index.js index c6b0a53f..a63d48cb 100755 --- a/demo/js/index.js +++ b/demo/js/index.js @@ -48,9 +48,11 @@ const pointData = { const interactPlugin = createInteractPlugin({ layers: [{ layerId: 'historic-monuments-prehistoric-symbol', - // labelProperty: 'name' - // idProperty: 'gid' - },{ + }, { + layerId: 'historic-monuments-roman-symbol', + }, { + layerId: 'historic-monuments-medieval-symbol', + }, { layerId: 'land-covers-110', // labelProperty: 'gid' // idProperty: 'gid'