@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
|
@@ -16,8 +16,9 @@ describe('onSetup / onStop lifecycle', () => {
|
|
|
16
16
|
expect(state.midpoints).toHaveLength(4)
|
|
17
17
|
expect(map._lastEditFeatureId).toBe('feat-1')
|
|
18
18
|
expect(map._drawEditContainer).toBe(state.container)
|
|
19
|
+
// draw.update is edit_vertex's own extra listener on top of the shared set (covered by
|
|
20
|
+
// editModeEvents.test.js).
|
|
19
21
|
expect(ctx.map.on).toHaveBeenCalledWith('draw.update', expect.any(Function))
|
|
20
|
-
expect(ctx.map.on).toHaveBeenCalledWith('draw.nudgevertex', expect.any(Function))
|
|
21
22
|
})
|
|
22
23
|
|
|
23
24
|
test('does not clear the undo stack when re-entering the same feature', () => {
|
|
@@ -28,13 +29,14 @@ describe('onSetup / onStop lifecycle', () => {
|
|
|
28
29
|
expect(map._undoStack.length).toBe(1)
|
|
29
30
|
})
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
// Shared listener teardown is covered by editModeEvents.test.js; draw.update is edit_vertex's
|
|
33
|
+
// own extra listener.
|
|
34
|
+
test('onStop removes the draw.update listener and clears editing container; DirectSelect touch stubs are inert', () => {
|
|
32
35
|
const { ctx, state, map } = createHarness()
|
|
33
36
|
expect(() => { ctx.onTouchStart(); ctx.onTouchMove(); ctx.onTouchEnd() }).not.toThrow()
|
|
34
37
|
ctx.onStop(state)
|
|
35
38
|
expect(map._drawEditContainer).toBeNull()
|
|
36
39
|
expect(map.off).toHaveBeenCalledWith('draw.update', expect.any(Function))
|
|
37
|
-
expect(map.off).toHaveBeenCalledWith('draw.nudgevertex', expect.any(Function))
|
|
38
40
|
})
|
|
39
41
|
|
|
40
42
|
test('onSetup edge cases: clears an active snap indicator, positions or skips the touch target, clears an explicit -1 selection', () => {
|
|
@@ -110,6 +112,11 @@ describe('selection, scale and update events', () => {
|
|
|
110
112
|
expect({ top: state.touchVertexTarget.style.top, left: state.touchVertexTarget.style.left }).not.toEqual(targetBefore)
|
|
111
113
|
})
|
|
112
114
|
|
|
115
|
+
test('draw.nudgevertex with no vertex selected moves nothing and skips repositioning the touch target', () => {
|
|
116
|
+
const { map } = createHarness(POLYGON(), { interfaceType: 'touch', selectedVertexIndex: -1 })
|
|
117
|
+
expect(() => map.fire('draw.nudgevertex', { dx: 1, dy: 0, isLargeStep: false })).not.toThrow()
|
|
118
|
+
})
|
|
119
|
+
|
|
113
120
|
test('onUpdate re-selects a changed vertex only when the vertex count is ambiguous', () => {
|
|
114
121
|
const { ctx, state } = createHarness()
|
|
115
122
|
ctx.onUpdate(state) // unique coords → no change
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
|
|
2
|
+
import { getSymbolAnchor, getSymbolViewBox } from '../../../../../src/utils/symbolUtils.js'
|
|
3
|
+
import { anchorToMaplibre, anchorToMaplibreOffset } from '../../../../../providers/maplibre/src/utils/symbolImages.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves and registers a drawn point's symbol-config icon (same schema as addMarker/dataset
|
|
7
|
+
* points — see src/config/symbolConfig.js), writing the resolved image id/anchor back onto
|
|
8
|
+
* the feature so styles.js's pointSymbol() layer can render it. icon-offset is handled
|
|
9
|
+
* separately below since it can't safely be a per-feature property.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const hasSymbolStyle = (properties) => !!(properties?.symbol || properties?.symbolSvgContent)
|
|
13
|
+
|
|
14
|
+
const POINT_SYMBOL_LAYER_ID = 'point-symbol'
|
|
15
|
+
|
|
16
|
+
// icon-offset can't be a raw per-feature `get` on an array property — MapLibre's GeoJSON
|
|
17
|
+
// sources silently JSON.stringify arrays, so it reads back a string at render time. Instead
|
|
18
|
+
// each symbolImageId's offset is folded into a `match` expression keyed on that (safe, string) property.
|
|
19
|
+
const buildIconOffsetExpression = (offsetsByImageId) => {
|
|
20
|
+
const expression = ['match', ['get', 'user_symbolImageId']]
|
|
21
|
+
for (const [imageId, offset] of Object.entries(offsetsByImageId)) {
|
|
22
|
+
expression.push(imageId, ['literal', offset])
|
|
23
|
+
}
|
|
24
|
+
expression.push(['literal', [0, 0]]) // fallback for any id not yet registered
|
|
25
|
+
return expression
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const registerSymbolIconOffset = (map, symbolImageId, offset) => {
|
|
29
|
+
map._symbolIconOffsetMap ??= {}
|
|
30
|
+
if (map._symbolIconOffsetMap[symbolImageId]) {
|
|
31
|
+
return // offset is deterministic per id — already registered, nothing changed
|
|
32
|
+
}
|
|
33
|
+
map._symbolIconOffsetMap[symbolImageId] = offset
|
|
34
|
+
const expression = buildIconOffsetExpression(map._symbolIconOffsetMap)
|
|
35
|
+
;['hot', 'cold'].forEach((suffix) => {
|
|
36
|
+
const layerId = `${POINT_SYMBOL_LAYER_ID}.${suffix}`
|
|
37
|
+
if (map.getLayer(layerId)) {
|
|
38
|
+
map.setLayoutProperty(layerId, 'icon-offset', expression)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// map.getPixelRatio() is only set once at map construction, so it won't reflect a later
|
|
44
|
+
// map-size change on its own. Callers reacting to MAP_SET_PIXEL_RATIO should pass the fresh
|
|
45
|
+
// value through as pixelRatioOverride instead.
|
|
46
|
+
export const getPixelRatio = (map) => map.getPixelRatio?.() || 1
|
|
47
|
+
|
|
48
|
+
// Does the async work for one point without touching the store — draw.add() is left to the
|
|
49
|
+
// caller so refreshAllPointSymbols can batch every point into one call (see its comment why).
|
|
50
|
+
// Returns null if there's nothing to write back (no symbol config, feature gone, or unresolvable id).
|
|
51
|
+
const resolvePointSymbolFeature = async ({ draw, mapProvider, map, featureId, properties, pixelRatioOverride }) => {
|
|
52
|
+
if (!hasSymbolStyle(properties)) {
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const mapStyle = map._drawCurrentMapStyle
|
|
57
|
+
const pixelRatio = pixelRatioOverride ?? getPixelRatio(map)
|
|
58
|
+
|
|
59
|
+
await mapProvider.addSymbolsToMap([properties], mapStyle, symbolRegistry)
|
|
60
|
+
|
|
61
|
+
const feature = draw.get(featureId)
|
|
62
|
+
if (!feature) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const symbolImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, pixelRatio)
|
|
67
|
+
if (!symbolImageId) {
|
|
68
|
+
return null
|
|
69
|
+
}
|
|
70
|
+
// addSymbolsToMap() (just awaited above) already mapped this id's active/selected variants
|
|
71
|
+
// into map._activeSymbolImageMap/_selectedSymbolImageMap — read them back so the highlight
|
|
72
|
+
// ring (highlightFeatures.js) can reference each point's own precomputed variant directly.
|
|
73
|
+
const symbolActiveImageId = map._activeSymbolImageMap?.[symbolImageId] ?? null
|
|
74
|
+
const symbolSelectedImageId = map._selectedSymbolImageMap?.[symbolImageId] ?? null
|
|
75
|
+
const symbolDef = symbolRegistry.getSymbolDef(properties)
|
|
76
|
+
const rawAnchor = getSymbolAnchor(properties, symbolDef)
|
|
77
|
+
const anchor = anchorToMaplibre(rawAnchor)
|
|
78
|
+
// icon-anchor only has 9 discrete positions, so an off-grid anchor (e.g. pin's [0.5, 0.9])
|
|
79
|
+
// loses precision snapping to the nearest one — icon-offset corrects that gap.
|
|
80
|
+
const offset = anchorToMaplibreOffset(rawAnchor, getSymbolViewBox(properties, symbolDef))
|
|
81
|
+
registerSymbolIconOffset(map, symbolImageId, offset)
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
...feature,
|
|
85
|
+
properties: {
|
|
86
|
+
...properties,
|
|
87
|
+
symbolImageId,
|
|
88
|
+
symbolIconAnchor: anchor,
|
|
89
|
+
symbolActiveImageId,
|
|
90
|
+
symbolSelectedImageId
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resolve one point feature's symbol image and write it back onto the feature.
|
|
97
|
+
* A no-op for points with no symbol config (nothing to render as an icon).
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} params
|
|
100
|
+
* @param {Object} params.draw - the raw MapboxDraw instance (needs get/add)
|
|
101
|
+
* @param {Object} params.mapProvider - MapLibreProvider (needs addSymbolsToMap)
|
|
102
|
+
* @param {Object} params.map - MapLibre map instance (needs _drawCurrentMapStyle, getPixelRatio)
|
|
103
|
+
* @param {string} params.featureId
|
|
104
|
+
* @param {Object} params.properties - the point feature's current properties
|
|
105
|
+
* @param {number} [params.pixelRatioOverride] - use this instead of map.getPixelRatio(), for
|
|
106
|
+
* callers (map-size refresh) that already know the freshly computed value
|
|
107
|
+
* @returns {Promise<void>}
|
|
108
|
+
*/
|
|
109
|
+
export const resolvePointSymbol = async (params) => {
|
|
110
|
+
try {
|
|
111
|
+
const feature = await resolvePointSymbolFeature(params)
|
|
112
|
+
if (!feature) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
// draw.setFeatureProperty() only marks the feature dirty for mapbox-gl-draw's own mode-
|
|
116
|
+
// dispatch loop, which won't run again until the next interaction. draw.add() on an
|
|
117
|
+
// existing id updates its properties and renders unconditionally, so it's used instead.
|
|
118
|
+
params.draw.add(feature)
|
|
119
|
+
} catch (err) {
|
|
120
|
+
// A silent failure here means a point simply never gets/keeps an icon, with nothing in
|
|
121
|
+
// the UI to explain why — surface it instead of letting the rejection vanish.
|
|
122
|
+
console.error('[draw] failed to resolve point symbol', params.featureId, err) // NOSONAR
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Re-resolve every existing drawn point's symbol — called on map style/pixel-ratio change,
|
|
128
|
+
* since the rasterised image (colours, and pixel-ratio-scaled dimensions) depends on both.
|
|
129
|
+
*
|
|
130
|
+
* @param {Object} params
|
|
131
|
+
* @param {Object} params.draw - the raw MapboxDraw instance (needs getAll)
|
|
132
|
+
* @param {Object} params.mapProvider
|
|
133
|
+
* @param {Object} params.map
|
|
134
|
+
* @param {number} [params.pixelRatioOverride] - see resolvePointSymbol
|
|
135
|
+
* @returns {Promise<void>}
|
|
136
|
+
*/
|
|
137
|
+
export const refreshAllPointSymbols = async ({ draw, mapProvider, map, pixelRatioOverride }) => {
|
|
138
|
+
const points = draw.getAll().features.filter(
|
|
139
|
+
(f) => f.geometry.type === 'Point' && hasSymbolStyle(f.properties)
|
|
140
|
+
)
|
|
141
|
+
const results = await Promise.allSettled(points.map((f) =>
|
|
142
|
+
resolvePointSymbolFeature({ draw, mapProvider, map, featureId: f.id, properties: f.properties, pixelRatioOverride })
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
results.filter((r) => r.status === 'rejected').forEach((r) => {
|
|
146
|
+
console.error('[draw] failed to resolve point symbol', r.reason) // NOSONAR
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// One combined draw.add() call triggers a single render. Adding one point at a time here
|
|
150
|
+
// would let mapbox-gl-draw's debounced render fire mid-batch, painting a not-yet-resolved
|
|
151
|
+
// point with its stale, now-unregistered image id ("Image X could not be loaded").
|
|
152
|
+
const features = results
|
|
153
|
+
.filter((r) => r.status === 'fulfilled' && r.value)
|
|
154
|
+
.map((r) => r.value)
|
|
155
|
+
if (features.length) {
|
|
156
|
+
draw.add({ type: 'FeatureCollection', features })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { hasSymbolStyle, getPixelRatio, resolvePointSymbol, refreshAllPointSymbols } from './pointSymbolImages.js'
|
|
2
|
+
import { symbolRegistry } from '../../../../../src/services/symbolRegistry.js'
|
|
3
|
+
|
|
4
|
+
const mapStyle = { id: 'outdoor', mapColorScheme: 'light' }
|
|
5
|
+
|
|
6
|
+
const createMap = ({ pixelRatio = 2 } = {}) => ({
|
|
7
|
+
_drawCurrentMapStyle: mapStyle,
|
|
8
|
+
getPixelRatio: () => pixelRatio,
|
|
9
|
+
// point-symbol.hot/.cold — registerSymbolIconOffset() looks these up to reapply the
|
|
10
|
+
// icon-offset match expression whenever a new symbolImageId's offset is registered.
|
|
11
|
+
getLayer: jest.fn(() => true),
|
|
12
|
+
setLayoutProperty: jest.fn()
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
const createMapProvider = () => ({
|
|
16
|
+
addSymbolsToMap: jest.fn(() => Promise.resolve())
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const createDraw = (features = []) => ({
|
|
20
|
+
get: jest.fn((id) => features.find((f) => f.id === id) ?? null),
|
|
21
|
+
add: jest.fn(),
|
|
22
|
+
getAll: jest.fn(() => ({ features }))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const point = (id, properties, coordinates = [1, 2]) =>
|
|
26
|
+
({ id, type: 'Feature', geometry: { type: 'Point', coordinates }, properties })
|
|
27
|
+
|
|
28
|
+
describe('hasSymbolStyle', () => {
|
|
29
|
+
it('is true when symbol is set', () => {
|
|
30
|
+
expect(hasSymbolStyle({ symbol: 'pin' })).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('is true when symbolSvgContent is set', () => {
|
|
34
|
+
expect(hasSymbolStyle({ symbolSvgContent: '<path/>' })).toBe(true)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('is false with neither, or no properties at all', () => {
|
|
38
|
+
expect(hasSymbolStyle({})).toBe(false)
|
|
39
|
+
expect(hasSymbolStyle(undefined)).toBe(false)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('getPixelRatio', () => {
|
|
44
|
+
it('reads map.getPixelRatio()', () => {
|
|
45
|
+
expect(getPixelRatio(createMap({ pixelRatio: 3 }))).toBe(3)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('falls back to 1 when getPixelRatio is unavailable or falsy', () => {
|
|
49
|
+
expect(getPixelRatio({})).toBe(1)
|
|
50
|
+
expect(getPixelRatio({ getPixelRatio: () => 0 })).toBe(1)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe('resolvePointSymbol', () => {
|
|
55
|
+
it('does nothing for a feature with no symbol config', async () => {
|
|
56
|
+
const map = createMap()
|
|
57
|
+
const mapProvider = createMapProvider()
|
|
58
|
+
const draw = createDraw()
|
|
59
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties: {} })
|
|
60
|
+
expect(mapProvider.addSymbolsToMap).not.toHaveBeenCalled()
|
|
61
|
+
expect(draw.add).not.toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('registers the symbol image and re-adds the feature with symbolImageId/symbolIconAnchor, forcing a render', async () => {
|
|
65
|
+
const map = createMap()
|
|
66
|
+
const mapProvider = createMapProvider()
|
|
67
|
+
const properties = { symbol: 'pin', label: 'a point' }
|
|
68
|
+
const draw = createDraw([point('p1', properties)])
|
|
69
|
+
|
|
70
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
71
|
+
|
|
72
|
+
expect(mapProvider.addSymbolsToMap).toHaveBeenCalledWith([properties], mapStyle, symbolRegistry)
|
|
73
|
+
const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
|
|
74
|
+
expect(draw.add).toHaveBeenCalledWith({
|
|
75
|
+
id: 'p1',
|
|
76
|
+
type: 'Feature',
|
|
77
|
+
geometry: { type: 'Point', coordinates: [1, 2] },
|
|
78
|
+
properties: {
|
|
79
|
+
...properties,
|
|
80
|
+
symbolImageId: expectedImageId,
|
|
81
|
+
symbolIconAnchor: 'bottom', // pin's default anchor [0.5, 0.9] snaps to icon-anchor 'bottom'
|
|
82
|
+
// Nothing registered in map._activeSymbolImageMap/_selectedSymbolImageMap here since
|
|
83
|
+
// addSymbolsToMap is mocked out (a no-op) in this test — see the dedicated test below
|
|
84
|
+
// for the case where it actually populates them.
|
|
85
|
+
symbolActiveImageId: null,
|
|
86
|
+
symbolSelectedImageId: null
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
// icon-offset corrects the precision lost snapping 0.9 to 1.0 against pin's 44px viewBox —
|
|
90
|
+
// registered into the point-symbol layers' icon-offset match expression instead of onto
|
|
91
|
+
// the feature (see registerSymbolIconOffset's comment for why).
|
|
92
|
+
expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 4.4])
|
|
93
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith('point-symbol.hot', 'icon-offset', [
|
|
94
|
+
'match', ['get', 'user_symbolImageId'], expectedImageId, ['literal', [0, 4.4]], ['literal', [0, 0]]
|
|
95
|
+
])
|
|
96
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith('point-symbol.cold', 'icon-offset', [
|
|
97
|
+
'match', ['get', 'user_symbolImageId'], expectedImageId, ['literal', [0, 4.4]], ['literal', [0, 0]]
|
|
98
|
+
])
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('only applies the icon-offset expression to point-symbol layers that actually exist on the map', async () => {
|
|
102
|
+
const map = createMap()
|
|
103
|
+
map.getLayer.mockImplementation((layerId) => layerId === 'point-symbol.cold') // hot copy absent
|
|
104
|
+
const mapProvider = createMapProvider()
|
|
105
|
+
const properties = { symbol: 'pin' }
|
|
106
|
+
const draw = createDraw([point('p1', properties)])
|
|
107
|
+
|
|
108
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
109
|
+
|
|
110
|
+
expect(map.setLayoutProperty).toHaveBeenCalledWith('point-symbol.cold', 'icon-offset', expect.anything())
|
|
111
|
+
expect(map.setLayoutProperty).not.toHaveBeenCalledWith('point-symbol.hot', 'icon-offset', expect.anything())
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('does not re-register or re-apply icon-offset for a symbolImageId already known (offset is deterministic per id)', async () => {
|
|
115
|
+
const map = createMap()
|
|
116
|
+
const mapProvider = createMapProvider()
|
|
117
|
+
const properties = { symbol: 'pin' }
|
|
118
|
+
const draw = createDraw([point('p1', properties), point('p2', properties)])
|
|
119
|
+
|
|
120
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
121
|
+
map.setLayoutProperty.mockClear()
|
|
122
|
+
|
|
123
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p2', properties })
|
|
124
|
+
|
|
125
|
+
expect(map.setLayoutProperty).not.toHaveBeenCalled()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('reads the active/selected variant ids back from map._activeSymbolImageMap/_selectedSymbolImageMap once addSymbolsToMap has registered them', async () => {
|
|
129
|
+
const map = createMap()
|
|
130
|
+
const properties = { symbol: 'pin' }
|
|
131
|
+
const draw = createDraw([point('p1', properties)])
|
|
132
|
+
const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
|
|
133
|
+
// Simulates what the real addSymbolsToMap (providers/maplibre/src/utils/symbolImages.js)
|
|
134
|
+
// does as a side effect: populate these maps, keyed by the normal variant's own id.
|
|
135
|
+
const mapProvider = {
|
|
136
|
+
addSymbolsToMap: jest.fn(() => {
|
|
137
|
+
map._activeSymbolImageMap = { [expectedImageId]: 'symbol-act-xyz' }
|
|
138
|
+
map._selectedSymbolImageMap = { [expectedImageId]: 'symbol-sel-xyz' }
|
|
139
|
+
return Promise.resolve()
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
144
|
+
|
|
145
|
+
expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({
|
|
146
|
+
properties: expect.objectContaining({
|
|
147
|
+
symbolActiveImageId: 'symbol-act-xyz',
|
|
148
|
+
symbolSelectedImageId: 'symbol-sel-xyz'
|
|
149
|
+
})
|
|
150
|
+
}))
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('does nothing when getSymbolImageId resolves null (e.g. an unresolvable symbol id)', async () => {
|
|
154
|
+
const map = createMap()
|
|
155
|
+
const mapProvider = createMapProvider()
|
|
156
|
+
const properties = { symbol: 'not-a-real-symbol' }
|
|
157
|
+
const draw = createDraw([point('p1', properties)])
|
|
158
|
+
const spy = jest.spyOn(symbolRegistry, 'getSymbolImageId').mockReturnValueOnce(null)
|
|
159
|
+
|
|
160
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
161
|
+
|
|
162
|
+
expect(draw.add).not.toHaveBeenCalled()
|
|
163
|
+
spy.mockRestore()
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('does not re-add the feature if it was deleted while registration was in flight', async () => {
|
|
167
|
+
const map = createMap()
|
|
168
|
+
const properties = { symbol: 'pin' }
|
|
169
|
+
let resolveRegistration
|
|
170
|
+
const mapProvider = { addSymbolsToMap: jest.fn(() => new Promise((resolve) => { resolveRegistration = resolve })) }
|
|
171
|
+
const draw = createDraw() // empty — feature already gone
|
|
172
|
+
|
|
173
|
+
const pending = resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
174
|
+
resolveRegistration()
|
|
175
|
+
await pending
|
|
176
|
+
|
|
177
|
+
expect(draw.add).not.toHaveBeenCalled()
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('uses a custom symbolAnchor when the style provides one, with no offset needed for an on-grid anchor', async () => {
|
|
181
|
+
const map = createMap()
|
|
182
|
+
const mapProvider = createMapProvider()
|
|
183
|
+
const properties = { symbol: 'circle', symbolAnchor: [0, 0] }
|
|
184
|
+
const draw = createDraw([point('p1', properties)])
|
|
185
|
+
|
|
186
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
187
|
+
|
|
188
|
+
expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({
|
|
189
|
+
properties: expect.objectContaining({ symbolIconAnchor: 'top-left' })
|
|
190
|
+
}))
|
|
191
|
+
const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
|
|
192
|
+
expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 0])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('computes a non-zero icon-offset for an off-grid custom symbolAnchor', async () => {
|
|
196
|
+
const map = createMap()
|
|
197
|
+
const mapProvider = createMapProvider()
|
|
198
|
+
const properties = { symbol: 'circle', symbolAnchor: [0.5, 0.8] }
|
|
199
|
+
const draw = createDraw([point('p1', properties)])
|
|
200
|
+
|
|
201
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
202
|
+
|
|
203
|
+
// circle's viewBox is 44×44 — anchor 0.8 snaps to icon-anchor 'bottom' (1.0), offset corrects the gap
|
|
204
|
+
expect(draw.add).toHaveBeenCalledWith(expect.objectContaining({
|
|
205
|
+
properties: expect.objectContaining({ symbolIconAnchor: 'bottom' })
|
|
206
|
+
}))
|
|
207
|
+
const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
|
|
208
|
+
expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 8.8])
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('respects a custom symbolViewBox when computing the offset', async () => {
|
|
212
|
+
const map = createMap()
|
|
213
|
+
const mapProvider = createMapProvider()
|
|
214
|
+
const properties = { symbol: 'pin', symbolViewBox: '0 0 100 100' }
|
|
215
|
+
const draw = createDraw([point('p1', properties)])
|
|
216
|
+
|
|
217
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
218
|
+
|
|
219
|
+
// pin's anchor [0.5, 0.9] against a 100×100 viewBox instead of the built-in 44×44
|
|
220
|
+
const expectedImageId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, 2)
|
|
221
|
+
expect(map._symbolIconOffsetMap[expectedImageId]).toEqual([0, 10])
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('surfaces (rather than swallows) a registration failure', async () => {
|
|
225
|
+
const map = createMap()
|
|
226
|
+
const properties = { symbol: 'pin' }
|
|
227
|
+
const mapProvider = { addSymbolsToMap: jest.fn(() => Promise.reject(new Error('rasterise failed'))) }
|
|
228
|
+
const draw = createDraw([point('p1', properties)])
|
|
229
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
230
|
+
|
|
231
|
+
await resolvePointSymbol({ draw, mapProvider, map, featureId: 'p1', properties })
|
|
232
|
+
|
|
233
|
+
expect(draw.add).not.toHaveBeenCalled()
|
|
234
|
+
expect(consoleError).toHaveBeenCalledWith('[draw] failed to resolve point symbol', 'p1', expect.any(Error))
|
|
235
|
+
consoleError.mockRestore()
|
|
236
|
+
})
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
describe('refreshAllPointSymbols', () => {
|
|
240
|
+
it('re-resolves every point feature with a symbol config, ignoring non-point and unstyled features', async () => {
|
|
241
|
+
const map = createMap()
|
|
242
|
+
const mapProvider = createMapProvider()
|
|
243
|
+
const pointWithSymbol = { id: 'p1', geometry: { type: 'Point' }, properties: { symbol: 'pin' } }
|
|
244
|
+
const pointWithoutSymbol = { id: 'p2', geometry: { type: 'Point' }, properties: {} }
|
|
245
|
+
const polygon = { id: 'poly1', geometry: { type: 'Polygon' }, properties: { symbol: 'pin' } }
|
|
246
|
+
const draw = createDraw([pointWithSymbol, pointWithoutSymbol, polygon])
|
|
247
|
+
|
|
248
|
+
await refreshAllPointSymbols({ draw, mapProvider, map })
|
|
249
|
+
|
|
250
|
+
expect(mapProvider.addSymbolsToMap).toHaveBeenCalledTimes(1)
|
|
251
|
+
expect(mapProvider.addSymbolsToMap).toHaveBeenCalledWith([pointWithSymbol.properties], mapStyle, symbolRegistry)
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
it('does nothing when there are no drawn points', async () => {
|
|
255
|
+
const map = createMap()
|
|
256
|
+
const mapProvider = createMapProvider()
|
|
257
|
+
const draw = createDraw([])
|
|
258
|
+
await refreshAllPointSymbols({ draw, mapProvider, map })
|
|
259
|
+
expect(mapProvider.addSymbolsToMap).not.toHaveBeenCalled()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// A style change re-resolves every point concurrently; each one's addSymbolsToMap()
|
|
263
|
+
// settles on its own schedule. A single combined draw.add() (rather than one call per
|
|
264
|
+
// point) is what keeps mapbox-gl-draw's debounced render from ever painting one point's
|
|
265
|
+
// fresh data alongside another's still-stale, now-unregistered image id — see
|
|
266
|
+
// refreshAllPointSymbols's own comment.
|
|
267
|
+
it('batches every resolved point into a single draw.add() call, not one per point', async () => {
|
|
268
|
+
const map = createMap()
|
|
269
|
+
const p1 = point('p1', { symbol: 'pin' })
|
|
270
|
+
const p2 = point('p2', { symbol: 'square' }, [3, 4])
|
|
271
|
+
const draw = createDraw([p1, p2])
|
|
272
|
+
// Stagger completion — p2 resolves after p1 to simulate genuinely concurrent, unevenly
|
|
273
|
+
// timed rasterisation.
|
|
274
|
+
let resolveP1, resolveP2
|
|
275
|
+
const mapProvider = {
|
|
276
|
+
addSymbolsToMap: jest.fn((configs) => new Promise((resolve) => {
|
|
277
|
+
const target = configs[0].symbol === 'pin' ? (v) => { resolveP1 = v } : (v) => { resolveP2 = v }
|
|
278
|
+
target(resolve)
|
|
279
|
+
}))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const pending = refreshAllPointSymbols({ draw, mapProvider, map })
|
|
283
|
+
resolveP2()
|
|
284
|
+
await Promise.resolve()
|
|
285
|
+
resolveP1()
|
|
286
|
+
await pending
|
|
287
|
+
|
|
288
|
+
expect(draw.add).toHaveBeenCalledTimes(1)
|
|
289
|
+
const [{ type, features }] = draw.add.mock.calls[0]
|
|
290
|
+
expect(type).toBe('FeatureCollection')
|
|
291
|
+
expect(features.map((f) => f.id).sort()).toEqual(['p1', 'p2'])
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it('logs and still applies the points that did resolve when one point fails', async () => {
|
|
295
|
+
const map = createMap()
|
|
296
|
+
const p1 = point('p1', { symbol: 'pin' })
|
|
297
|
+
const p2 = point('p2', { symbol: 'not-a-real-symbol' })
|
|
298
|
+
const draw = createDraw([p1, p2])
|
|
299
|
+
const mapProvider = {
|
|
300
|
+
addSymbolsToMap: jest.fn((configs) =>
|
|
301
|
+
configs[0].symbol === 'not-a-real-symbol' ? Promise.reject(new Error('rasterise failed')) : Promise.resolve()
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
305
|
+
|
|
306
|
+
await refreshAllPointSymbols({ draw, mapProvider, map })
|
|
307
|
+
|
|
308
|
+
expect(consoleError).toHaveBeenCalledWith('[draw] failed to resolve point symbol', expect.any(Error))
|
|
309
|
+
expect(draw.add).toHaveBeenCalledTimes(1)
|
|
310
|
+
const [{ features }] = draw.add.mock.calls[0]
|
|
311
|
+
expect(features.map((f) => f.id)).toEqual(['p1'])
|
|
312
|
+
consoleError.mockRestore()
|
|
313
|
+
})
|
|
314
|
+
})
|
|
@@ -95,12 +95,17 @@ function patchColorMethods (proto, orig, colors) {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// Skip when disabled or
|
|
98
|
+
// Skip when disabled, zooming, or (for unmarked calls only, while suspended — see
|
|
99
|
+
// snapHelpers.js's setAutoSnapSuspended) mid-drag; clean up internal arrays to prevent memory
|
|
100
|
+
// accumulation
|
|
99
101
|
function patchSnapMethod (proto, orig) {
|
|
100
102
|
proto.snapToClosestPoint = function (e) {
|
|
101
103
|
if (!this.status || this.map?._isZooming) {
|
|
102
104
|
return undefined
|
|
103
105
|
}
|
|
106
|
+
if (this._autoSnapSuspended && !e._explicit) {
|
|
107
|
+
return undefined
|
|
108
|
+
}
|
|
104
109
|
try {
|
|
105
110
|
const result = orig.snapToClosestPoint.call(this, e)
|
|
106
111
|
if (this.closeFeatures?.length > MAX_SNAP_FEATURE_CACHE) {
|
|
@@ -235,4 +235,25 @@ describe('snapToClosestPoint', () => {
|
|
|
235
235
|
expect(self.snapStatus).toBe(false)
|
|
236
236
|
expect(self.snapCoords).toBeNull()
|
|
237
237
|
})
|
|
238
|
+
|
|
239
|
+
// While an edit mode's own offset-aware drag has suspended it (setAutoSnapSuspended, see its
|
|
240
|
+
// own comment), the snap library's own uncontrolled mousemove-driven auto-query must be
|
|
241
|
+
// skipped — otherwise it re-queries at the raw cursor and fights the mode's own query, which
|
|
242
|
+
// is why the two used to visibly fall out of sync (the indicator tracking the cursor while
|
|
243
|
+
// the dragged feature snapped to a different, offset-corrected result).
|
|
244
|
+
test('skips an unmarked call while auto-snap is suspended', () => {
|
|
245
|
+
const self = { status: true, map: {}, _autoSnapSuspended: true }
|
|
246
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBeUndefined()
|
|
247
|
+
expect(MapboxSnap.__orig.snapToClosestPoint).not.toHaveBeenCalled()
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('still runs an explicit call while auto-snap is suspended', () => {
|
|
251
|
+
const self = { status: true, map: {}, _autoSnapSuspended: true }
|
|
252
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call(self, { _explicit: true })).toBe('orig-snap')
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
test('runs unmarked calls normally once auto-snap is no longer suspended', () => {
|
|
256
|
+
const self = { status: true, map: {}, _autoSnapSuspended: false }
|
|
257
|
+
expect(MapboxSnap.prototype.snapToClosestPoint.call(self, {})).toBe('orig-snap')
|
|
258
|
+
})
|
|
238
259
|
})
|
|
@@ -13,8 +13,12 @@ function cleanupOldSnap (map) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// Snap indicators (and all snap processing) are only meaningful while actively
|
|
16
|
-
// drawing or editing
|
|
17
|
-
|
|
16
|
+
// drawing or editing — outside these modes there is nothing to snap to. This list is
|
|
17
|
+
// independent of (and must be kept in sync with) MaplibreDrawAdapter's own DRAW_MODES set,
|
|
18
|
+
// which only controls when the snap *indicator* gets cleared on mode exit — this one gates
|
|
19
|
+
// snap.status itself, which both the visual indicator AND actual snap-to-coordinate
|
|
20
|
+
// (isSnapActive() in snapHelpers.js) depend on.
|
|
21
|
+
const SNAP_ACTIVE_MODES = new Set(['draw_polygon', 'draw_line', 'draw_point', 'edit_vertex', 'edit_point'])
|
|
18
22
|
|
|
19
23
|
/**
|
|
20
24
|
* Externally-controlled status: ignore library writes so status is only changed
|
|
@@ -89,6 +89,18 @@ describe('createSnapInstance', () => {
|
|
|
89
89
|
expect(snap.status).toBe(true)
|
|
90
90
|
})
|
|
91
91
|
|
|
92
|
+
test('status is true for every draw/edit mode, including point placement/edit', () => {
|
|
93
|
+
const map = makeMap()
|
|
94
|
+
const draw = { getMode: jest.fn(() => 'draw_polygon') }
|
|
95
|
+
const snap = createSnapInstance(map, draw, {}, { ...config, status: false })
|
|
96
|
+
snap.setSnapStatus(true)
|
|
97
|
+
|
|
98
|
+
for (const mode of ['draw_polygon', 'draw_line', 'draw_point', 'edit_vertex', 'edit_point']) {
|
|
99
|
+
draw.getMode.mockReturnValue(mode)
|
|
100
|
+
expect(snap.status).toBe(true)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
|
|
92
104
|
test('setSnapLayers overrides, resets and ignores invalid input', () => {
|
|
93
105
|
const map = makeMap()
|
|
94
106
|
const snap = createSnapInstance(map, {}, {}, { ...config, layers: ['a'] })
|