@defra/interactive-map 0.0.39-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/api.md +4 -4
- package/docs/assets/images/slot-map.svg +61 -262
- package/docs/examples/draw-tools.mdx +3 -3
- package/docs/plugins/draw.md +80 -17
- 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/esm/im-map-styles-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.js +2 -3
- package/plugins/datasets/src/adapters/esri/esriLayerAdapter.test.js +175 -0
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.js +1 -1
- package/plugins/datasets/src/adapters/esri/registry/esriDataset.test.js +55 -0
- package/plugins/datasets/src/adapters/loadLayerAdapter.js +1 -1
- package/plugins/datasets/src/adapters/loadLayerAdapter.test.js +61 -0
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +1 -3
- package/plugins/datasets/src/adapters/maplibre/maplibreLayerAdapter.test.js +17 -3
- 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 -5
- 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 +15 -4
- package/plugins/datasets/src/registry/dataset.test.js +61 -0
- package/plugins/datasets/src/registry/datasetRegistry.js +12 -9
- package/plugins/datasets/src/registry/datasetRegistry.test.js +17 -5
- package/plugins/datasets/src/registry/globalDataset.js +0 -3
- 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 +12 -4
- package/plugins/draw/src/DrawInit.test.jsx +51 -10
- package/plugins/draw/src/adapters/maplibre/MaplibreDrawAdapter.js +92 -12
- 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/drawMode/pointerHandlers.js +1 -1
- package/plugins/draw/src/adapters/maplibre/modes/drawMode/pointerHandlers.test.js +1 -1
- 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 +5 -28
- 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 +4 -54
- 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 +11 -4
- 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.js +1 -0
- package/plugins/draw/src/adapters/openlayers/draw/DrawMode.test.js +27 -1
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.js +24 -4
- package/plugins/draw/src/adapters/openlayers/draw/drawInput.test.js +47 -3
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.js +1 -1
- package/plugins/draw/src/adapters/openlayers/edit/EditMode.test.js +32 -1
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.js +9 -5
- package/plugins/draw/src/adapters/openlayers/edit/keyboardHandler.test.js +34 -1
- 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 +12 -5
- package/plugins/draw/src/adapters/openlayers/edit/nudge.test.js +26 -3
- 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 +21 -14
- package/plugins/draw/src/events.test.js +15 -1
- 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/useCrossHairVisibility.js +12 -4
- package/plugins/interact/src/hooks/useCrossHairVisibility.test.js +88 -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 +7 -5
- 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/sonar-project.properties +7 -1
- package/src/App/components/Actions/Actions.test.jsx +47 -0
- package/src/App/components/Attributions/Attributions.module.scss +10 -0
- package/src/App/components/Hints/Hints.jsx +7 -2
- package/src/App/components/Hints/Hints.test.jsx +13 -0
- package/src/App/components/KeyboardHelp/KeyboardHelp.jsx +2 -3
- package/src/App/components/KeyboardHelp/KeyboardHelp.test.jsx +3 -6
- package/src/App/components/{MoveControl/MoveControl.jsx → MoveControls/MoveControls.jsx} +10 -10
- package/src/App/components/{MoveControl/MoveControl.module.scss → MoveControls/MoveControls.module.scss} +60 -29
- package/src/App/components/{MoveControl/MoveControl.test.jsx → MoveControls/MoveControls.test.jsx} +48 -48
- 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/components/Viewport/Viewport.jsx +9 -1
- package/src/App/components/Viewport/Viewport.test.jsx +11 -0
- 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/initialiseApp.js +11 -8
- package/src/App/initialiseApp.test.js +15 -3
- package/src/App/layout/Layout.jsx +12 -11
- package/src/App/layout/Layout.test.jsx +2 -0
- package/src/App/layout/layout.module.scss +31 -17
- package/src/App/registry/keyboardShortcutRegistry.js +25 -13
- package/src/App/registry/keyboardShortcutRegistry.test.js +49 -32
- package/src/App/registry/pluginRegistry.js +2 -3
- package/src/App/registry/pluginRegistry.test.js +16 -3
- 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 +4 -2
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +8 -0
- package/src/InteractiveMap/InteractiveMap.js +8 -2
- package/src/InteractiveMap/InteractiveMap.test.js +20 -3
- package/src/config/appConfig.js +17 -15
- package/src/config/appConfig.test.js +22 -22
- package/src/config/defaults.js +3 -1
- package/src/config/events.js +2 -0
- package/src/config/mergeConfig.js +7 -0
- package/src/config/mergeConfig.test.js +20 -0
- package/src/scss/main.scss +1 -1
- 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/test-utils.js +5 -0
- package/src/types.js +25 -7
- 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
|
@@ -6,152 +6,158 @@ const DEBOUNCE_DELAY = 200
|
|
|
6
6
|
const EVICTION_THRESHOLD = 1.2 // Trigger eviction at 120% of maxFeatures
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} options
|
|
11
|
-
* @param {Object} options.registryDataset - registryDataset instance
|
|
12
|
-
* @param {Object} options.map - Map instance
|
|
13
|
-
* @param {string} options.sourceId - Source ID for the map
|
|
14
|
-
* @param {Function} options.onUpdate - Callback when source data should be updated
|
|
15
|
-
* @returns {Object} { destroy, clear, refresh }
|
|
9
|
+
* Convert features Map to FeatureCollection
|
|
16
10
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
const toFeatureCollection = (features) => ({
|
|
12
|
+
type: 'FeatureCollection',
|
|
13
|
+
features: Array.from(features.values()).map(entry => entry.feature)
|
|
14
|
+
})
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Get feature ID from a feature
|
|
18
|
+
*/
|
|
19
|
+
const getFeatureId = (feature, idProperty) => {
|
|
20
|
+
if (idProperty) {
|
|
21
|
+
return feature.properties?.[idProperty] ?? feature.id
|
|
22
|
+
}
|
|
23
|
+
return feature.id
|
|
24
|
+
}
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Evict features that are outside the current viewport
|
|
28
|
+
* Uses "least recently visible" strategy - evicts features furthest from viewport first
|
|
29
|
+
*/
|
|
30
|
+
const evictIfNeeded = (features, currentBbox, maxFeatures) => {
|
|
31
|
+
if (!maxFeatures || features.size <= maxFeatures * EVICTION_THRESHOLD) {
|
|
32
|
+
return
|
|
33
|
+
}
|
|
25
34
|
|
|
26
|
-
|
|
27
|
-
let currentController = null
|
|
35
|
+
const targetSize = maxFeatures
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const toFeatureCollection = () => ({
|
|
33
|
-
type: 'FeatureCollection',
|
|
34
|
-
features: Array.from(features.values()).map(entry => entry.feature)
|
|
35
|
-
})
|
|
37
|
+
// Partition features: in-viewport vs out-of-viewport
|
|
38
|
+
const inView = []
|
|
39
|
+
const outOfView = []
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return feature.properties?.[idProperty] ?? feature.id
|
|
41
|
+
for (const [id, data] of features) {
|
|
42
|
+
if (bboxIntersects(data.bbox, currentBbox)) {
|
|
43
|
+
inView.push(id)
|
|
44
|
+
} else {
|
|
45
|
+
outOfView.push({ id, lastSeenAt: data.lastSeenAt })
|
|
43
46
|
}
|
|
44
|
-
return feature.id
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
* Uses "least recently visible" strategy - evicts features furthest from viewport first
|
|
50
|
-
*/
|
|
51
|
-
const evictIfNeeded = (currentBbox) => {
|
|
52
|
-
if (!maxFeatures || features.size <= maxFeatures * EVICTION_THRESHOLD) {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
49
|
+
// Sort out-of-view by last seen time (least recently seen first)
|
|
50
|
+
outOfView.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
|
|
55
51
|
|
|
56
|
-
|
|
52
|
+
// Evict least-recently-seen out-of-view features until under target
|
|
53
|
+
const toEvict = features.size - targetSize
|
|
54
|
+
for (let i = 0; i < toEvict && i < outOfView.length; i++) {
|
|
55
|
+
features.delete(outOfView[i].id)
|
|
56
|
+
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
58
|
+
// If still over target (viewport has too many), evict least recently seen in-view
|
|
59
|
+
if (features.size > targetSize) {
|
|
60
|
+
const remaining = features.size - targetSize
|
|
61
|
+
const inViewSorted = inView
|
|
62
|
+
.map(id => ({ id, lastSeenAt: features.get(id).lastSeenAt }))
|
|
63
|
+
.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
|
|
61
64
|
|
|
62
|
-
for (
|
|
63
|
-
|
|
64
|
-
inView.push(id)
|
|
65
|
-
} else {
|
|
66
|
-
outOfView.push({ id, lastSeenAt: data.lastSeenAt })
|
|
67
|
-
}
|
|
65
|
+
for (let i = 0; i < remaining && i < inViewSorted.length; i++) {
|
|
66
|
+
features.delete(inViewSorted[i].id)
|
|
68
67
|
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
69
70
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Merge freshly fetched GeoJSON features into the cache, deduplicating by ID
|
|
73
|
+
* and refreshing lastSeenAt for features that were re-fetched
|
|
74
|
+
*/
|
|
75
|
+
const mergeFetchedFeatures = (features, data, idProperty) => {
|
|
76
|
+
const now = Date.now()
|
|
77
|
+
data.features.forEach(feature => {
|
|
78
|
+
const id = getFeatureId(feature, idProperty)
|
|
79
|
+
if (id == null) {
|
|
80
|
+
console.warn('Feature missing ID, skipping:', feature)
|
|
81
|
+
return
|
|
77
82
|
}
|
|
78
83
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
features.set(id, {
|
|
85
|
+
feature,
|
|
86
|
+
bbox: getGeometryBbox(feature.geometry),
|
|
87
|
+
lastSeenAt: now
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
}
|
|
85
91
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Fetch data for the current viewport and merge it into the feature cache.
|
|
94
|
+
* Mutates state.fetchedBbox / state.currentController as a side effect.
|
|
95
|
+
*/
|
|
96
|
+
const fetchViewportData = async (state, { map, dynamicGeoJSON, onUpdate }) => {
|
|
97
|
+
const { url: baseUrl, idProperty, transformRequest, maxFeatures, minZoom = 0 } = dynamicGeoJSON
|
|
98
|
+
|
|
99
|
+
const zoom = map.getZoom()
|
|
100
|
+
if (zoom < minZoom) {
|
|
101
|
+
return
|
|
90
102
|
}
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return
|
|
99
|
-
}
|
|
104
|
+
const currentBbox = getBboxArray(map)
|
|
105
|
+
|
|
106
|
+
// Skip if current viewport is already covered
|
|
107
|
+
if (state.fetchedBbox && bboxContains(state.fetchedBbox, currentBbox)) {
|
|
108
|
+
return
|
|
109
|
+
}
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
// Abort any in-flight request — new viewport takes priority
|
|
112
|
+
if (state.currentController) {
|
|
113
|
+
state.currentController.abort()
|
|
114
|
+
}
|
|
115
|
+
state.currentController = new AbortController()
|
|
102
116
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
117
|
+
try {
|
|
118
|
+
const context = { bbox: currentBbox, zoom }
|
|
119
|
+
const data = await fetchGeoJSON(baseUrl, context, transformRequest, state.currentController.signal)
|
|
107
120
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
121
|
+
mergeFetchedFeatures(state.features, data, idProperty)
|
|
122
|
+
|
|
123
|
+
// Expand tracked bbox
|
|
124
|
+
state.fetchedBbox = expandBbox(state.fetchedBbox, currentBbox)
|
|
125
|
+
|
|
126
|
+
// Evict if over limit; if features were removed, fetchedBbox no longer
|
|
127
|
+
// covers those regions — reset to current viewport to force re-fetch on return
|
|
128
|
+
const sizeBeforeEviction = state.features.size
|
|
129
|
+
evictIfNeeded(state.features, currentBbox, maxFeatures)
|
|
130
|
+
if (state.features.size < sizeBeforeEviction) {
|
|
131
|
+
state.fetchedBbox = currentBbox
|
|
111
132
|
}
|
|
112
|
-
currentController = new AbortController()
|
|
113
|
-
|
|
114
|
-
try {
|
|
115
|
-
const context = { bbox: currentBbox, zoom }
|
|
116
|
-
const data = await fetchGeoJSON(baseUrl, context, transformRequest, currentController.signal)
|
|
117
|
-
|
|
118
|
-
const now = Date.now()
|
|
119
|
-
|
|
120
|
-
// Add/update features with deduplication, refreshing lastSeenAt on each fetch
|
|
121
|
-
data.features.forEach(feature => {
|
|
122
|
-
const id = getFeatureId(feature)
|
|
123
|
-
if (id == null) {
|
|
124
|
-
console.warn('Feature missing ID, skipping:', feature)
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
features.set(id, {
|
|
129
|
-
feature,
|
|
130
|
-
bbox: getGeometryBbox(feature.geometry),
|
|
131
|
-
lastSeenAt: now
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
// Expand tracked bbox
|
|
136
|
-
fetchedBbox = expandBbox(fetchedBbox, currentBbox)
|
|
137
|
-
|
|
138
|
-
// Evict if over limit; if features were removed, fetchedBbox no longer
|
|
139
|
-
// covers those regions — reset to current viewport to force re-fetch on return
|
|
140
|
-
const sizeBeforeEviction = features.size
|
|
141
|
-
evictIfNeeded(currentBbox)
|
|
142
|
-
if (features.size < sizeBeforeEviction) {
|
|
143
|
-
fetchedBbox = currentBbox
|
|
144
|
-
}
|
|
145
133
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
console.error(`Failed to fetch dynamic GeoJSON for ${dynamicGeoJSON.id}:`, error)
|
|
134
|
+
// Update map source
|
|
135
|
+
onUpdate(dynamicGeoJSON.id, toFeatureCollection(state.features))
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error.name === 'AbortError') {
|
|
138
|
+
return
|
|
153
139
|
}
|
|
140
|
+
console.error(`Failed to fetch dynamic GeoJSON for ${dynamicGeoJSON.id}:`, error)
|
|
154
141
|
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a dynamic GeoJSON source that fetches data based on viewport
|
|
146
|
+
* @param {Object} options
|
|
147
|
+
* @param {Object} options.dynamicGeoJSON - dynamicGeoJSON config from the registry dataset
|
|
148
|
+
* @param {Object} options.map - Map instance
|
|
149
|
+
* @param {Function} options.onUpdate - Callback when source data should be updated
|
|
150
|
+
* @returns {Object} { destroy, clear, refresh, getFeatureCount, reapply }
|
|
151
|
+
*/
|
|
152
|
+
export const createDynamicSource = ({ dynamicGeoJSON, map, onUpdate }) => {
|
|
153
|
+
// state.features: id → { feature, bbox, lastSeenAt }
|
|
154
|
+
const state = {
|
|
155
|
+
features: new Map(),
|
|
156
|
+
fetchedBbox: null,
|
|
157
|
+
currentController: null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fetchData = () => fetchViewportData(state, { map, dynamicGeoJSON, onUpdate })
|
|
155
161
|
|
|
156
162
|
// Debounced fetch handler
|
|
157
163
|
const debouncedFetch = debounce(fetchData, DEBOUNCE_DELAY)
|
|
@@ -173,8 +179,8 @@ export const createDynamicSource = ({ dynamicGeoJSON, map, onUpdate }) => {
|
|
|
173
179
|
destroy () {
|
|
174
180
|
map.off('moveend', handleMoveEnd)
|
|
175
181
|
debouncedFetch.cancel()
|
|
176
|
-
if (currentController) {
|
|
177
|
-
currentController.abort()
|
|
182
|
+
if (state.currentController) {
|
|
183
|
+
state.currentController.abort()
|
|
178
184
|
}
|
|
179
185
|
},
|
|
180
186
|
|
|
@@ -182,8 +188,8 @@ export const createDynamicSource = ({ dynamicGeoJSON, map, onUpdate }) => {
|
|
|
182
188
|
* Clear all cached features and reset fetch tracking
|
|
183
189
|
*/
|
|
184
190
|
clear () {
|
|
185
|
-
features.clear()
|
|
186
|
-
fetchedBbox = null
|
|
191
|
+
state.features.clear()
|
|
192
|
+
state.fetchedBbox = null
|
|
187
193
|
onUpdate(dynamicGeoJSON.id, { type: 'FeatureCollection', features: [] })
|
|
188
194
|
},
|
|
189
195
|
|
|
@@ -191,8 +197,8 @@ export const createDynamicSource = ({ dynamicGeoJSON, map, onUpdate }) => {
|
|
|
191
197
|
* Force refresh - clear cache and fetch current viewport
|
|
192
198
|
*/
|
|
193
199
|
refresh () {
|
|
194
|
-
features.clear()
|
|
195
|
-
fetchedBbox = null
|
|
200
|
+
state.features.clear()
|
|
201
|
+
state.fetchedBbox = null
|
|
196
202
|
fetchData()
|
|
197
203
|
},
|
|
198
204
|
|
|
@@ -200,15 +206,15 @@ export const createDynamicSource = ({ dynamicGeoJSON, map, onUpdate }) => {
|
|
|
200
206
|
* Get current feature count
|
|
201
207
|
*/
|
|
202
208
|
getFeatureCount () {
|
|
203
|
-
return features.size
|
|
209
|
+
return state.features.size
|
|
204
210
|
},
|
|
205
211
|
|
|
206
212
|
/**
|
|
207
213
|
* Re-push cached features to the source (e.g., after style change)
|
|
208
214
|
*/
|
|
209
215
|
reapply () {
|
|
210
|
-
if (features.size > 0) {
|
|
211
|
-
onUpdate(dynamicGeoJSON.id, toFeatureCollection())
|
|
216
|
+
if (state.features.size > 0) {
|
|
217
|
+
onUpdate(dynamicGeoJSON.id, toFeatureCollection(state.features))
|
|
212
218
|
}
|
|
213
219
|
}
|
|
214
220
|
}
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createDynamicSource } from './createDynamicSource.js'
|
|
2
|
+
import { fetchGeoJSON } from './fetchGeoJSON.js'
|
|
3
|
+
|
|
4
|
+
jest.mock('./fetchGeoJSON.js', () => ({
|
|
5
|
+
fetchGeoJSON: jest.fn()
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
const DEBOUNCE_DELAY = 200
|
|
11
|
+
|
|
12
|
+
const makeBounds = ([west, south, east, north]) => ({
|
|
13
|
+
getWest: () => west,
|
|
14
|
+
getSouth: () => south,
|
|
15
|
+
getEast: () => east,
|
|
16
|
+
getNorth: () => north
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
// Minimal map stub: tracks a mutable bbox/zoom and the registered moveend handler
|
|
20
|
+
const makeMap = ({ bbox = [-1, -1, 1, 1], zoom = 10 } = {}) => {
|
|
21
|
+
const state = { bbox, zoom }
|
|
22
|
+
const handlers = {}
|
|
23
|
+
return {
|
|
24
|
+
getZoom: jest.fn(() => state.zoom),
|
|
25
|
+
getBounds: jest.fn(() => makeBounds(state.bbox)),
|
|
26
|
+
on: jest.fn((event, handler) => { handlers[event] = handler }),
|
|
27
|
+
off: jest.fn(),
|
|
28
|
+
pan (bbox) { state.bbox = bbox },
|
|
29
|
+
fireMoveEnd () { handlers.moveend?.() }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const featureCollection = (features) => ({ type: 'FeatureCollection', features })
|
|
34
|
+
|
|
35
|
+
// Point feature - its bbox collapses to the point itself, handy for in/out-of-viewport tests
|
|
36
|
+
const makePointFeature = (id, [x, y], properties = {}) => ({
|
|
37
|
+
type: 'Feature',
|
|
38
|
+
id,
|
|
39
|
+
properties,
|
|
40
|
+
geometry: { type: 'Point', coordinates: [x, y] }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const flushMicrotasks = async () => {
|
|
44
|
+
await Promise.resolve()
|
|
45
|
+
await Promise.resolve()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const advanceDebounce = async () => {
|
|
49
|
+
jest.advanceTimersByTime(DEBOUNCE_DELAY)
|
|
50
|
+
await flushMicrotasks()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const makeDynamicGeoJSON = (overrides = {}) => ({
|
|
54
|
+
id: 'test-source',
|
|
55
|
+
url: 'https://api.example.com/features',
|
|
56
|
+
transformRequest: jest.fn((url, context) => ({ url: `${url}?bbox=${context.bbox.join(',')}` })),
|
|
57
|
+
...overrides
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
jest.useFakeTimers()
|
|
62
|
+
fetchGeoJSON.mockReset()
|
|
63
|
+
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
|
64
|
+
jest.spyOn(console, 'error').mockImplementation(() => {})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
jest.useRealTimers()
|
|
69
|
+
console.warn.mockRestore()
|
|
70
|
+
console.error.mockRestore()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// ─── tests ─────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
describe('createDynamicSource', () => {
|
|
76
|
+
it('fetches the initial viewport on creation and pushes features to onUpdate', async () => {
|
|
77
|
+
const map = makeMap({ bbox: [-1, -1, 1, 1], zoom: 10 })
|
|
78
|
+
const onUpdate = jest.fn()
|
|
79
|
+
const dynamicGeoJSON = makeDynamicGeoJSON({ idProperty: 'featureId' })
|
|
80
|
+
const feature = makePointFeature('ignored', [0, 0], { featureId: 'abc' })
|
|
81
|
+
// No featureId in properties - falls back to feature.id despite idProperty being configured
|
|
82
|
+
const featureWithoutIdProperty = makePointFeature('fallback-id', [0, 0])
|
|
83
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([feature, featureWithoutIdProperty]))
|
|
84
|
+
|
|
85
|
+
const instance = createDynamicSource({ dynamicGeoJSON, map, onUpdate })
|
|
86
|
+
await flushMicrotasks()
|
|
87
|
+
|
|
88
|
+
expect(fetchGeoJSON).toHaveBeenCalledWith(
|
|
89
|
+
dynamicGeoJSON.url,
|
|
90
|
+
{ bbox: [-1, -1, 1, 1], zoom: 10 },
|
|
91
|
+
dynamicGeoJSON.transformRequest,
|
|
92
|
+
expect.anything()
|
|
93
|
+
)
|
|
94
|
+
expect(instance.getFeatureCount()).toBe(2)
|
|
95
|
+
expect(onUpdate).toHaveBeenCalledWith('test-source', featureCollection([feature, featureWithoutIdProperty]))
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('falls back to feature.id when no idProperty is configured, and skips features with no resolvable id', async () => {
|
|
99
|
+
const map = makeMap()
|
|
100
|
+
const onUpdate = jest.fn()
|
|
101
|
+
const dynamicGeoJSON = makeDynamicGeoJSON()
|
|
102
|
+
const withId = makePointFeature('has-id', [0, 0])
|
|
103
|
+
const withoutId = { type: 'Feature', properties: {}, geometry: { type: 'Point', coordinates: [0, 0] } }
|
|
104
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([withId, withoutId]))
|
|
105
|
+
|
|
106
|
+
const instance = createDynamicSource({ dynamicGeoJSON, map, onUpdate })
|
|
107
|
+
await flushMicrotasks()
|
|
108
|
+
|
|
109
|
+
expect(instance.getFeatureCount()).toBe(1)
|
|
110
|
+
expect(console.warn).toHaveBeenCalledWith('Feature missing ID, skipping:', withoutId)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('skips fetching below minZoom, and destroy() is a no-op with no in-flight request', async () => {
|
|
114
|
+
const map = makeMap({ zoom: 2 })
|
|
115
|
+
const dynamicGeoJSON = makeDynamicGeoJSON({ minZoom: 5 })
|
|
116
|
+
|
|
117
|
+
const instance = createDynamicSource({ dynamicGeoJSON, map, onUpdate: jest.fn() })
|
|
118
|
+
await flushMicrotasks()
|
|
119
|
+
|
|
120
|
+
expect(fetchGeoJSON).not.toHaveBeenCalled()
|
|
121
|
+
expect(() => instance.destroy()).not.toThrow()
|
|
122
|
+
expect(map.off).toHaveBeenCalledWith('moveend', expect.any(Function))
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('skips re-fetching when the new viewport is already covered by the last fetch', async () => {
|
|
126
|
+
const map = makeMap({ bbox: [-1, -1, 1, 1] })
|
|
127
|
+
const dynamicGeoJSON = makeDynamicGeoJSON()
|
|
128
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([]))
|
|
129
|
+
|
|
130
|
+
createDynamicSource({ dynamicGeoJSON, map, onUpdate: jest.fn() })
|
|
131
|
+
await flushMicrotasks()
|
|
132
|
+
|
|
133
|
+
map.pan([-0.5, -0.5, 0.5, 0.5]) // fully inside the fetched bbox
|
|
134
|
+
map.fireMoveEnd()
|
|
135
|
+
await advanceDebounce()
|
|
136
|
+
|
|
137
|
+
expect(fetchGeoJSON).toHaveBeenCalledTimes(1)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it('debounces rapid map movement into a single re-fetch', async () => {
|
|
141
|
+
const map = makeMap({ bbox: [-1, -1, 1, 1] })
|
|
142
|
+
const dynamicGeoJSON = makeDynamicGeoJSON()
|
|
143
|
+
fetchGeoJSON.mockResolvedValue(featureCollection([]))
|
|
144
|
+
|
|
145
|
+
createDynamicSource({ dynamicGeoJSON, map, onUpdate: jest.fn() })
|
|
146
|
+
await flushMicrotasks()
|
|
147
|
+
|
|
148
|
+
map.pan([10, 10, 12, 12])
|
|
149
|
+
map.fireMoveEnd()
|
|
150
|
+
jest.advanceTimersByTime(50)
|
|
151
|
+
map.fireMoveEnd()
|
|
152
|
+
jest.advanceTimersByTime(50)
|
|
153
|
+
map.fireMoveEnd()
|
|
154
|
+
await advanceDebounce()
|
|
155
|
+
|
|
156
|
+
expect(fetchGeoJSON).toHaveBeenCalledTimes(2) // initial + one debounced re-fetch
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('aborts the previous in-flight request when a new viewport is fetched before it resolves', async () => {
|
|
160
|
+
let resolveFirst
|
|
161
|
+
fetchGeoJSON.mockImplementationOnce(() => new Promise((resolve) => { resolveFirst = resolve }))
|
|
162
|
+
const map = makeMap({ bbox: [-1, -1, 1, 1] })
|
|
163
|
+
|
|
164
|
+
createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map, onUpdate: jest.fn() })
|
|
165
|
+
await flushMicrotasks()
|
|
166
|
+
const firstSignal = fetchGeoJSON.mock.calls[0][3]
|
|
167
|
+
expect(firstSignal.aborted).toBe(false)
|
|
168
|
+
|
|
169
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([]))
|
|
170
|
+
map.pan([10, 10, 12, 12])
|
|
171
|
+
map.fireMoveEnd()
|
|
172
|
+
await advanceDebounce()
|
|
173
|
+
|
|
174
|
+
expect(firstSignal.aborted).toBe(true)
|
|
175
|
+
resolveFirst(featureCollection([]))
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('evicts least-recently-seen features once over the maxFeatures threshold, out-of-view first then in-view', async () => {
|
|
179
|
+
const map = makeMap({ bbox: [-1, -1, 1, 1] })
|
|
180
|
+
const dynamicGeoJSON = makeDynamicGeoJSON({ maxFeatures: 1 })
|
|
181
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([makePointFeature('a', [0, 0])]))
|
|
182
|
+
|
|
183
|
+
const instance = createDynamicSource({ dynamicGeoJSON, map, onUpdate: jest.fn() })
|
|
184
|
+
await flushMicrotasks()
|
|
185
|
+
expect(instance.getFeatureCount()).toBe(1) // under threshold (1 <= 1 * 1.2), no eviction yet
|
|
186
|
+
|
|
187
|
+
// New viewport, far from 'a': one in-view feature pushes total to 2 (> 1 * 1.2).
|
|
188
|
+
// Evicting the single out-of-view feature ('a') alone reaches the target, so the
|
|
189
|
+
// in-view eviction pass is skipped.
|
|
190
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([makePointFeature('b', [10, 10])]))
|
|
191
|
+
map.pan([10, 10, 12, 12])
|
|
192
|
+
map.fireMoveEnd()
|
|
193
|
+
await advanceDebounce()
|
|
194
|
+
expect(instance.getFeatureCount()).toBe(1)
|
|
195
|
+
|
|
196
|
+
// New viewport again, far from 'b': two in-view features push total to 3 (> 1 * 1.2).
|
|
197
|
+
// 'b' is out-of-view and evicted first; still over target, so the least-recently-seen
|
|
198
|
+
// in-view feature ('c') is evicted next, leaving only 'd'.
|
|
199
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([
|
|
200
|
+
makePointFeature('c', [20, 20]),
|
|
201
|
+
makePointFeature('d', [21, 21])
|
|
202
|
+
]))
|
|
203
|
+
map.pan([20, 20, 22, 22])
|
|
204
|
+
map.fireMoveEnd()
|
|
205
|
+
await advanceDebounce()
|
|
206
|
+
expect(instance.getFeatureCount()).toBe(1)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('sorts multiple out-of-view features by lastSeenAt when evicting', async () => {
|
|
210
|
+
const map = makeMap({ bbox: [-1, -1, 1, 1] })
|
|
211
|
+
const dynamicGeoJSON = makeDynamicGeoJSON({ maxFeatures: 2 })
|
|
212
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([
|
|
213
|
+
makePointFeature('a', [0, 0]),
|
|
214
|
+
makePointFeature('b', [0.5, 0.5])
|
|
215
|
+
]))
|
|
216
|
+
|
|
217
|
+
const instance = createDynamicSource({ dynamicGeoJSON, map, onUpdate: jest.fn() })
|
|
218
|
+
await flushMicrotasks()
|
|
219
|
+
expect(instance.getFeatureCount()).toBe(2) // under threshold (2 <= 2 * 1.2), no eviction yet
|
|
220
|
+
|
|
221
|
+
// New viewport, far from 'a' and 'b': both become out-of-view at once, exercising the
|
|
222
|
+
// out-of-view sort-by-lastSeenAt comparator (a no-op array has nothing to compare)
|
|
223
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([makePointFeature('c', [10, 10])]))
|
|
224
|
+
map.pan([10, 10, 12, 12])
|
|
225
|
+
map.fireMoveEnd()
|
|
226
|
+
await advanceDebounce()
|
|
227
|
+
|
|
228
|
+
expect(instance.getFeatureCount()).toBe(2) // one of a/b evicted, 'c' kept
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('swallows AbortError from the fetch without logging', async () => {
|
|
232
|
+
const error = Object.assign(new Error('aborted'), { name: 'AbortError' })
|
|
233
|
+
fetchGeoJSON.mockRejectedValueOnce(error)
|
|
234
|
+
const onUpdate = jest.fn()
|
|
235
|
+
|
|
236
|
+
createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map: makeMap(), onUpdate })
|
|
237
|
+
await flushMicrotasks()
|
|
238
|
+
|
|
239
|
+
expect(console.error).not.toHaveBeenCalled()
|
|
240
|
+
expect(onUpdate).not.toHaveBeenCalled()
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('logs unexpected fetch errors', async () => {
|
|
244
|
+
const error = new Error('network down')
|
|
245
|
+
fetchGeoJSON.mockRejectedValueOnce(error)
|
|
246
|
+
|
|
247
|
+
createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map: makeMap(), onUpdate: jest.fn() })
|
|
248
|
+
await flushMicrotasks()
|
|
249
|
+
|
|
250
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
251
|
+
expect.stringContaining('Failed to fetch dynamic GeoJSON for test-source'),
|
|
252
|
+
error
|
|
253
|
+
)
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
it('destroy() unregisters moveend, cancels the pending debounce, and aborts the in-flight request', async () => {
|
|
257
|
+
fetchGeoJSON.mockImplementationOnce(() => new Promise(() => {})) // never resolves
|
|
258
|
+
const map = makeMap()
|
|
259
|
+
|
|
260
|
+
const instance = createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map, onUpdate: jest.fn() })
|
|
261
|
+
await flushMicrotasks()
|
|
262
|
+
const signal = fetchGeoJSON.mock.calls[0][3]
|
|
263
|
+
|
|
264
|
+
map.fireMoveEnd() // schedule a debounced re-fetch
|
|
265
|
+
instance.destroy()
|
|
266
|
+
|
|
267
|
+
expect(map.off).toHaveBeenCalledWith('moveend', expect.any(Function))
|
|
268
|
+
expect(signal.aborted).toBe(true)
|
|
269
|
+
|
|
270
|
+
await advanceDebounce()
|
|
271
|
+
expect(fetchGeoJSON).toHaveBeenCalledTimes(1) // debounced call never fired
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('clear() empties the cache and pushes an empty FeatureCollection', async () => {
|
|
275
|
+
const onUpdate = jest.fn()
|
|
276
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([makePointFeature('a', [0, 0])]))
|
|
277
|
+
|
|
278
|
+
const instance = createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map: makeMap(), onUpdate })
|
|
279
|
+
await flushMicrotasks()
|
|
280
|
+
|
|
281
|
+
instance.clear()
|
|
282
|
+
|
|
283
|
+
expect(instance.getFeatureCount()).toBe(0)
|
|
284
|
+
expect(onUpdate).toHaveBeenLastCalledWith('test-source', featureCollection([]))
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('refresh() clears the cache and re-fetches the current viewport', async () => {
|
|
288
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([makePointFeature('a', [0, 0])]))
|
|
289
|
+
const instance = createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map: makeMap(), onUpdate: jest.fn() })
|
|
290
|
+
await flushMicrotasks()
|
|
291
|
+
|
|
292
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([makePointFeature('b', [0, 0])]))
|
|
293
|
+
instance.refresh()
|
|
294
|
+
await flushMicrotasks()
|
|
295
|
+
|
|
296
|
+
expect(fetchGeoJSON).toHaveBeenCalledTimes(2)
|
|
297
|
+
expect(instance.getFeatureCount()).toBe(1)
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it('reapply() re-pushes cached features, and does nothing when the cache is empty', async () => {
|
|
301
|
+
const onUpdate = jest.fn()
|
|
302
|
+
const feature = makePointFeature('a', [0, 0])
|
|
303
|
+
fetchGeoJSON.mockResolvedValueOnce(featureCollection([feature]))
|
|
304
|
+
|
|
305
|
+
const instance = createDynamicSource({ dynamicGeoJSON: makeDynamicGeoJSON(), map: makeMap(), onUpdate })
|
|
306
|
+
await flushMicrotasks()
|
|
307
|
+
onUpdate.mockClear()
|
|
308
|
+
|
|
309
|
+
instance.reapply()
|
|
310
|
+
expect(onUpdate).toHaveBeenCalledWith('test-source', featureCollection([feature]))
|
|
311
|
+
|
|
312
|
+
onUpdate.mockClear()
|
|
313
|
+
instance.clear()
|
|
314
|
+
onUpdate.mockClear()
|
|
315
|
+
instance.reapply()
|
|
316
|
+
expect(onUpdate).not.toHaveBeenCalled()
|
|
317
|
+
})
|
|
318
|
+
})
|