@defra/interactive-map 0.0.16-alpha → 0.0.17-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/slots.md +16 -15
- package/docs/api.md +3 -3
- package/docs/getting-started.md +4 -1
- package/docs/plugins/datasets.md +561 -0
- package/docs/plugins.md +1 -1
- package/package.json +2 -2
- package/plugins/beta/datasets/dist/css/index.css +85 -15
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.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 +23 -8
- package/plugins/beta/datasets/src/adapters/maplibre/index.js +18 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerBuilders.js +113 -0
- package/plugins/beta/datasets/src/adapters/maplibre/layerIds.js +69 -0
- package/plugins/beta/datasets/src/adapters/maplibre/maplibreLayerAdapter.js +338 -0
- package/plugins/beta/datasets/src/adapters/maplibre/patternRegistry.js +48 -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 +8 -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/datasets.js +29 -55
- package/plugins/beta/datasets/src/defaults.js +42 -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/manifest.js +24 -16
- package/plugins/beta/datasets/src/panels/Key.jsx +128 -50
- package/plugins/beta/datasets/src/panels/Key.module.scss +48 -9
- package/plugins/beta/datasets/src/panels/Layers.jsx +132 -29
- package/plugins/beta/datasets/src/panels/Layers.module.scss +50 -8
- package/plugins/beta/datasets/src/reducer.js +128 -9
- package/plugins/beta/datasets/src/styles/patterns.js +157 -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 +78 -0
- package/plugins/beta/draw-ml/dist/css/index.css +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-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/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/src/interact.scss +0 -7
- package/plugins/interact/src/manifest.js +14 -18
- package/plugins/interact/src/manifest.test.js +3 -1
- package/plugins/search/dist/css/index.css +1 -1
- package/plugins/search/src/components/Form/Form.module.scss +2 -1
- 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/src/utils/highlightFeatures.js +1 -0
- package/providers/maplibre/src/utils/highlightFeatures.test.js +1 -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/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/useLayoutMeasurements.js +84 -18
- package/src/App/hooks/useLayoutMeasurements.test.js +124 -17
- package/src/App/layout/Layout.jsx +12 -7
- package/src/App/layout/Layout.test.jsx +2 -2
- 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 +3 -1
- 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/InteractiveMap/InteractiveMap.js +4 -0
- package/src/config/appConfig.js +5 -2
- package/src/config/events.js +28 -0
- package/src/scss/main.scss +1 -0
- package/src/scss/settings/_dimensions.scss +0 -1
- package/src/utils/getSafeZoneInset.js +9 -7
- package/src/utils/getSafeZoneInset.test.js +10 -10
- 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
|
@@ -1,19 +1,58 @@
|
|
|
1
|
-
|
|
1
|
+
// When groups are present, every direct child gets a border-top
|
|
2
|
+
.im-c-datasets-key--has-groups > * {
|
|
3
|
+
border-top: 1px solid var(--button-hover-color);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// When no groups, only the first child gets a border-top
|
|
7
|
+
.im-c-datasets-key:not(.im-c-datasets-key--has-groups) > *:first-child {
|
|
8
|
+
border-top: 1px solid var(--button-hover-color);
|
|
9
|
+
}
|
|
2
10
|
|
|
11
|
+
.im-c-datasets-key__group:not(:last-child) {
|
|
12
|
+
padding-bottom: 5px;
|
|
3
13
|
}
|
|
4
14
|
|
|
5
|
-
.im-c-datasets-
|
|
15
|
+
.im-c-datasets-key__group-heading {
|
|
16
|
+
padding-top: 15px;
|
|
17
|
+
padding-bottom: 10px;
|
|
18
|
+
margin: 0;
|
|
19
|
+
font-size: 1rem;
|
|
20
|
+
font-weight: bold;
|
|
21
|
+
color: var(--foreground-color);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.im-c-datasets-key__item {
|
|
6
25
|
display: flex;
|
|
7
26
|
align-items: start;
|
|
8
|
-
padding-top:
|
|
9
|
-
padding-bottom:
|
|
10
|
-
align-self: auto;
|
|
27
|
+
padding-top: 10px;
|
|
28
|
+
padding-bottom: 10px;
|
|
11
29
|
font-size: 1rem;
|
|
12
|
-
|
|
30
|
+
|
|
31
|
+
// When mixed with groups, flat items match group heading spacing
|
|
32
|
+
.im-c-datasets-key--has-groups > & {
|
|
33
|
+
padding-top: 15px;
|
|
34
|
+
padding-bottom: 15px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// First item inside a group — heading already provides top spacing
|
|
38
|
+
.im-c-datasets-key__group &:first-child {
|
|
39
|
+
padding-top: 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// No-groups: first item needs 15px below the single border
|
|
44
|
+
.im-c-datasets-key:not(.im-c-datasets-key--has-groups) > .im-c-datasets-key__item:first-child {
|
|
45
|
+
padding-top: 15px;
|
|
13
46
|
}
|
|
14
47
|
|
|
15
|
-
|
|
48
|
+
// Last item in all scenarios — flat or inside the last group
|
|
49
|
+
.im-c-datasets-key > .im-c-datasets-key__item:last-child,
|
|
50
|
+
.im-c-datasets-key__group:last-child .im-c-datasets-key__item:last-child {
|
|
51
|
+
padding-bottom: 5px;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.im-c-datasets-key__item svg {
|
|
16
55
|
position: relative;
|
|
17
56
|
flex-shrink: 0;
|
|
18
|
-
margin:
|
|
19
|
-
}
|
|
57
|
+
margin: 0 13px 0 2px;
|
|
58
|
+
}
|
|
@@ -1,38 +1,141 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
|
-
import {
|
|
3
|
-
import { hideDataset } from '../api/hideDataset'
|
|
2
|
+
import { setDatasetVisibility } from '../api/setDatasetVisibility'
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
4
|
+
const CHECKBOX_LABEL_CLASS = 'im-c-datasets-layers__item-label govuk-label govuk-checkboxes__label'
|
|
5
|
+
|
|
6
|
+
const hasToggleableSublayers = (dataset) => dataset.sublayers?.some(sublayer => sublayer.toggleVisibility)
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collapse the filtered dataset list into ordered render items:
|
|
10
|
+
* { type: 'sublayers', dataset } — dataset with sublayers (takes precedence)
|
|
11
|
+
* { type: 'group', groupLabel, datasets } — datasets sharing a groupLabel
|
|
12
|
+
* { type: 'flat', dataset } — standalone dataset
|
|
13
|
+
*/
|
|
14
|
+
const buildRenderItems = (datasets) => {
|
|
15
|
+
const seenGroups = new Set()
|
|
16
|
+
const items = []
|
|
17
|
+
datasets.forEach(dataset => {
|
|
18
|
+
if (hasToggleableSublayers(dataset)) {
|
|
19
|
+
items.push({ type: 'sublayers', dataset })
|
|
20
|
+
return
|
|
12
21
|
}
|
|
22
|
+
if (dataset.groupLabel) {
|
|
23
|
+
if (seenGroups.has(dataset.groupLabel)) {
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
seenGroups.add(dataset.groupLabel)
|
|
27
|
+
items.push({
|
|
28
|
+
type: 'group',
|
|
29
|
+
groupLabel: dataset.groupLabel,
|
|
30
|
+
datasets: datasets.filter(d => !hasToggleableSublayers(d) && d.groupLabel === dataset.groupLabel)
|
|
31
|
+
})
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
items.push({ type: 'flat', dataset })
|
|
35
|
+
})
|
|
36
|
+
return items
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const Layers = ({ pluginState }) => {
|
|
40
|
+
const handleDatasetChange = (e) => {
|
|
41
|
+
const { value, checked } = e.target
|
|
42
|
+
setDatasetVisibility({ pluginState }, checked, { datasetId: value })
|
|
13
43
|
}
|
|
14
44
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
const handleSublayerChange = (e) => {
|
|
46
|
+
const { checked } = e.target
|
|
47
|
+
const datasetId = e.target.dataset.datasetId
|
|
48
|
+
const sublayerId = e.target.dataset.sublayerId
|
|
49
|
+
setDatasetVisibility({ pluginState }, checked, { datasetId, sublayerId })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const renderDatasetItem = (dataset) => {
|
|
53
|
+
const itemClass = `im-c-datasets-layers__item govuk-checkboxes govuk-checkboxes--small${dataset.visibility === 'hidden' ? '' : ' im-c-datasets-layers__item--checked'}`
|
|
54
|
+
return (
|
|
55
|
+
<div key={dataset.id} className={itemClass} data-module='govuk-checkboxes'>
|
|
56
|
+
<div className='govuk-checkboxes__item'>
|
|
57
|
+
<input
|
|
58
|
+
className='govuk-checkboxes__input'
|
|
59
|
+
id={dataset.id}
|
|
60
|
+
name='layers'
|
|
61
|
+
type='checkbox'
|
|
62
|
+
value={dataset.id}
|
|
63
|
+
checked={dataset.visibility !== 'hidden'}
|
|
64
|
+
onChange={handleDatasetChange}
|
|
65
|
+
/>
|
|
66
|
+
<label className={CHECKBOX_LABEL_CLASS} htmlFor={dataset.id}>
|
|
67
|
+
{dataset.label}
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
35
70
|
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const visibleDatasets = (pluginState.datasets || [])
|
|
75
|
+
.filter(dataset => dataset.toggleVisibility || hasToggleableSublayers(dataset))
|
|
76
|
+
|
|
77
|
+
const renderItems = buildRenderItems(visibleDatasets)
|
|
78
|
+
const hasGroups = renderItems.some(item => item.type === 'sublayers' || item.type === 'group')
|
|
79
|
+
const containerClass = `im-c-datasets-layers${hasGroups ? ' im-c-datasets-layers--has-groups' : ''}`
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className={containerClass}>
|
|
83
|
+
{renderItems.map(item => {
|
|
84
|
+
if (item.type === 'sublayers') {
|
|
85
|
+
const { dataset } = item
|
|
86
|
+
const anySublayerChecked = dataset.sublayers
|
|
87
|
+
.filter(sublayer => sublayer.toggleVisibility)
|
|
88
|
+
.some(sublayer => dataset.sublayerVisibility?.[sublayer.id] !== 'hidden')
|
|
89
|
+
const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anySublayerChecked ? ' im-c-datasets-layers-group--items-checked' : ''}`
|
|
90
|
+
return (
|
|
91
|
+
<div key={dataset.id} className={wrapperClass}>
|
|
92
|
+
<fieldset className='im-c-datasets-layers-group__fieldset'>
|
|
93
|
+
<legend className='im-c-datasets-layers-group__legend'>{dataset.label}</legend>
|
|
94
|
+
{dataset.sublayers
|
|
95
|
+
.filter(sublayer => sublayer.toggleVisibility)
|
|
96
|
+
.map(sublayer => {
|
|
97
|
+
const sublayerVisible = dataset.sublayerVisibility?.[sublayer.id] !== 'hidden'
|
|
98
|
+
const inputId = `${dataset.id}-${sublayer.id}`
|
|
99
|
+
const itemClass = `im-c-datasets-layers__item govuk-checkboxes govuk-checkboxes--small${sublayerVisible ? ' im-c-datasets-layers__item--checked' : ''}`
|
|
100
|
+
return (
|
|
101
|
+
<div key={sublayer.id} className={itemClass} data-module='govuk-checkboxes'>
|
|
102
|
+
<div className='govuk-checkboxes__item'>
|
|
103
|
+
<input
|
|
104
|
+
className='govuk-checkboxes__input'
|
|
105
|
+
id={inputId}
|
|
106
|
+
type='checkbox'
|
|
107
|
+
checked={sublayerVisible}
|
|
108
|
+
data-dataset-id={dataset.id}
|
|
109
|
+
data-sublayer-id={sublayer.id}
|
|
110
|
+
onChange={handleSublayerChange}
|
|
111
|
+
/>
|
|
112
|
+
<label className={CHECKBOX_LABEL_CLASS} htmlFor={inputId}>
|
|
113
|
+
{sublayer.label}
|
|
114
|
+
</label>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
})}
|
|
119
|
+
</fieldset>
|
|
120
|
+
</div>
|
|
121
|
+
)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (item.type === 'group') {
|
|
125
|
+
const anyDatasetChecked = item.datasets.some(d => d.visibility !== 'hidden')
|
|
126
|
+
const wrapperClass = `govuk-form-group im-c-datasets-layers-group${anyDatasetChecked ? ' im-c-datasets-layers-group--items-checked' : ''}`
|
|
127
|
+
return (
|
|
128
|
+
<div key={item.groupLabel} className={wrapperClass}>
|
|
129
|
+
<fieldset className='im-c-datasets-layers-group__fieldset'>
|
|
130
|
+
<legend className='im-c-datasets-layers-group__legend'>{item.groupLabel}</legend>
|
|
131
|
+
{item.datasets.map(dataset => renderDatasetItem(dataset))}
|
|
132
|
+
</fieldset>
|
|
133
|
+
</div>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return renderDatasetItem(item.dataset)
|
|
138
|
+
})}
|
|
36
139
|
</div>
|
|
37
140
|
)
|
|
38
141
|
}
|
|
@@ -1,8 +1,37 @@
|
|
|
1
|
+
// When groups are present, every direct child (groups and flat items) gets a border-top
|
|
2
|
+
.im-c-datasets-layers--has-groups > * {
|
|
3
|
+
border-top: 1px solid var(--button-hover-color);
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// When no groups, only the first child gets a border-top with 15px padding
|
|
7
|
+
.im-c-datasets-layers:not(.im-c-datasets-layers--has-groups) > *:first-child {
|
|
8
|
+
border-top: 1px solid var(--button-hover-color);
|
|
9
|
+
padding-top: 10px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.im-c-datasets-layers > *:last-child {
|
|
13
|
+
margin-bottom: -5px;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.im-c-datasets-layers-group:not(:last-child) {
|
|
17
|
+
padding-bottom: 5px;
|
|
18
|
+
}
|
|
19
|
+
|
|
1
20
|
.im-c-datasets-layers__item {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
21
|
+
padding-bottom: 5px;
|
|
22
|
+
|
|
23
|
+
.im-c-datasets-layers--has-groups > & {
|
|
24
|
+
padding-top: 5px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Items inside a group have no individual padding or spacing between them
|
|
28
|
+
.im-c-datasets-layers-group & {
|
|
29
|
+
padding-top: 0;
|
|
30
|
+
padding-bottom: 0;
|
|
31
|
+
|
|
32
|
+
&:not(:last-child) {
|
|
33
|
+
margin-bottom: 0;
|
|
34
|
+
}
|
|
6
35
|
}
|
|
7
36
|
}
|
|
8
37
|
|
|
@@ -19,10 +48,6 @@
|
|
|
19
48
|
color: var(--foreground-color);
|
|
20
49
|
}
|
|
21
50
|
|
|
22
|
-
.im-c-datasets-layers__item--checked {
|
|
23
|
-
border-color: var(--app-border-color);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
51
|
// GovUK style overide
|
|
27
52
|
.im-c-datasets-layers__item .govuk-checkboxes__item {
|
|
28
53
|
flex-wrap: nowrap;
|
|
@@ -31,3 +56,20 @@
|
|
|
31
56
|
margin-left: -3px;
|
|
32
57
|
}
|
|
33
58
|
}
|
|
59
|
+
|
|
60
|
+
.im-c-datasets-layers-group__fieldset {
|
|
61
|
+
// Reset browser fieldset defaults
|
|
62
|
+
border: none;
|
|
63
|
+
padding: 0;
|
|
64
|
+
margin: 0;
|
|
65
|
+
min-width: 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.im-c-datasets-layers-group__legend {
|
|
69
|
+
padding-top: 15px;
|
|
70
|
+
padding-bottom: 10px;
|
|
71
|
+
padding-inline: 0;
|
|
72
|
+
font-size: 1rem;
|
|
73
|
+
font-weight: bold;
|
|
74
|
+
color: var(--foreground-color);
|
|
75
|
+
}
|
|
@@ -1,16 +1,27 @@
|
|
|
1
|
+
import { applyDatasetDefaults } from './defaults.js'
|
|
2
|
+
|
|
1
3
|
const initialState = {
|
|
2
4
|
datasets: null,
|
|
3
|
-
hiddenFeatures: {} // { [layerId]: { idProperty: string, ids: string[] } }
|
|
5
|
+
hiddenFeatures: {}, // { [layerId]: { idProperty: string, ids: string[] } }
|
|
6
|
+
layerAdapter: null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const initSublayerVisibility = (dataset) => {
|
|
10
|
+
if (!dataset.sublayers?.length) {
|
|
11
|
+
return dataset
|
|
12
|
+
}
|
|
13
|
+
const sublayerVisibility = {}
|
|
14
|
+
dataset.sublayers.forEach(sublayer => {
|
|
15
|
+
sublayerVisibility[sublayer.id] = 'visible'
|
|
16
|
+
})
|
|
17
|
+
return { ...dataset, sublayerVisibility }
|
|
4
18
|
}
|
|
5
19
|
|
|
6
20
|
const setDatasets = (state, payload) => {
|
|
7
21
|
const { datasets, datasetDefaults } = payload
|
|
8
22
|
return {
|
|
9
23
|
...state,
|
|
10
|
-
datasets: datasets.map(dataset => (
|
|
11
|
-
...datasetDefaults,
|
|
12
|
-
...dataset
|
|
13
|
-
}))
|
|
24
|
+
datasets: datasets.map(dataset => initSublayerVisibility(applyDatasetDefaults(dataset, datasetDefaults)))
|
|
14
25
|
}
|
|
15
26
|
}
|
|
16
27
|
|
|
@@ -20,7 +31,7 @@ const addDataset = (state, payload) => {
|
|
|
20
31
|
...state,
|
|
21
32
|
datasets: [
|
|
22
33
|
...(state.datasets || []),
|
|
23
|
-
|
|
34
|
+
initSublayerVisibility(applyDatasetDefaults(dataset, datasetDefaults))
|
|
24
35
|
]
|
|
25
36
|
}
|
|
26
37
|
}
|
|
@@ -43,6 +54,14 @@ const setDatasetVisibility = (state, payload) => {
|
|
|
43
54
|
}
|
|
44
55
|
}
|
|
45
56
|
|
|
57
|
+
const setGlobalVisibility = (state, payload) => {
|
|
58
|
+
const { visibility } = payload
|
|
59
|
+
return {
|
|
60
|
+
...state,
|
|
61
|
+
datasets: state.datasets?.map(dataset => ({ ...dataset, visibility }))
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
const hideFeatures = (state, payload) => {
|
|
47
66
|
const { layerId, idProperty, featureIds } = payload
|
|
48
67
|
const existing = state.hiddenFeatures[layerId]
|
|
@@ -61,12 +80,15 @@ const hideFeatures = (state, payload) => {
|
|
|
61
80
|
const showFeatures = (state, payload) => {
|
|
62
81
|
const { layerId, featureIds } = payload
|
|
63
82
|
const existing = state.hiddenFeatures[layerId]
|
|
64
|
-
if (!existing)
|
|
83
|
+
if (!existing) {
|
|
84
|
+
return state
|
|
85
|
+
}
|
|
65
86
|
|
|
66
87
|
const newIds = existing.ids.filter(id => !featureIds.includes(id))
|
|
67
88
|
|
|
68
89
|
if (newIds.length === 0) {
|
|
69
|
-
const
|
|
90
|
+
const rest = { ...state.hiddenFeatures }
|
|
91
|
+
delete rest[layerId]
|
|
70
92
|
return { ...state, hiddenFeatures: rest }
|
|
71
93
|
}
|
|
72
94
|
|
|
@@ -79,13 +101,110 @@ const showFeatures = (state, payload) => {
|
|
|
79
101
|
}
|
|
80
102
|
}
|
|
81
103
|
|
|
104
|
+
const setSublayerVisibility = (state, payload) => {
|
|
105
|
+
const { datasetId, sublayerId, visibility } = payload
|
|
106
|
+
return {
|
|
107
|
+
...state,
|
|
108
|
+
datasets: state.datasets?.map(dataset => {
|
|
109
|
+
if (dataset.id !== datasetId) {
|
|
110
|
+
return dataset
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
...dataset,
|
|
114
|
+
sublayerVisibility: {
|
|
115
|
+
...dataset.sublayerVisibility,
|
|
116
|
+
[sublayerId]: visibility
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const setDatasetStyle = (state, payload) => {
|
|
124
|
+
const { datasetId, styleChanges } = payload
|
|
125
|
+
return {
|
|
126
|
+
...state,
|
|
127
|
+
datasets: state.datasets?.map(dataset =>
|
|
128
|
+
dataset.id === datasetId ? { ...dataset, ...styleChanges } : dataset
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const setSublayerStyle = (state, payload) => {
|
|
134
|
+
const { datasetId, sublayerId, styleChanges } = payload
|
|
135
|
+
return {
|
|
136
|
+
...state,
|
|
137
|
+
datasets: state.datasets?.map(dataset => {
|
|
138
|
+
if (dataset.id !== datasetId) {
|
|
139
|
+
return dataset
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
...dataset,
|
|
143
|
+
sublayers: dataset.sublayers?.map(sublayer =>
|
|
144
|
+
sublayer.id === sublayerId
|
|
145
|
+
? { ...sublayer, style: { ...sublayer.style, ...styleChanges } }
|
|
146
|
+
: sublayer
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const setOpacity = (state, payload) => {
|
|
154
|
+
const { datasetId, opacity } = payload
|
|
155
|
+
return {
|
|
156
|
+
...state,
|
|
157
|
+
datasets: state.datasets?.map(dataset =>
|
|
158
|
+
dataset.id === datasetId ? { ...dataset, opacity } : dataset
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const setGlobalOpacity = (state, payload) => {
|
|
164
|
+
const { opacity } = payload
|
|
165
|
+
return {
|
|
166
|
+
...state,
|
|
167
|
+
datasets: state.datasets?.map(dataset => ({ ...dataset, opacity }))
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const setSublayerOpacity = (state, payload) => {
|
|
172
|
+
const { datasetId, sublayerId, opacity } = payload
|
|
173
|
+
return {
|
|
174
|
+
...state,
|
|
175
|
+
datasets: state.datasets?.map(dataset => {
|
|
176
|
+
if (dataset.id !== datasetId) {
|
|
177
|
+
return dataset
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
...dataset,
|
|
181
|
+
sublayers: dataset.sublayers?.map(sublayer =>
|
|
182
|
+
sublayer.id === sublayerId
|
|
183
|
+
? { ...sublayer, style: { ...sublayer.style, opacity } }
|
|
184
|
+
: sublayer
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const setLayerAdapter = (state, payload) => ({ ...state, layerAdapter: payload })
|
|
192
|
+
|
|
82
193
|
const actions = {
|
|
83
194
|
SET_DATASETS: setDatasets,
|
|
84
195
|
ADD_DATASET: addDataset,
|
|
85
196
|
REMOVE_DATASET: removeDataset,
|
|
86
197
|
SET_DATASET_VISIBILITY: setDatasetVisibility,
|
|
198
|
+
SET_GLOBAL_VISIBILITY: setGlobalVisibility,
|
|
199
|
+
SET_SUBLAYER_VISIBILITY: setSublayerVisibility,
|
|
200
|
+
SET_DATASET_STYLE: setDatasetStyle,
|
|
201
|
+
SET_SUBLAYER_STYLE: setSublayerStyle,
|
|
202
|
+
SET_OPACITY: setOpacity,
|
|
203
|
+
SET_GLOBAL_OPACITY: setGlobalOpacity,
|
|
204
|
+
SET_SUBLAYER_OPACITY: setSublayerOpacity,
|
|
87
205
|
HIDE_FEATURES: hideFeatures,
|
|
88
|
-
SHOW_FEATURES: showFeatures
|
|
206
|
+
SHOW_FEATURES: showFeatures,
|
|
207
|
+
SET_LAYER_ADAPTER: setLayerAdapter
|
|
89
208
|
}
|
|
90
209
|
|
|
91
210
|
export {
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { getValueForStyle } from '../../../../../src/utils/getValueForStyle.js'
|
|
2
|
+
|
|
3
|
+
// ─── Built-in pattern library ────────────────────────────────────────────────
|
|
4
|
+
// Each value is the inner SVG content (paths only, no wrapper).
|
|
5
|
+
// Paths are authored in a 16×16 coordinate space (power-of-two, tiles seamlessly).
|
|
6
|
+
// Use {{foreground}} and {{background}} tokens for colours.
|
|
7
|
+
|
|
8
|
+
const BUILT_IN_PATTERNS = {
|
|
9
|
+
'cross-hatch': '<path d="M0 4.486V3.485h3.5V.001h1v3.484h7.002V.001h1v3.484h3.5v1.001h-3.5v7h3.5v.999h-3.5v3.516h-1v-3.516H4.499v3.516h-1v-3.516H0v-.999h3.5v-7H0zm11.501 0H4.499v7h7.002v-7z" fill="{{foreground}}"/>',
|
|
10
|
+
'diagonal-cross-hatch': '<path d="M0 8.707V7.293L7.293 0h1.414L16 7.293v1.414L8.707 16H7.293L0 8.707zM.707 8L8 15.293 15.293 8 8 .707.707 8z" fill="{{foreground}}"/>',
|
|
11
|
+
'forward-diagonal-hatch': '<path d="M16 8.707V7.293L7.293 16h1.414L16 8.707zm-16 0L8.707 0H7.293L0 7.293v1.414z" fill="{{foreground}}"/>',
|
|
12
|
+
'backward-diagonal-hatch': '<path d="M0 8.707V7.293L8.707 16H7.293L0 8.707zm16 0L7.293 0h1.414L16 7.293v1.414z" fill="{{foreground}}"/>',
|
|
13
|
+
'horizontal-hatch': '<path d="M0 4.5V3.499h15.999V4.5H0zm0 7h15.999V12.5H0v-1.001z" fill="{{foreground}}"/>',
|
|
14
|
+
'vertical-hatch': '<path d="M3.501 16.001V0h1v16.001h-1zm7.998 0V0h1v16.001h-1z" fill="{{foreground}}"/>',
|
|
15
|
+
dot: '<path d="M3.999 2A2 2 0 0 1 6 3.999C6 5.103 5.103 6 3.999 6a2 2 0 0 1-1.999-2.001A2 2 0 0 1 3.999 2zm0 7.999C5.103 10 6 10.897 6 12.001A2 2 0 0 1 3.999 14a2 2 0 0 1-1.999-1.999A2 2 0 0 1 3.999 10zM11.999 2A2 2 0 0 1 14 3.999C14 5.103 13.103 6 11.999 6S10 5.103 10 3.999A2 2 0 0 1 11.999 2zm0 7.999c1.104 0 2.001.897 2.001 2.001A2 2 0 0 1 11.999 14 2 2 0 0 1 10 12.001c0-1.104.897-2.001 1.999-2.001z" fill="{{foreground}}"/>',
|
|
16
|
+
diamond: '<path d="M4 .465L7.535 4 4 7.535.465 4 4 .465zm0 7.999l3.535 3.535L4 15.535.465 11.999 4 8.464zm8-8l3.535 3.535-3.536 3.536L8.464 4 12 .464zm0 8.001L15.536 12 12 15.536 8.465 12 12 8.465z" fill="{{foreground}}"/>'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Plugin-controlled border path used in the key symbol (20×20 coordinate space).
|
|
20
|
+
// This is always rendered as the first element, before the user-supplied content.
|
|
21
|
+
const KEY_BORDER_PATH = '<path d="M19 2.862v14.275c0 1.028-.835 1.862-1.862 1.862H2.863c-1.028 0-1.862-.835-1.862-1.862V2.862C1.001 1.834 1.836 1 2.863 1h14.275C18.166 1 19 1.835 19 2.862z" fill="{{background}}" stroke="{{foreground}}" stroke-width="2"/>'
|
|
22
|
+
|
|
23
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export const hashString = (str) => {
|
|
26
|
+
let hash = 0
|
|
27
|
+
for (const ch of str) {
|
|
28
|
+
hash = ((hash << 5) - hash) + ch.codePointAt(0)
|
|
29
|
+
hash = hash & hash
|
|
30
|
+
}
|
|
31
|
+
return Math.abs(hash).toString(36) // NOSONAR: base36 encoding for compact alphanumeric hash string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const injectColors = (content, foreground, background) =>
|
|
35
|
+
content
|
|
36
|
+
.replace(/\{\{foreground\}\}/g, foreground || 'black')
|
|
37
|
+
.replace(/\{\{background\}\}/g, background || 'transparent')
|
|
38
|
+
|
|
39
|
+
// ─── Public API ───────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Returns true if a dataset has a fill pattern configured.
|
|
43
|
+
* @param {Object} dataset
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
export const hasPattern = (dataset) => !!(dataset.fillPattern || dataset.fillPatternSvgContent)
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns the raw (un-coloured) inner SVG content for a dataset's pattern.
|
|
50
|
+
* Custom fillPatternSvgContent takes precedence over built-in fillPattern ids.
|
|
51
|
+
* @param {Object} dataset
|
|
52
|
+
* @returns {string|null}
|
|
53
|
+
*/
|
|
54
|
+
export const getPatternInnerContent = (dataset) => {
|
|
55
|
+
if (dataset.fillPatternSvgContent) {
|
|
56
|
+
return dataset.fillPatternSvgContent
|
|
57
|
+
}
|
|
58
|
+
if (dataset.fillPattern && BUILT_IN_PATTERNS[dataset.fillPattern]) {
|
|
59
|
+
return BUILT_IN_PATTERNS[dataset.fillPattern]
|
|
60
|
+
}
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns a deterministic image ID for a pattern + resolved colour combination.
|
|
66
|
+
* @param {Object} dataset
|
|
67
|
+
* @param {string} mapStyleId
|
|
68
|
+
* @returns {string|null}
|
|
69
|
+
*/
|
|
70
|
+
export const getPatternImageId = (dataset, mapStyleId) => {
|
|
71
|
+
const innerContent = getPatternInnerContent(dataset)
|
|
72
|
+
if (!innerContent) {
|
|
73
|
+
return null
|
|
74
|
+
}
|
|
75
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
76
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
77
|
+
return `pattern-${hashString(innerContent + fg + bg)}`
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns colour-injected inner SVG path content for use in the Key symbol.
|
|
82
|
+
* The caller is responsible for wrapping this in the SVG element and border path.
|
|
83
|
+
* @param {Object} dataset
|
|
84
|
+
* @param {string} mapStyleId
|
|
85
|
+
* @returns {{ border: string, content: string }|null}
|
|
86
|
+
*/
|
|
87
|
+
export const getKeyPatternPaths = (dataset, mapStyleId) => {
|
|
88
|
+
const innerContent = getPatternInnerContent(dataset)
|
|
89
|
+
if (!innerContent) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
93
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
94
|
+
const borderStroke = getValueForStyle(dataset.stroke, mapStyleId) || fg
|
|
95
|
+
return {
|
|
96
|
+
border: injectColors(KEY_BORDER_PATH, borderStroke, bg),
|
|
97
|
+
content: injectColors(innerContent, fg, bg)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Rasterisation ────────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
// Module-level cache: imageId → ImageData. Avoids re-rasterising identical patterns.
|
|
104
|
+
const imageDataCache = new Map()
|
|
105
|
+
|
|
106
|
+
const rasteriseToImageData = (svgString, width, height) =>
|
|
107
|
+
new Promise((resolve, reject) => {
|
|
108
|
+
const blob = new Blob([svgString], { type: 'image/svg+xml' })
|
|
109
|
+
const url = URL.createObjectURL(blob)
|
|
110
|
+
const img = new Image(width, height)
|
|
111
|
+
img.onload = () => {
|
|
112
|
+
const canvas = document.createElement('canvas')
|
|
113
|
+
canvas.width = width
|
|
114
|
+
canvas.height = height
|
|
115
|
+
const ctx = canvas.getContext('2d')
|
|
116
|
+
ctx.drawImage(img, 0, 0, width, height)
|
|
117
|
+
URL.revokeObjectURL(url)
|
|
118
|
+
resolve(ctx.getImageData(0, 0, width, height))
|
|
119
|
+
}
|
|
120
|
+
img.onerror = () => {
|
|
121
|
+
URL.revokeObjectURL(url)
|
|
122
|
+
reject(new Error(`Failed to rasterise pattern SVG: ${svgString.slice(0, 80)}`))
|
|
123
|
+
}
|
|
124
|
+
img.src = url
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Rasterises a dataset's pattern SVG to ImageData, using an in-memory cache
|
|
129
|
+
* to avoid re-rasterising identical patterns. Framework-agnostic — callers
|
|
130
|
+
* are responsible for registering the result with their map framework.
|
|
131
|
+
*
|
|
132
|
+
* @param {Object} dataset
|
|
133
|
+
* @param {string} mapStyleId
|
|
134
|
+
* @returns {Promise<{ imageId: string, imageData: ImageData }|null>}
|
|
135
|
+
*/
|
|
136
|
+
export const rasterisePattern = async (dataset, mapStyleId) => {
|
|
137
|
+
const innerContent = getPatternInnerContent(dataset)
|
|
138
|
+
if (!innerContent) {
|
|
139
|
+
return null
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fg = getValueForStyle(dataset.fillPatternForegroundColor, mapStyleId) || 'black'
|
|
143
|
+
const bg = getValueForStyle(dataset.fillPatternBackgroundColor, mapStyleId) || 'transparent'
|
|
144
|
+
const imageId = `pattern-${hashString(innerContent + fg + bg)}`
|
|
145
|
+
|
|
146
|
+
let imageData = imageDataCache.get(imageId)
|
|
147
|
+
if (!imageData) {
|
|
148
|
+
const colored = injectColors(innerContent, fg, bg)
|
|
149
|
+
const bgRect = `<rect width="16" height="16" fill="${bg}"/>`
|
|
150
|
+
// pixelRatio: 2 means the map treats this as an 8×8 logical tile — crisp on retina screens.
|
|
151
|
+
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">${bgRect}${colored}</svg>`
|
|
152
|
+
imageData = await rasteriseToImageData(svgString, 16, 16)
|
|
153
|
+
imageDataCache.set(imageId, imageData)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { imageId, imageData }
|
|
157
|
+
}
|