@defra/interactive-map 0.0.26-alpha → 0.0.27-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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/assets/css/docusaurus.scss +4 -0
- package/package.json +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/defaults.js +14 -14
- package/plugins/beta/draw-ml/src/mapboxDraw.js +5 -0
- package/plugins/beta/draw-ml/src/mapboxSnap.js +1 -1
- package/plugins/beta/draw-ml/src/modes/editVertex/touchHandlers.js +10 -2
- package/plugins/beta/draw-ml/src/modes/editVertexMode.js +2 -0
- package/plugins/beta/draw-ml/src/reducer.js +4 -1
- package/plugins/beta/draw-ml/src/styles.js +42 -39
- package/plugins/beta/draw-ol/dist/css/index.css +13 -0
- package/plugins/beta/draw-ol/dist/esm/DrawMode.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/EditMode.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/geometryHelpers.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/im-draw-ol-plugin.js +1 -0
- package/plugins/beta/draw-ol/dist/esm/index.js +1 -0
- package/plugins/beta/draw-ol/src/DrawInit.jsx +67 -0
- package/plugins/beta/draw-ol/src/api/addFeature.js +29 -0
- package/plugins/beta/draw-ol/src/api/deleteFeature.js +11 -0
- package/plugins/beta/draw-ol/src/api/editFeature.js +50 -0
- package/plugins/beta/draw-ol/src/api/newLine.js +44 -0
- package/plugins/beta/draw-ol/src/api/newPolygon.js +44 -0
- package/plugins/beta/draw-ol/src/core/OLDrawManager.js +150 -0
- package/plugins/beta/draw-ol/src/core/featureStore.js +65 -0
- package/plugins/beta/draw-ol/src/core/styles.js +98 -0
- package/plugins/beta/draw-ol/src/core/undoStack.js +31 -0
- package/plugins/beta/draw-ol/src/defaults.js +16 -0
- package/plugins/beta/draw-ol/src/draw/DrawMode.js +113 -0
- package/plugins/beta/draw-ol/src/draw/drawInput.js +199 -0
- package/plugins/beta/draw-ol/src/draw.scss +17 -0
- package/plugins/beta/draw-ol/src/edit/EditMode.js +424 -0
- package/plugins/beta/draw-ol/src/edit/keyboardHandler.js +227 -0
- package/plugins/beta/draw-ol/src/edit/midpointLayer.js +57 -0
- package/plugins/beta/draw-ol/src/edit/touchHandler.js +171 -0
- package/plugins/beta/draw-ol/src/edit/undoOps.js +95 -0
- package/plugins/beta/draw-ol/src/edit/vertexHitTest.js +72 -0
- package/plugins/beta/draw-ol/src/edit/vertexLayer.js +49 -0
- package/plugins/beta/draw-ol/src/edit/vertexOps.js +99 -0
- package/plugins/beta/draw-ol/src/events.js +114 -0
- package/plugins/beta/draw-ol/src/index.js +12 -0
- package/plugins/beta/draw-ol/src/manifest.js +121 -0
- package/plugins/beta/draw-ol/src/olDraw.js +38 -0
- package/plugins/beta/draw-ol/src/reducer.js +41 -0
- package/plugins/beta/draw-ol/src/snap/snapEngine.js +109 -0
- package/plugins/beta/draw-ol/src/snap/snapGeometry.js +200 -0
- package/plugins/beta/draw-ol/src/snap/snapIndicator.js +81 -0
- package/plugins/beta/draw-ol/src/snap/snapInteraction.js +56 -0
- package/plugins/beta/draw-ol/src/snap/snapManager.js +90 -0
- package/plugins/beta/draw-ol/src/utils/flattenStyleProperties.js +47 -0
- package/plugins/beta/draw-ol/src/utils/geometryHelpers.js +97 -0
- package/plugins/beta/draw-ol/src/utils/olCoords.js +35 -0
- package/plugins/beta/draw-ol/src/utils/resolveColors.js +39 -0
- package/plugins/beta/draw-ol/src/utils/spatial.js +28 -0
- package/plugins/beta/draw-ol/src/utils/touchTarget.js +61 -0
- 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/defaults.js +1 -0
- package/plugins/interact/src/hooks/useInteractionHandlers.js +150 -43
- package/plugins/interact/src/hooks/useInteractionHandlers.test.js +240 -8
- package/plugins/interact/src/reducer.js +7 -0
- package/plugins/interact/src/reducer.test.js +16 -0
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +1 -1
- package/providers/beta/openlayers/dist/umd/index.js +1 -1
- package/providers/beta/openlayers/src/appEvents.js +9 -13
- package/providers/beta/openlayers/src/appEvents.test.js +25 -17
- package/providers/beta/openlayers/src/mapEvents.js +32 -16
- package/providers/beta/openlayers/src/mapEvents.test.js +43 -10
- package/providers/beta/openlayers/src/openlayersProvider.js +45 -26
- package/providers/beta/openlayers/src/openlayersProvider.test.js +12 -34
- package/providers/beta/openlayers/src/utils/highlightFeatures.js +245 -0
- package/providers/beta/openlayers/src/utils/hoverCursor.js +65 -0
- package/providers/beta/openlayers/src/utils/hoverCursor.test.js +235 -0
- package/providers/beta/openlayers/src/utils/openLayersFixes.js +11 -0
- package/providers/beta/openlayers/src/utils/openLayersFixes.test.js +53 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.js +77 -0
- package/providers/beta/openlayers/src/utils/queryFeatures.test.js +181 -0
- package/providers/beta/openlayers/src/utils/tileLayers.js +19 -1
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +74 -3
- package/providers/beta/openlayers/src/utils/vtTileFragments.js +86 -0
- package/rollup.esm.mjs +6 -0
- package/src/App/controls/keyboardActions.js +2 -2
- package/src/InteractiveMap/InteractiveMap.js +38 -10
- package/src/InteractiveMap/InteractiveMap.test.js +282 -356
- package/src/InteractiveMap/historyManager.js +7 -0
- package/src/InteractiveMap/historyManager.test.js +11 -0
- package/webpack.dev.mjs +3 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { queryFeatures } from './queryFeatures.js'
|
|
2
|
+
|
|
3
|
+
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
4
|
+
import VectorLayer from 'ol/layer/Vector.js'
|
|
5
|
+
import { renderFeatureToGeoJSON } from './vtTileFragments.js'
|
|
6
|
+
|
|
7
|
+
jest.mock('ol/layer/VectorTile.js', () => ({ __esModule: true, default: class VectorTileLayer {} }))
|
|
8
|
+
jest.mock('ol/layer/Vector.js', () => ({ __esModule: true, default: class VectorLayer {} }))
|
|
9
|
+
jest.mock('ol/format/GeoJSON.js', () => ({
|
|
10
|
+
__esModule: true,
|
|
11
|
+
default: jest.fn(() => ({
|
|
12
|
+
writeGeometryObject: jest.fn(geom => ({ type: 'mock', geom }))
|
|
13
|
+
}))
|
|
14
|
+
}))
|
|
15
|
+
jest.mock('./vtTileFragments.js', () => ({
|
|
16
|
+
renderFeatureToGeoJSON: jest.fn(f => ({ type: 'mock-vt', feature: f }))
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
const makeMap = (hits = []) => ({
|
|
20
|
+
forEachFeatureAtPixel: jest.fn((pixel, cb, opts) => {
|
|
21
|
+
for (const [feature, layer] of hits) cb(feature, layer)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const makeVTLayer = () => Object.assign(new VectorTileLayer(), { get: () => undefined })
|
|
26
|
+
|
|
27
|
+
const makeVTFeature = ({ id = undefined, styleLayerId = 'roads', type = 'fill', props = {} } = {}) => ({
|
|
28
|
+
getId: () => id,
|
|
29
|
+
get: (key) => {
|
|
30
|
+
if (key === 'mapbox-layer') return { id: styleLayerId, type }
|
|
31
|
+
return undefined
|
|
32
|
+
},
|
|
33
|
+
getProperties: () => ({ 'mapbox-layer': { id: styleLayerId }, ...props })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const makeVectorLayer = (layerId, isHighlight = false) => Object.assign(new VectorLayer(), {
|
|
37
|
+
get: (key) => {
|
|
38
|
+
if (key === 'layerId') return layerId
|
|
39
|
+
if (key === '_highlight') return isHighlight || undefined
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const makeVectorFeature = (id = 'f1', geom = { type: 'Point' }, props = {}) => ({
|
|
45
|
+
getId: () => id,
|
|
46
|
+
getGeometry: () => geom,
|
|
47
|
+
getProperties: () => props
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
describe('queryFeatures', () => {
|
|
51
|
+
beforeEach(() => jest.clearAllMocks())
|
|
52
|
+
|
|
53
|
+
/* ------------------------------------------------------------------ */
|
|
54
|
+
/* Guard */
|
|
55
|
+
/* ------------------------------------------------------------------ */
|
|
56
|
+
|
|
57
|
+
it('returns empty array when point is null', () => {
|
|
58
|
+
expect(queryFeatures(makeMap(), null)).toEqual([])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns empty array when point is undefined', () => {
|
|
62
|
+
expect(queryFeatures(makeMap())).toEqual([])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
/* ------------------------------------------------------------------ */
|
|
66
|
+
/* Options */
|
|
67
|
+
/* ------------------------------------------------------------------ */
|
|
68
|
+
|
|
69
|
+
it('passes point.x/y as pixel and radius as hitTolerance', () => {
|
|
70
|
+
const map = makeMap()
|
|
71
|
+
queryFeatures(map, { x: 20, y: 30 }, { radius: 5 })
|
|
72
|
+
expect(map.forEachFeatureAtPixel).toHaveBeenCalledWith(
|
|
73
|
+
[20, 30],
|
|
74
|
+
expect.any(Function),
|
|
75
|
+
{ hitTolerance: 5 }
|
|
76
|
+
)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('defaults radius to 10', () => {
|
|
80
|
+
const map = makeMap()
|
|
81
|
+
queryFeatures(map, { x: 0, y: 0 })
|
|
82
|
+
expect(map.forEachFeatureAtPixel).toHaveBeenCalledWith(
|
|
83
|
+
expect.anything(),
|
|
84
|
+
expect.any(Function),
|
|
85
|
+
{ hitTolerance: 10 }
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/* ------------------------------------------------------------------ */
|
|
90
|
+
/* VectorTile layer features */
|
|
91
|
+
/* ------------------------------------------------------------------ */
|
|
92
|
+
|
|
93
|
+
it('returns a result for a VT feature', () => {
|
|
94
|
+
const feature = makeVTFeature({ id: 42, styleLayerId: 'roads' })
|
|
95
|
+
const map = makeMap([[feature, makeVTLayer()]])
|
|
96
|
+
const results = queryFeatures(map, { x: 0, y: 0 })
|
|
97
|
+
expect(results).toHaveLength(1)
|
|
98
|
+
expect(results[0]).toMatchObject({ id: 42, layer: { id: 'roads' } })
|
|
99
|
+
expect(renderFeatureToGeoJSON).toHaveBeenCalledWith(feature)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('skips VT features with no mapbox-layer id', () => {
|
|
103
|
+
const feature = { getId: () => 1, get: () => undefined, getProperties: () => ({}) }
|
|
104
|
+
const map = makeMap([[feature, makeVTLayer()]])
|
|
105
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toEqual([])
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('skips background-type VT features', () => {
|
|
109
|
+
const feature = makeVTFeature({ type: 'background' })
|
|
110
|
+
const map = makeMap([[feature, makeVTLayer()]])
|
|
111
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toEqual([])
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('deduplicates VT features with the same styleLayerId and feature id', () => {
|
|
115
|
+
const feature = makeVTFeature({ id: 1, styleLayerId: 'roads' })
|
|
116
|
+
const map = makeMap([[feature, makeVTLayer()], [feature, makeVTLayer()]])
|
|
117
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toHaveLength(1)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('uses property hash as id when VT feature has no explicit id', () => {
|
|
121
|
+
const feature = makeVTFeature({ id: undefined, styleLayerId: 'roads', props: { name: 'A' } })
|
|
122
|
+
const map = makeMap([[feature, makeVTLayer()]])
|
|
123
|
+
const results = queryFeatures(map, { x: 0, y: 0 })
|
|
124
|
+
expect(results).toHaveLength(1)
|
|
125
|
+
expect(results[0].id).toBeUndefined()
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('deduplicates id-less VT features with identical properties', () => {
|
|
129
|
+
const f1 = makeVTFeature({ id: undefined, styleLayerId: 'roads', props: { name: 'A' } })
|
|
130
|
+
const f2 = makeVTFeature({ id: undefined, styleLayerId: 'roads', props: { name: 'A' } })
|
|
131
|
+
const map = makeMap([[f1, makeVTLayer()], [f2, makeVTLayer()]])
|
|
132
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toHaveLength(1)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
/* ------------------------------------------------------------------ */
|
|
136
|
+
/* Vector layer features */
|
|
137
|
+
/* ------------------------------------------------------------------ */
|
|
138
|
+
|
|
139
|
+
it('returns a result for a Vector layer feature', () => {
|
|
140
|
+
const feature = makeVectorFeature('f1', { type: 'Point' })
|
|
141
|
+
const map = makeMap([[feature, makeVectorLayer('draw')]])
|
|
142
|
+
const results = queryFeatures(map, { x: 0, y: 0 })
|
|
143
|
+
expect(results).toHaveLength(1)
|
|
144
|
+
expect(results[0]).toMatchObject({ id: 'f1', layer: { id: 'draw' } })
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('skips Vector layer features with no layerId', () => {
|
|
148
|
+
const feature = makeVectorFeature('f1')
|
|
149
|
+
const map = makeMap([[feature, makeVectorLayer(undefined)]])
|
|
150
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toEqual([])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('skips highlight overlay Vector layers', () => {
|
|
154
|
+
const feature = makeVectorFeature('f1')
|
|
155
|
+
const map = makeMap([[feature, makeVectorLayer('draw', true)]])
|
|
156
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toEqual([])
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it('deduplicates Vector layer features with the same layerId and feature id', () => {
|
|
160
|
+
const feature = makeVectorFeature('f1')
|
|
161
|
+
const map = makeMap([[feature, makeVectorLayer('draw')], [feature, makeVectorLayer('draw')]])
|
|
162
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toHaveLength(1)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
/* ------------------------------------------------------------------ */
|
|
166
|
+
/* Mixed / other layers */
|
|
167
|
+
/* ------------------------------------------------------------------ */
|
|
168
|
+
|
|
169
|
+
it('skips features from other layer types', () => {
|
|
170
|
+
const feature = makeVectorFeature('f1')
|
|
171
|
+
const map = makeMap([[feature, {}]]) // plain object, not VectorTileLayer or VectorLayer
|
|
172
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toEqual([])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('returns results from both VT and Vector layers in one call', () => {
|
|
176
|
+
const vtFeature = makeVTFeature({ id: 1, styleLayerId: 'roads' })
|
|
177
|
+
const vecFeature = makeVectorFeature('f1')
|
|
178
|
+
const map = makeMap([[vtFeature, makeVTLayer()], [vecFeature, makeVectorLayer('draw')]])
|
|
179
|
+
expect(queryFeatures(map, { x: 0, y: 0 })).toHaveLength(2)
|
|
180
|
+
})
|
|
181
|
+
})
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import XYZ from 'ol/source/XYZ.js'
|
|
2
2
|
import VectorTileSource from 'ol/source/VectorTile.js'
|
|
3
|
+
import TileLayer from 'ol/layer/Tile.js'
|
|
3
4
|
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
4
5
|
import OGCVectorTile from 'ol/source/OGCVectorTile.js'
|
|
5
6
|
import MVT from 'ol/format/MVT.js'
|
|
6
7
|
import TileGrid from 'ol/tilegrid/TileGrid.js'
|
|
7
8
|
import TileState from 'ol/TileState.js'
|
|
8
|
-
import { stylefunction } from 'ol-mapbox-style'
|
|
9
|
+
import { stylefunction, recordStyleLayer } from 'ol-mapbox-style'
|
|
9
10
|
import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from '../defaults.js'
|
|
10
11
|
|
|
12
|
+
// Enable style-layer name recording so feature.get('mapbox-layer').id returns the
|
|
13
|
+
// style layer name — required for snap layer filtering by style layer name.
|
|
14
|
+
recordStyleLayer(true)
|
|
15
|
+
|
|
11
16
|
const CRS = 'EPSG:27700'
|
|
12
17
|
|
|
13
18
|
export function fetchWithTransform (url, resourceType, transformRequest) {
|
|
@@ -45,6 +50,19 @@ export function createTileSource (url, transformRequest) {
|
|
|
45
50
|
})
|
|
46
51
|
}
|
|
47
52
|
|
|
53
|
+
export async function createMapStyleLayer (mapStyle, transformRequest) {
|
|
54
|
+
if (mapStyle.type === 'raster') {
|
|
55
|
+
const source = createTileSource(mapStyle.url, transformRequest)
|
|
56
|
+
return { layer: new TileLayer({ source }), source }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (mapStyle.type === 'ogc-vt') {
|
|
60
|
+
return createOGCVectorTileLayer(mapStyle.url, transformRequest, mapStyle)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return createVectorTileLayer(mapStyle.url, transformRequest, mapStyle)
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
// Insert extension before any query string to match Mapbox GL sprite convention
|
|
49
67
|
function resolveSprite (spriteBase, transformRequest) {
|
|
50
68
|
const queryIdx = spriteBase.indexOf('?')
|
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import XYZ from 'ol/source/XYZ.js'
|
|
2
2
|
import TileGrid from 'ol/tilegrid/TileGrid.js'
|
|
3
|
+
import TileLayer from 'ol/layer/Tile.js'
|
|
3
4
|
import VectorTileSource from 'ol/source/VectorTile.js'
|
|
4
5
|
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
5
6
|
import { stylefunction } from 'ol-mapbox-style'
|
|
6
|
-
import { createTileSource, createVectorTileLayer } from './tileLayers.js'
|
|
7
|
+
import { createTileSource, createMapStyleLayer, createVectorTileLayer } from './tileLayers.js'
|
|
7
8
|
import { TILE_GRID_RESOLUTIONS, TILE_GRID_ORIGIN, TILE_SIZE } from '../defaults.js'
|
|
8
9
|
|
|
9
10
|
const mockTileGridInstance = {}
|
|
10
11
|
const mockSourceInstance = {}
|
|
12
|
+
const mockTileLayerInstance = {}
|
|
11
13
|
const mockVectorTileSourceInstance = {}
|
|
14
|
+
const mockOGCVectorTileSourceInstance = { supportedMediaTypes: [] }
|
|
12
15
|
const mockVectorTileLayerInstance = {}
|
|
13
|
-
const mockMVTInstance = {}
|
|
16
|
+
const mockMVTInstance = { supportedMediaTypes: [] }
|
|
14
17
|
|
|
15
18
|
jest.mock('ol/source/XYZ.js', () => ({ __esModule: true, default: jest.fn(() => mockSourceInstance) }))
|
|
19
|
+
jest.mock('ol/layer/Tile.js', () => ({ __esModule: true, default: jest.fn(() => mockTileLayerInstance) }))
|
|
16
20
|
jest.mock('ol/tilegrid/TileGrid.js', () => ({ __esModule: true, default: jest.fn(() => mockTileGridInstance) }))
|
|
17
21
|
jest.mock('ol/TileState.js', () => ({ __esModule: true, default: { ERROR: 'error' } }))
|
|
18
22
|
jest.mock('ol/source/VectorTile.js', () => ({ __esModule: true, default: jest.fn(() => mockVectorTileSourceInstance) }))
|
|
23
|
+
jest.mock('ol/source/OGCVectorTile.js', () => ({ __esModule: true, default: jest.fn(() => mockOGCVectorTileSourceInstance) }))
|
|
19
24
|
jest.mock('ol/layer/VectorTile.js', () => ({ __esModule: true, default: jest.fn(() => mockVectorTileLayerInstance) }))
|
|
20
25
|
jest.mock('ol/format/MVT.js', () => ({ __esModule: true, default: jest.fn(() => mockMVTInstance) }))
|
|
21
|
-
jest.mock('ol-mapbox-style', () => ({ __esModule: true, stylefunction: jest.fn() }))
|
|
26
|
+
jest.mock('ol-mapbox-style', () => ({ __esModule: true, stylefunction: jest.fn(), recordStyleLayer: jest.fn() }))
|
|
22
27
|
|
|
23
28
|
const flushPromises = () => new Promise(resolve => setTimeout(resolve, 0))
|
|
24
29
|
|
|
@@ -41,6 +46,38 @@ const mockServiceJson = {
|
|
|
41
46
|
|
|
42
47
|
const mockSpritesJson = { myIcon: { x: 0, y: 0, width: 16, height: 16, pixelRatio: 1 } }
|
|
43
48
|
|
|
49
|
+
const mockOGCStyleJson = {
|
|
50
|
+
sources: { ngd: { url: 'https://example.com/ogc/tiles' } },
|
|
51
|
+
sprite: 'https://example.com/sprites/sprite',
|
|
52
|
+
layers: []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mockTilesetJson = {
|
|
56
|
+
links: [{ rel: 'http://www.opengis.net/def/rel/ogc/1.0/tiling-scheme', href: 'https://example.com/ogc/tms' }]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mockTMSJson = {
|
|
60
|
+
tileMatrices: [
|
|
61
|
+
{ cellSize: 896, pointOfOrigin: [-238375, 1376256], tileHeight: 256, tileWidth: 256 },
|
|
62
|
+
{ cellSize: 448, pointOfOrigin: [-238375, 1376256], tileHeight: 256, tileWidth: 256 }
|
|
63
|
+
]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function makeOGCFetchMock (styleJson = mockOGCStyleJson) {
|
|
67
|
+
return jest.fn().mockImplementation(url => {
|
|
68
|
+
if (url === 'https://example.com/ogc/tiles') {
|
|
69
|
+
return Promise.resolve({ json: () => Promise.resolve(mockTilesetJson) })
|
|
70
|
+
}
|
|
71
|
+
if (url === 'https://example.com/ogc/tms') {
|
|
72
|
+
return Promise.resolve({ json: () => Promise.resolve(mockTMSJson) })
|
|
73
|
+
}
|
|
74
|
+
if (url.endsWith('.json') || url.includes('.json?')) {
|
|
75
|
+
return Promise.resolve({ json: () => Promise.resolve(mockSpritesJson) })
|
|
76
|
+
}
|
|
77
|
+
return Promise.resolve({ json: () => Promise.resolve(styleJson) })
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
44
81
|
function makeVectorFetchMock (styleJson = mockStyleJson) {
|
|
45
82
|
return jest.fn().mockImplementation(url => {
|
|
46
83
|
if (url === styleJson.sources[Object.keys(styleJson.sources)[0]].url) {
|
|
@@ -146,6 +183,40 @@ describe('tileLoadFunction (via transformRequest)', () => {
|
|
|
146
183
|
})
|
|
147
184
|
})
|
|
148
185
|
|
|
186
|
+
describe('createMapStyleLayer', () => {
|
|
187
|
+
const styleUrl = 'https://example.com/styles'
|
|
188
|
+
|
|
189
|
+
beforeEach(() => {
|
|
190
|
+
jest.clearAllMocks()
|
|
191
|
+
global.fetch = makeVectorFetchMock()
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('creates a raster tile layer and source when mapStyle.type is raster', async () => {
|
|
195
|
+
const result = await createMapStyleLayer({ url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }, null)
|
|
196
|
+
expect(TileLayer).toHaveBeenCalledWith({ source: mockSourceInstance })
|
|
197
|
+
expect(result).toEqual({ layer: mockTileLayerInstance, source: mockSourceInstance })
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('creates an OGC vector tile layer and source when mapStyle.type is ogc-vt', async () => {
|
|
201
|
+
global.fetch = makeOGCFetchMock()
|
|
202
|
+
const result = await createMapStyleLayer({ url: 'https://example.com/ogc-styles', type: 'ogc-vt' }, null)
|
|
203
|
+
expect(VectorTileLayer).toHaveBeenCalledWith({ source: mockOGCVectorTileSourceInstance, declutter: true })
|
|
204
|
+
expect(result).toEqual({ layer: mockVectorTileLayerInstance, source: mockOGCVectorTileSourceInstance })
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('creates a vector tile layer and source when mapStyle.type is vector', async () => {
|
|
208
|
+
const result = await createMapStyleLayer({ url: styleUrl, type: 'vector' }, null)
|
|
209
|
+
expect(VectorTileLayer).toHaveBeenCalledWith({ source: mockVectorTileSourceInstance, declutter: true })
|
|
210
|
+
expect(result).toEqual({ layer: mockVectorTileLayerInstance, source: mockVectorTileSourceInstance })
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
it('creates a vector tile layer and source when mapStyle.type is missing', async () => {
|
|
214
|
+
const result = await createMapStyleLayer({ url: styleUrl }, null)
|
|
215
|
+
expect(VectorTileLayer).toHaveBeenCalledWith({ source: mockVectorTileSourceInstance, declutter: true })
|
|
216
|
+
expect(result).toEqual({ layer: mockVectorTileLayerInstance, source: mockVectorTileSourceInstance })
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
149
220
|
describe('createVectorTileLayer', () => {
|
|
150
221
|
const styleUrl = 'https://example.com/styles'
|
|
151
222
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import VectorTileLayer from 'ol/layer/VectorTile.js'
|
|
2
|
+
import TileState from 'ol/TileState.js'
|
|
3
|
+
const toPairs = (flat, start, end) => {
|
|
4
|
+
const coords = []
|
|
5
|
+
for (let i = start; i < end; i += 2) {
|
|
6
|
+
coords.push([flat[i], flat[i + 1]])
|
|
7
|
+
}
|
|
8
|
+
return coords
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const renderFeatureToGeoJSON = (feature) => {
|
|
12
|
+
const type = feature.getType()
|
|
13
|
+
const flat = feature.getFlatCoordinates()
|
|
14
|
+
|
|
15
|
+
if (type === 'Point') {
|
|
16
|
+
return { type: 'Point', coordinates: [flat[0], flat[1]] }
|
|
17
|
+
}
|
|
18
|
+
if (type === 'LineString') {
|
|
19
|
+
return { type: 'LineString', coordinates: toPairs(flat, 0, flat.length) }
|
|
20
|
+
}
|
|
21
|
+
if (type === 'Polygon') {
|
|
22
|
+
const ends = feature.getEnds()
|
|
23
|
+
let prev = 0
|
|
24
|
+
const rings = ends.map(end => {
|
|
25
|
+
const ring = toPairs(flat, prev, end)
|
|
26
|
+
prev = end
|
|
27
|
+
return ring
|
|
28
|
+
})
|
|
29
|
+
return { type: 'Polygon', coordinates: rings }
|
|
30
|
+
}
|
|
31
|
+
if (type === 'MultiPolygon') {
|
|
32
|
+
const endss = feature.getEndss()
|
|
33
|
+
let offset = 0
|
|
34
|
+
const polys = endss.map(ends => {
|
|
35
|
+
let prev = offset
|
|
36
|
+
const rings = ends.map(end => {
|
|
37
|
+
const ring = toPairs(flat, prev, end)
|
|
38
|
+
prev = end
|
|
39
|
+
return ring
|
|
40
|
+
})
|
|
41
|
+
offset = ends[ends.length - 1]
|
|
42
|
+
return rings
|
|
43
|
+
})
|
|
44
|
+
return { type: 'MultiPolygon', coordinates: polys }
|
|
45
|
+
}
|
|
46
|
+
// MultiLineString / MultiPoint fallback
|
|
47
|
+
return { type, coordinates: toPairs(flat, 0, flat.length) }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collects all loaded VT tile fragments for a feature across all VectorTileLayers.
|
|
52
|
+
* OL clips VT features at tile boundaries, so a single logical feature appears as
|
|
53
|
+
* multiple RenderFeature instances (one per tile). This gathers them all so callers
|
|
54
|
+
* can work with the full feature geometry rather than a single clipped fragment.
|
|
55
|
+
*/
|
|
56
|
+
export const collectTileFragments = (map, layerId, featureId, idProperty) => {
|
|
57
|
+
const fragments = []
|
|
58
|
+
|
|
59
|
+
map.getLayers().forEach(mapLayer => {
|
|
60
|
+
if (!(mapLayer instanceof VectorTileLayer)) {
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
const source = mapLayer.getSource()
|
|
64
|
+
const sourceTiles = source?.sourceTiles_
|
|
65
|
+
if (!sourceTiles) {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
Object.values(sourceTiles).forEach(tile => {
|
|
69
|
+
if (tile.getState() !== TileState.LOADED) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
tile.getFeatures().forEach(feature => {
|
|
73
|
+
if (feature.get('mapbox-layer')?.id !== layerId) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const fid = idProperty ? feature.get(idProperty) : feature.getId()
|
|
77
|
+
if (String(fid) !== String(featureId)) {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
fragments.push(renderFeatureToGeoJSON(feature))
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
return fragments
|
|
86
|
+
}
|
package/rollup.esm.mjs
CHANGED
|
@@ -290,6 +290,12 @@ const ALL_BUILDS = [
|
|
|
290
290
|
outDir: 'plugins/beta/draw-es/dist/esm',
|
|
291
291
|
manualChunks: (id) => { if (id.includes('/manifest')) return 'im-draw-es-plugin' }
|
|
292
292
|
},
|
|
293
|
+
{
|
|
294
|
+
entryPath: './plugins/beta/draw-ol/src/index.js',
|
|
295
|
+
outDir: 'plugins/beta/draw-ol/dist/esm',
|
|
296
|
+
extraExternals: [/^ol\//],
|
|
297
|
+
manualChunks: (id) => { if (id.includes('/manifest')) return 'im-draw-ol-plugin' }
|
|
298
|
+
},
|
|
293
299
|
{
|
|
294
300
|
entryPath: './plugins/beta/frame/src/index.js',
|
|
295
301
|
outDir: 'plugins/beta/frame/dist/esm',
|
|
@@ -50,7 +50,7 @@ export const createKeyboardActions = (mapProvider, announce, {
|
|
|
50
50
|
},
|
|
51
51
|
|
|
52
52
|
highlightNextLabel: (e) => {
|
|
53
|
-
if (!readMapText
|
|
53
|
+
if (!readMapText || !mapProvider.highlightNextLabel) {
|
|
54
54
|
return
|
|
55
55
|
}
|
|
56
56
|
const label = mapProvider.highlightNextLabel(e.key)
|
|
@@ -58,7 +58,7 @@ export const createKeyboardActions = (mapProvider, announce, {
|
|
|
58
58
|
},
|
|
59
59
|
|
|
60
60
|
highlightLabelAtCenter: () => {
|
|
61
|
-
if (!readMapText
|
|
61
|
+
if (!readMapText || !mapProvider.highlightLabelAtCenter) {
|
|
62
62
|
return
|
|
63
63
|
}
|
|
64
64
|
const label = mapProvider.highlightLabelAtCenter()
|
|
@@ -38,6 +38,8 @@ export default class InteractiveMap {
|
|
|
38
38
|
_interfaceDetectorCleanup = null
|
|
39
39
|
_hybridBehaviourCleanup = null
|
|
40
40
|
_isHidden = false // tracks if map is hidden but preserved (hybrid mode)
|
|
41
|
+
_isClosingViaBack = false // prevents double history.back() while popstate is pending
|
|
42
|
+
_isLoading = false // prevents concurrent loadApp() calls
|
|
41
43
|
|
|
42
44
|
/**
|
|
43
45
|
* Create a new InteractiveMap instance.
|
|
@@ -103,12 +105,17 @@ export default class InteractiveMap {
|
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
_handleButtonClick (e) {
|
|
106
|
-
if (this.config.manageHistoryState) {
|
|
107
|
-
history.pushState({ isBack: true }, '', e.currentTarget.getAttribute('href'))
|
|
108
|
-
}
|
|
109
108
|
if (this._isHidden) {
|
|
109
|
+
if (this.config.manageHistoryState) {
|
|
110
|
+
history.pushState({ isBack: true }, '', e.currentTarget.getAttribute('href'))
|
|
111
|
+
}
|
|
110
112
|
this.showApp()
|
|
113
|
+
} else if (this._root) {
|
|
114
|
+
// app already open — no-op, no push
|
|
111
115
|
} else {
|
|
116
|
+
if (this.config.manageHistoryState) {
|
|
117
|
+
history.pushState({ isBack: true }, '', e.currentTarget.getAttribute('href'))
|
|
118
|
+
}
|
|
112
119
|
this.loadApp()
|
|
113
120
|
}
|
|
114
121
|
}
|
|
@@ -128,22 +135,35 @@ export default class InteractiveMap {
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
_handleExitClick () {
|
|
131
|
-
if (this.config.preserveStateOnClose) {
|
|
132
|
-
this.hideApp()
|
|
133
|
-
} else {
|
|
134
|
-
this.removeApp()
|
|
135
|
-
}
|
|
136
|
-
|
|
137
138
|
if (!this.config.manageHistoryState) {
|
|
139
|
+
if (this.config.preserveStateOnClose) {
|
|
140
|
+
this.hideApp()
|
|
141
|
+
} else {
|
|
142
|
+
this.removeApp()
|
|
143
|
+
}
|
|
138
144
|
return
|
|
139
145
|
}
|
|
140
146
|
|
|
141
147
|
// If this history entry was pushed by the map's open button, go back so the
|
|
142
148
|
// ?mv= entry is preserved as a forward entry (browser forward re-opens the map).
|
|
143
|
-
//
|
|
149
|
+
// Don't close synchronously here — let the popstate handler (syncMapInstance →
|
|
150
|
+
// closeMap) do it. This prevents a race where the button becomes clickable before
|
|
151
|
+
// history.back() resolves, which would push a ghost ?mv= entry into history.
|
|
144
152
|
if (history.state?.isBack) {
|
|
153
|
+
if (this._isClosingViaBack) {
|
|
154
|
+
return
|
|
155
|
+
}
|
|
156
|
+
this._isClosingViaBack = true
|
|
145
157
|
history.back()
|
|
158
|
+
// Safety net: clear flag if popstate never fires (e.g. nothing to go back to)
|
|
159
|
+
setTimeout(() => { this._isClosingViaBack = false }, 1000)
|
|
146
160
|
} else {
|
|
161
|
+
// Direct URL / bookmark: no popstate coming, so close immediately.
|
|
162
|
+
if (this.config.preserveStateOnClose) {
|
|
163
|
+
this.hideApp()
|
|
164
|
+
} else {
|
|
165
|
+
this.removeApp()
|
|
166
|
+
}
|
|
147
167
|
const key = this.config.mapViewQueryParam
|
|
148
168
|
const newUrl = this._removeMapParamFromUrl(location.href, key)
|
|
149
169
|
history.replaceState(history.state, '', newUrl)
|
|
@@ -157,6 +177,11 @@ export default class InteractiveMap {
|
|
|
157
177
|
* @returns {Promise<void>}
|
|
158
178
|
*/
|
|
159
179
|
async loadApp () {
|
|
180
|
+
if (this._root || this._isLoading) {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
this._isLoading = true
|
|
184
|
+
|
|
160
185
|
if (this._openButton) {
|
|
161
186
|
this._openButton.style.display = 'none'
|
|
162
187
|
}
|
|
@@ -203,9 +228,12 @@ export default class InteractiveMap {
|
|
|
203
228
|
updateDOMState(this)
|
|
204
229
|
this.eventBus.emit(events.APP_OPENED, { statePreserved: false })
|
|
205
230
|
} catch (err) {
|
|
231
|
+
this._openButton?.removeAttribute('style')
|
|
206
232
|
renderError(this.rootEl, this.config.genericErrorText)
|
|
207
233
|
console.error(err)
|
|
208
234
|
throw err
|
|
235
|
+
} finally {
|
|
236
|
+
this._isLoading = false
|
|
209
237
|
}
|
|
210
238
|
}
|
|
211
239
|
|