@defra/interactive-map 0.0.16-alpha → 0.0.18-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/assets/images/slot-map.svg +264 -0
- 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/context.md +53 -7
- package/docs/api/map-style-config.md +41 -2
- package/docs/api/marker-config.md +53 -11
- package/docs/api/slots.md +16 -15
- package/docs/api/symbol-config.md +160 -0
- package/docs/api/symbol-registry.md +115 -0
- package/docs/api.md +25 -22
- package/docs/getting-started.md +4 -1
- package/docs/plugins/datasets.md +657 -0
- package/docs/plugins/interact.md +68 -43
- package/docs/plugins/search.md +15 -3
- package/docs/plugins.md +1 -1
- package/package.json +2 -2
- package/plugins/beta/datasets/dist/css/index.css +103 -15
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/esm/index.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/index.js +1 -1
- package/plugins/beta/datasets/src/DatasetsInit.jsx +29 -9
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +159 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +75 -0
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +440 -0
- package/plugins/beta/datasets/src/adapters/maplibre/patternImages.js +27 -0
- package/plugins/beta/datasets/src/adapters/maplibre/symbolImages.js +31 -0
- package/plugins/beta/datasets/src/api/addDataset.js +2 -8
- package/plugins/beta/datasets/src/api/getOpacity.js +17 -0
- package/plugins/beta/datasets/src/api/getStyle.js +13 -0
- package/plugins/beta/datasets/src/api/removeDataset.js +2 -44
- package/plugins/beta/datasets/src/api/setData.js +10 -0
- package/plugins/beta/datasets/src/api/setDatasetVisibility.js +37 -0
- package/plugins/beta/datasets/src/api/setFeatureVisibility.js +22 -0
- package/plugins/beta/datasets/src/api/setOpacity.js +29 -0
- package/plugins/beta/datasets/src/api/setStyle.js +22 -0
- package/plugins/beta/datasets/src/components/EmptyKey.jsx +7 -0
- package/plugins/beta/datasets/src/components/EmptyKey.test.jsx +21 -0
- package/plugins/beta/datasets/src/components/KeySvg.jsx +24 -0
- package/plugins/beta/datasets/src/components/KeySvgLine.jsx +19 -0
- package/plugins/beta/datasets/src/components/KeySvgPattern.jsx +15 -0
- package/plugins/beta/datasets/src/components/KeySvgRect.jsx +22 -0
- package/plugins/beta/datasets/src/components/KeySvgSymbol.jsx +16 -0
- package/plugins/beta/datasets/src/components/svgProperties.js +20 -0
- package/plugins/beta/datasets/src/datasets.js +39 -56
- package/plugins/beta/datasets/src/defaults.js +44 -8
- package/plugins/beta/datasets/src/fetch/createDynamicSource.js +34 -25
- package/plugins/beta/datasets/src/fetch/fetchGeoJSON.js +2 -2
- package/plugins/beta/datasets/src/index.js +2 -1
- package/plugins/beta/datasets/src/manifest.js +25 -17
- package/plugins/beta/datasets/src/panels/Key.jsx +51 -51
- package/plugins/beta/datasets/src/panels/Key.module.scss +59 -9
- package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
- package/plugins/beta/datasets/src/panels/Layers.module.scss +56 -8
- package/plugins/beta/datasets/src/reducer.js +134 -9
- package/plugins/beta/datasets/src/reducers/keyReducer.js +34 -0
- package/plugins/beta/datasets/src/utils/bbox.js +7 -5
- package/plugins/beta/datasets/src/utils/filters.js +5 -2
- package/plugins/beta/datasets/src/utils/mergeSublayer.js +86 -0
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/DrawInit.jsx +3 -2
- package/plugins/beta/draw-ml/dist/css/index.css +21 -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-ml/dist/umd/index.js +1 -1
- package/plugins/beta/draw-ml/src/DrawInit.jsx +4 -3
- package/plugins/beta/draw-ml/src/draw.scss +0 -7
- package/plugins/beta/draw-ml/src/manifest.js +16 -16
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/umd/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/src/Frame.jsx +5 -5
- 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/beta/map-styles/src/MapStyles.jsx +5 -4
- package/plugins/beta/map-styles/src/MapStylesInit.jsx +5 -4
- package/plugins/beta/map-styles/src/manifest.js +1 -1
- package/plugins/beta/scale-bar/dist/css/index.css +1 -1
- package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/dist/umd/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/src/index.test.js +3 -3
- package/plugins/beta/scale-bar/src/manifest.js +3 -3
- package/plugins/beta/scale-bar/src/scaleBar.scss +2 -1
- package/plugins/interact/dist/css/index.css +1 -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/InteractInit.jsx +14 -5
- package/plugins/interact/src/InteractInit.test.js +26 -6
- package/plugins/interact/src/api/enable.test.js +7 -7
- package/plugins/interact/src/defaults.js +4 -6
- package/plugins/interact/src/events.js +9 -6
- package/plugins/interact/src/events.test.js +28 -4
- package/plugins/interact/src/hooks/useHighlightSync.js +3 -3
- package/plugins/interact/src/hooks/useHighlightSync.test.js +6 -6
- package/plugins/interact/src/hooks/useHoverCursor.js +10 -0
- package/plugins/interact/src/hooks/useHoverCursor.test.js +44 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +111 -69
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +147 -32
- package/plugins/interact/src/interact.scss +0 -7
- package/plugins/interact/src/manifest.js +14 -18
- package/plugins/interact/src/manifest.test.js +3 -1
- package/plugins/interact/src/reducer.js +23 -4
- package/plugins/interact/src/reducer.test.js +60 -11
- package/plugins/interact/src/utils/buildStylesMap.js +17 -4
- package/plugins/interact/src/utils/buildStylesMap.test.js +16 -2
- package/plugins/interact/src/utils/featureQueries.js +11 -6
- package/plugins/interact/src/utils/featureQueries.test.js +8 -1
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/src/Search.jsx +3 -1
- package/plugins/search/src/components/Form/Form.module.scss +2 -1
- package/plugins/search/src/events/fetchSuggestions.js +6 -4
- package/plugins/search/src/events/fetchSuggestions.test.js +26 -4
- package/plugins/search/src/events/formHandlers.js +3 -3
- package/plugins/search/src/events/formHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -2
- package/plugins/search/src/events/suggestionHandlers.test.js +1 -1
- package/plugins/search/src/utils/updateMap.js +3 -3
- package/plugins/search/src/utils/updateMap.test.js +3 -3
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-framework.js.LICENSE.txt +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/appEvents.js +7 -0
- package/providers/maplibre/src/appEvents.test.js +18 -4
- package/providers/maplibre/src/maplibreProvider.js +52 -0
- package/providers/maplibre/src/maplibreProvider.test.js +105 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +37 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +153 -95
- package/providers/maplibre/src/utils/hoverCursor.js +61 -0
- package/providers/maplibre/src/utils/hoverCursor.test.js +130 -0
- package/providers/maplibre/src/utils/patternImages.js +70 -0
- package/providers/maplibre/src/utils/patternImages.test.js +180 -0
- package/providers/maplibre/src/utils/queryFeatures.js +38 -16
- package/providers/maplibre/src/utils/queryFeatures.test.js +20 -3
- package/providers/maplibre/src/utils/rasteriseToImageData.js +30 -0
- package/providers/maplibre/src/utils/rasteriseToImageData.test.js +69 -0
- package/providers/maplibre/src/utils/symbolImages.js +147 -0
- package/providers/maplibre/src/utils/symbolImages.test.js +248 -0
- package/src/App/components/Actions/Actions.jsx +2 -2
- package/src/App/components/Actions/Actions.module.scss +0 -7
- package/src/App/components/Actions/Actions.test.jsx +1 -1
- package/src/App/components/Icon/Icon.jsx +3 -2
- package/src/App/components/Icon/Icon.module.scss +4 -0
- package/src/App/components/Icon/Icon.test.jsx +43 -4
- package/src/App/components/MapButton/MapButton.jsx +42 -17
- package/src/App/components/MapButton/MapButton.module.scss +4 -13
- package/src/App/components/MapButton/MapButton.test.jsx +27 -3
- package/src/App/components/Markers/Markers.jsx +122 -27
- package/src/App/components/Markers/Markers.module.scss +0 -10
- package/src/App/components/Markers/Markers.test.jsx +246 -0
- package/src/App/components/PopupMenu/PopupMenu.jsx +51 -274
- package/src/App/components/PopupMenu/PopupMenu.module.scss +14 -7
- package/src/App/components/PopupMenu/PopupMenu.test.jsx +70 -1
- package/src/App/components/PopupMenu/usePopupMenu.js +258 -0
- package/src/App/hooks/useButtonStateEvaluator.js +12 -2
- package/src/App/hooks/useButtonStateEvaluator.test.js +38 -4
- package/src/App/hooks/useInterfaceAPI.js +6 -0
- package/src/App/hooks/useInterfaceAPI.test.js +156 -0
- package/src/App/hooks/useLayoutMeasurements.js +84 -18
- package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
- package/src/App/hooks/useMarkersAPI.js +2 -5
- package/src/App/hooks/useMarkersAPI.test.js +4 -4
- package/src/App/layout/Layout.jsx +14 -9
- package/src/App/layout/Layout.test.jsx +6 -4
- package/src/App/layout/layout.module.scss +67 -29
- package/src/App/registry/pluginRegistry.js +1 -1
- package/src/App/renderer/HtmlElementHost.jsx +2 -1
- package/src/App/renderer/HtmlElementHost.test.jsx +7 -7
- package/src/App/renderer/mapButtons.js +1 -1
- package/src/App/renderer/mapPanels.test.js +2 -2
- package/src/App/renderer/slotHelpers.js +2 -2
- package/src/App/renderer/slotHelpers.test.js +5 -5
- package/src/App/renderer/slots.js +9 -5
- package/src/App/store/AppProvider.jsx +3 -1
- package/src/App/store/AppProvider.test.jsx +1 -1
- package/src/App/store/ServiceProvider.jsx +8 -4
- package/src/App/store/appActionsMap.js +16 -0
- package/src/App/store/appActionsMap.test.js +27 -0
- package/src/App/store/appDispatchMiddleware.js +1 -1
- package/src/App/store/appDispatchMiddleware.test.js +2 -2
- package/src/App/store/appReducer.js +2 -0
- package/src/App/store/mapActionsMap.js +4 -6
- package/src/App/store/mapActionsMap.test.js +3 -2
- package/src/App/store/mapReducer.js +2 -1
- package/src/InteractiveMap/InteractiveMap.js +4 -0
- package/src/config/appConfig.js +5 -8
- package/src/config/appConfig.test.js +1 -2
- package/src/config/defaults.js +0 -2
- package/src/config/events.js +28 -0
- package/src/config/mapTheme.js +56 -0
- package/src/config/patternConfig.js +16 -0
- package/src/config/symbolConfig.js +80 -0
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_colors.scss +0 -9
- package/src/scss/settings/_dimensions.scss +0 -1
- package/src/services/patternRegistry.js +40 -0
- package/src/services/patternRegistry.test.js +48 -0
- package/src/services/symbolRegistry.js +113 -0
- package/src/services/symbolRegistry.test.js +262 -0
- package/src/types.js +93 -11
- package/src/utils/getSafeZoneInset.js +9 -7
- package/src/utils/getSafeZoneInset.test.js +10 -10
- package/src/utils/patternUtils.js +94 -0
- package/src/utils/patternUtils.test.js +160 -0
- package/src/utils/symbolUtils.js +85 -0
- package/src/utils/symbolUtils.test.js +156 -0
- package/webpack.dev.mjs +1 -1
- package/docs/api/slot-map.svg +0 -1
- package/plugins/beta/datasets/src/api/hideDataset.js +0 -14
- package/plugins/beta/datasets/src/api/hideFeatures.js +0 -41
- package/plugins/beta/datasets/src/api/showDataset.js +0 -14
- package/plugins/beta/datasets/src/api/showFeatures.js +0 -44
- package/plugins/beta/datasets/src/handleSetMapStyle.js +0 -54
- package/plugins/beta/datasets/src/mapLayers.js +0 -164
- /package/src/{utils → services}/logger.js +0 -0
- /package/src/{utils → services}/logger.test.js +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const setOpacity = ({ pluginState }, opacity, options) => {
|
|
2
|
+
const { datasetId, sublayerId } = options || {}
|
|
3
|
+
|
|
4
|
+
if (sublayerId) {
|
|
5
|
+
const dataset = pluginState.datasets?.find(d => d.id === datasetId)
|
|
6
|
+
if (!dataset) {
|
|
7
|
+
return
|
|
8
|
+
}
|
|
9
|
+
pluginState.dispatch({ type: 'SET_SUBLAYER_OPACITY', payload: { datasetId, sublayerId, opacity } })
|
|
10
|
+
pluginState.layerAdapter?.setSublayerOpacity(datasetId, sublayerId, opacity)
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (datasetId) {
|
|
15
|
+
const dataset = pluginState.datasets?.find(d => d.id === datasetId)
|
|
16
|
+
if (!dataset) {
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
pluginState.dispatch({ type: 'SET_OPACITY', payload: { datasetId, opacity } })
|
|
20
|
+
pluginState.layerAdapter?.setOpacity(datasetId, opacity)
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Global
|
|
25
|
+
pluginState.dispatch({ type: 'SET_GLOBAL_OPACITY', payload: { opacity } })
|
|
26
|
+
pluginState.datasets?.forEach(d => {
|
|
27
|
+
pluginState.layerAdapter?.setOpacity(d.id, opacity)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const setStyle = ({ pluginState, mapState }, style, { datasetId, sublayerId } = {}) => {
|
|
2
|
+
const dataset = pluginState.datasets?.find(d => d.id === datasetId)
|
|
3
|
+
if (!dataset) {
|
|
4
|
+
return
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
if (sublayerId) {
|
|
8
|
+
pluginState.dispatch({ type: 'SET_SUBLAYER_STYLE', payload: { datasetId, sublayerId, styleChanges: style } })
|
|
9
|
+
const updatedSublayerDataset = {
|
|
10
|
+
...dataset,
|
|
11
|
+
sublayers: dataset.sublayers?.map(sublayer =>
|
|
12
|
+
sublayer.id === sublayerId ? { ...sublayer, style: { ...sublayer.style, ...style } } : sublayer
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
pluginState.layerAdapter?.setSublayerStyle(updatedSublayerDataset, sublayerId, mapState.mapStyle)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pluginState.dispatch({ type: 'SET_DATASET_STYLE', payload: { datasetId, styleChanges: style } })
|
|
20
|
+
const updatedDataset = { ...dataset, ...style }
|
|
21
|
+
pluginState.layerAdapter?.setStyle(updatedDataset, mapState.mapStyle)
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { EmptyKey } from './EmptyKey'
|
|
3
|
+
|
|
4
|
+
describe('EmptyKey', () => {
|
|
5
|
+
const text = 'No features available'
|
|
6
|
+
|
|
7
|
+
it('renders the wrapper div with the correct class', async () => {
|
|
8
|
+
const { container } = render(<EmptyKey text={text} />)
|
|
9
|
+
expect(container.querySelector('.im-c-datasets-key')).toBeTruthy()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders the empty message paragraph with the correct class', async () => {
|
|
13
|
+
const { container } = render(<EmptyKey text={text} />)
|
|
14
|
+
expect(container.querySelector('.im-c-datasets-key__empty-message')).toBeTruthy()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('renders the provided text', async () => {
|
|
18
|
+
render(<EmptyKey text={text} />)
|
|
19
|
+
expect(screen.getByText(text)).toBeTruthy()
|
|
20
|
+
})
|
|
21
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { hasSymbol, getSymbolDef } from '../../../../../src/utils/symbolUtils.js'
|
|
2
|
+
import { hasPattern } from '../../../../../src/utils/patternUtils.js'
|
|
3
|
+
import { KeySvgPattern } from './KeySvgPattern.jsx'
|
|
4
|
+
import { KeySvgSymbol } from './KeySvgSymbol.jsx'
|
|
5
|
+
import { KeySvgLine } from './KeySvgLine.jsx'
|
|
6
|
+
import { KeySvgRect } from './KeySvgRect.jsx'
|
|
7
|
+
|
|
8
|
+
export const KeySvg = (props) => {
|
|
9
|
+
const { symbolRegistry } = props
|
|
10
|
+
const symbolDef = hasSymbol(props) && getSymbolDef(props, symbolRegistry)
|
|
11
|
+
if (symbolDef) {
|
|
12
|
+
return <KeySvgSymbol {...props} symbolDef={symbolDef} />
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (hasPattern(props)) {
|
|
16
|
+
return <KeySvgPattern {...props} />
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (props.keySymbolShape === 'line') {
|
|
20
|
+
return <KeySvgLine {...props} />
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return <KeySvgRect {...props} />
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { svgProps, SVG_SIZE, SVG_CENTER } from './svgProperties.js'
|
|
2
|
+
import { getValueForStyle } from '../../../../../src/utils/getValueForStyle'
|
|
3
|
+
|
|
4
|
+
export const KeySvgLine = (props) => {
|
|
5
|
+
const { mapStyle } = props
|
|
6
|
+
return (
|
|
7
|
+
<svg {...svgProps}>
|
|
8
|
+
<line
|
|
9
|
+
x1={props.strokeWidth / 2}
|
|
10
|
+
y1={SVG_CENTER}
|
|
11
|
+
x2={SVG_SIZE - props.strokeWidth / 2}
|
|
12
|
+
y2={SVG_CENTER}
|
|
13
|
+
stroke={getValueForStyle(props.stroke, mapStyle.id)}
|
|
14
|
+
strokeWidth={props.strokeWidth}
|
|
15
|
+
strokeLinecap='round'
|
|
16
|
+
/>
|
|
17
|
+
</svg>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { getKeyPatternPaths } from '../../../../../src/utils/patternUtils.js'
|
|
2
|
+
import { svgProps } from './svgProperties.js'
|
|
3
|
+
const PATTERN_INSET = 2
|
|
4
|
+
|
|
5
|
+
export const KeySvgPattern = (props) => {
|
|
6
|
+
const { patternRegistry, mapStyle } = props
|
|
7
|
+
|
|
8
|
+
const paths = getKeyPatternPaths(props, mapStyle.id, patternRegistry)
|
|
9
|
+
return (
|
|
10
|
+
<svg {...svgProps}>
|
|
11
|
+
<g dangerouslySetInnerHTML={{ __html: paths.border }} />
|
|
12
|
+
<g transform={`translate(${PATTERN_INSET}, ${PATTERN_INSET})`} dangerouslySetInnerHTML={{ __html: paths.content }} />
|
|
13
|
+
</svg>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { getValueForStyle } from '../../../../../src/utils/getValueForStyle'
|
|
2
|
+
import { svgProps, SVG_SIZE } from './svgProperties.js'
|
|
3
|
+
|
|
4
|
+
export const KeySvgRect = (props) => {
|
|
5
|
+
const { mapStyle } = props
|
|
6
|
+
return (
|
|
7
|
+
<svg {...svgProps}>
|
|
8
|
+
<rect
|
|
9
|
+
x={props.strokeWidth / 2}
|
|
10
|
+
y={props.strokeWidth / 2}
|
|
11
|
+
width={SVG_SIZE - props.strokeWidth}
|
|
12
|
+
height={SVG_SIZE - props.strokeWidth}
|
|
13
|
+
rx={props.strokeWidth}
|
|
14
|
+
ry={props.strokeWidth}
|
|
15
|
+
fill={getValueForStyle(props.fill, mapStyle.id)}
|
|
16
|
+
stroke={getValueForStyle(props.stroke, mapStyle.id)}
|
|
17
|
+
strokeWidth={props.strokeWidth}
|
|
18
|
+
strokeLinejoin='round'
|
|
19
|
+
/>
|
|
20
|
+
</svg>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getSymbolStyleColors, getSymbolViewBox } from '../../../../../src/utils/symbolUtils.js'
|
|
2
|
+
import { svgSymbolProps } from './svgProperties.js'
|
|
3
|
+
|
|
4
|
+
export const KeySvgSymbol = (props) => {
|
|
5
|
+
const { symbolRegistry, mapStyle, symbolDef } = props
|
|
6
|
+
const mapColorScheme = mapStyle?.appColorScheme ?? 'light'
|
|
7
|
+
const keyMapStyle = { ...mapStyle, mapColorScheme }
|
|
8
|
+
|
|
9
|
+
const resolvedSvg = symbolRegistry.resolve(symbolDef, getSymbolStyleColors(props), keyMapStyle)
|
|
10
|
+
const viewBox = getSymbolViewBox(props, symbolDef)
|
|
11
|
+
return (
|
|
12
|
+
<svg {...svgSymbolProps} viewBox={viewBox}>
|
|
13
|
+
<g dangerouslySetInnerHTML={{ __html: resolvedSvg }} />
|
|
14
|
+
</svg>
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const SVG_SIZE = 20
|
|
2
|
+
export const SVG_CENTER = SVG_SIZE / 2
|
|
3
|
+
const SVG_SYMBOL_SIZE = 38
|
|
4
|
+
|
|
5
|
+
export const svgProps = {
|
|
6
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
7
|
+
width: SVG_SIZE,
|
|
8
|
+
height: SVG_SIZE,
|
|
9
|
+
viewBox: `0 0 ${SVG_SIZE} ${SVG_SIZE}`,
|
|
10
|
+
className: 'am-c-datasets-key-symbol',
|
|
11
|
+
'aria-hidden': 'true',
|
|
12
|
+
focusable: 'false'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const svgSymbolProps = {
|
|
16
|
+
...svgProps,
|
|
17
|
+
width: SVG_SYMBOL_SIZE,
|
|
18
|
+
height: SVG_SYMBOL_SIZE,
|
|
19
|
+
className: 'am-c-datasets-key-symbol am-c-datasets-key-symbol--point'
|
|
20
|
+
}
|
|
@@ -1,81 +1,71 @@
|
|
|
1
|
-
import { handleSetMapStyle } from './handleSetMapStyle.js'
|
|
2
|
-
import { addMapLayers, getSourceId, getLayersUsingSource, isDynamicSource, updateSourceData } from './mapLayers.js'
|
|
3
1
|
import { createDynamicSource } from './fetch/createDynamicSource.js'
|
|
2
|
+
// NOSONAR: applyDatasetDefaults and datasetDefaults are used in processedDatasets.map
|
|
3
|
+
import { applyDatasetDefaults, datasetDefaults } from './defaults.js'
|
|
4
|
+
|
|
5
|
+
const isDynamicSource = (dataset) =>
|
|
6
|
+
typeof dataset.geojson === 'string' &&
|
|
7
|
+
!!dataset.idProperty &&
|
|
8
|
+
typeof dataset.transformRequest === 'function'
|
|
4
9
|
|
|
5
10
|
export const createDatasets = ({
|
|
11
|
+
adapter,
|
|
6
12
|
pluginConfig,
|
|
7
13
|
pluginStateRef,
|
|
8
|
-
|
|
14
|
+
mapStyle,
|
|
9
15
|
mapProvider,
|
|
10
16
|
events,
|
|
11
17
|
eventBus
|
|
12
18
|
}) => {
|
|
13
|
-
const { map } = mapProvider
|
|
14
19
|
const { datasets } = pluginConfig
|
|
15
20
|
|
|
16
|
-
// Track dynamic sources for cleanup
|
|
17
21
|
const dynamicSources = new Map()
|
|
18
22
|
|
|
19
23
|
const getDatasets = () => pluginStateRef.current.datasets || datasets
|
|
20
24
|
const getHiddenFeatures = () => pluginStateRef.current.hiddenFeatures || {}
|
|
21
25
|
|
|
22
|
-
//
|
|
23
|
-
datasets.
|
|
24
|
-
|
|
26
|
+
// Initialise all datasets via the adapter, then set up dynamic sources
|
|
27
|
+
const processedDatasets = datasets.map(d => applyDatasetDefaults(d, datasetDefaults))
|
|
28
|
+
adapter.init(processedDatasets, mapStyle).then(() => {
|
|
29
|
+
processedDatasets.forEach(dataset => {
|
|
30
|
+
if (!isDynamicSource(dataset)) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
25
33
|
|
|
26
|
-
// Initialize dynamic source if applicable
|
|
27
|
-
if (isDynamicSource(dataset)) {
|
|
28
|
-
const sourceId = getSourceId(dataset)
|
|
29
34
|
const dynamicSource = createDynamicSource({
|
|
30
35
|
dataset,
|
|
31
|
-
map,
|
|
32
|
-
|
|
33
|
-
onUpdate: (id, geojson) => updateSourceData(map, id, geojson)
|
|
36
|
+
map: mapProvider.map,
|
|
37
|
+
onUpdate: (datasetId, geojson) => adapter.setData(datasetId, geojson)
|
|
34
38
|
})
|
|
35
39
|
dynamicSources.set(dataset.id, dynamicSource)
|
|
36
|
-
}
|
|
37
|
-
})
|
|
40
|
+
})
|
|
38
41
|
|
|
39
|
-
// Emit ready event once map has processed the layers
|
|
40
|
-
map.once('idle', () => {
|
|
41
42
|
eventBus.emit('datasets:ready')
|
|
42
43
|
})
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
getDatasets,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
let currentMapStyle = mapStyle
|
|
46
|
+
|
|
47
|
+
// Handle basemap style changes — delegate entirely to the adapter
|
|
48
|
+
const onSetStyle = (newMapStyle) => {
|
|
49
|
+
currentMapStyle = newMapStyle
|
|
50
|
+
adapter.onStyleChange(getDatasets(), newMapStyle, getHiddenFeatures(), dynamicSources)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const onSizeChange = () => {
|
|
54
|
+
adapter.onSizeChange(getDatasets(), currentMapStyle)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
eventBus.on(events.MAP_SET_STYLE, onSetStyle)
|
|
58
|
+
eventBus.on(events.MAP_SIZE_CHANGE, onSizeChange)
|
|
53
59
|
|
|
54
60
|
return {
|
|
55
61
|
remove () {
|
|
56
|
-
eventBus.off(events.MAP_SET_STYLE,
|
|
62
|
+
eventBus.off(events.MAP_SET_STYLE, onSetStyle)
|
|
63
|
+
eventBus.off(events.MAP_SIZE_CHANGE, onSizeChange)
|
|
57
64
|
|
|
58
65
|
// Clean up dynamic sources
|
|
59
66
|
dynamicSources.forEach(source => source.destroy())
|
|
60
67
|
dynamicSources.clear()
|
|
61
|
-
|
|
62
|
-
const allDatasets = getDatasets()
|
|
63
|
-
const removedSourceIds = new Set()
|
|
64
|
-
|
|
65
|
-
// Remove layers and sources
|
|
66
|
-
allDatasets.forEach(dataset => {
|
|
67
|
-
const sourceId = getSourceId(dataset)
|
|
68
|
-
const layers = getLayersUsingSource(map, sourceId)
|
|
69
|
-
|
|
70
|
-
// Remove all layers using this source
|
|
71
|
-
layers.forEach(id => map.removeLayer(id))
|
|
72
|
-
|
|
73
|
-
// Remove the source once
|
|
74
|
-
if (!removedSourceIds.has(sourceId) && map.getSource(sourceId)) {
|
|
75
|
-
map.removeSource(sourceId)
|
|
76
|
-
removedSourceIds.add(sourceId)
|
|
77
|
-
}
|
|
78
|
-
})
|
|
68
|
+
adapter.destroy(getDatasets())
|
|
79
69
|
},
|
|
80
70
|
|
|
81
71
|
/**
|
|
@@ -83,10 +73,7 @@ export const createDatasets = ({
|
|
|
83
73
|
* @param {string} datasetId - Dataset ID to refresh
|
|
84
74
|
*/
|
|
85
75
|
refreshDataset (datasetId) {
|
|
86
|
-
|
|
87
|
-
if (dynamicSource) {
|
|
88
|
-
dynamicSource.refresh()
|
|
89
|
-
}
|
|
76
|
+
dynamicSources.get(datasetId)?.refresh()
|
|
90
77
|
},
|
|
91
78
|
|
|
92
79
|
/**
|
|
@@ -94,10 +81,7 @@ export const createDatasets = ({
|
|
|
94
81
|
* @param {string} datasetId - Dataset ID to clear
|
|
95
82
|
*/
|
|
96
83
|
clearDatasetCache (datasetId) {
|
|
97
|
-
|
|
98
|
-
if (dynamicSource) {
|
|
99
|
-
dynamicSource.clear()
|
|
100
|
-
}
|
|
84
|
+
dynamicSources.get(datasetId)?.clear()
|
|
101
85
|
},
|
|
102
86
|
|
|
103
87
|
/**
|
|
@@ -106,8 +90,7 @@ export const createDatasets = ({
|
|
|
106
90
|
* @returns {number|null} Feature count or null if not a dynamic source
|
|
107
91
|
*/
|
|
108
92
|
getFeatureCount (datasetId) {
|
|
109
|
-
|
|
110
|
-
return dynamicSource ? dynamicSource.getFeatureCount() : null
|
|
93
|
+
return dynamicSources.get(datasetId)?.getFeatureCount() ?? null
|
|
111
94
|
}
|
|
112
95
|
}
|
|
113
96
|
}
|
|
@@ -1,15 +1,51 @@
|
|
|
1
1
|
const datasetDefaults = {
|
|
2
|
-
stroke: '#d4351c',
|
|
3
|
-
strokeWidth: 2,
|
|
4
|
-
fill: 'transparent',
|
|
5
|
-
symbolDescription: 'red outline',
|
|
6
2
|
minZoom: 6,
|
|
7
3
|
maxZoom: 24,
|
|
8
4
|
showInKey: false,
|
|
9
|
-
|
|
10
|
-
visibility: 'visible'
|
|
5
|
+
toggleVisibility: false,
|
|
6
|
+
visibility: 'visible',
|
|
7
|
+
style: {
|
|
8
|
+
stroke: '#d4351c',
|
|
9
|
+
strokeWidth: 2,
|
|
10
|
+
fill: 'transparent',
|
|
11
|
+
symbolDescription: 'red outline'
|
|
12
|
+
}
|
|
11
13
|
}
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
// All properties considered style properties — must be provided via dataset.style, not at the top level.
|
|
16
|
+
const STYLE_PROPS = [
|
|
17
|
+
'stroke', 'strokeWidth', 'strokeDashArray',
|
|
18
|
+
'fill', 'fillPattern', 'fillPatternSvgContent', 'fillPatternForegroundColor', 'fillPatternBackgroundColor',
|
|
19
|
+
'opacity', 'symbolDescription', 'keySymbolShape',
|
|
20
|
+
'symbol', 'symbolSvgContent', 'symbolViewBox', 'symbolAnchor',
|
|
21
|
+
'symbolBackgroundColor', 'symbolForegroundColor', 'symbolHaloWidth', 'symbolGraphic'
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// Props whose presence in a style object indicates a custom visual style.
|
|
25
|
+
// When any are set, the default symbolDescription is not appropriate.
|
|
26
|
+
const VISUAL_STYLE_PROPS = ['stroke', 'fill', 'fillPattern', 'fillPatternSvgContent', 'symbol', 'symbolSvgContent']
|
|
27
|
+
|
|
28
|
+
const hasCustomVisualStyle = (style) =>
|
|
29
|
+
VISUAL_STYLE_PROPS.some(prop => prop in style)
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Merge a dataset config with defaults, flattening the nested `style` object.
|
|
33
|
+
* Style properties must be provided via dataset.style — top-level occurrences are ignored.
|
|
34
|
+
* symbolDescription from defaults.style is dropped when custom visual styles
|
|
35
|
+
* are present and the dataset doesn't explicitly set its own symbolDescription.
|
|
36
|
+
*/
|
|
37
|
+
const applyDatasetDefaults = (dataset, defaults) => {
|
|
38
|
+
const style = dataset.style || {}
|
|
39
|
+
const mergedStyle = { ...defaults.style, ...style }
|
|
40
|
+
if (!('symbolDescription' in style) && hasCustomVisualStyle(style)) {
|
|
41
|
+
delete mergedStyle.symbolDescription
|
|
42
|
+
}
|
|
43
|
+
const topLevel = { ...dataset }
|
|
44
|
+
delete topLevel.style
|
|
45
|
+
STYLE_PROPS.forEach(prop => delete topLevel[prop])
|
|
46
|
+
const topLevelDefaults = { ...defaults }
|
|
47
|
+
delete topLevelDefaults.style
|
|
48
|
+
return { ...topLevelDefaults, ...topLevel, ...mergedStyle }
|
|
15
49
|
}
|
|
50
|
+
|
|
51
|
+
export { datasetDefaults, hasCustomVisualStyle, applyDatasetDefaults }
|
|
@@ -14,17 +14,17 @@ const EVICTION_THRESHOLD = 1.2 // Trigger eviction at 120% of maxFeatures
|
|
|
14
14
|
* @param {Function} options.onUpdate - Callback when source data should be updated
|
|
15
15
|
* @returns {Object} { destroy, clear, refresh }
|
|
16
16
|
*/
|
|
17
|
-
export const createDynamicSource = ({ dataset, map,
|
|
17
|
+
export const createDynamicSource = ({ dataset, map, onUpdate }) => {
|
|
18
18
|
const { geojson: baseUrl, idProperty, transformRequest, maxFeatures, minZoom = 0 } = dataset
|
|
19
19
|
|
|
20
|
-
// Feature cache: id → { feature, bbox,
|
|
20
|
+
// Feature cache: id → { feature, bbox, lastSeenAt }
|
|
21
21
|
const features = new Map()
|
|
22
22
|
|
|
23
23
|
// Track the bbox we've fetched data for
|
|
24
24
|
let fetchedBbox = null
|
|
25
25
|
|
|
26
|
-
//
|
|
27
|
-
let
|
|
26
|
+
// Abort controller for the in-flight request
|
|
27
|
+
let currentController = null
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* Convert features Map to FeatureCollection
|
|
@@ -63,25 +63,25 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
|
|
|
63
63
|
if (bboxIntersects(data.bbox, currentBbox)) {
|
|
64
64
|
inView.push(id)
|
|
65
65
|
} else {
|
|
66
|
-
outOfView.push({ id,
|
|
66
|
+
outOfView.push({ id, lastSeenAt: data.lastSeenAt })
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// Sort out-of-view by
|
|
71
|
-
outOfView.sort((a, b) => a.
|
|
70
|
+
// Sort out-of-view by last seen time (least recently seen first)
|
|
71
|
+
outOfView.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
|
|
72
72
|
|
|
73
|
-
// Evict
|
|
73
|
+
// Evict least-recently-seen out-of-view features until under target
|
|
74
74
|
const toEvict = features.size - targetSize
|
|
75
75
|
for (let i = 0; i < toEvict && i < outOfView.length; i++) {
|
|
76
76
|
features.delete(outOfView[i].id)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
// If still over target (viewport has too many), evict
|
|
79
|
+
// If still over target (viewport has too many), evict least recently seen in-view
|
|
80
80
|
if (features.size > targetSize) {
|
|
81
81
|
const remaining = features.size - targetSize
|
|
82
82
|
const inViewSorted = inView
|
|
83
|
-
.map(id => ({ id,
|
|
84
|
-
.sort((a, b) => a.
|
|
83
|
+
.map(id => ({ id, lastSeenAt: features.get(id).lastSeenAt }))
|
|
84
|
+
.sort((a, b) => a.lastSeenAt - b.lastSeenAt)
|
|
85
85
|
|
|
86
86
|
for (let i = 0; i < remaining && i < inViewSorted.length; i++) {
|
|
87
87
|
features.delete(inViewSorted[i].id)
|
|
@@ -105,19 +105,19 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
|
|
|
105
105
|
return
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
// Abort any in-flight request — new viewport takes priority
|
|
109
|
+
if (currentController) {
|
|
110
|
+
currentController.abort()
|
|
110
111
|
}
|
|
111
|
-
|
|
112
|
-
isLoading = true
|
|
112
|
+
currentController = new AbortController()
|
|
113
113
|
|
|
114
114
|
try {
|
|
115
115
|
const context = { bbox: currentBbox, zoom, dataset }
|
|
116
|
-
const data = await fetchGeoJSON(baseUrl, context, transformRequest)
|
|
116
|
+
const data = await fetchGeoJSON(baseUrl, context, transformRequest, currentController.signal)
|
|
117
117
|
|
|
118
118
|
const now = Date.now()
|
|
119
119
|
|
|
120
|
-
// Add/update features with deduplication
|
|
120
|
+
// Add/update features with deduplication, refreshing lastSeenAt on each fetch
|
|
121
121
|
data.features.forEach(feature => {
|
|
122
122
|
const id = getFeatureId(feature)
|
|
123
123
|
if (id == null) {
|
|
@@ -128,22 +128,28 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
|
|
|
128
128
|
features.set(id, {
|
|
129
129
|
feature,
|
|
130
130
|
bbox: getGeometryBbox(feature.geometry),
|
|
131
|
-
|
|
131
|
+
lastSeenAt: now
|
|
132
132
|
})
|
|
133
133
|
})
|
|
134
134
|
|
|
135
135
|
// Expand tracked bbox
|
|
136
136
|
fetchedBbox = expandBbox(fetchedBbox, currentBbox)
|
|
137
137
|
|
|
138
|
-
// Evict if over limit
|
|
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
|
|
139
141
|
evictIfNeeded(currentBbox)
|
|
142
|
+
if (features.size < sizeBeforeEviction) {
|
|
143
|
+
fetchedBbox = currentBbox
|
|
144
|
+
}
|
|
140
145
|
|
|
141
146
|
// Update map source
|
|
142
|
-
onUpdate(
|
|
147
|
+
onUpdate(dataset.id, toFeatureCollection())
|
|
143
148
|
} catch (error) {
|
|
149
|
+
if (error.name === 'AbortError') {
|
|
150
|
+
return
|
|
151
|
+
}
|
|
144
152
|
console.error(`Failed to fetch dynamic GeoJSON for ${dataset.id}:`, error)
|
|
145
|
-
} finally {
|
|
146
|
-
isLoading = false
|
|
147
153
|
}
|
|
148
154
|
}
|
|
149
155
|
|
|
@@ -162,11 +168,14 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
|
|
|
162
168
|
|
|
163
169
|
return {
|
|
164
170
|
/**
|
|
165
|
-
* Clean up event listeners
|
|
171
|
+
* Clean up event listeners and cancel any in-flight request
|
|
166
172
|
*/
|
|
167
173
|
destroy () {
|
|
168
174
|
map.off('moveend', handleMoveEnd)
|
|
169
175
|
debouncedFetch.cancel()
|
|
176
|
+
if (currentController) {
|
|
177
|
+
currentController.abort()
|
|
178
|
+
}
|
|
170
179
|
},
|
|
171
180
|
|
|
172
181
|
/**
|
|
@@ -175,7 +184,7 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
|
|
|
175
184
|
clear () {
|
|
176
185
|
features.clear()
|
|
177
186
|
fetchedBbox = null
|
|
178
|
-
onUpdate(
|
|
187
|
+
onUpdate(dataset.id, { type: 'FeatureCollection', features: [] })
|
|
179
188
|
},
|
|
180
189
|
|
|
181
190
|
/**
|
|
@@ -199,7 +208,7 @@ export const createDynamicSource = ({ dataset, map, sourceId, onUpdate }) => {
|
|
|
199
208
|
*/
|
|
200
209
|
reapply () {
|
|
201
210
|
if (features.size > 0) {
|
|
202
|
-
onUpdate(
|
|
211
|
+
onUpdate(dataset.id, toFeatureCollection())
|
|
203
212
|
}
|
|
204
213
|
}
|
|
205
214
|
}
|
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
* @param {Function} transformRequest - Function to transform the request (builds URL with bbox, adds headers)
|
|
6
6
|
* @returns {Promise<Object>} GeoJSON FeatureCollection
|
|
7
7
|
*/
|
|
8
|
-
export const fetchGeoJSON = async (baseUrl, context, transformRequest) => {
|
|
8
|
+
export const fetchGeoJSON = async (baseUrl, context, transformRequest, signal) => {
|
|
9
9
|
const result = transformRequest(baseUrl, context)
|
|
10
10
|
|
|
11
11
|
// Handle both string and object return values
|
|
12
12
|
const config = typeof result === 'string' ? { url: result } : result
|
|
13
13
|
const { url, headers = {} } = config
|
|
14
14
|
|
|
15
|
-
const response = await fetch(url, { headers })
|
|
15
|
+
const response = await fetch(url, { headers, signal })
|
|
16
16
|
|
|
17
17
|
if (!response.ok) {
|
|
18
18
|
throw new Error(`Failed to fetch GeoJSON: ${response.status} ${response.statusText}`)
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
// /plugins/datasets/index.js
|
|
2
2
|
import './datasets.scss'
|
|
3
3
|
|
|
4
|
-
export default function createPlugin (options = {}) {
|
|
4
|
+
export default function createPlugin (options = { }) {
|
|
5
5
|
const plugin = {
|
|
6
|
+
noKeyItemText: 'No features displayed',
|
|
6
7
|
...options,
|
|
7
8
|
id: 'datasets',
|
|
8
9
|
load: async () => {
|