@defra/interactive-map 0.0.40-alpha → 0.0.42-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +7 -8
- package/docs/api/control-definition.md +17 -1
- package/docs/api/panel-definition.md +5 -0
- package/docs/api/slots.md +50 -6
- package/docs/assets/images/slot-map.svg +61 -262
- package/docs/examples/add-polygons.mdx +8 -2
- package/docs/examples/add-symbols.mdx +8 -2
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/datasets.md +23 -15
- package/docs/plugins/draw.md +79 -16
- package/docs/plugins/map-key.md +147 -0
- package/docs/plugins/search.md +1 -1
- package/docs/plugins.md +5 -1
- package/govuk-prototype-kit.config.json +2 -0
- package/jest.setup.js +5 -0
- package/package.json +9 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/umd/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/index.js +1 -1
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +1 -1
- package/plugins/datasets/dist/css/index.css +1 -1
- package/plugins/datasets/dist/esm/esriLayerAdapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-esri-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-ml-adapter.js +1 -1
- package/plugins/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/datasets/dist/umd/index.js +1 -1
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +14 -0
- package/plugins/datasets/src/api/setOpacity.test.js +9 -0
- package/plugins/datasets/src/components/LayersMenu/LayersMenu.jsx +10 -25
- package/plugins/datasets/src/components/LayersMenu/LayersMenu.test.jsx +0 -42
- package/plugins/datasets/src/datasets.scss +0 -1
- package/plugins/datasets/src/fetch/createDynamicSource.js +135 -129
- package/plugins/datasets/src/fetch/createDynamicSource.test.js +318 -0
- package/plugins/datasets/src/fetch/fetchGeoJSON.test.js +83 -0
- package/plugins/datasets/src/index.test.js +36 -0
- package/plugins/datasets/src/initialise/DatasetsInit.jsx +17 -7
- package/plugins/datasets/src/initialise/DatasetsInit.test.jsx +262 -0
- package/plugins/datasets/src/initialise/initialiseDatasets.js +16 -4
- package/plugins/datasets/src/initialise/initialiseDatasets.test.js +42 -0
- package/plugins/datasets/src/manifest.js +0 -38
- package/plugins/datasets/src/manifest.test.js +49 -0
- package/plugins/datasets/src/reducers/datasetsToMenu.test.js +54 -1
- package/plugins/datasets/src/reducers/pluginState.js +3 -16
- package/plugins/datasets/src/reducers/pluginState.test.js +23 -0
- package/plugins/datasets/src/registry/dataset.js +13 -1
- package/plugins/datasets/src/registry/dataset.test.js +61 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +12 -6
- package/plugins/datasets/src/registry/datasetRegistry.test.js +16 -4
- package/plugins/datasets/src/registry/isVisibleWhen.js +6 -3
- package/plugins/datasets/src/registry/isVisibleWhen.test.js +11 -1
- package/plugins/datasets/src/utils/bbox.test.js +125 -0
- package/plugins/draw/dist/esm/im-draw-ml-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/esm/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/esm/index.js +1 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js +2 -1
- package/plugins/draw/dist/umd/im-draw-ml-adapter.js.LICENSE.txt +1 -0
- package/plugins/draw/dist/umd/im-draw-ol-adapter.js +1 -1
- package/plugins/draw/dist/umd/im-draw-plugin.js +1 -1
- package/plugins/draw/dist/umd/index.js +1 -1
- package/plugins/draw/src/DrawInit.jsx +10 -2
- package/plugins/draw/src/DrawInit.test.jsx +51 -10
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +91 -11
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.test.js +246 -4
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.js +109 -14
- package/plugins/draw/src/adapters/maplibre/mapboxDraw.test.js +174 -11
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/clickHandlers.test.js +12 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.js +9 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/lifecycle.test.js +24 -3
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.js +194 -0
- package/plugins/draw/src/adapters/maplibre/modes/drawPointMode.test.js +457 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/__helpers__/harness.js +102 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.js +82 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/keyboardHandlers.test.js +83 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.js +55 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointOperations.test.js +62 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.js +138 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/pointerHandlers.test.js +197 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.js +92 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/touchHandlers.test.js +101 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.js +46 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode/undoHandlers.test.js +42 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.js +151 -0
- package/plugins/draw/src/adapters/maplibre/modes/editPointMode.test.js +156 -0
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.js +4 -27
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/keyboardHandlers.test.js +24 -39
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/pointerHandlers.js +4 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.js +20 -23
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/touchHandlers.test.js +25 -5
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.js +6 -36
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/undoHandlers.test.js +7 -49
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.js +3 -53
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode/vertexOperations.test.js +8 -33
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.js +29 -72
- package/plugins/draw/src/adapters/maplibre/modes/editVertexMode.test.js +10 -3
- package/plugins/draw/src/adapters/maplibre/pointSymbolImages.js +158 -0
- package/plugins/draw/src/adapters/maplibre/pointSymbolImages.test.js +314 -0
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.js +6 -1
- package/plugins/draw/src/adapters/maplibre/snap/prototypePatches.test.js +21 -0
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.js +6 -2
- package/plugins/draw/src/adapters/maplibre/snap/snapInstance.test.js +12 -0
- package/plugins/draw/src/adapters/maplibre/styles.js +38 -11
- package/plugins/draw/src/adapters/maplibre/styles.test.js +39 -0
- package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.js +79 -0
- package/plugins/draw/src/adapters/maplibre/utils/editModeEvents.test.js +135 -0
- package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.js +42 -0
- package/plugins/draw/src/adapters/maplibre/utils/geometryValidation.test.js +85 -0
- package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.js +34 -0
- package/plugins/draw/src/adapters/maplibre/utils/keyboardShortcuts.test.js +78 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.js +16 -3
- package/plugins/draw/src/adapters/maplibre/utils/snapHelpers.test.js +21 -3
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.js +59 -0
- package/plugins/draw/src/adapters/maplibre/utils/snapMovement.test.js +73 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.js +33 -0
- package/plugins/draw/src/adapters/maplibre/utils/touchDragMath.test.js +70 -0
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.js +27 -2
- package/plugins/draw/src/adapters/openlayers/OLDrawAdapter.test.js +93 -0
- package/plugins/draw/src/adapters/openlayers/__helpers__/harness.js +9 -0
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.js +19 -1
- package/plugins/draw/src/adapters/openlayers/core/OLDrawManager.test.js +34 -1
- package/plugins/draw/src/adapters/openlayers/core/styles.js +73 -1
- package/plugins/draw/src/adapters/openlayers/core/styles.test.js +122 -3
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +12 -0
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +8 -2
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +31 -0
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +33 -0
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.js +5 -3
- package/plugins/draw/src/adapters/openlayers/edit/modifyInteraction.test.js +11 -0
- package/plugins/draw/src/adapters/openlayers/edit/nudge.js +10 -3
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +24 -1
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.js +18 -13
- package/plugins/draw/src/adapters/openlayers/edit/touchHandler.test.js +46 -1
- package/plugins/draw/src/adapters/openlayers/olDraw.js +11 -4
- package/plugins/draw/src/adapters/openlayers/olDraw.test.js +48 -6
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.js +156 -0
- package/plugins/draw/src/adapters/openlayers/point/drawPointMode.test.js +246 -0
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.js +267 -0
- package/plugins/draw/src/adapters/openlayers/point/editPointMode.test.js +327 -0
- package/plugins/draw/src/adapters/openlayers/point/pointDragInteraction.js +71 -0
- package/plugins/draw/src/adapters/openlayers/point/pointOps.js +34 -0
- package/plugins/draw/src/adapters/openlayers/point/pointOps.test.js +32 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.js +82 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSelectionState.test.js +107 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.js +83 -0
- package/plugins/draw/src/adapters/openlayers/point/pointSymbolImages.test.js +214 -0
- package/plugins/draw/src/adapters/openlayers/snap/snapIndicator.js +3 -1
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.js +6 -9
- package/plugins/draw/src/adapters/openlayers/snap/snapInteraction.test.js +15 -4
- package/plugins/draw/src/adapters/openlayers/utils/olCoords.js +1 -1
- package/plugins/draw/src/api/addFeature.js +16 -1
- package/plugins/draw/src/api/addFeature.test.js +62 -5
- package/plugins/draw/src/api/createNewShape.js +41 -0
- package/plugins/draw/src/api/createNewShape.test.js +63 -0
- package/plugins/draw/src/api/editFeature.js +7 -4
- package/plugins/draw/src/api/editFeature.test.js +16 -0
- package/plugins/draw/src/api/newLine.js +2 -40
- package/plugins/draw/src/api/newPoint.js +42 -0
- package/plugins/draw/src/api/newPoint.test.js +117 -0
- package/plugins/draw/src/api/newPolygon.js +2 -40
- package/plugins/draw/src/api/setStyle.js +43 -0
- package/plugins/draw/src/api/setStyle.test.js +75 -0
- package/plugins/draw/src/events.js +17 -10
- package/plugins/draw/src/events.test.js +14 -0
- package/plugins/draw/src/manifest.js +18 -6
- package/plugins/draw/src/manifest.test.js +32 -1
- package/plugins/draw/src/reducer.test.js +7 -0
- package/plugins/draw/src/utils/stripInternalProperties.js +19 -0
- package/plugins/draw/src/utils/stripInternalProperties.test.js +50 -0
- package/plugins/draw/src/utils/symbolKeys.js +5 -0
- package/plugins/draw/src/validation/liveDrawChecks.test.js +54 -0
- package/plugins/draw/src/validation/liveStroke.test.js +64 -1
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/umd/index.js +1 -1
- package/plugins/interact/src/hooks/useHighlightSync.js +33 -12
- package/plugins/interact/src/hooks/useHighlightSync.test.js +96 -25
- package/plugins/interact/src/manifest.js +1 -1
- package/plugins/interact/src/utils/featureQueries.js +18 -0
- package/plugins/interact/src/utils/featureQueries.test.js +15 -0
- package/plugins/interact/src/utils/spatial.js +9 -3
- package/plugins/interact/src/utils/spatial.test.js +12 -1
- package/plugins/map-key/dist/css/index.css +68 -0
- package/plugins/map-key/dist/esm/im-map-key-plugin.js +1 -0
- package/plugins/map-key/dist/esm/index.js +1 -0
- package/plugins/map-key/dist/umd/im-map-key-plugin.js +2 -0
- package/plugins/map-key/dist/umd/im-map-key-plugin.js.LICENSE.txt +1 -0
- package/plugins/map-key/dist/umd/index.js +2 -0
- package/plugins/map-key/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/map-key/package.json +28 -0
- package/plugins/map-key/src/components/Key/EmptyKey.jsx +7 -0
- package/plugins/{datasets → map-key}/src/components/Key/EmptyKey.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/Key.jsx +43 -0
- package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +21 -14
- package/plugins/map-key/src/components/Key/Key.test.jsx +73 -0
- package/plugins/map-key/src/components/Key/KeyGroupItem.jsx +16 -0
- package/plugins/map-key/src/components/Key/KeyGroupItem.test.jsx +53 -0
- package/plugins/map-key/src/components/Key/KeyItem.jsx +26 -0
- package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
- package/plugins/map-key/src/components/Key/KeySvg.jsx +41 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
- package/plugins/map-key/src/components/Key/KeySvgLine.jsx +22 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.jsx +7 -2
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +8 -2
- package/plugins/map-key/src/components/Key/KeySvgRect.jsx +26 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.jsx +8 -3
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +17 -6
- package/plugins/map-key/src/components/Key/MapKey.jsx +53 -0
- package/plugins/map-key/src/components/Key/MapKey.test.jsx +147 -0
- package/plugins/{datasets → map-key}/src/components/Key/svgProperties.js +2 -2
- package/plugins/{datasets → map-key}/src/components/Key/svgProperties.test.js +2 -2
- package/plugins/map-key/src/index.js +13 -0
- package/plugins/map-key/src/index.test.js +31 -0
- package/plugins/map-key/src/initialise/MapKeyInit.jsx +15 -0
- package/plugins/map-key/src/initialise/MapKeyInit.test.jsx +39 -0
- package/plugins/map-key/src/manifest.js +30 -0
- package/plugins/map-key/src/manifest.test.js +63 -0
- package/plugins/map-key/src/mapKey.scss +4 -0
- package/plugins/map-key/src/registry/getDatasetRegistry.js +6 -0
- package/plugins/map-key/src/registry/getDatasetRegistry.test.js +23 -0
- package/plugins/map-key/src/registry/index.js +3 -0
- package/plugins/map-key/src/registry/index.test.js +15 -0
- package/plugins/menu/dist/css/index.css +80 -0
- package/plugins/menu/dist/esm/im-menu-plugin.js +1 -0
- package/plugins/menu/dist/esm/index.js +1 -0
- package/plugins/menu/dist/umd/im-menu-plugin.js +1 -0
- package/plugins/menu/dist/umd/index.js +2 -0
- package/plugins/menu/dist/umd/index.js.LICENSE.txt +1 -0
- package/plugins/menu/package.json +28 -0
- package/plugins/menu/src/components/Menu/CheckboxGroupWrapper.jsx +21 -0
- package/plugins/menu/src/components/Menu/GroupLegend.jsx +17 -0
- package/plugins/menu/src/components/Menu/GroupLegend.test.jsx +68 -0
- package/plugins/menu/src/components/Menu/Menu.jsx +19 -0
- package/plugins/menu/src/components/Menu/Menu.module.scss +90 -0
- package/plugins/menu/src/components/Menu/Menu.test.jsx +161 -0
- package/plugins/menu/src/components/Menu/MenuCheckbox.jsx +35 -0
- package/plugins/menu/src/components/Menu/MenuCheckbox.test.jsx +93 -0
- package/plugins/menu/src/components/Menu/MenuGroup.jsx +11 -0
- package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.jsx → menu/src/components/Menu/MenuRadio.jsx} +6 -4
- package/plugins/{datasets/src/components/LayersMenu/LayersMenuRadio.test.jsx → menu/src/components/Menu/MenuRadio.test.jsx} +18 -18
- package/plugins/menu/src/components/Menu/RadioGroupWrapper.jsx +32 -0
- package/plugins/{datasets/src/components/LayersMenu/LayersRadioGroupWrapper.test.jsx → menu/src/components/Menu/RadioGroupWrapper.test.jsx} +21 -21
- package/plugins/menu/src/components/Menu/useIdPrefix.jsx +4 -0
- package/plugins/menu/src/index.js +13 -0
- package/plugins/menu/src/index.test.js +31 -0
- package/plugins/menu/src/initialise/MenuInit.jsx +24 -0
- package/plugins/menu/src/initialise/MenuInit.test.jsx +50 -0
- package/plugins/menu/src/initialise/createMenu.js +10 -0
- package/plugins/menu/src/initialise/createMenu.test.js +37 -0
- package/plugins/menu/src/manifest.js +35 -0
- package/plugins/menu/src/manifest.test.js +63 -0
- package/plugins/menu/src/menu.scss +4 -0
- package/plugins/menu/src/reducers/menuStateReducer.test.js +38 -0
- package/plugins/menu/src/reducers/pluginState.js +35 -0
- package/plugins/menu/src/reducers/pluginState.test.js +57 -0
- package/plugins/menu/src/registry/getDatasetRegistry.js +9 -0
- package/plugins/menu/src/registry/getDatasetRegistry.test.js +23 -0
- package/plugins/menu/src/registry/index.js +3 -0
- package/plugins/menu/src/registry/index.test.js +15 -0
- package/plugins/menu/src/registry/isVisibleWhen.js +43 -0
- package/plugins/menu/src/registry/isVisibleWhen.test.js +48 -0
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -3
- package/plugins/search/src/components/Form/Form.module.scss +9 -20
- package/plugins/search/src/index.js +0 -1
- package/plugins/search/src/index.test.js +2 -4
- package/plugins/search/src/manifest.js +1 -1
- package/plugins/search/src/search.scss +17 -16
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/openlayersProvider.js +4 -1
- package/providers/beta/openlayers/src/openlayersProvider.test.js +18 -0
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +52 -5
- package/providers/beta/openlayers/src/utils/highlightFeatures.test.js +176 -0
- package/providers/beta/openlayers/src/utils/symbolImages.js +40 -0
- package/providers/beta/openlayers/src/utils/symbolImages.test.js +58 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/utils/drawLayerBuckets.js +14 -0
- package/providers/maplibre/src/utils/drawLayerBuckets.test.js +18 -0
- package/providers/maplibre/src/utils/highlightFeatures.js +80 -55
- package/providers/maplibre/src/utils/highlightFeatures.test.js +156 -17
- package/providers/maplibre/src/utils/hoverCursor.js +30 -14
- package/providers/maplibre/src/utils/hoverCursor.test.js +35 -0
- package/providers/maplibre/src/utils/patternImages.js +1 -1
- package/providers/maplibre/src/utils/symbolImages.js +41 -3
- package/providers/maplibre/src/utils/symbolImages.test.js +40 -6
- package/rollup.esm.mjs +11 -1
- package/src/App/components/Actions/Actions.test.jsx +47 -0
- package/src/App/components/Hints/Hints.test.jsx +13 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +25 -23
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +22 -5
- package/src/App/components/MoveControls/MoveControls.module.scss +41 -10
- package/src/App/components/Panel/Panel.jsx +65 -8
- package/src/App/components/Panel/Panel.module.scss +49 -15
- package/src/App/components/Panel/Panel.test.jsx +114 -1
- package/src/App/components/Tabs/Tabs.jsx +43 -17
- package/src/App/components/Tabs/Tabs.module.scss +32 -5
- package/src/App/components/Tabs/Tabs.test.jsx +40 -10
- package/src/App/components/Tooltip/Tooltip.test.jsx +12 -14
- package/src/App/hooks/useFeatureItems.js +16 -7
- package/src/App/hooks/useFeatureItems.test.js +57 -0
- package/src/App/hooks/useLayoutMeasurements.js +95 -84
- package/src/App/hooks/useLayoutMeasurements.test.js +61 -1
- package/src/App/layout/Layout.jsx +9 -10
- package/src/App/layout/Layout.test.jsx +2 -0
- package/src/App/layout/layout.module.scss +31 -17
- package/src/App/registry/pluginRegistry.js +1 -1
- package/src/App/registry/pluginRegistry.test.js +12 -0
- package/src/App/renderer/HtmlElementHost.jsx +18 -5
- package/src/App/renderer/HtmlElementHost.test.jsx +54 -0
- package/src/App/renderer/groupByKey.js +28 -0
- package/src/App/renderer/groupByKey.test.js +39 -0
- package/src/App/renderer/groupIntoTabs.js +42 -0
- package/src/App/renderer/groupIntoTabs.test.js +95 -0
- package/src/App/renderer/mapButtons.js +97 -129
- package/src/App/renderer/mapButtons.test.js +67 -83
- package/src/App/renderer/mapControls.js +4 -1
- package/src/App/renderer/mapControls.test.js +32 -0
- package/src/App/renderer/mapPanels.js +53 -11
- package/src/App/renderer/mapPanels.test.js +95 -2
- package/src/App/renderer/orderItems.js +28 -0
- package/src/App/renderer/orderItems.test.js +50 -0
- package/src/App/renderer/slotAggregator.js +2 -18
- package/src/App/renderer/slotHelpers.js +3 -1
- package/src/App/renderer/slotHelpers.test.js +5 -0
- package/src/App/renderer/slots.js +3 -0
- package/src/App/store/AppProvider.jsx +1 -0
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +8 -0
- package/src/config/appConfig.js +2 -2
- package/src/config/events.js +2 -0
- package/src/scss/settings/_colors.scss +1 -0
- package/src/scss/settings/_dimensions.scss +2 -3
- package/src/scss/settings/_transition.scss +1 -1
- package/src/services/eventBus.js +44 -0
- package/src/services/eventBus.test.js +83 -0
- package/src/services/symbolRegistry.js +1 -1
- package/src/types.js +31 -3
- package/webpack.umd.mjs +2 -0
- package/plugins/datasets/src/components/Key/EmptyKey.jsx +0 -7
- package/plugins/datasets/src/components/Key/Key.jsx +0 -50
- package/plugins/datasets/src/components/Key/KeyGroupItem.jsx +0 -18
- package/plugins/datasets/src/components/Key/KeyItem.jsx +0 -23
- package/plugins/datasets/src/components/Key/KeySvg.jsx +0 -22
- package/plugins/datasets/src/components/Key/KeySvgLine.jsx +0 -19
- package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
- package/plugins/datasets/src/components/LayersMenu/LayersRadioGroupWrapper.jsx +0 -41
- /package/plugins/{datasets → menu}/src/reducers/menuStateReducer.js +0 -0
- /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.js +0 -0
- /package/{providers/maplibre/src → src}/utils/rasteriseToImageData.test.js +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { siblingDrawLayerId } from './drawLayerBuckets.js'
|
|
2
|
+
|
|
3
|
+
test('returns the .hot sibling for a .cold layer id', () => {
|
|
4
|
+
expect(siblingDrawLayerId('point-symbol.cold')).toBe('point-symbol.hot')
|
|
5
|
+
})
|
|
6
|
+
|
|
7
|
+
test('returns the .cold sibling for a .hot layer id', () => {
|
|
8
|
+
expect(siblingDrawLayerId('point-symbol.hot')).toBe('point-symbol.cold')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('returns null for a layer id with no cold/hot suffix', () => {
|
|
12
|
+
expect(siblingDrawLayerId('field-parcels')).toBeNull()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
test('does not false-positive on a layer id that merely contains "cold" or "hot" mid-string', () => {
|
|
16
|
+
expect(siblingDrawLayerId('coldstore-layer')).toBeNull()
|
|
17
|
+
expect(siblingDrawLayerId('hotfix-layer')).toBeNull()
|
|
18
|
+
})
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { siblingDrawLayerId } from './drawLayerBuckets.js'
|
|
2
|
+
|
|
1
3
|
const ICON_IMAGE = 'icon-image'
|
|
2
4
|
|
|
3
5
|
const ACTIVE_PREFIX = 'active-highlight'
|
|
@@ -10,44 +12,55 @@ const usesSelectedStyle = (prefix) => prefix === SELECTED_PREFIX || prefix === A
|
|
|
10
12
|
const getActiveImageId = (map, imageId) => map._activeSymbolImageMap?.[imageId] ?? null
|
|
11
13
|
const getSelectedImageId = (map, imageId) => map._selectedSymbolImageMap?.[imageId] ?? null
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
// Grouped by layerId, not sourceId: mapbox-gl-draw stores every geometry type in the same
|
|
16
|
+
// cold/hot sources, so a point (symbol layer) and a polygon (fill layer) selected together
|
|
17
|
+
// share a source — grouping by source would collapse them into one group that can only
|
|
18
|
+
// resolve one geometry-type path, silently dropping the other's highlight.
|
|
19
|
+
//
|
|
20
|
+
// A selection's recorded layerId can also go stale if the feature later moves to its cold/hot
|
|
21
|
+
// sibling bucket — see drawLayerBuckets.js.
|
|
22
|
+
const addFeatureUnderLayer = (map, featuresByLayer, layerId, { featureId, idProperty, geometry }) => {
|
|
23
|
+
const layer = map.getLayer(layerId)
|
|
24
|
+
if (!layer) {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
18
27
|
|
|
19
|
-
|
|
20
|
-
|
|
28
|
+
if (!featuresByLayer[layerId]) {
|
|
29
|
+
featuresByLayer[layerId] = {
|
|
30
|
+
ids: new Set(),
|
|
31
|
+
fillIds: new Set(),
|
|
32
|
+
idProperty,
|
|
33
|
+
sourceId: layer.source,
|
|
34
|
+
hasFillGeometry: false
|
|
21
35
|
}
|
|
36
|
+
}
|
|
22
37
|
|
|
23
|
-
|
|
38
|
+
if (geometry && (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon')) {
|
|
39
|
+
featuresByLayer[layerId].hasFillGeometry = true
|
|
40
|
+
featuresByLayer[layerId].fillIds.add(featureId)
|
|
41
|
+
}
|
|
24
42
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
ids: new Set(),
|
|
28
|
-
fillIds: new Set(),
|
|
29
|
-
idProperty,
|
|
30
|
-
layerId,
|
|
31
|
-
hasFillGeometry: false
|
|
32
|
-
}
|
|
33
|
-
}
|
|
43
|
+
featuresByLayer[layerId].ids.add(featureId)
|
|
44
|
+
}
|
|
34
45
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
featuresBySource[sourceId].hasFillGeometry = true
|
|
38
|
-
featuresBySource[sourceId].fillIds.add(featureId)
|
|
39
|
-
}
|
|
46
|
+
const groupFeaturesByLayer = (map, selectedFeatures) => {
|
|
47
|
+
const featuresByLayer = {}
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
selectedFeatures?.forEach((feature) => {
|
|
50
|
+
addFeatureUnderLayer(map, featuresByLayer, feature.layerId, feature)
|
|
51
|
+
const sibling = siblingDrawLayerId(feature.layerId)
|
|
52
|
+
if (sibling) {
|
|
53
|
+
addFeatureUnderLayer(map, featuresByLayer, sibling, feature)
|
|
54
|
+
}
|
|
42
55
|
})
|
|
43
56
|
|
|
44
|
-
return
|
|
57
|
+
return featuresByLayer
|
|
45
58
|
}
|
|
46
59
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
if (!
|
|
50
|
-
const base = `${prefix}-${
|
|
60
|
+
const cleanupStaleLayers = (map, previousLayerIds, currentLayerIds, prefix) => {
|
|
61
|
+
previousLayerIds.forEach(layerId => {
|
|
62
|
+
if (!currentLayerIds.has(layerId)) {
|
|
63
|
+
const base = `${prefix}-${layerId}`
|
|
51
64
|
;[`${base}-fill`, `${base}-line`, `${base}-symbol`].forEach(id => {
|
|
52
65
|
if (map.getLayer(id)) {
|
|
53
66
|
map.setFilter(id, ['==', 'id', ''])
|
|
@@ -57,9 +70,9 @@ const cleanupStaleSources = (map, previousSources, currentSources, prefix) => {
|
|
|
57
70
|
})
|
|
58
71
|
}
|
|
59
72
|
|
|
60
|
-
const
|
|
73
|
+
const clearPrefixLayers = (map, prefix) => {
|
|
61
74
|
const key = `_${prefix.replaceAll('-', '')}Sources`
|
|
62
|
-
|
|
75
|
+
cleanupStaleLayers(map, map[key] || new Set(), new Set(), prefix)
|
|
63
76
|
map[key] = new Set()
|
|
64
77
|
}
|
|
65
78
|
|
|
@@ -82,6 +95,9 @@ const applyHighlightLayer = (map, id, type, sourceId, srcLayer, paint, filter) =
|
|
|
82
95
|
}
|
|
83
96
|
|
|
84
97
|
const applySymbolHighlightLayer = (map, id, sourceId, srcLayer, originalLayerId, imageId, filter) => {
|
|
98
|
+
// icon-offset only exists on layers needing anchor precision correction (draw's point-symbol
|
|
99
|
+
// layer) — read off the original layer, omitted when absent.
|
|
100
|
+
const iconOffset = map.getLayoutProperty(originalLayerId, 'icon-offset')
|
|
85
101
|
if (!map.getLayer(id)) {
|
|
86
102
|
map.addLayer({
|
|
87
103
|
id,
|
|
@@ -91,11 +107,15 @@ const applySymbolHighlightLayer = (map, id, sourceId, srcLayer, originalLayerId,
|
|
|
91
107
|
layout: {
|
|
92
108
|
[ICON_IMAGE]: imageId,
|
|
93
109
|
'icon-anchor': map.getLayoutProperty(originalLayerId, 'icon-anchor') ?? 'center',
|
|
110
|
+
...(iconOffset !== undefined && { 'icon-offset': iconOffset }),
|
|
94
111
|
'icon-allow-overlap': true
|
|
95
112
|
}
|
|
96
113
|
})
|
|
97
114
|
}
|
|
98
115
|
map.setLayoutProperty(id, ICON_IMAGE, imageId)
|
|
116
|
+
if (iconOffset !== undefined) {
|
|
117
|
+
map.setLayoutProperty(id, 'icon-offset', iconOffset)
|
|
118
|
+
}
|
|
99
119
|
map.setFilter(id, filter)
|
|
100
120
|
map.moveLayer(id)
|
|
101
121
|
}
|
|
@@ -115,8 +135,16 @@ const calculateBounds = (LngLatBounds, renderedFeatures) => {
|
|
|
115
135
|
return [bounds.getWest(), bounds.getSouth(), bounds.getEast(), bounds.getNorth()]
|
|
116
136
|
}
|
|
117
137
|
|
|
138
|
+
// Draw's point-symbol layer has a per-feature data-driven icon-image, so there's no single
|
|
139
|
+
// shared image id to reverse-map — pointSymbolImages.js precomputes each point's own active/
|
|
140
|
+
// selected variant as a property, and the highlight layer just reads that directly.
|
|
118
141
|
const applySymbolGeomHighlight = (map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId) => {
|
|
119
142
|
const imageId = map.getLayoutProperty(layerId, ICON_IMAGE)
|
|
143
|
+
if (Array.isArray(imageId)) {
|
|
144
|
+
const property = getSymbolImageId === getActiveImageId ? 'user_symbolActiveImageId' : 'user_symbolSelectedImageId'
|
|
145
|
+
applySymbolHighlightLayer(map, `${base}-symbol`, sourceId, srcLayer, layerId, ['get', property], filter)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
120
148
|
const symbolImageId = getSymbolImageId(map, imageId)
|
|
121
149
|
if (symbolImageId) {
|
|
122
150
|
applySymbolHighlightLayer(map, `${base}-symbol`, sourceId, srcLayer, layerId, symbolImageId, filter)
|
|
@@ -161,26 +189,23 @@ const applyLineHighlight = (map, base, sourceId, srcLayer, lineColor, lineWidth,
|
|
|
161
189
|
applyHighlightLayer(map, `${base}-line`, 'line', sourceId, srcLayer, { 'line-color': lineColor, 'line-width': lineWidth }, filter)
|
|
162
190
|
}
|
|
163
191
|
|
|
164
|
-
|
|
165
|
-
applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Orchestrates highlight layers for a single source. prefix drives which visual style is used:
|
|
192
|
+
// Orchestrates highlight layers for a single layer. prefix drives which visual style is used:
|
|
169
193
|
// active-highlight → yellow active ring (stroke only)
|
|
170
194
|
// active-highlight-inner → black thin ring drawn on top of active
|
|
171
195
|
// selected-highlight → black thin ring + fill for polygon features
|
|
172
|
-
const
|
|
173
|
-
const { ids, fillIds, idProperty,
|
|
196
|
+
const applyLayerHighlight = (map, layerId, featuresByLayer, stylesMap, prefix, getSymbolImageId) => {
|
|
197
|
+
const { ids, fillIds, idProperty, sourceId, hasFillGeometry } = featuresByLayer[layerId]
|
|
174
198
|
const baseLayer = map.getLayer(layerId)
|
|
199
|
+
// Only the recorded layerId is registered in stylesMap — falls back to its cold/hot sibling
|
|
200
|
+
// (same drawn layer, identical styling) rather than requiring both to be registered.
|
|
201
|
+
const style = stylesMap[layerId] ?? stylesMap[siblingDrawLayerId(layerId)]
|
|
175
202
|
|
|
176
|
-
if (!baseLayer || !
|
|
203
|
+
if (!baseLayer || !style) {
|
|
177
204
|
return
|
|
178
205
|
}
|
|
179
|
-
|
|
180
|
-
const style = stylesMap[layerId]
|
|
181
206
|
const srcLayer = baseLayer.sourceLayer
|
|
182
207
|
const geom = hasFillGeometry ? 'fill' : baseLayer.type
|
|
183
|
-
const base = `${prefix}-${
|
|
208
|
+
const base = `${prefix}-${layerId}`
|
|
184
209
|
const { fill } = style
|
|
185
210
|
const isSelected = prefix === SELECTED_PREFIX
|
|
186
211
|
const selectedStyle = usesSelectedStyle(prefix)
|
|
@@ -202,7 +227,7 @@ const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix
|
|
|
202
227
|
applyLineHighlight(map, base, sourceId, srcLayer, lineColor, lineWidth, filter)
|
|
203
228
|
break
|
|
204
229
|
case 'symbol':
|
|
205
|
-
|
|
230
|
+
applySymbolGeomHighlight(map, base, sourceId, srcLayer, layerId, filter, getSymbolImageId)
|
|
206
231
|
break
|
|
207
232
|
default:
|
|
208
233
|
break
|
|
@@ -210,16 +235,16 @@ const applySourceHighlight = (map, sourceId, featuresBySource, stylesMap, prefix
|
|
|
210
235
|
}
|
|
211
236
|
|
|
212
237
|
const applyFeatureHighlights = (map, features, stylesMap, prefix, getSymbolImageId) => {
|
|
213
|
-
const
|
|
214
|
-
const
|
|
238
|
+
const featuresByLayer = groupFeaturesByLayer(map, features)
|
|
239
|
+
const currentLayerIds = new Set(Object.keys(featuresByLayer))
|
|
215
240
|
const storageKey = `_${prefix.replaceAll('-', '')}Sources`
|
|
216
|
-
const
|
|
241
|
+
const previousLayerIds = map[storageKey] || new Set()
|
|
217
242
|
|
|
218
|
-
|
|
219
|
-
map[storageKey] =
|
|
220
|
-
|
|
243
|
+
cleanupStaleLayers(map, previousLayerIds, currentLayerIds, prefix)
|
|
244
|
+
map[storageKey] = currentLayerIds
|
|
245
|
+
currentLayerIds.forEach(layerId => applyLayerHighlight(map, layerId, featuresByLayer, stylesMap, prefix, getSymbolImageId))
|
|
221
246
|
|
|
222
|
-
return
|
|
247
|
+
return featuresByLayer
|
|
223
248
|
}
|
|
224
249
|
|
|
225
250
|
/**
|
|
@@ -247,21 +272,21 @@ export function updateHighlightedFeatures ({ LngLatBounds, map, selectedFeatures
|
|
|
247
272
|
// Black selected stroke on top of yellow active (mirrors resolveActive for symbols)
|
|
248
273
|
applyFeatureHighlights(map, activeFeatures, stylesMap, ACTIVE_INNER_PREFIX, getSelectedImageId)
|
|
249
274
|
} else {
|
|
250
|
-
|
|
251
|
-
|
|
275
|
+
clearPrefixLayers(map, ACTIVE_PREFIX)
|
|
276
|
+
clearPrefixLayers(map, ACTIVE_INNER_PREFIX)
|
|
252
277
|
}
|
|
253
278
|
|
|
254
279
|
// Selection features
|
|
255
|
-
let
|
|
280
|
+
let featuresByLayer = {}
|
|
256
281
|
if (selectedFeatures?.length) {
|
|
257
|
-
|
|
282
|
+
featuresByLayer = applyFeatureHighlights(map, selectedFeatures, stylesMap, SELECTED_PREFIX, getSelectedImageId)
|
|
258
283
|
} else {
|
|
259
|
-
|
|
284
|
+
clearPrefixLayers(map, SELECTED_PREFIX)
|
|
260
285
|
}
|
|
261
286
|
|
|
262
287
|
// Bounds only from selected features
|
|
263
288
|
const renderedFeatures = []
|
|
264
|
-
Object.entries(
|
|
289
|
+
Object.entries(featuresByLayer).forEach(([layerId, { ids, idProperty }]) => {
|
|
265
290
|
renderedFeatures.push(
|
|
266
291
|
...map
|
|
267
292
|
.queryRenderedFeatures({ layers: [layerId] })
|
|
@@ -54,7 +54,7 @@ describe('Highlighting Utils — active (cursor) fill and line', () => {
|
|
|
54
54
|
if (id.includes('stale')) { return {} }
|
|
55
55
|
if (id === 'l1') { return { source: 's1', type: 'fill' } }
|
|
56
56
|
if (id === 'l2') { return { source: 's2', type: 'line' } }
|
|
57
|
-
if (id === 'active-highlight-
|
|
57
|
+
if (id === 'active-highlight-l2-fill') { return {} }
|
|
58
58
|
return null
|
|
59
59
|
})
|
|
60
60
|
|
|
@@ -62,19 +62,19 @@ describe('Highlighting Utils — active (cursor) fill and line', () => {
|
|
|
62
62
|
|
|
63
63
|
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-stale-fill', EMPTY_FILTER)
|
|
64
64
|
expect(map.setFilter).toHaveBeenCalledWith(STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
65
|
-
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-
|
|
66
|
-
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-
|
|
65
|
+
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-l2-fill', EMPTY_FILTER)
|
|
66
|
+
expect(map.setFilter).toHaveBeenCalledWith('active-highlight-l2-line', expect.arrayContaining([['get', 'customId']]))
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
test('null _activehighlightSources falls back to empty set; line geom skips absent fill layer', () => {
|
|
70
70
|
map._activehighlightSources = null
|
|
71
71
|
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
72
72
|
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
|
|
73
|
-
expect(map.setFilter).not.toHaveBeenCalledWith('active-highlight-
|
|
73
|
+
expect(map.setFilter).not.toHaveBeenCalledWith('active-highlight-l1-fill', expect.anything())
|
|
74
74
|
})
|
|
75
75
|
|
|
76
|
-
test('persistent
|
|
77
|
-
map._activehighlightSources = new Set(['stale', '
|
|
76
|
+
test('persistent layer skips cleanup; missing stale layers skip setFilter', () => {
|
|
77
|
+
map._activehighlightSources = new Set(['stale', 'l1'])
|
|
78
78
|
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
79
79
|
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap: { l1: { stroke: 'red' } } })
|
|
80
80
|
expect(map.setFilter).not.toHaveBeenCalledWith(expect.stringContaining('stale'), expect.anything())
|
|
@@ -85,10 +85,10 @@ describe('Highlighting Utils — active (cursor) fill and line', () => {
|
|
|
85
85
|
const stylesMap = { l1: { stroke: 'yellow', selectionStroke: 'black', strokeWidth: 3, activeStrokeWidth: 9 } }
|
|
86
86
|
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }], stylesMap })
|
|
87
87
|
|
|
88
|
-
const activeColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-
|
|
88
|
+
const activeColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-l1-line' && c[1] === LINE_COLOR)
|
|
89
89
|
expect(activeColorCall?.[2]).toBe('yellow')
|
|
90
90
|
|
|
91
|
-
const overlayColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-inner-
|
|
91
|
+
const overlayColorCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'active-highlight-inner-l1-line' && c[1] === LINE_COLOR)
|
|
92
92
|
expect(overlayColorCall?.[2]).toBe('black')
|
|
93
93
|
})
|
|
94
94
|
|
|
@@ -123,8 +123,8 @@ describe('Highlighting Utils — selected fill and line', () => {
|
|
|
123
123
|
})
|
|
124
124
|
updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: SELECTED_FEATURES, stylesMap: STYLES })
|
|
125
125
|
expect(map.setFilter).toHaveBeenCalledWith(SEL_STALE_SYMBOL_LAYER, EMPTY_FILTER)
|
|
126
|
-
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ id: 'selected-highlight-
|
|
127
|
-
const linePaintCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'selected-highlight-
|
|
126
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ id: 'selected-highlight-l1-line' }))
|
|
127
|
+
const linePaintCall = map.setPaintProperty.mock.calls.find(c => c[0] === 'selected-highlight-l1-line' && c[1] === LINE_COLOR)
|
|
128
128
|
expect(linePaintCall).toBeTruthy()
|
|
129
129
|
expect(linePaintCall[2]).toBe('#ffdd00')
|
|
130
130
|
})
|
|
@@ -169,11 +169,11 @@ describe('Highlighting Utils — layer management', () => {
|
|
|
169
169
|
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
170
170
|
if (id === 'l1') { return { source: 's1', type: 'line' } }
|
|
171
171
|
if (id === 'l2') { return { source: 's2', type: 'line', sourceLayer: 'tiles' } }
|
|
172
|
-
if (id === 'active-highlight-
|
|
172
|
+
if (id === 'active-highlight-l1-line') { return {} }
|
|
173
173
|
return null
|
|
174
174
|
})
|
|
175
175
|
updateHighlightedFeatures({ LngLatBounds, map, activeFeatures: [{ featureId: 1, layerId: 'l1' }, { featureId: 2, layerId: 'l2' }], stylesMap: { l1: { stroke: 'blue' }, l2: { stroke: 'green' } } })
|
|
176
|
-
// active-highlight-
|
|
176
|
+
// active-highlight-l1-line reused; active-highlight-l2-line, active-highlight-inner-l1-line, active-highlight-inner-l2-line are new
|
|
177
177
|
expect(map.addLayer).toHaveBeenCalledTimes(3)
|
|
178
178
|
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ 'source-layer': 'tiles' }))
|
|
179
179
|
})
|
|
@@ -187,7 +187,7 @@ describe('Highlighting Utils — layer management', () => {
|
|
|
187
187
|
|
|
188
188
|
describe('Highlighting Utils — symbol layers (active cursor)', () => {
|
|
189
189
|
const ACTIVE_IMAGE = 'symbol-act-abc123'
|
|
190
|
-
const HIGHLIGHT_LAYER = 'active-highlight-
|
|
190
|
+
const HIGHLIGHT_LAYER = 'active-highlight-l1-symbol'
|
|
191
191
|
const ICON_IMAGE = 'icon-image'
|
|
192
192
|
const ICON_ANCHOR = 'icon-anchor'
|
|
193
193
|
const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
|
|
@@ -258,6 +258,41 @@ describe('Highlighting Utils — symbol layers (active cursor)', () => {
|
|
|
258
258
|
expect(map.addLayer).not.toHaveBeenCalled()
|
|
259
259
|
})
|
|
260
260
|
|
|
261
|
+
test('copies icon-offset from the original layer when present (draw points need it, datasets do not)', () => {
|
|
262
|
+
map.getLayoutProperty.mockImplementation((_id, prop) => { // NOSONAR
|
|
263
|
+
if (prop === ICON_IMAGE) { return SYMBOL_IMAGE }
|
|
264
|
+
if (prop === 'icon-offset') { return [0, 4.4] }
|
|
265
|
+
return null
|
|
266
|
+
})
|
|
267
|
+
run()
|
|
268
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
269
|
+
layout: expect.objectContaining({ 'icon-offset': [0, 4.4] })
|
|
270
|
+
}))
|
|
271
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, 'icon-offset', [0, 4.4])
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('omits icon-offset entirely when the original layer has none', () => {
|
|
275
|
+
map.getLayoutProperty.mockImplementation((_id, prop) => (prop === ICON_IMAGE ? SYMBOL_IMAGE : undefined)) // NOSONAR
|
|
276
|
+
run()
|
|
277
|
+
const call = map.addLayer.mock.calls[0][0]
|
|
278
|
+
expect(call.layout).not.toHaveProperty('icon-offset')
|
|
279
|
+
expect(map.setLayoutProperty).not.toHaveBeenCalledWith(HIGHLIGHT_LAYER, 'icon-offset', expect.anything())
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
// Draw's point-symbol layer has a per-feature data-driven icon-image (each point can carry
|
|
283
|
+
// a different symbol config) — the highlight layer must reference the SAME per-feature
|
|
284
|
+
// expression against the precomputed symbolActiveImageId/symbolSelectedImageId property
|
|
285
|
+
// (pointSymbolImages.js) rather than reverse-mapping a single shared id.
|
|
286
|
+
test('references the per-feature symbolActiveImageId property when icon-image is a data-driven expression', () => {
|
|
287
|
+
map.getLayoutProperty.mockImplementation((_id, prop) => // NOSONAR
|
|
288
|
+
prop === ICON_IMAGE ? ['get', 'user_symbolImageId'] : null)
|
|
289
|
+
run()
|
|
290
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
291
|
+
layout: expect.objectContaining({ [ICON_IMAGE]: ['get', 'user_symbolActiveImageId'] })
|
|
292
|
+
}))
|
|
293
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith(HIGHLIGHT_LAYER, ICON_IMAGE, ['get', 'user_symbolActiveImageId'])
|
|
294
|
+
})
|
|
295
|
+
|
|
261
296
|
test('cleans up stale symbol highlight layer', () => {
|
|
262
297
|
map._activehighlightSources = new Set(['stale'])
|
|
263
298
|
map.getLayer.mockImplementation(id => id === STALE_SYMBOL_LAYER ? { type: 'symbol' } : null) // NOSONAR
|
|
@@ -268,7 +303,7 @@ describe('Highlighting Utils — symbol layers (active cursor)', () => {
|
|
|
268
303
|
|
|
269
304
|
describe('Highlighting Utils — symbol layers (committed selection)', () => {
|
|
270
305
|
const SELECTED_IMAGE = 'symbol-sel-abc123'
|
|
271
|
-
const SEL_HIGHLIGHT_LAYER = 'selected-highlight-
|
|
306
|
+
const SEL_HIGHLIGHT_LAYER = 'selected-highlight-l1-symbol'
|
|
272
307
|
const ICON_IMAGE = 'icon-image'
|
|
273
308
|
const POINT_FEATURE = { featureId: 1, layerId: 'l1', geometry: { type: 'Point' } }
|
|
274
309
|
|
|
@@ -298,6 +333,15 @@ describe('Highlighting Utils — symbol layers (committed selection)', () => {
|
|
|
298
333
|
run()
|
|
299
334
|
expect(map.addLayer).not.toHaveBeenCalled()
|
|
300
335
|
})
|
|
336
|
+
|
|
337
|
+
test('references the per-feature symbolSelectedImageId property when icon-image is a data-driven expression', () => {
|
|
338
|
+
map.getLayoutProperty.mockImplementation((_id, prop) => // NOSONAR
|
|
339
|
+
prop === ICON_IMAGE ? ['get', 'user_symbolImageId'] : null)
|
|
340
|
+
run()
|
|
341
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
342
|
+
layout: expect.objectContaining({ [ICON_IMAGE]: ['get', 'user_symbolSelectedImageId'] })
|
|
343
|
+
}))
|
|
344
|
+
})
|
|
301
345
|
})
|
|
302
346
|
|
|
303
347
|
// ─── fill-extrusion layers (3D buildings) ────────────────────────────────────
|
|
@@ -305,7 +349,7 @@ describe('Highlighting Utils — symbol layers (committed selection)', () => {
|
|
|
305
349
|
describe('Highlighting Utils — fill-extrusion layers', () => {
|
|
306
350
|
const LAYER_ID = 'buildings 3D'
|
|
307
351
|
const STYLES = { [LAYER_ID]: { stroke: '#aaa', selectionStroke: '#0073cc', strokeWidth: 3, activeStrokeWidth: 6 } }
|
|
308
|
-
const LINE_LAYER_ID = 'selected-highlight-
|
|
352
|
+
const LINE_LAYER_ID = 'selected-highlight-buildings 3D-line'
|
|
309
353
|
|
|
310
354
|
let map
|
|
311
355
|
|
|
@@ -409,12 +453,12 @@ describe('Highlighting Utils — missing style entry', () => {
|
|
|
409
453
|
const map = makeMap()
|
|
410
454
|
let callCount = 0
|
|
411
455
|
map.getLayer.mockImplementation((id) => {
|
|
412
|
-
// First call from
|
|
456
|
+
// First call from groupFeaturesByLayer - return a valid layer
|
|
413
457
|
if (id === 'l1' && callCount === 0) {
|
|
414
458
|
callCount++
|
|
415
459
|
return { source: 's1', type: 'line' }
|
|
416
460
|
}
|
|
417
|
-
// Subsequent calls from
|
|
461
|
+
// Subsequent calls from applyLayerHighlight - return null to trigger early return
|
|
418
462
|
return null
|
|
419
463
|
})
|
|
420
464
|
|
|
@@ -448,3 +492,98 @@ describe('Highlighting Utils — missing style entry', () => {
|
|
|
448
492
|
expect(map.addLayer).not.toHaveBeenCalled()
|
|
449
493
|
})
|
|
450
494
|
})
|
|
495
|
+
|
|
496
|
+
// mapbox-gl-draw duplicates every one of its own layers into a `.cold`/`.hot` sibling pair,
|
|
497
|
+
// each backed by its own source — a feature moves out of cold and into hot the instant any of
|
|
498
|
+
// its own properties change, only cooling back down on a later render with nothing changed.
|
|
499
|
+
// A selection payload's layerId (e.g. 'point-symbol.cold') is captured once, at selection time
|
|
500
|
+
// — if the feature later moves to the sibling bucket, the recorded layer still exists (so a
|
|
501
|
+
// naive "does the layer exist" check looks fine) but its source no longer holds the feature.
|
|
502
|
+
describe('Highlighting Utils — mapbox-gl-draw cold/hot sibling layers', () => {
|
|
503
|
+
test('highlights a selected feature that has moved from its recorded .cold layer into the sibling .hot bucket', () => {
|
|
504
|
+
const map = makeMap()
|
|
505
|
+
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
506
|
+
if (id === 'point-symbol.cold') { return { source: 'mapbox-gl-draw-cold', type: 'symbol' } }
|
|
507
|
+
if (id === 'point-symbol.hot') { return { source: 'mapbox-gl-draw-hot', type: 'symbol' } }
|
|
508
|
+
return null
|
|
509
|
+
})
|
|
510
|
+
map.getLayoutProperty.mockImplementation((id, prop) => // NOSONAR
|
|
511
|
+
prop === 'icon-image' ? SYMBOL_IMAGE : undefined)
|
|
512
|
+
map._selectedSymbolImageMap = { [SYMBOL_IMAGE]: 'symbol-sel-xyz' }
|
|
513
|
+
|
|
514
|
+
// The feature's own current geometry lives on the hot source (it moved there when its
|
|
515
|
+
// properties were last refreshed) — only stylesMap['point-symbol.cold'] is registered,
|
|
516
|
+
// matching how the demo only registers the .cold layerId for selectability.
|
|
517
|
+
updateHighlightedFeatures({
|
|
518
|
+
LngLatBounds,
|
|
519
|
+
map,
|
|
520
|
+
selectedFeatures: [{ featureId: 'p1', layerId: 'point-symbol.cold', geometry: { type: 'Point' } }],
|
|
521
|
+
stylesMap: { 'point-symbol.cold': { stroke: 'red', selectionStroke: 'black', strokeWidth: 2 } }
|
|
522
|
+
})
|
|
523
|
+
|
|
524
|
+
// A highlight layer gets created/filtered for BOTH the recorded source and its sibling —
|
|
525
|
+
// whichever one the feature actually lives in right now, this finds it.
|
|
526
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ source: 'mapbox-gl-draw-cold' }))
|
|
527
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ source: 'mapbox-gl-draw-hot' }))
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
test('does not touch a non-draw layer id that happens to not end in .cold/.hot', () => {
|
|
531
|
+
const map = makeMap()
|
|
532
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null) // NOSONAR
|
|
533
|
+
|
|
534
|
+
updateHighlightedFeatures({
|
|
535
|
+
LngLatBounds,
|
|
536
|
+
map,
|
|
537
|
+
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
538
|
+
stylesMap: { l1: { stroke: 'red', selectionStroke: 'black', strokeWidth: 2 } }
|
|
539
|
+
})
|
|
540
|
+
|
|
541
|
+
expect(map.getLayer).not.toHaveBeenCalledWith('l1.hot')
|
|
542
|
+
expect(map.getLayer).not.toHaveBeenCalledWith('l1.cold')
|
|
543
|
+
})
|
|
544
|
+
})
|
|
545
|
+
|
|
546
|
+
// mapbox-gl-draw stores every geometry type (point, polygon, line) in the same cold/hot
|
|
547
|
+
// sources — a point and a polygon selected together share a source, even though they're
|
|
548
|
+
// rendered by different layers (point-symbol vs fill-inactive). Grouping by source alone would
|
|
549
|
+
// collapse both into one group that can only resolve one geometry-type path, silently dropping
|
|
550
|
+
// whichever feature's highlight didn't win.
|
|
551
|
+
describe('Highlighting Utils — multi-select across geometry types sharing a source', () => {
|
|
552
|
+
test('a selected point and a selected polygon sharing the same mapbox-gl-draw source both get their own highlight', () => {
|
|
553
|
+
const map = makeMap()
|
|
554
|
+
map.getLayer.mockImplementation(id => { // NOSONAR
|
|
555
|
+
if (id === 'point-symbol.cold') { return { source: 'mapbox-gl-draw-cold', type: 'symbol' } }
|
|
556
|
+
if (id === 'fill-inactive.cold') { return { source: 'mapbox-gl-draw-cold', type: 'fill' } }
|
|
557
|
+
return null
|
|
558
|
+
})
|
|
559
|
+
map.getLayoutProperty.mockImplementation((id, prop) => // NOSONAR
|
|
560
|
+
prop === 'icon-image' ? SYMBOL_IMAGE : undefined)
|
|
561
|
+
map._selectedSymbolImageMap = { [SYMBOL_IMAGE]: 'symbol-sel-xyz' }
|
|
562
|
+
|
|
563
|
+
updateHighlightedFeatures({
|
|
564
|
+
LngLatBounds,
|
|
565
|
+
map,
|
|
566
|
+
selectedFeatures: [
|
|
567
|
+
{ featureId: 'pt1', layerId: 'point-symbol.cold', geometry: { type: 'Point' } },
|
|
568
|
+
{ featureId: 'poly1', layerId: 'fill-inactive.cold', geometry: { type: 'Polygon' } }
|
|
569
|
+
],
|
|
570
|
+
stylesMap: {
|
|
571
|
+
'point-symbol.cold': { stroke: 'red', selectionStroke: 'black', strokeWidth: 2 },
|
|
572
|
+
'fill-inactive.cold': { stroke: 'red', selectionStroke: 'black', fill: 'blue', strokeWidth: 2 }
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
577
|
+
id: 'selected-highlight-point-symbol.cold-symbol',
|
|
578
|
+
type: 'symbol'
|
|
579
|
+
}))
|
|
580
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
581
|
+
id: 'selected-highlight-fill-inactive.cold-fill',
|
|
582
|
+
type: 'fill'
|
|
583
|
+
}))
|
|
584
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({
|
|
585
|
+
id: 'selected-highlight-fill-inactive.cold-line',
|
|
586
|
+
type: 'line'
|
|
587
|
+
}))
|
|
588
|
+
})
|
|
589
|
+
})
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
import { siblingDrawLayerId } from './drawLayerBuckets.js'
|
|
2
|
+
|
|
3
|
+
// layerIds is a config-derived list, built once and never refreshed as features move between
|
|
4
|
+
// mapbox-gl-draw's cold/hot buckets (see drawLayerBuckets.js) — adding each sibling keeps a
|
|
5
|
+
// query finding a feature that has since moved to the other bucket.
|
|
6
|
+
const expandDrawLayerSiblings = (layerIds) => {
|
|
7
|
+
const expanded = new Set(layerIds)
|
|
8
|
+
layerIds.forEach(id => {
|
|
9
|
+
const sibling = siblingDrawLayerId(id)
|
|
10
|
+
if (sibling) {
|
|
11
|
+
expanded.add(sibling)
|
|
12
|
+
}
|
|
13
|
+
})
|
|
14
|
+
return [...expanded]
|
|
15
|
+
}
|
|
16
|
+
|
|
14
17
|
const splitLayers = (map, layerIds) => {
|
|
15
18
|
const lineLayers = []
|
|
16
19
|
const otherLayers = []
|
|
@@ -29,6 +32,19 @@ const splitLayers = (map, layerIds) => {
|
|
|
29
32
|
return { lineLayers, otherLayers }
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Attaches a mousemove listener that changes the map cursor to a pointer when
|
|
37
|
+
* hovering over any of the specified layers.
|
|
38
|
+
*
|
|
39
|
+
* Line layers use a 10px tolerance bbox. Stroke layers that have a companion
|
|
40
|
+
* fill layer are skipped — the fill handles hover. Fill and symbol layers use
|
|
41
|
+
* exact point hit-testing.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} map - MapLibre map instance
|
|
44
|
+
* @param {string[]} layerIds - Layer IDs to watch
|
|
45
|
+
* @param {Function|null} prevHandler - Previous mousemove handler to remove
|
|
46
|
+
* @returns {Function|null} The new handler, or null if layerIds is empty
|
|
47
|
+
*/
|
|
32
48
|
export const setupHoverCursor = (map, layerIds, prevHandler) => {
|
|
33
49
|
const canvas = map.getCanvas()
|
|
34
50
|
|
|
@@ -42,7 +58,7 @@ export const setupHoverCursor = (map, layerIds, prevHandler) => {
|
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
const handler = (e) => {
|
|
45
|
-
const existingLayers = layerIds.filter(id => map.getLayer(id))
|
|
61
|
+
const existingLayers = expandDrawLayerSiblings(layerIds).filter(id => map.getLayer(id))
|
|
46
62
|
if (existingLayers.length === 0) {
|
|
47
63
|
canvas.style.cursor = ''
|
|
48
64
|
return
|
|
@@ -127,4 +127,39 @@ describe('setupHoverCursor', () => {
|
|
|
127
127
|
{ layers: ['hedge-stroke'] }
|
|
128
128
|
)
|
|
129
129
|
})
|
|
130
|
+
|
|
131
|
+
/* ------------------------------------------------------------------ */
|
|
132
|
+
/* mapbox-gl-draw cold/hot sibling layers */
|
|
133
|
+
/* ------------------------------------------------------------------ */
|
|
134
|
+
|
|
135
|
+
// mapbox-gl-draw moves a feature out of its `.cold` layer/source into a `.hot` sibling the
|
|
136
|
+
// instant any of its own properties change — only registering the config's recorded
|
|
137
|
+
// '.cold' id would otherwise mean the cursor stops recognising a feature the moment it's
|
|
138
|
+
// moved, only recovering once something else happens to settle it back.
|
|
139
|
+
it('queries a .cold layerId\'s .hot sibling too, and detects a hit that only the sibling has', () => {
|
|
140
|
+
const canvas = { style: { cursor: '' } }
|
|
141
|
+
const map = {
|
|
142
|
+
...makeMap({ 'point-symbol.cold': 'symbol', 'point-symbol.hot': 'symbol' }, []),
|
|
143
|
+
getCanvas: () => canvas,
|
|
144
|
+
queryRenderedFeatures: jest.fn((_point, { layers }) =>
|
|
145
|
+
layers.includes('point-symbol.hot') ? [{ id: 'p1' }] : [])
|
|
146
|
+
}
|
|
147
|
+
const handler = setupHoverCursor(map, ['point-symbol.cold'], null)
|
|
148
|
+
move(handler)
|
|
149
|
+
expect(map.queryRenderedFeatures).toHaveBeenCalledWith(
|
|
150
|
+
expect.objectContaining({ x: 10, y: 10 }),
|
|
151
|
+
{ layers: expect.arrayContaining(['point-symbol.cold', 'point-symbol.hot']) }
|
|
152
|
+
)
|
|
153
|
+
expect(canvas.style.cursor).toBe('pointer')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('does not add a sibling for a layer id with no cold/hot suffix', () => {
|
|
157
|
+
const map = makeMap({ 'field-parcels': 'fill' }, [])
|
|
158
|
+
const handler = setupHoverCursor(map, ['field-parcels'], null)
|
|
159
|
+
move(handler)
|
|
160
|
+
expect(map.queryRenderedFeatures).toHaveBeenCalledWith(
|
|
161
|
+
expect.objectContaining({ x: 10, y: 10 }),
|
|
162
|
+
{ layers: ['field-parcels'] }
|
|
163
|
+
)
|
|
164
|
+
})
|
|
130
165
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { injectColors, getEffectivePixelRatio } from '../../../../src/utils/patternUtils.js'
|
|
2
2
|
import { getValueForStyle } from '../../../../src/utils/getValueForStyle.js'
|
|
3
|
-
import { rasteriseToImageData } from '
|
|
3
|
+
import { rasteriseToImageData } from '../../../../src/utils/rasteriseToImageData.js'
|
|
4
4
|
|
|
5
5
|
// Module-level cache: imageId → ImageData. Avoids re-rasterising identical patterns.
|
|
6
6
|
const imageDataCache = new Map()
|