@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,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hashString,
|
|
3
|
+
injectColors,
|
|
4
|
+
hasPattern,
|
|
5
|
+
getPatternInnerContent,
|
|
6
|
+
getPatternImageId,
|
|
7
|
+
getKeyPatternPaths,
|
|
8
|
+
KEY_BORDER_PATH
|
|
9
|
+
} from './patternUtils.js'
|
|
10
|
+
|
|
11
|
+
const mockRegistry = {
|
|
12
|
+
get: (id) => id === 'dot' ? { id: 'dot', svgContent: '<path d="M4 4" fill="{{foregroundColor}}"/>' } : undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('hashString', () => {
|
|
16
|
+
test('returns a non-empty string', () => {
|
|
17
|
+
expect(typeof hashString('hello')).toBe('string')
|
|
18
|
+
expect(hashString('hello').length).toBeGreaterThan(0)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('is deterministic', () => {
|
|
22
|
+
expect(hashString('hello')).toBe(hashString('hello'))
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('produces different values for different inputs', () => {
|
|
26
|
+
expect(hashString('a')).not.toBe(hashString('b'))
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('injectColors', () => {
|
|
31
|
+
test('replaces {{foregroundColor}} and {{backgroundColor}} tokens', () => {
|
|
32
|
+
const result = injectColors('fill="{{foregroundColor}}" bg="{{backgroundColor}}"', 'red', 'blue')
|
|
33
|
+
expect(result).toBe('fill="red" bg="blue"')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('replaces all occurrences', () => {
|
|
37
|
+
const result = injectColors('{{foregroundColor}} {{foregroundColor}}', 'red', 'blue')
|
|
38
|
+
expect(result).toBe('red red')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('uses fallback "black" when foregroundColor is falsy', () => {
|
|
42
|
+
expect(injectColors('{{foregroundColor}}', '', 'blue')).toBe('black')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('uses fallback "transparent" when backgroundColor is falsy', () => {
|
|
46
|
+
expect(injectColors('{{backgroundColor}}', 'red', '')).toBe('transparent')
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('hasPattern', () => {
|
|
51
|
+
test('returns true when fillPattern is set', () => {
|
|
52
|
+
expect(hasPattern({ fillPattern: 'dot' })).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('returns true when fillPatternSvgContent is set', () => {
|
|
56
|
+
expect(hasPattern({ fillPatternSvgContent: '<path/>' })).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test('returns false when neither is set', () => {
|
|
60
|
+
expect(hasPattern({})).toBe(false)
|
|
61
|
+
expect(hasPattern({ fill: 'red' })).toBe(false)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('getPatternInnerContent', () => {
|
|
66
|
+
test('returns fillPatternSvgContent when set (inline SVG takes precedence)', () => {
|
|
67
|
+
const dataset = { fillPatternSvgContent: '<path d="custom"/>', fillPattern: 'dot' }
|
|
68
|
+
expect(getPatternInnerContent(dataset, mockRegistry)).toBe('<path d="custom"/>')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('returns svgContent from registry for a named fillPattern', () => {
|
|
72
|
+
const dataset = { fillPattern: 'dot' }
|
|
73
|
+
expect(getPatternInnerContent(dataset, mockRegistry)).toBe('<path d="M4 4" fill="{{foregroundColor}}"/>')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('returns null for an unregistered fillPattern name', () => {
|
|
77
|
+
const dataset = { fillPattern: 'unknown-pattern' }
|
|
78
|
+
expect(getPatternInnerContent(dataset, mockRegistry)).toBeNull()
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test('returns null when no pattern is configured', () => {
|
|
82
|
+
expect(getPatternInnerContent({}, mockRegistry)).toBeNull()
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('getPatternImageId', () => {
|
|
87
|
+
test('returns a deterministic string id', () => {
|
|
88
|
+
const dataset = { fillPattern: 'dot', fillPatternForegroundColor: 'red', fillPatternBackgroundColor: 'blue' }
|
|
89
|
+
const id = getPatternImageId(dataset, 'style-a', mockRegistry)
|
|
90
|
+
expect(typeof id).toBe('string')
|
|
91
|
+
expect(id).toMatch(/^pattern-/)
|
|
92
|
+
expect(id).toBe(getPatternImageId(dataset, 'style-a', mockRegistry))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
test('returns null when no pattern content is found', () => {
|
|
96
|
+
expect(getPatternImageId({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('produces different ids for different colours', () => {
|
|
100
|
+
const base = { fillPattern: 'dot' }
|
|
101
|
+
const idA = getPatternImageId({ ...base, fillPatternForegroundColor: 'red' }, 'style-a', mockRegistry)
|
|
102
|
+
const idB = getPatternImageId({ ...base, fillPatternForegroundColor: 'blue' }, 'style-a', mockRegistry)
|
|
103
|
+
expect(idA).not.toBe(idB)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('falls back to "black" foreground and "transparent" background when colours are absent', () => {
|
|
107
|
+
const id = getPatternImageId({ fillPattern: 'dot' }, 'style-a', mockRegistry)
|
|
108
|
+
const idExplicit = getPatternImageId(
|
|
109
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'black', fillPatternBackgroundColor: 'transparent' },
|
|
110
|
+
'style-a',
|
|
111
|
+
mockRegistry
|
|
112
|
+
)
|
|
113
|
+
expect(id).toBe(idExplicit)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
describe('getKeyPatternPaths', () => {
|
|
118
|
+
test('returns border and content strings with colours injected', () => {
|
|
119
|
+
const dataset = {
|
|
120
|
+
fillPattern: 'dot',
|
|
121
|
+
fillPatternForegroundColor: 'red',
|
|
122
|
+
fillPatternBackgroundColor: 'white',
|
|
123
|
+
stroke: 'black'
|
|
124
|
+
}
|
|
125
|
+
const result = getKeyPatternPaths(dataset, 'style-a', mockRegistry)
|
|
126
|
+
expect(result).not.toBeNull()
|
|
127
|
+
expect(result.border).toContain('black') // stroke colour
|
|
128
|
+
expect(result.border).toContain('white') // background colour
|
|
129
|
+
expect(result.content).toContain('red') // foreground colour
|
|
130
|
+
expect(result.border).not.toContain('{{foregroundColor}}')
|
|
131
|
+
expect(result.content).not.toContain('{{foregroundColor}}')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
test('returns null when no pattern content is found', () => {
|
|
135
|
+
expect(getKeyPatternPaths({ fillPattern: 'unknown' }, 'style-a', mockRegistry)).toBeNull()
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
test('falls back to "black" fg and "transparent" bg when colour properties are absent', () => {
|
|
139
|
+
const result = getKeyPatternPaths({ fillPattern: 'dot' }, 'style-a', mockRegistry)
|
|
140
|
+
expect(result).not.toBeNull()
|
|
141
|
+
expect(result.content).toContain('black')
|
|
142
|
+
expect(result.border).toContain('transparent')
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('border stroke falls back to foreground colour when stroke is absent', () => {
|
|
146
|
+
const result = getKeyPatternPaths(
|
|
147
|
+
{ fillPattern: 'dot', fillPatternForegroundColor: 'green' },
|
|
148
|
+
'style-a',
|
|
149
|
+
mockRegistry
|
|
150
|
+
)
|
|
151
|
+
expect(result).not.toBeNull()
|
|
152
|
+
// borderStroke falls back to fg ('green'), so the border uses green for both stroke and background
|
|
153
|
+
expect(result.border).toContain('green')
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
test('KEY_BORDER_PATH contains foregroundColor and backgroundColor tokens', () => {
|
|
157
|
+
expect(KEY_BORDER_PATH).toContain('{{foregroundColor}}')
|
|
158
|
+
expect(KEY_BORDER_PATH).toContain('{{backgroundColor}}')
|
|
159
|
+
})
|
|
160
|
+
})
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Symbol style props in dataset style that carry token values.
|
|
2
|
+
// These use the 'symbol' prefix to distinguish them from fill/stroke props at the same level.
|
|
3
|
+
// The prefix is stripped before passing tokens to the registry (e.g. symbolBackgroundColor → backgroundColor).
|
|
4
|
+
const SYMBOL_STYLE_PROPS = new Set([
|
|
5
|
+
'symbolBackgroundColor', 'symbolForegroundColor',
|
|
6
|
+
'symbolHaloWidth', 'symbolGraphic'
|
|
7
|
+
])
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns true if this dataset should be rendered as a symbol (point) layer.
|
|
11
|
+
* @param {Object} dataset
|
|
12
|
+
* @returns {boolean}
|
|
13
|
+
*/
|
|
14
|
+
export const hasSymbol = (dataset) => !!(dataset.symbol || dataset.symbolSvgContent)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves the symbolDef for a dataset's symbol config.
|
|
18
|
+
*
|
|
19
|
+
* dataset.symbol is a string symbol ID (e.g. 'pin').
|
|
20
|
+
* dataset.symbolSvgContent is inline SVG content for a custom symbol.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} dataset
|
|
23
|
+
* @param {Object} symbolRegistry
|
|
24
|
+
* @returns {Object|undefined}
|
|
25
|
+
*/
|
|
26
|
+
export const getSymbolDef = (dataset, symbolRegistry) => {
|
|
27
|
+
if (dataset.symbolSvgContent) {
|
|
28
|
+
return { svg: dataset.symbolSvgContent }
|
|
29
|
+
}
|
|
30
|
+
if (dataset.symbol) {
|
|
31
|
+
return symbolRegistry.get(dataset.symbol)
|
|
32
|
+
}
|
|
33
|
+
return undefined
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Extracts token overrides from a dataset's flat symbol style props.
|
|
38
|
+
* Strips the 'symbol' prefix to produce internal token names (e.g. symbolBackgroundColor → backgroundColor).
|
|
39
|
+
* Returns an empty object when no symbol is configured.
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} dataset
|
|
42
|
+
* @returns {Object}
|
|
43
|
+
*/
|
|
44
|
+
export const getSymbolStyleColors = (dataset) => {
|
|
45
|
+
if (!hasSymbol(dataset)) { return {} }
|
|
46
|
+
const tokens = {}
|
|
47
|
+
SYMBOL_STYLE_PROPS.forEach(key => {
|
|
48
|
+
if (dataset[key] != null) {
|
|
49
|
+
// Strip 'symbol' prefix: symbolBackgroundColor → backgroundColor
|
|
50
|
+
const tokenKey = key.charAt(6).toLowerCase() + key.slice(7) // NOSONAR
|
|
51
|
+
tokens[tokenKey] = dataset[key]
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
return tokens
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Returns the viewBox string for a dataset's symbol.
|
|
59
|
+
* Precedence: dataset.symbolViewBox → symbolDef viewBox → default.
|
|
60
|
+
*
|
|
61
|
+
* @param {Object} dataset
|
|
62
|
+
* @param {Object|undefined} symbolDef
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export const getSymbolViewBox = (dataset, symbolDef) => {
|
|
66
|
+
if (dataset.symbolViewBox) {
|
|
67
|
+
return dataset.symbolViewBox
|
|
68
|
+
}
|
|
69
|
+
return symbolDef?.viewBox ?? '0 0 38 38'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns the anchor for a dataset's symbol as [x, y] in 0–1 space.
|
|
74
|
+
* Precedence: dataset.symbolAnchor → symbolDef anchor → [0.5, 0.5].
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} dataset
|
|
77
|
+
* @param {Object|undefined} symbolDef
|
|
78
|
+
* @returns {number[]}
|
|
79
|
+
*/
|
|
80
|
+
export const getSymbolAnchor = (dataset, symbolDef) => {
|
|
81
|
+
if (dataset.symbolAnchor) {
|
|
82
|
+
return dataset.symbolAnchor
|
|
83
|
+
}
|
|
84
|
+
return symbolDef?.anchor ?? [0.5, 0.5]
|
|
85
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
hasSymbol,
|
|
3
|
+
getSymbolDef,
|
|
4
|
+
getSymbolStyleColors,
|
|
5
|
+
getSymbolViewBox,
|
|
6
|
+
getSymbolAnchor
|
|
7
|
+
} from './symbolUtils.js'
|
|
8
|
+
|
|
9
|
+
const mockRegistry = (defs = {}) => ({
|
|
10
|
+
get: jest.fn((id) => defs[id])
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
// ─── hasSymbol ────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('hasSymbol', () => {
|
|
16
|
+
it('returns true when dataset has a symbol string', () => {
|
|
17
|
+
expect(hasSymbol({ symbol: 'pin' })).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns true when dataset has symbolSvgContent', () => {
|
|
21
|
+
expect(hasSymbol({ symbolSvgContent: '<circle/>' })).toBe(true)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('returns false when symbol is absent', () => {
|
|
25
|
+
expect(hasSymbol({})).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns false when symbol is null', () => {
|
|
29
|
+
expect(hasSymbol({ symbol: null })).toBe(false)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// ─── getSymbolDef ─────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
describe('getSymbolDef', () => {
|
|
36
|
+
it('returns undefined when dataset has no symbol', () => {
|
|
37
|
+
expect(getSymbolDef({}, mockRegistry())).toBeUndefined()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('looks up string symbol id in the registry', () => {
|
|
41
|
+
const pinDef = { id: 'pin', svg: '<g/>' }
|
|
42
|
+
const registry = mockRegistry({ pin: pinDef })
|
|
43
|
+
expect(getSymbolDef({ symbol: 'pin' }, registry)).toBe(pinDef)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('returns undefined for an unregistered string symbol', () => {
|
|
47
|
+
expect(getSymbolDef({ symbol: 'missing' }, mockRegistry())).toBeUndefined()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns inline def from symbolSvgContent with svg key', () => {
|
|
51
|
+
const dataset = { symbolSvgContent: '<circle/>', symbolViewBox: '0 0 10 10' }
|
|
52
|
+
const result = getSymbolDef(dataset, mockRegistry())
|
|
53
|
+
expect(result.svg).toBe('<circle/>')
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('symbolSvgContent takes precedence over symbol id', () => {
|
|
57
|
+
const pinDef = { id: 'pin', svg: '<g/>' }
|
|
58
|
+
const registry = mockRegistry({ pin: pinDef })
|
|
59
|
+
const result = getSymbolDef({ symbol: 'pin', symbolSvgContent: '<circle/>' }, registry)
|
|
60
|
+
expect(result.svg).toBe('<circle/>')
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ─── getSymbolStyleColors ─────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe('getSymbolStyleColors', () => {
|
|
67
|
+
it('returns empty object when dataset has no symbol', () => {
|
|
68
|
+
expect(getSymbolStyleColors({})).toEqual({})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('returns empty object for string symbol with no token props', () => {
|
|
72
|
+
expect(getSymbolStyleColors({ symbol: 'pin' })).toEqual({})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('strips symbol prefix from token props', () => {
|
|
76
|
+
const dataset = {
|
|
77
|
+
symbol: 'pin',
|
|
78
|
+
symbolBackgroundColor: '#ff0000',
|
|
79
|
+
symbolForegroundColor: '#ffffff',
|
|
80
|
+
symbolHaloWidth: '2',
|
|
81
|
+
symbolGraphic: 'cross'
|
|
82
|
+
}
|
|
83
|
+
expect(getSymbolStyleColors(dataset)).toEqual({
|
|
84
|
+
backgroundColor: '#ff0000',
|
|
85
|
+
foregroundColor: '#ffffff',
|
|
86
|
+
haloWidth: '2',
|
|
87
|
+
graphic: 'cross'
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it('works with symbolSvgContent instead of symbol id', () => {
|
|
92
|
+
const dataset = { symbolSvgContent: '<circle/>', symbolBackgroundColor: '#0000ff' }
|
|
93
|
+
expect(getSymbolStyleColors(dataset)).toEqual({ backgroundColor: '#0000ff' })
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('omits token props that are null or undefined', () => {
|
|
97
|
+
const dataset = { symbol: 'pin', symbolBackgroundColor: '#ff0000', symbolForegroundColor: null }
|
|
98
|
+
const result = getSymbolStyleColors(dataset)
|
|
99
|
+
expect(result).toEqual({ backgroundColor: '#ff0000' })
|
|
100
|
+
expect(result).not.toHaveProperty('foregroundColor')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('supports style-keyed colour objects', () => {
|
|
104
|
+
const dataset = {
|
|
105
|
+
symbol: 'pin',
|
|
106
|
+
symbolBackgroundColor: { outdoor: '#1d70b8', dark: '#5694ca' }
|
|
107
|
+
}
|
|
108
|
+
expect(getSymbolStyleColors(dataset)).toEqual({
|
|
109
|
+
backgroundColor: { outdoor: '#1d70b8', dark: '#5694ca' }
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// ─── getSymbolViewBox ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
describe('getSymbolViewBox', () => {
|
|
117
|
+
it('returns symbolViewBox from dataset', () => {
|
|
118
|
+
const dataset = { symbol: 'custom', symbolViewBox: '0 0 24 24' }
|
|
119
|
+
expect(getSymbolViewBox(dataset, undefined)).toBe('0 0 24 24')
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('falls back to symbolDef viewBox', () => {
|
|
123
|
+
const symbolDef = { id: 'pin', viewBox: '0 0 38 38' }
|
|
124
|
+
expect(getSymbolViewBox({ symbol: 'pin' }, symbolDef)).toBe('0 0 38 38')
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('returns default viewBox when neither source has one', () => {
|
|
128
|
+
expect(getSymbolViewBox({ symbol: 'pin' }, {})).toBe('0 0 38 38')
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
it('returns default viewBox when symbolDef is undefined', () => {
|
|
132
|
+
expect(getSymbolViewBox({ symbol: 'pin' }, undefined)).toBe('0 0 38 38')
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// ─── getSymbolAnchor ──────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe('getSymbolAnchor', () => {
|
|
139
|
+
it('returns symbolAnchor from dataset', () => {
|
|
140
|
+
const dataset = { symbol: 'custom', symbolAnchor: [0.5, 0.9] }
|
|
141
|
+
expect(getSymbolAnchor(dataset, undefined)).toEqual([0.5, 0.9])
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('falls back to symbolDef anchor', () => {
|
|
145
|
+
const symbolDef = { id: 'pin', anchor: [0.5, 0.9] }
|
|
146
|
+
expect(getSymbolAnchor({ symbol: 'pin' }, symbolDef)).toEqual([0.5, 0.9])
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('returns default [0.5, 0.5] when neither source has an anchor', () => {
|
|
150
|
+
expect(getSymbolAnchor({ symbol: 'pin' }, {})).toEqual([0.5, 0.5])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('returns default [0.5, 0.5] when symbolDef is undefined', () => {
|
|
154
|
+
expect(getSymbolAnchor({ symbol: 'pin' }, undefined)).toEqual([0.5, 0.5])
|
|
155
|
+
})
|
|
156
|
+
})
|
package/webpack.dev.mjs
CHANGED
|
@@ -18,7 +18,7 @@ export default {
|
|
|
18
18
|
],
|
|
19
19
|
entry: {
|
|
20
20
|
index: path.join(__dirname, 'demo/js/index.js'),
|
|
21
|
-
|
|
21
|
+
draw: path.join(__dirname, 'demo/js/draw.js'),
|
|
22
22
|
farming: path.join(__dirname, 'demo/js/farming.js'),
|
|
23
23
|
planning: path.join(__dirname, 'demo/js/planning.js')
|
|
24
24
|
},
|
package/docs/api/slot-map.svg
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 838 629" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="1.5" xmlns:v="https://vecta.io/nano"><defs><style>@font-face{font-family:"CourierNewPS-BoldMT";src:url(data:application/font-woff;charset=utf-8;base64,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) format("woff"); font-weight:bold;font-style:normal;}</style></defs><style><![CDATA[.B{font-family:CourierNewPS-BoldMT, Courier New, monospace}.C{font-weight:700}.D{font-size:18px}.E{fill:#0969da}.F{font-family:ArialMT, Arial, sans-serif}.G{font-size:14px}.H{fill:#484949}]]></style><g fill="none" stroke="#d1d9e0"><path d="M171 121v327m496-327v327M171 284H1m836 0H667"/><path d="M1 61h836" stroke-width=".999"/><path d="M1 1h836v627H1z" stroke-width=".993"/><path d="M1 568h836M1 121l836 .001M279 61v60m280-60v60"/><path d="M1 448h836M1 508h836"/><path d="M251 508v-60"/><path d="M294 368h250v60H294z" stroke-width="1.001"/></g><text x="386.349" y="27.083" class="B C D E">banner</text><text x="383.347" y="43.182" class="F G H">mobile only</text><text x="381.44" y="594.177" class="B C D E">actions</text><text x="383.839" y="610.276" class="F G H">mobile only</text><text x="381.351" y="394.259" class="B C D E">actions</text><text x="375.571" y="410.358" class="F G H">tablet/desktop</text><text x="110.175" y="481.538" class="F G H">Logo</text><text x="386.195" y="534.177" class="B C D E">bottom</text><text x="383.193" y="550.276" class="F G H">mobile only</text><text x="96.226" y="94.819" class="B C D E">top-left</text><text x="364.776" y="94.819" class="B C D E">top-middle</text><text x="648.825" y="94.819" class="B C D E">top-right</text><text x="685.986" y="368.985" class="B C D E">right-bottom</text><text x="686.61" y="481.819" class="B C D E">footer-right</text><text x="25.795" y="371.554" class="B C D E">left-bottom</text><text x="702.664" y="203.591" class="B C D E">right-top</text><text x="42.472" y="206.319" class="B C D E">left-top</text></svg>
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const hideDataset = ({ mapProvider, pluginState }, datasetId) => {
|
|
2
|
-
const map = mapProvider.map
|
|
3
|
-
|
|
4
|
-
// Update map layer visibility
|
|
5
|
-
if (map.getLayer(datasetId)) {
|
|
6
|
-
map.setLayoutProperty(datasetId, 'visibility', 'none')
|
|
7
|
-
}
|
|
8
|
-
if (map.getLayer(`${datasetId}-stroke`)) {
|
|
9
|
-
map.setLayoutProperty(`${datasetId}-stroke`, 'visibility', 'none')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Update state
|
|
13
|
-
pluginState.dispatch({ type: 'SET_DATASET_VISIBILITY', payload: { id: datasetId, visibility: 'hidden' } })
|
|
14
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { applyExclusionFilter } from '../utils/filters.js'
|
|
2
|
-
|
|
3
|
-
export const hideFeatures = ({ mapProvider, pluginState }, { featureIds, idProperty, datasetId }) => {
|
|
4
|
-
const map = mapProvider.map
|
|
5
|
-
|
|
6
|
-
// Get dataset to access original filter and determine layer IDs
|
|
7
|
-
const dataset = pluginState.datasets?.find(d => d.id === datasetId)
|
|
8
|
-
if (!dataset) {
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const originalFilter = dataset.filter || null
|
|
13
|
-
const hasFill = !!dataset.fill
|
|
14
|
-
const hasStroke = !!dataset.stroke
|
|
15
|
-
const fillLayerId = hasFill ? datasetId : null
|
|
16
|
-
|
|
17
|
-
let strokeLayerId = null
|
|
18
|
-
if (hasStroke) {
|
|
19
|
-
strokeLayerId = hasFill ? `${datasetId}-stroke` : datasetId
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Get current hidden state and calculate all hidden IDs
|
|
23
|
-
const existingHidden = pluginState.hiddenFeatures[datasetId]
|
|
24
|
-
const allHiddenIds = existingHidden
|
|
25
|
-
? [...new Set([...existingHidden.ids, ...featureIds])]
|
|
26
|
-
: featureIds
|
|
27
|
-
|
|
28
|
-
// Update state (store by datasetId, not individual layer IDs)
|
|
29
|
-
pluginState.dispatch({
|
|
30
|
-
type: 'HIDE_FEATURES',
|
|
31
|
-
payload: { layerId: datasetId, idProperty, featureIds }
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
// Apply filter to both layers
|
|
35
|
-
if (fillLayerId) {
|
|
36
|
-
applyExclusionFilter(map, fillLayerId, originalFilter, idProperty, allHiddenIds)
|
|
37
|
-
}
|
|
38
|
-
if (strokeLayerId) {
|
|
39
|
-
applyExclusionFilter(map, strokeLayerId, originalFilter, idProperty, allHiddenIds)
|
|
40
|
-
}
|
|
41
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
export const showDataset = ({ mapProvider, pluginState }, datasetId) => {
|
|
2
|
-
const map = mapProvider.map
|
|
3
|
-
|
|
4
|
-
// Update map layer visibility
|
|
5
|
-
if (map.getLayer(datasetId)) {
|
|
6
|
-
map.setLayoutProperty(datasetId, 'visibility', 'visible')
|
|
7
|
-
}
|
|
8
|
-
if (map.getLayer(`${datasetId}-stroke`)) {
|
|
9
|
-
map.setLayoutProperty(`${datasetId}-stroke`, 'visibility', 'visible')
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Update state
|
|
13
|
-
pluginState.dispatch({ type: 'SET_DATASET_VISIBILITY', payload: { id: datasetId, visibility: 'visible' } })
|
|
14
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { applyExclusionFilter } from '../utils/filters.js'
|
|
2
|
-
|
|
3
|
-
export const showFeatures = ({ mapProvider, pluginState }, { featureIds, idProperty, datasetId }) => {
|
|
4
|
-
const map = mapProvider.map
|
|
5
|
-
|
|
6
|
-
// Get current hidden state before update
|
|
7
|
-
const existingHidden = pluginState.hiddenFeatures[datasetId]
|
|
8
|
-
if (!existingHidden) {
|
|
9
|
-
return
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
// Get dataset to access original filter and determine layer IDs
|
|
13
|
-
const dataset = pluginState.datasets?.find(d => d.id === datasetId)
|
|
14
|
-
if (!dataset) {
|
|
15
|
-
return
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const originalFilter = dataset.filter || null
|
|
19
|
-
const hasFill = !!dataset.fill
|
|
20
|
-
const hasStroke = !!dataset.stroke
|
|
21
|
-
const fillLayerId = hasFill ? datasetId : null
|
|
22
|
-
|
|
23
|
-
let strokeLayerId = null
|
|
24
|
-
if (hasStroke) {
|
|
25
|
-
strokeLayerId = hasFill ? `${datasetId}-stroke` : datasetId
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Calculate remaining hidden IDs
|
|
29
|
-
const remainingHiddenIds = existingHidden.ids.filter(id => !featureIds.includes(id))
|
|
30
|
-
|
|
31
|
-
// Update state
|
|
32
|
-
pluginState.dispatch({
|
|
33
|
-
type: 'SHOW_FEATURES',
|
|
34
|
-
payload: { layerId: datasetId, featureIds }
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
// Apply filter to both layers (or restore original if nothing hidden)
|
|
38
|
-
if (fillLayerId) {
|
|
39
|
-
applyExclusionFilter(map, fillLayerId, originalFilter, idProperty, remainingHiddenIds)
|
|
40
|
-
}
|
|
41
|
-
if (strokeLayerId) {
|
|
42
|
-
applyExclusionFilter(map, strokeLayerId, originalFilter, idProperty, remainingHiddenIds)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { addMapLayers } from './mapLayers.js'
|
|
2
|
-
import { applyExclusionFilter } from './utils/filters.js'
|
|
3
|
-
|
|
4
|
-
export const handleSetMapStyle = ({
|
|
5
|
-
map,
|
|
6
|
-
events,
|
|
7
|
-
eventBus,
|
|
8
|
-
getDatasets,
|
|
9
|
-
getHiddenFeatures,
|
|
10
|
-
getDynamicSources
|
|
11
|
-
}) => {
|
|
12
|
-
const onSetStyle = (e) => {
|
|
13
|
-
map.once('idle', () => {
|
|
14
|
-
const newStyleId = e.id
|
|
15
|
-
const datasets = getDatasets()
|
|
16
|
-
const hiddenFeatures = getHiddenFeatures()
|
|
17
|
-
const dynamicSources = getDynamicSources ? getDynamicSources() : new Map()
|
|
18
|
-
|
|
19
|
-
// Re-add all layers with correct colors for new style
|
|
20
|
-
datasets.forEach(dataset => {
|
|
21
|
-
addMapLayers(map, newStyleId, dataset)
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
// Reapply cached data for dynamic sources
|
|
25
|
-
dynamicSources.forEach(source => {
|
|
26
|
-
source.reapply()
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
// Reapply hidden features filters
|
|
30
|
-
Object.entries(hiddenFeatures).forEach(([datasetId, { idProperty, ids }]) => {
|
|
31
|
-
const dataset = datasets.find(d => d.id === datasetId)
|
|
32
|
-
if (!dataset) {
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const originalFilter = dataset.filter || null
|
|
37
|
-
const hasFill = !!dataset.fill
|
|
38
|
-
const hasStroke = !!dataset.stroke
|
|
39
|
-
const fillLayerId = hasFill ? datasetId : null
|
|
40
|
-
const strokeLayerId = hasStroke ? (hasFill ? `${datasetId}-stroke` : datasetId) : null
|
|
41
|
-
|
|
42
|
-
if (fillLayerId) {
|
|
43
|
-
applyExclusionFilter(map, fillLayerId, originalFilter, idProperty, ids)
|
|
44
|
-
}
|
|
45
|
-
if (strokeLayerId) {
|
|
46
|
-
applyExclusionFilter(map, strokeLayerId, originalFilter, idProperty, ids)
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
eventBus.on(events.MAP_SET_STYLE, onSetStyle)
|
|
53
|
-
return onSetStyle
|
|
54
|
-
}
|