@defra/interactive-map 0.0.40-alpha → 0.0.41-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/control-definition.md +9 -1
- package/docs/api/slots.md +26 -5
- package/docs/assets/images/slot-map.svg +61 -262
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/draw.md +79 -16
- package/docs/plugins/search.md +1 -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/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/editFeature.js +7 -4
- package/plugins/draw/src/api/editFeature.test.js +16 -0
- package/plugins/draw/src/api/newPoint.js +42 -0
- package/plugins/draw/src/api/newPoint.test.js +117 -0
- 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/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 +62 -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 +41 -0
- package/plugins/{datasets → map-key}/src/components/Key/Key.module.scss +15 -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 +29 -0
- package/plugins/map-key/src/components/Key/KeyItem.test.jsx +67 -0
- package/plugins/map-key/src/components/Key/KeySvg.jsx +42 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvg.test.jsx +14 -12
- package/plugins/map-key/src/components/Key/KeySvgLine.jsx +28 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgLine.test.jsx +1 -1
- package/plugins/map-key/src/components/Key/KeySvgPattern.jsx +23 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgPattern.test.jsx +2 -2
- package/plugins/map-key/src/components/Key/KeySvgRect.jsx +33 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgRect.test.jsx +1 -1
- package/plugins/map-key/src/components/Key/KeySvgSymbol.jsx +27 -0
- package/plugins/{datasets → map-key}/src/components/Key/KeySvgSymbol.test.jsx +5 -6
- package/plugins/map-key/src/components/Key/MapKey.jsx +50 -0
- package/plugins/map-key/src/components/Key/MapKey.test.jsx +130 -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/MoveControls/MoveControls.module.scss +41 -10
- package/src/App/components/Panel/Panel.jsx +18 -3
- package/src/App/components/Panel/Panel.module.scss +18 -8
- package/src/App/components/Panel/Panel.test.jsx +34 -0
- 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/mapButtons.js +28 -12
- package/src/App/renderer/mapButtons.test.js +17 -4
- package/src/App/renderer/mapControls.js +3 -1
- package/src/App/renderer/mapControls.test.js +16 -0
- package/src/App/renderer/mapPanels.js +44 -9
- package/src/App/renderer/mapPanels.test.js +51 -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/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 +11 -1
- 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/KeySvgPattern.jsx +0 -13
- package/plugins/datasets/src/components/Key/KeySvgRect.jsx +0 -22
- package/plugins/datasets/src/components/Key/KeySvgSymbol.jsx +0 -16
- 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,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Coordinate mutation for edit_point. A Point has one coordinate, always selected — no
|
|
3
|
+
* ring/segment bookkeeping the way edit/undoOps.js and edit/vertexOps.js need for a
|
|
4
|
+
* Polygon/LineString. movePoint's (olFeature, _index, coord) signature matches
|
|
5
|
+
* edit/vertexOps.js's moveVertex exactly (the unused index is only there so nudge.js/
|
|
6
|
+
* touchHandler.js can call either interchangeably via the injected moveCoord parameter).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Signature-compatible with edit/vertexOps.js's moveVertex — _index is unused, a Point has none. */
|
|
10
|
+
export const movePoint = (olFeature, _index, coord) => {
|
|
11
|
+
olFeature.getGeometry().setCoordinates([...coord])
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Op type stays 'move_vertex' (not 'move_point') — deriveModifyOp (edit/modifyInteraction.js,
|
|
15
|
+
// reused unchanged) always produces that type, and keeping it lets applyPointUndo below mirror
|
|
16
|
+
// edit/undoOps.js's applyUndo dispatch pattern with the shared leaf handlers' onVertexMoved
|
|
17
|
+
// callbacks (which push {type: 'move_vertex', ...}) working completely unmodified.
|
|
18
|
+
export const undoMovePoint = (olFeature, op) => {
|
|
19
|
+
const { previousCoord } = op
|
|
20
|
+
olFeature.getGeometry().setCoordinates([...previousCoord])
|
|
21
|
+
return 0
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Dispatch the correct undo handler based on operation type — mirrors edit/undoOps.js's
|
|
26
|
+
* applyUndo, but with the single op type a Point ever produces.
|
|
27
|
+
* @returns {number} vertex index to re-select after undo (always 0), or -1 for none
|
|
28
|
+
*/
|
|
29
|
+
export const applyPointUndo = (olFeature, op) => {
|
|
30
|
+
if (op.type === 'move_vertex') {
|
|
31
|
+
return undoMovePoint(olFeature, op)
|
|
32
|
+
}
|
|
33
|
+
return -1
|
|
34
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { movePoint, undoMovePoint, applyPointUndo } from './pointOps.js'
|
|
2
|
+
import { pointFeature } from '../__helpers__/harness.js'
|
|
3
|
+
|
|
4
|
+
describe('movePoint', () => {
|
|
5
|
+
test('writes the coordinate directly, ignoring the unused index', () => {
|
|
6
|
+
const feature = pointFeature([0, 0])
|
|
7
|
+
movePoint(feature, 0, [5, 5])
|
|
8
|
+
expect(feature.getGeometry().getCoordinates()).toEqual([5, 5])
|
|
9
|
+
})
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('undoMovePoint', () => {
|
|
13
|
+
test('restores the previous coordinate and returns vertex index 0', () => {
|
|
14
|
+
const feature = pointFeature([5, 5])
|
|
15
|
+
expect(undoMovePoint(feature, { previousCoord: [1, 2] })).toBe(0)
|
|
16
|
+
expect(feature.getGeometry().getCoordinates()).toEqual([1, 2])
|
|
17
|
+
})
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
describe('applyPointUndo', () => {
|
|
21
|
+
test('dispatches move_vertex to undoMovePoint', () => {
|
|
22
|
+
const feature = pointFeature([5, 5])
|
|
23
|
+
expect(applyPointUndo(feature, { type: 'move_vertex', previousCoord: [3, 4] })).toBe(0)
|
|
24
|
+
expect(feature.getGeometry().getCoordinates()).toEqual([3, 4])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('returns -1 for an unknown operation type', () => {
|
|
28
|
+
const feature = pointFeature([5, 5])
|
|
29
|
+
expect(applyPointUndo(feature, { type: 'unknown' })).toBe(-1)
|
|
30
|
+
expect(feature.getGeometry().getCoordinates()).toEqual([5, 5])
|
|
31
|
+
})
|
|
32
|
+
})
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
2
|
+
|
|
3
|
+
// Deferred commit-level geometrychange emitter (feature + change phase + vertex index)
|
|
4
|
+
// consumed by the validation layer — copied from edit/selectionState.js's own (identical
|
|
5
|
+
// shape, no ring dependency to strip).
|
|
6
|
+
const createGeometryValidationEmitter = (manager, store, olFeature) => (phase, vertexIndex) => {
|
|
7
|
+
if (!phase) { return }
|
|
8
|
+
setTimeout(() => {
|
|
9
|
+
manager.emit(ADAPTER_EVENTS.GEOMETRY_CHANGE, { feature: store.toGeoJSON(olFeature), phase, vertexIndex })
|
|
10
|
+
}, 0)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Mutable edit_point state, matching edit/selectionState.js's public surface
|
|
15
|
+
* ({state, getState, setState, syncGeom, emitGeometryValidation, setHooks, destroy}) closely
|
|
16
|
+
* enough that edit/modifyInteraction.js, edit/touchHandler.js and edit/keyboardHandler.js all
|
|
17
|
+
* run over it unmodified (beyond the moveCoord injection) — but a point has one coordinate,
|
|
18
|
+
* always selected, and no ring/midpoint list to navigate, so this is a standalone
|
|
19
|
+
* implementation rather than a wrapper around the ring-shaped one. No handle layer to keep in
|
|
20
|
+
* step either — the point's own symbol icon is the only visual, styled by the draw layer.
|
|
21
|
+
*
|
|
22
|
+
* @param {{ manager, store, olFeature, interfaceType }} options
|
|
23
|
+
* @returns {{ state, getState, setState, syncGeom, emitGeometryValidation, setHooks, destroy }}
|
|
24
|
+
*/
|
|
25
|
+
export const createPointSelectionState = ({ manager, store, olFeature, interfaceType }) => {
|
|
26
|
+
const state = {
|
|
27
|
+
olFeature,
|
|
28
|
+
selectedVertexIndex: 0,
|
|
29
|
+
selectedVertexType: 'vertex',
|
|
30
|
+
vertices: [],
|
|
31
|
+
midpoints: [],
|
|
32
|
+
interfaceType: interfaceType ?? 'mouse'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const hooks = { onUpdate: null }
|
|
36
|
+
const setHooks = ({ onUpdate }) => { hooks.onUpdate = onUpdate }
|
|
37
|
+
|
|
38
|
+
const refreshCoord = () => {
|
|
39
|
+
state.vertices = [olFeature.getGeometry().getCoordinates()]
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The point is always selected — a selectedVertexIndex/Type write (e.g. Escape or
|
|
43
|
+
// Alt+Arrow in the reused edit/keyboardHandler.js, which try to deselect/navigate) is
|
|
44
|
+
// silently dropped rather than stranding the point with no selection. Every other update
|
|
45
|
+
// (interfaceType, vertices) still applies normally.
|
|
46
|
+
const setState = (updates) => {
|
|
47
|
+
const { selectedVertexIndex, selectedVertexType, ...rest } = updates
|
|
48
|
+
Object.assign(state, rest)
|
|
49
|
+
if (updates.vertices !== undefined) {
|
|
50
|
+
hooks.onUpdate?.()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Never emits VERTEX_CHANGE — that event unconditionally releases
|
|
55
|
+
// mapProvider.activeMoveTarget (events.js's onVertexChange). The D-pad claim is made once
|
|
56
|
+
// below (VERTEX_SELECTION) and held for the whole edit session.
|
|
57
|
+
const syncGeom = () => {
|
|
58
|
+
refreshCoord()
|
|
59
|
+
manager.emit(ADAPTER_EVENTS.UPDATE, store.toGeoJSON(olFeature))
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const emitGeometryValidation = createGeometryValidationEmitter(manager, store, olFeature)
|
|
63
|
+
|
|
64
|
+
// Keeps the cached coordinate in sync on every geometry change (e.g. during a Modify drag)
|
|
65
|
+
const onGeometryChange = () => refreshCoord()
|
|
66
|
+
olFeature.getGeometry().on('change', onGeometryChange)
|
|
67
|
+
|
|
68
|
+
refreshCoord()
|
|
69
|
+
manager.emit(ADAPTER_EVENTS.VERTEX_SELECTION, { index: 0, numVertices: 1 })
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
state,
|
|
73
|
+
getState: () => state,
|
|
74
|
+
setState,
|
|
75
|
+
syncGeom,
|
|
76
|
+
emitGeometryValidation,
|
|
77
|
+
setHooks,
|
|
78
|
+
destroy () {
|
|
79
|
+
olFeature.getGeometry().un('change', onGeometryChange)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createPointSelectionState } from './pointSelectionState.js'
|
|
2
|
+
import { createFakeManager, pointFeature } from '../__helpers__/harness.js'
|
|
3
|
+
import { ADAPTER_EVENTS } from '../../../adapterEvents.js'
|
|
4
|
+
|
|
5
|
+
const setup = (interfaceType) => {
|
|
6
|
+
const manager = createFakeManager()
|
|
7
|
+
const olFeature = pointFeature([5, 5])
|
|
8
|
+
const store = { toGeoJSON: jest.fn(() => ({ id: 'f1' })) }
|
|
9
|
+
const selection = createPointSelectionState({ manager, store, olFeature, interfaceType })
|
|
10
|
+
return { manager, olFeature, store, selection }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('getState returns the live state object', () => {
|
|
14
|
+
const { selection } = setup()
|
|
15
|
+
expect(selection.getState()).toBe(selection.state)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('defaults to mouse interface when none is given, and is always selected at index 0', () => {
|
|
19
|
+
const { selection } = setup()
|
|
20
|
+
expect(selection.state.interfaceType).toBe('mouse')
|
|
21
|
+
expect(selection.state.selectedVertexIndex).toBe(0)
|
|
22
|
+
expect(selection.state.selectedVertexType).toBe('vertex')
|
|
23
|
+
expect(setup('touch').selection.state.interfaceType).toBe('touch')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('claims the D-pad exactly once at construction, never VERTEX_CHANGE', () => {
|
|
27
|
+
const { manager } = setup()
|
|
28
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, { index: 0, numVertices: 1 })
|
|
29
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, expect.anything())
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('populates the initial coordinate at construction', () => {
|
|
33
|
+
const { selection } = setup()
|
|
34
|
+
expect(selection.state.vertices).toEqual([[5, 5]])
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('a selectedVertexIndex/Type write is silently dropped — the point stays always-selected', () => {
|
|
38
|
+
const { selection, manager } = setup()
|
|
39
|
+
manager.emit.mockClear()
|
|
40
|
+
selection.setState({ selectedVertexIndex: -1, selectedVertexType: null })
|
|
41
|
+
expect(selection.state.selectedVertexIndex).toBe(0)
|
|
42
|
+
expect(selection.state.selectedVertexType).toBe('vertex')
|
|
43
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_SELECTION, expect.anything())
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('other setState updates apply normally, without firing onUpdate unless vertices changes', () => {
|
|
47
|
+
const { selection } = setup()
|
|
48
|
+
const onUpdate = jest.fn()
|
|
49
|
+
selection.setHooks({ onUpdate })
|
|
50
|
+
selection.setState({ interfaceType: 'keyboard' })
|
|
51
|
+
expect(selection.state.interfaceType).toBe('keyboard')
|
|
52
|
+
expect(onUpdate).not.toHaveBeenCalled()
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('a vertices update fires the onUpdate hook', () => {
|
|
56
|
+
const { selection } = setup()
|
|
57
|
+
const onUpdate = jest.fn()
|
|
58
|
+
selection.setHooks({ onUpdate })
|
|
59
|
+
selection.setState({ vertices: [[9, 9]] })
|
|
60
|
+
expect(onUpdate).toHaveBeenCalled()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('syncGeom derives the coordinate from the geometry and emits UPDATE only (never VERTEX_CHANGE)', () => {
|
|
64
|
+
const { selection, manager, store, olFeature } = setup()
|
|
65
|
+
manager.emit.mockClear()
|
|
66
|
+
olFeature.getGeometry().setCoordinates([7, 8])
|
|
67
|
+
selection.syncGeom()
|
|
68
|
+
expect(selection.state.vertices).toEqual([[7, 8]])
|
|
69
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.UPDATE, store.toGeoJSON())
|
|
70
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.VERTEX_CHANGE, expect.anything())
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test('emitGeometryValidation emits a deferred commit-level geometrychange with the change phase', () => {
|
|
74
|
+
jest.useFakeTimers()
|
|
75
|
+
const { selection, manager, store } = setup()
|
|
76
|
+
manager.emit.mockClear()
|
|
77
|
+
|
|
78
|
+
selection.emitGeometryValidation('commit-move', 0)
|
|
79
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.anything())
|
|
80
|
+
|
|
81
|
+
jest.runAllTimers()
|
|
82
|
+
expect(manager.emit).toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, {
|
|
83
|
+
feature: store.toGeoJSON(),
|
|
84
|
+
phase: 'commit-move',
|
|
85
|
+
vertexIndex: 0
|
|
86
|
+
})
|
|
87
|
+
jest.useRealTimers()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('emitGeometryValidation is a no-op without a change phase', () => {
|
|
91
|
+
jest.useFakeTimers()
|
|
92
|
+
const { selection, manager } = setup()
|
|
93
|
+
manager.emit.mockClear()
|
|
94
|
+
selection.emitGeometryValidation(undefined, 0)
|
|
95
|
+
jest.runAllTimers()
|
|
96
|
+
expect(manager.emit).not.toHaveBeenCalledWith(ADAPTER_EVENTS.GEOMETRY_CHANGE, expect.anything())
|
|
97
|
+
jest.useRealTimers()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('geometry changes refresh the cached coordinate until destroy unbinds the listener', () => {
|
|
101
|
+
const { selection, olFeature } = setup()
|
|
102
|
+
olFeature.getGeometry().setCoordinates([20, 20])
|
|
103
|
+
expect(selection.state.vertices).toEqual([[20, 20]])
|
|
104
|
+
selection.destroy()
|
|
105
|
+
olFeature.getGeometry().setCoordinates([30, 30])
|
|
106
|
+
expect(selection.state.vertices).toEqual([[20, 20]])
|
|
107
|
+
})
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js'
|
|
2
|
+
import { getOrCreateSymbolImage } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves a drawn point's symbol-config icon and writes the resolved image id back onto the
|
|
6
|
+
* OL feature, so core/styles.js's createFeatureStyle() can render it via an ol/style/Icon.
|
|
7
|
+
* Also resolves and caches the 'active'/'selected' variants as symbolActiveImageId/
|
|
8
|
+
* symbolSelectedImageId, read directly by highlightFeatures.js's selection overlay.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const VARIANTS = ['normal', 'active', 'selected']
|
|
12
|
+
const PROPERTY_FOR_VARIANT = { normal: 'symbolImageId', active: 'symbolActiveImageId', selected: 'symbolSelectedImageId' }
|
|
13
|
+
|
|
14
|
+
export const hasSymbolStyle = (properties) => !!(properties?.symbol || properties?.symbolSvgContent)
|
|
15
|
+
|
|
16
|
+
// Combines the browser's native devicePixelRatio with this app's own map-size scale factor
|
|
17
|
+
// (mapProvider.drawScale), matching MapController.jsx's construction-time formula.
|
|
18
|
+
export const getPixelRatio = (mapProvider) => (globalThis.devicePixelRatio || 1) * (mapProvider?.drawScale ?? 1)
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve one point feature's symbol image and write it back onto the feature.
|
|
22
|
+
* A no-op for points with no symbol config (nothing to render as an icon).
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} params
|
|
25
|
+
* @param {Object} params.manager - OLDrawManager (needs store.source, mapStyle)
|
|
26
|
+
* @param {Object} params.mapProvider
|
|
27
|
+
* @param {import('ol/Feature.js').default} params.olFeature
|
|
28
|
+
* @returns {Promise<void>}
|
|
29
|
+
*/
|
|
30
|
+
export const resolvePointSymbol = async ({ manager, mapProvider, olFeature }) => {
|
|
31
|
+
const properties = olFeature.getProperties()
|
|
32
|
+
if (!hasSymbolStyle(properties)) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pixelRatio = getPixelRatio(mapProvider)
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const results = await Promise.all(
|
|
40
|
+
VARIANTS.map((variant) => symbolRegistry.rasteriseSymbolImage(properties, manager.mapStyle, variant, pixelRatio))
|
|
41
|
+
)
|
|
42
|
+
// All three variants resolve from the same symbol config, so either all succeed or (an
|
|
43
|
+
// unresolvable symbol id) all fail together — gating on 'normal' alone is enough.
|
|
44
|
+
if (!results[0]) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The feature may have been deleted/cancelled while rasterising was in flight.
|
|
49
|
+
if (!manager.store.source.hasFeature(olFeature)) {
|
|
50
|
+
return
|
|
51
|
+
}
|
|
52
|
+
VARIANTS.forEach((variant, i) => {
|
|
53
|
+
const result = results[i]
|
|
54
|
+
if (!result) { return }
|
|
55
|
+
getOrCreateSymbolImage(result.imageId, result.imageData)
|
|
56
|
+
olFeature.set(PROPERTY_FOR_VARIANT[variant], result.imageId)
|
|
57
|
+
})
|
|
58
|
+
// ol/style/Icon draws its source canvas at native pixel size — core/styles.js reads this
|
|
59
|
+
// back to apply `scale: 1 / symbolPixelRatio`, correcting for the canvas being rasterised
|
|
60
|
+
// at pixelRatio device pixels for crispness.
|
|
61
|
+
olFeature.set('symbolPixelRatio', pixelRatio)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// A silent failure here means a point simply never gets/keeps an icon, with nothing in
|
|
64
|
+
// the UI to explain why — surface it instead of letting the rejection vanish.
|
|
65
|
+
console.error('[draw] failed to resolve point symbol', olFeature.getId(), err) // NOSONAR
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Re-resolve every existing drawn point's symbol — called on map style/size change, since
|
|
71
|
+
* the rasterised image (colours, and pixel-ratio-scaled dimensions) depends on both.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} params
|
|
74
|
+
* @param {Object} params.manager
|
|
75
|
+
* @param {Object} params.mapProvider
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
export const refreshAllPointSymbols = ({ manager, mapProvider }) => {
|
|
79
|
+
const points = manager.store.source.getFeatures().filter(
|
|
80
|
+
(f) => f.getGeometry()?.getType() === 'Point' && hasSymbolStyle(f.getProperties())
|
|
81
|
+
)
|
|
82
|
+
return Promise.all(points.map((olFeature) => resolvePointSymbol({ manager, mapProvider, olFeature })))
|
|
83
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { hasSymbolStyle, getPixelRatio, resolvePointSymbol, refreshAllPointSymbols } from './pointSymbolImages.js'
|
|
2
|
+
import { symbolRegistry } from '../../../../../../src/services/symbolRegistry.js'
|
|
3
|
+
import { getCachedSymbolImage, clearSymbolImageCache } from '../../../../../../providers/beta/openlayers/src/utils/symbolImages.js'
|
|
4
|
+
|
|
5
|
+
const mapStyle = { id: 'outdoor', mapColorScheme: 'light' }
|
|
6
|
+
|
|
7
|
+
const createMapProvider = ({ drawScale = 1 } = {}) => ({ drawScale })
|
|
8
|
+
|
|
9
|
+
// A minimal ol.Feature stand-in — resolvePointSymbol only ever calls
|
|
10
|
+
// getProperties()/set() on it, and manager.store.source.hasFeature() to check liveness.
|
|
11
|
+
const createOlFeature = (properties) => {
|
|
12
|
+
const props = { ...properties }
|
|
13
|
+
return {
|
|
14
|
+
getProperties: () => props,
|
|
15
|
+
set: jest.fn((key, value) => { props[key] = value })
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const createManager = ({ features = [] } = {}) => ({
|
|
20
|
+
mapStyle,
|
|
21
|
+
store: {
|
|
22
|
+
source: {
|
|
23
|
+
hasFeature: jest.fn((f) => features.includes(f)),
|
|
24
|
+
getFeatures: jest.fn(() => features)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// symbolRegistry.rasteriseSymbolImage() draws an SVG through a real `new Image()`/onload
|
|
30
|
+
// round-trip that jsdom never resolves — every other test file touching it (e.g. the
|
|
31
|
+
// MapLibre adapter's pointSymbolImages.test.js) instead lets a mocked provider call
|
|
32
|
+
// (addSymbolsToMap) stand in for it. This adapter calls rasteriseSymbolImage directly (it
|
|
33
|
+
// *is* the last-mile pipeline, with no provider-level call to mock instead), so it's stubbed
|
|
34
|
+
// here with a fake resolved ImageData keyed to the properties/pixelRatio, matching
|
|
35
|
+
// getSymbolImageId's own id shape closely enough for assertions to key off it.
|
|
36
|
+
const stubRasterise = () => jest.spyOn(symbolRegistry, 'rasteriseSymbolImage')
|
|
37
|
+
.mockImplementation(async (style, ms, variant, pixelRatio) => ({
|
|
38
|
+
// variant folded into the id (mirroring the real act-/sel- prefixing) so normal/active/
|
|
39
|
+
// selected resolve to distinct ids, the way real symbol configs do.
|
|
40
|
+
imageId: `${variant}-${symbolRegistry.getSymbolImageId(style, ms, false, pixelRatio)}`,
|
|
41
|
+
imageData: { width: 10, height: 10 }
|
|
42
|
+
}))
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
clearSymbolImageCache()
|
|
46
|
+
HTMLCanvasElement.prototype.getContext = jest.fn(function () {
|
|
47
|
+
this._ctx ??= { putImageData: jest.fn() }
|
|
48
|
+
return this._ctx
|
|
49
|
+
})
|
|
50
|
+
stubRasterise()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
afterEach(() => jest.restoreAllMocks())
|
|
54
|
+
|
|
55
|
+
describe('hasSymbolStyle', () => {
|
|
56
|
+
it('is true when symbol is set', () => {
|
|
57
|
+
expect(hasSymbolStyle({ symbol: 'pin' })).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('is true when symbolSvgContent is set', () => {
|
|
61
|
+
expect(hasSymbolStyle({ symbolSvgContent: '<path/>' })).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('is false with neither, or no properties at all', () => {
|
|
65
|
+
expect(hasSymbolStyle({})).toBe(false)
|
|
66
|
+
expect(hasSymbolStyle(undefined)).toBe(false)
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('getPixelRatio', () => {
|
|
71
|
+
it('combines devicePixelRatio and mapProvider.drawScale', () => {
|
|
72
|
+
const restore = globalThis.devicePixelRatio
|
|
73
|
+
globalThis.devicePixelRatio = 2
|
|
74
|
+
expect(getPixelRatio(createMapProvider({ drawScale: 1.5 }))).toBe(3)
|
|
75
|
+
globalThis.devicePixelRatio = restore
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('falls back to 1 for either factor when unset', () => {
|
|
79
|
+
const restore = globalThis.devicePixelRatio
|
|
80
|
+
delete globalThis.devicePixelRatio
|
|
81
|
+
expect(getPixelRatio(createMapProvider({ drawScale: undefined }))).toBe(1)
|
|
82
|
+
expect(getPixelRatio(undefined)).toBe(1)
|
|
83
|
+
globalThis.devicePixelRatio = restore
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('resolvePointSymbol', () => {
|
|
88
|
+
it('does nothing for a feature with no symbol config', async () => {
|
|
89
|
+
const olFeature = createOlFeature({})
|
|
90
|
+
const manager = createManager()
|
|
91
|
+
await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
|
|
92
|
+
expect(olFeature.set).not.toHaveBeenCalled()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('rasterises, caches the canvas and writes symbolImageId/symbolPixelRatio back onto the feature', async () => {
|
|
96
|
+
const properties = { symbol: 'pin' }
|
|
97
|
+
const olFeature = createOlFeature(properties)
|
|
98
|
+
const manager = createManager({ features: [olFeature] })
|
|
99
|
+
const mapProvider = createMapProvider({ drawScale: 2 })
|
|
100
|
+
|
|
101
|
+
await resolvePointSymbol({ manager, mapProvider, olFeature })
|
|
102
|
+
|
|
103
|
+
const pixelRatio = getPixelRatio(mapProvider)
|
|
104
|
+
const baseId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, pixelRatio)
|
|
105
|
+
expect(olFeature.set).toHaveBeenCalledWith('symbolImageId', `normal-${baseId}`)
|
|
106
|
+
// symbolPixelRatio is what core/styles.js later uses to scale the (pixelRatio×-oversized)
|
|
107
|
+
// cached canvas back down to its intended CSS display size.
|
|
108
|
+
expect(olFeature.set).toHaveBeenCalledWith('symbolPixelRatio', pixelRatio)
|
|
109
|
+
expect(getCachedSymbolImage(`normal-${baseId}`)).toBeInstanceOf(HTMLCanvasElement)
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('also rasterises, caches and writes the active/selected variants — one image id each, keyed to the same symbol config', async () => {
|
|
113
|
+
const properties = { symbol: 'pin' }
|
|
114
|
+
const olFeature = createOlFeature(properties)
|
|
115
|
+
const manager = createManager({ features: [olFeature] })
|
|
116
|
+
const mapProvider = createMapProvider()
|
|
117
|
+
|
|
118
|
+
await resolvePointSymbol({ manager, mapProvider, olFeature })
|
|
119
|
+
|
|
120
|
+
const pixelRatio = getPixelRatio(mapProvider)
|
|
121
|
+
const baseId = symbolRegistry.getSymbolImageId(properties, mapStyle, false, pixelRatio)
|
|
122
|
+
expect(olFeature.set).toHaveBeenCalledWith('symbolActiveImageId', `active-${baseId}`)
|
|
123
|
+
expect(olFeature.set).toHaveBeenCalledWith('symbolSelectedImageId', `selected-${baseId}`)
|
|
124
|
+
expect(getCachedSymbolImage(`active-${baseId}`)).toBeInstanceOf(HTMLCanvasElement)
|
|
125
|
+
expect(getCachedSymbolImage(`selected-${baseId}`)).toBeInstanceOf(HTMLCanvasElement)
|
|
126
|
+
// objectContaining, not the exact `properties` object — resolvePointSymbol's own
|
|
127
|
+
// property-write calls mutate that same (by-reference) object in place afterwards, and
|
|
128
|
+
// jest's mock.calls records the reference, not a snapshot at call time.
|
|
129
|
+
const symbolMatch = expect.objectContaining({ symbol: 'pin' })
|
|
130
|
+
expect(symbolRegistry.rasteriseSymbolImage).toHaveBeenCalledWith(symbolMatch, mapStyle, 'normal', pixelRatio)
|
|
131
|
+
expect(symbolRegistry.rasteriseSymbolImage).toHaveBeenCalledWith(symbolMatch, mapStyle, 'active', pixelRatio)
|
|
132
|
+
expect(symbolRegistry.rasteriseSymbolImage).toHaveBeenCalledWith(symbolMatch, mapStyle, 'selected', pixelRatio)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('skips just the active/selected properties if only those variants resolve null (normal still succeeds)', async () => {
|
|
136
|
+
const properties = { symbol: 'pin' }
|
|
137
|
+
const olFeature = createOlFeature(properties)
|
|
138
|
+
const manager = createManager({ features: [olFeature] })
|
|
139
|
+
symbolRegistry.rasteriseSymbolImage
|
|
140
|
+
.mockImplementationOnce(async (style, ms, variant, pixelRatio) => // normal — real stub behaviour
|
|
141
|
+
({ imageId: `${variant}-${symbolRegistry.getSymbolImageId(style, ms, false, pixelRatio)}`, imageData: { width: 10, height: 10 } }))
|
|
142
|
+
.mockResolvedValueOnce(null) // active
|
|
143
|
+
.mockResolvedValueOnce(null) // selected
|
|
144
|
+
|
|
145
|
+
await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
|
|
146
|
+
|
|
147
|
+
expect(olFeature.set).toHaveBeenCalledWith('symbolImageId', expect.any(String))
|
|
148
|
+
expect(olFeature.set).not.toHaveBeenCalledWith('symbolActiveImageId', expect.anything())
|
|
149
|
+
expect(olFeature.set).not.toHaveBeenCalledWith('symbolSelectedImageId', expect.anything())
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('does nothing when rasterisation resolves null (e.g. an unresolvable symbol id)', async () => {
|
|
153
|
+
const properties = { symbol: 'not-a-real-symbol' }
|
|
154
|
+
const olFeature = createOlFeature(properties)
|
|
155
|
+
const manager = createManager({ features: [olFeature] })
|
|
156
|
+
symbolRegistry.rasteriseSymbolImage.mockResolvedValueOnce(null)
|
|
157
|
+
|
|
158
|
+
await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
|
|
159
|
+
|
|
160
|
+
expect(olFeature.set).not.toHaveBeenCalled()
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
it('does not write back if the feature was removed from the source while rasterising was in flight', async () => {
|
|
164
|
+
const properties = { symbol: 'pin' }
|
|
165
|
+
const olFeature = createOlFeature(properties)
|
|
166
|
+
const manager = createManager({ features: [] }) // not present in the source
|
|
167
|
+
|
|
168
|
+
await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
|
|
169
|
+
|
|
170
|
+
expect(olFeature.set).not.toHaveBeenCalled()
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('surfaces (rather than swallows) a rasterisation failure', async () => {
|
|
174
|
+
const properties = { symbol: 'pin' }
|
|
175
|
+
const olFeature = createOlFeature(properties)
|
|
176
|
+
olFeature.getId = () => 'p1'
|
|
177
|
+
const manager = createManager({ features: [olFeature] })
|
|
178
|
+
const error = new Error('rasterise failed')
|
|
179
|
+
symbolRegistry.rasteriseSymbolImage.mockRejectedValueOnce(error)
|
|
180
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
181
|
+
|
|
182
|
+
await resolvePointSymbol({ manager, mapProvider: createMapProvider(), olFeature })
|
|
183
|
+
|
|
184
|
+
expect(olFeature.set).not.toHaveBeenCalled()
|
|
185
|
+
expect(consoleError).toHaveBeenCalledWith('[draw] failed to resolve point symbol', 'p1', error)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe('refreshAllPointSymbols', () => {
|
|
190
|
+
const feature = (getType, properties) => {
|
|
191
|
+
const f = createOlFeature(properties)
|
|
192
|
+
f.getGeometry = () => ({ getType: () => getType })
|
|
193
|
+
return f
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
it('re-resolves every point feature with a symbol config, ignoring non-point and unstyled features', async () => {
|
|
197
|
+
const pointWithSymbol = feature('Point', { symbol: 'pin' })
|
|
198
|
+
const pointWithoutSymbol = feature('Point', {})
|
|
199
|
+
const polygon = feature('Polygon', { symbol: 'pin' })
|
|
200
|
+
const manager = createManager({ features: [pointWithSymbol, pointWithoutSymbol, polygon] })
|
|
201
|
+
manager.store.source.hasFeature = jest.fn(() => true)
|
|
202
|
+
|
|
203
|
+
await refreshAllPointSymbols({ manager, mapProvider: createMapProvider() })
|
|
204
|
+
|
|
205
|
+
expect(pointWithSymbol.set).toHaveBeenCalledWith('symbolImageId', expect.any(String))
|
|
206
|
+
expect(pointWithoutSymbol.set).not.toHaveBeenCalled()
|
|
207
|
+
expect(polygon.set).not.toHaveBeenCalled()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('does nothing when there are no drawn points', async () => {
|
|
211
|
+
const manager = createManager({ features: [] })
|
|
212
|
+
await expect(refreshAllPointSymbols({ manager, mapProvider: createMapProvider() })).resolves.toEqual([])
|
|
213
|
+
})
|
|
214
|
+
})
|
|
@@ -37,7 +37,9 @@ export const createSnapIndicator = (map, colors) => {
|
|
|
37
37
|
const layer = new VectorLayer({
|
|
38
38
|
source,
|
|
39
39
|
style: (f) => styles[f.get('snapType')] ?? null,
|
|
40
|
-
zIndex
|
|
40
|
+
// Below the draw layer (zIndex 100) and its vertex/midpoint handles (101-103), matching
|
|
41
|
+
// the ML adapter — the indicator sits behind the symbols/vertices it's snapping to.
|
|
42
|
+
zIndex: 99,
|
|
41
43
|
updateWhileAnimating: true,
|
|
42
44
|
updateWhileInteracting: true
|
|
43
45
|
})
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* mapBrowserEvent.coordinate to the nearest snap candidate before any draw
|
|
4
4
|
* or modify interaction sees it.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The visual indicator updates on both pointermove (hover preview) and pointerdrag (an active
|
|
7
|
+
* OL Modify drag), staying visible through to release — nothing hides it on release; it only
|
|
8
|
+
* clears on a fresh query, a mode change, or snapping being switched off.
|
|
9
9
|
*
|
|
10
10
|
* Must be added to the map AFTER the Draw/Modify interaction so it is processed
|
|
11
11
|
* first (OL iterates interactions in reverse-add order).
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import Interaction from 'ol/interaction/Interaction.js'
|
|
16
16
|
|
|
17
17
|
const SNAP_EVENTS = new Set(['pointermove', 'pointerdrag', 'pointerdown', 'pointerup', 'singleclick', 'click'])
|
|
18
|
+
const INDICATOR_EVENTS = new Set(['pointermove', 'pointerdrag'])
|
|
18
19
|
|
|
19
20
|
const processSnapEvent = (mapBrowserEvent, engine, indicator, snapRadius, isIndicatorActive) => {
|
|
20
21
|
const { type } = mapBrowserEvent
|
|
@@ -33,13 +34,9 @@ const processSnapEvent = (mapBrowserEvent, engine, indicator, snapRadius, isIndi
|
|
|
33
34
|
mapBrowserEvent.coordinate = result.coord.slice()
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
if (type
|
|
37
|
+
// Hover preview and active drag both keep the indicator live; no update for click/down/up.
|
|
38
|
+
if (INDICATOR_EVENTS.has(type) && isIndicatorActive()) {
|
|
38
39
|
result ? indicator.show(result.coord, result.type) : indicator.hide()
|
|
39
|
-
} else if (type === 'pointerdrag') {
|
|
40
|
-
indicator.hide()
|
|
41
|
-
} else {
|
|
42
|
-
// no indicator update for click/down/up events
|
|
43
40
|
}
|
|
44
41
|
}
|
|
45
42
|
|
|
@@ -35,7 +35,7 @@ test('rewrites the event coordinate to a copy of the snap candidate for all poin
|
|
|
35
35
|
}
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
test('the indicator follows free mouse movement
|
|
38
|
+
test('the indicator follows both free mouse movement and an active drag', () => {
|
|
39
39
|
const { engine, indicator, fire } = setup()
|
|
40
40
|
fire('pointermove')
|
|
41
41
|
expect(indicator.show).toHaveBeenCalledWith([9, 9], 'vertex')
|
|
@@ -44,17 +44,28 @@ test('the indicator follows free mouse movement only', () => {
|
|
|
44
44
|
fire('pointermove')
|
|
45
45
|
expect(indicator.hide).toHaveBeenCalledTimes(1)
|
|
46
46
|
|
|
47
|
+
// A drag (e.g. edit_vertex's mouse-drag path, edit/modifyInteraction.js) keeps it live too —
|
|
48
|
+
// it must stay visible for as long as the dragged vertex sits on a candidate, not just on a
|
|
49
|
+
// free hover. See snapInteraction.js's own comment for why: this used to force-hide it on
|
|
50
|
+
// every pointerdrag, so the indicator could never actually be seen mid-drag.
|
|
47
51
|
engine.query.mockReturnValue({ type: 'edge', coord: [5, 5] })
|
|
48
52
|
fire('pointerdrag')
|
|
49
|
-
expect(indicator.
|
|
53
|
+
expect(indicator.show).toHaveBeenCalledWith([5, 5], 'edge')
|
|
54
|
+
|
|
55
|
+
engine.query.mockReturnValue(null)
|
|
56
|
+
fire('pointerdrag')
|
|
57
|
+
expect(indicator.hide).toHaveBeenCalledTimes(2)
|
|
58
|
+
|
|
50
59
|
fire('singleclick')
|
|
51
|
-
expect(indicator.show).toHaveBeenCalledTimes(
|
|
60
|
+
expect(indicator.show).toHaveBeenCalledTimes(2) // clicks never touch the indicator
|
|
61
|
+
expect(indicator.hide).toHaveBeenCalledTimes(2)
|
|
52
62
|
})
|
|
53
63
|
|
|
54
|
-
test('the indicator stays untouched during pointermove when the indicator is gated off', () => {
|
|
64
|
+
test('the indicator stays untouched during pointermove or a drag when the indicator is gated off', () => {
|
|
55
65
|
const { indicator, fire } = setup({ indicatorActive: false })
|
|
56
66
|
const { event } = fire('pointermove')
|
|
57
67
|
expect(event.coordinate).toEqual([9, 9]) // snapping still applies
|
|
68
|
+
fire('pointerdrag')
|
|
58
69
|
expect(indicator.show).not.toHaveBeenCalled()
|
|
59
70
|
expect(indicator.hide).not.toHaveBeenCalled()
|
|
60
71
|
})
|
|
@@ -16,7 +16,7 @@ export const pixelToCoord = (map, pixel) => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/** Pixel distance between two { x, y } points */
|
|
19
|
-
export const pixelDist = (a, b) => Math.
|
|
19
|
+
export const pixelDist = (a, b) => Math.hypot(a.x - b.x, a.y - b.y)
|
|
20
20
|
|
|
21
21
|
/** OL pixel array [x, y] → { x, y } */
|
|
22
22
|
export const arrayToPixel = ([x, y]) => ({ x, y })
|