@defra/interactive-map 0.0.23-alpha → 0.0.25-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/light-raster-thumb.jpg +0 -0
- package/assets/images/outdoor-raster-thumb.jpg +0 -0
- package/assets/images/road-raster-thumb.jpg +0 -0
- package/dist/css/index.css +1 -1
- package/docs/api/map-style-config.md +39 -2
- package/docs/plugins/search.md +70 -0
- package/package.json +17 -2
- 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/api/editFeature.js +9 -4
- package/plugins/beta/draw-ml/src/modes/createDrawMode.js +11 -3
- package/plugins/beta/scale-bar/dist/css/index.css +31 -1
- package/plugins/interact/src/hooks/useAttachEvents.test.js +3 -0
- package/plugins/search/dist/css/index.css +250 -1
- package/plugins/search/dist/esm/im-search-plugin.js +1 -1
- package/plugins/search/dist/esm/index.js +1 -1
- package/plugins/search/dist/umd/im-search-plugin.js +1 -1
- package/plugins/search/dist/umd/index.js +1 -1
- package/plugins/search/src/Search.jsx +2 -1
- package/plugins/search/src/components/Form/Form.jsx +11 -7
- package/plugins/search/src/components/Form/Form.test.jsx +23 -1
- package/plugins/search/src/datasets.js +2 -2
- package/plugins/search/src/datasets.test.js +1 -1
- package/plugins/search/src/defaults.js +7 -1
- package/plugins/search/src/events/fetchSuggestions.js +13 -10
- package/plugins/search/src/events/fetchSuggestions.test.js +14 -1
- package/plugins/search/src/events/formHandlers.js +2 -4
- package/plugins/search/src/events/formHandlers.test.js +3 -3
- package/plugins/search/src/events/inputHandlers.js +1 -1
- package/plugins/search/src/events/inputHandlers.test.js +1 -1
- package/plugins/search/src/events/suggestionHandlers.js +2 -1
- package/plugins/search/src/events/suggestionHandlers.test.js +7 -1
- package/plugins/search/src/index.js +3 -7
- package/plugins/search/src/reducer.js +10 -5
- package/plugins/search/src/reducer.test.js +2 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +3 -3
- package/providers/beta/openlayers/dist/esm/im-openlayers-provider.js +1 -0
- package/providers/beta/openlayers/dist/esm/index.js +1 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js +2 -0
- package/providers/beta/openlayers/dist/umd/im-openlayers-provider.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/dist/umd/index.js +2 -0
- package/providers/beta/openlayers/dist/umd/index.js.LICENSE.txt +1 -0
- package/providers/beta/openlayers/src/appEvents.js +39 -0
- package/providers/beta/openlayers/src/appEvents.test.js +117 -0
- package/providers/beta/openlayers/src/defaults.js +36 -0
- package/providers/beta/openlayers/src/index.js +21 -0
- package/providers/beta/openlayers/src/mapEvents.js +132 -0
- package/providers/beta/openlayers/src/mapEvents.test.js +198 -0
- package/providers/beta/openlayers/src/openlayersProvider.js +249 -0
- package/providers/beta/openlayers/src/openlayersProvider.test.js +386 -0
- package/providers/beta/openlayers/src/utils/spatial.js +146 -0
- package/providers/beta/openlayers/src/utils/spatial.test.js +119 -0
- package/providers/beta/openlayers/src/utils/tileLayers.js +133 -0
- package/providers/beta/openlayers/src/utils/tileLayers.test.js +251 -0
- package/providers/beta/openlayers/src/utils/zoom.js +26 -0
- package/providers/beta/openlayers/src/utils/zoom.test.js +23 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/utils/highlightFeatures.js +17 -7
- package/providers/maplibre/src/utils/highlightFeatures.test.js +17 -0
- package/providers/maplibre/src/utils/maplibreFixes.js +1 -1
- package/providers/maplibre/src/utils/patternImages.js +14 -9
- package/providers/maplibre/src/utils/patternImages.test.js +2 -1
- package/rollup.esm.mjs +14 -3
- package/src/App/hooks/useFeatureItems.test.js +1 -0
- package/src/App/hooks/useLayoutMeasurements.test.js +0 -1
- package/src/App/hooks/useMarkersAPI.test.js +1 -0
- package/src/App/hooks/useModalPanelBehaviour.test.js +2 -1
- package/src/App/layout/layout.module.scss +1 -0
- package/src/App/store/AppProvider.test.jsx +5 -0
- package/src/App/store/MapProvider.test.jsx +8 -1
- package/src/services/announcer.test.js +5 -7
- package/src/types.js +11 -1
- package/webpack.dev.mjs +14 -5
- package/webpack.umd.mjs +1 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import OlMap from 'ol/Map.js'
|
|
2
|
+
import View from 'ol/View.js'
|
|
3
|
+
import TileLayer from 'ol/layer/Tile.js'
|
|
4
|
+
import { defaults as defaultInteractions } from 'ol/interaction/defaults.js'
|
|
5
|
+
import proj4 from 'proj4'
|
|
6
|
+
import { register } from 'ol/proj/proj4.js'
|
|
7
|
+
import { supportedShortcuts, DEFAULTS } from './defaults.js'
|
|
8
|
+
import { getViewResolutionConfig, ZOOM_ALIGNMENT } from './utils/zoom.js'
|
|
9
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
10
|
+
import { attachAppEvents, createTileSource, createVectorTileLayer, createOGCVectorTileLayer } from './appEvents.js'
|
|
11
|
+
import { getAreaDimensions, getCardinalMove, getExtentFromGeoJSON, getPaddedExtent, isGeometryObscured } from './utils/spatial.js'
|
|
12
|
+
|
|
13
|
+
const CRS = 'EPSG:27700'
|
|
14
|
+
|
|
15
|
+
// OL view padding is [top, right, bottom, left]; app passes { top, right, bottom, left }
|
|
16
|
+
const toPaddingArray = (padding) => {
|
|
17
|
+
if (!padding) {
|
|
18
|
+
return undefined
|
|
19
|
+
}
|
|
20
|
+
const { top = 0, right = 0, bottom = 0, left = 0 } = padding
|
|
21
|
+
return [top, right, bottom, left]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Register British National Grid with proj4 so OL can use it
|
|
25
|
+
proj4.defs(CRS, '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs')
|
|
26
|
+
register(proj4)
|
|
27
|
+
|
|
28
|
+
export default class OpenLayersProvider {
|
|
29
|
+
constructor ({ mapProviderConfig = {}, events, eventBus }) {
|
|
30
|
+
this.events = events
|
|
31
|
+
this.eventBus = eventBus
|
|
32
|
+
this.capabilities = {
|
|
33
|
+
supportedShortcuts,
|
|
34
|
+
supportsMapSizes: true
|
|
35
|
+
}
|
|
36
|
+
Object.assign(this, mapProviderConfig)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async initMap (config) {
|
|
40
|
+
const { container, padding, mapStyle, mapSize, center, zoom, bounds, minZoom, maxZoom, transformRequest, pixelRatio } = config
|
|
41
|
+
this.mapStyleId = mapStyle?.id
|
|
42
|
+
this.mapSize = mapSize
|
|
43
|
+
const { events, eventBus } = this
|
|
44
|
+
|
|
45
|
+
let tileLayer, source
|
|
46
|
+
if (mapStyle.type === 'raster') {
|
|
47
|
+
source = createTileSource(mapStyle.url, transformRequest)
|
|
48
|
+
tileLayer = new TileLayer({ source })
|
|
49
|
+
} else if (mapStyle.type === 'ogc-vt') {
|
|
50
|
+
const vectorTile = await createOGCVectorTileLayer(mapStyle.url, transformRequest)
|
|
51
|
+
tileLayer = vectorTile.layer
|
|
52
|
+
source = vectorTile.source
|
|
53
|
+
} else {
|
|
54
|
+
const vectorTile = await createVectorTileLayer(mapStyle.url, transformRequest)
|
|
55
|
+
tileLayer = vectorTile.layer
|
|
56
|
+
source = vectorTile.source
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const viewResolutions = getViewResolutionConfig(this.zoomAlignment ?? ZOOM_ALIGNMENT.UK)
|
|
60
|
+
|
|
61
|
+
const view = new View({
|
|
62
|
+
projection: CRS,
|
|
63
|
+
center,
|
|
64
|
+
zoom: zoom ?? viewResolutions.defaultMinZoom,
|
|
65
|
+
minZoom: Math.max(minZoom ?? viewResolutions.defaultMinZoom, viewResolutions.defaultMinZoom),
|
|
66
|
+
maxZoom: maxZoom ?? viewResolutions.maxZoom,
|
|
67
|
+
resolutions: viewResolutions.resolutions,
|
|
68
|
+
constrainResolution: false,
|
|
69
|
+
padding: toPaddingArray(padding)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const map = new OlMap({
|
|
73
|
+
target: container,
|
|
74
|
+
layers: [tileLayer],
|
|
75
|
+
view,
|
|
76
|
+
controls: [],
|
|
77
|
+
interactions: defaultInteractions({ doubleClickZoom: false }),
|
|
78
|
+
pixelRatio
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
if (bounds) {
|
|
82
|
+
map.once('rendercomplete', () => {
|
|
83
|
+
view.fit(bounds, { size: map.getSize(), duration: 0 })
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.mapEventHandles = attachMapEvents({
|
|
88
|
+
map,
|
|
89
|
+
source,
|
|
90
|
+
events,
|
|
91
|
+
eventBus,
|
|
92
|
+
getZoom: this.getZoom.bind(this),
|
|
93
|
+
getCenter: this.getCenter.bind(this),
|
|
94
|
+
getBounds: this.getBounds.bind(this),
|
|
95
|
+
getResolution: this.getResolution.bind(this)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
this.appEventHandles = attachAppEvents({
|
|
99
|
+
mapProvider: this,
|
|
100
|
+
layer: tileLayer,
|
|
101
|
+
layerType: mapStyle.type ?? 'vector',
|
|
102
|
+
transformRequest,
|
|
103
|
+
events,
|
|
104
|
+
eventBus,
|
|
105
|
+
map
|
|
106
|
+
}) || []
|
|
107
|
+
|
|
108
|
+
this.map = map
|
|
109
|
+
this.view = view
|
|
110
|
+
this.tileLayer = tileLayer
|
|
111
|
+
this.source = source
|
|
112
|
+
|
|
113
|
+
// MAP_READY is synchronous — OL map is immediately interactive after construction
|
|
114
|
+
eventBus.emit(events.MAP_READY, {
|
|
115
|
+
map: this.map,
|
|
116
|
+
mapStyleId: this.mapStyleId,
|
|
117
|
+
mapSize: this.mapSize,
|
|
118
|
+
crs: this.crs
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
destroyMap () {
|
|
123
|
+
this.mapEventHandles?.remove()
|
|
124
|
+
this.appEventHandles?.remove()
|
|
125
|
+
|
|
126
|
+
this.mapEventHandles = null
|
|
127
|
+
this.appEventHandles = null
|
|
128
|
+
|
|
129
|
+
if (this.map) {
|
|
130
|
+
this.map.setTarget(null)
|
|
131
|
+
this.map = null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
this.view = null
|
|
135
|
+
this.tileLayer = null
|
|
136
|
+
this.source = null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ==========================
|
|
140
|
+
// Side-effects
|
|
141
|
+
// ==========================
|
|
142
|
+
|
|
143
|
+
setView ({ center, zoom }) {
|
|
144
|
+
this.view.animate({
|
|
145
|
+
center: center ?? this.view.getCenter(),
|
|
146
|
+
zoom: zoom ?? this.view.getZoom(),
|
|
147
|
+
duration: DEFAULTS.animationDuration
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
zoomIn (zoomDelta) {
|
|
152
|
+
this.view.animate({
|
|
153
|
+
zoom: this.view.getZoom() + zoomDelta,
|
|
154
|
+
duration: DEFAULTS.animationDuration
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
zoomOut (zoomDelta) {
|
|
159
|
+
this.view.animate({
|
|
160
|
+
zoom: this.view.getZoom() - zoomDelta,
|
|
161
|
+
duration: DEFAULTS.animationDuration
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
panBy (offset) {
|
|
166
|
+
const center = this.view.getCenter()
|
|
167
|
+
const resolution = this.view.getResolution()
|
|
168
|
+
// Pixel x/y → easting increases right, northing increases up (flip y)
|
|
169
|
+
this.view.animate({
|
|
170
|
+
center: [center[0] + offset[0] * resolution, center[1] - offset[1] * resolution],
|
|
171
|
+
duration: DEFAULTS.animationDuration
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fitToBounds (bounds) {
|
|
176
|
+
const extent = Array.isArray(bounds) ? bounds : getExtentFromGeoJSON(bounds)
|
|
177
|
+
this.view.fit(extent, { duration: DEFAULTS.animationDuration })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
setPadding (padding) {
|
|
181
|
+
this.view.padding = toPaddingArray(padding)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ==========================
|
|
185
|
+
// Read-only getters
|
|
186
|
+
// ==========================
|
|
187
|
+
|
|
188
|
+
getCenter () {
|
|
189
|
+
const center = this.view.getCenter()
|
|
190
|
+
const p = DEFAULTS.coordinatePrecision
|
|
191
|
+
return [
|
|
192
|
+
Math.round(center[0] * Math.pow(10, p)) / Math.pow(10, p),
|
|
193
|
+
Math.round(center[1] * Math.pow(10, p)) / Math.pow(10, p)
|
|
194
|
+
]
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
getZoom () {
|
|
198
|
+
return this.view.getZoom()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
getBounds () {
|
|
202
|
+
const extent = this.view.calculateExtent(this.map.getSize())
|
|
203
|
+
return extent.map(n => Math.round(n * 100) / 100)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
getFeaturesAtPoint (_point, _options) {
|
|
207
|
+
// Raster tiles have no queryable features
|
|
208
|
+
return []
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
getVisibleFeatures (_layerIds) {
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ==========================
|
|
216
|
+
// Spatial helpers
|
|
217
|
+
// ==========================
|
|
218
|
+
|
|
219
|
+
getAreaDimensions () {
|
|
220
|
+
return getAreaDimensions(getPaddedExtent(this.map))
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
getCardinalMove (from, to) {
|
|
224
|
+
return getCardinalMove(from, to)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
getResolution () {
|
|
228
|
+
return this.view.getResolution()
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
mapToScreen (coords) {
|
|
232
|
+
if (!this.map) {
|
|
233
|
+
return { x: 0, y: 0 }
|
|
234
|
+
}
|
|
235
|
+
const pixel = this.map.getPixelFromCoordinate(coords)
|
|
236
|
+
if (!pixel) {
|
|
237
|
+
return { x: 0, y: 0 }
|
|
238
|
+
}
|
|
239
|
+
return { x: pixel[0], y: pixel[1] }
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
screenToMap (point) {
|
|
243
|
+
return this.map.getCoordinateFromPixel([point.x, point.y])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
isGeometryObscured (geojson, panelRect) {
|
|
247
|
+
return isGeometryObscured(geojson, panelRect, this.map)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// Mock-prefixed variables are allowed in jest.mock factories by babel-plugin-jest-hoist
|
|
2
|
+
import OpenLayersProvider from './openlayersProvider.js'
|
|
3
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
4
|
+
import { attachAppEvents, createTileSource, createVectorTileLayer } from './appEvents.js'
|
|
5
|
+
import { getExtentFromGeoJSON, isGeometryObscured } from './utils/spatial.js'
|
|
6
|
+
|
|
7
|
+
const mockAnimate = jest.fn()
|
|
8
|
+
const mockGetCenter = jest.fn(() => [400000.126, 300000.455])
|
|
9
|
+
const mockGetZoom = jest.fn(() => 7)
|
|
10
|
+
const mockGetResolution = jest.fn(() => 56)
|
|
11
|
+
const mockCalculateExtent = jest.fn(() => [1.005, 2.014, 3.994, 4.0])
|
|
12
|
+
const mockViewFit = jest.fn()
|
|
13
|
+
const mockViewInstance = {
|
|
14
|
+
animate: mockAnimate,
|
|
15
|
+
getCenter: mockGetCenter,
|
|
16
|
+
getZoom: mockGetZoom,
|
|
17
|
+
getResolution: mockGetResolution,
|
|
18
|
+
calculateExtent: mockCalculateExtent,
|
|
19
|
+
fit: mockViewFit,
|
|
20
|
+
padding: null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mockMapOnce = jest.fn()
|
|
24
|
+
const mockMapGetSize = jest.fn(() => [800, 600])
|
|
25
|
+
const mockMapSetTarget = jest.fn()
|
|
26
|
+
const mockMapGetPixel = jest.fn(() => [100, 200])
|
|
27
|
+
const mockMapGetCoord = jest.fn(() => [400000, 300000])
|
|
28
|
+
const mockMapInstance = {
|
|
29
|
+
once: mockMapOnce,
|
|
30
|
+
getSize: mockMapGetSize,
|
|
31
|
+
setTarget: mockMapSetTarget,
|
|
32
|
+
getPixelFromCoordinate: mockMapGetPixel,
|
|
33
|
+
getCoordinateFromPixel: mockMapGetCoord,
|
|
34
|
+
getView: jest.fn(() => mockViewInstance)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mockSource = {}
|
|
38
|
+
const mockTileLayer = { setSource: jest.fn() }
|
|
39
|
+
const mockVectorTileLayer = {}
|
|
40
|
+
const mockMapEventHandlesRemove = jest.fn()
|
|
41
|
+
const mockAppEventHandlesRemove = jest.fn()
|
|
42
|
+
|
|
43
|
+
jest.mock('ol/Map.js', () => ({ __esModule: true, default: jest.fn(() => mockMapInstance) }))
|
|
44
|
+
jest.mock('ol/View.js', () => ({ __esModule: true, default: jest.fn(() => mockViewInstance) }))
|
|
45
|
+
jest.mock('ol/layer/Tile.js', () => ({ __esModule: true, default: jest.fn(() => mockTileLayer) }))
|
|
46
|
+
jest.mock('ol/interaction/defaults.js', () => ({ __esModule: true, defaults: jest.fn(() => []) }))
|
|
47
|
+
jest.mock('proj4', () => {
|
|
48
|
+
const fn = jest.fn()
|
|
49
|
+
fn.defs = jest.fn()
|
|
50
|
+
return { __esModule: true, default: fn }
|
|
51
|
+
})
|
|
52
|
+
jest.mock('ol/proj/proj4.js', () => ({ __esModule: true, register: jest.fn() }))
|
|
53
|
+
jest.mock('./mapEvents.js', () => ({
|
|
54
|
+
__esModule: true,
|
|
55
|
+
attachMapEvents: jest.fn(() => ({ remove: mockMapEventHandlesRemove }))
|
|
56
|
+
}))
|
|
57
|
+
jest.mock('./appEvents.js', () => ({
|
|
58
|
+
__esModule: true,
|
|
59
|
+
createTileSource: jest.fn(() => mockSource),
|
|
60
|
+
createVectorTileLayer: jest.fn(async () => ({ layer: mockVectorTileLayer, source: mockSource })),
|
|
61
|
+
attachAppEvents: jest.fn(() => ({ remove: mockAppEventHandlesRemove }))
|
|
62
|
+
}))
|
|
63
|
+
jest.mock('./utils/spatial.js', () => ({
|
|
64
|
+
__esModule: true,
|
|
65
|
+
getAreaDimensions: jest.fn(() => '1 mile by 2 miles'),
|
|
66
|
+
getCardinalMove: jest.fn(() => 'north 100m'),
|
|
67
|
+
getExtentFromGeoJSON: jest.fn(() => [10, 20, 30, 40]),
|
|
68
|
+
getPaddedExtent: jest.fn(() => [0, 0, 800, 600]),
|
|
69
|
+
isGeometryObscured: jest.fn(() => false)
|
|
70
|
+
}))
|
|
71
|
+
|
|
72
|
+
const events = {
|
|
73
|
+
MAP_READY: 'map:ready',
|
|
74
|
+
MAP_SET_STYLE: 'map:setstyle',
|
|
75
|
+
MAP_STYLE_CHANGE: 'map:stylechange'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const defaultInitConfig = {
|
|
79
|
+
container: document.createElement('div'),
|
|
80
|
+
padding: null,
|
|
81
|
+
mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}' },
|
|
82
|
+
mapSize: null,
|
|
83
|
+
center: [400000, 300000],
|
|
84
|
+
zoom: 7,
|
|
85
|
+
bounds: null,
|
|
86
|
+
minZoom: null,
|
|
87
|
+
maxZoom: null,
|
|
88
|
+
transformRequest: null
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function makeProvider (mapProviderConfig = {}) {
|
|
92
|
+
const eventBus = { emit: jest.fn(), on: jest.fn(), off: jest.fn() }
|
|
93
|
+
const provider = new OpenLayersProvider({ events, eventBus, mapProviderConfig })
|
|
94
|
+
return { provider, eventBus }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe('OpenLayersProvider', () => {
|
|
98
|
+
beforeEach(() => {
|
|
99
|
+
jest.clearAllMocks()
|
|
100
|
+
mockViewInstance.padding = null
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
describe('constructor', () => {
|
|
104
|
+
it('sets events and eventBus on the instance', () => {
|
|
105
|
+
const eventBus = { emit: jest.fn() }
|
|
106
|
+
const provider = new OpenLayersProvider({ events, eventBus, mapProviderConfig: {} })
|
|
107
|
+
expect(provider.events).toBe(events)
|
|
108
|
+
expect(provider.eventBus).toBe(eventBus)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('sets capabilities with supportsMapSizes true and supportedShortcuts array', () => {
|
|
112
|
+
const { provider } = makeProvider()
|
|
113
|
+
expect(provider.capabilities.supportsMapSizes).toBe(true)
|
|
114
|
+
expect(Array.isArray(provider.capabilities.supportedShortcuts)).toBe(true)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('spreads mapProviderConfig properties onto instance', () => {
|
|
118
|
+
const { provider } = makeProvider({ zoomAlignment: 'world', customProp: 'hello' })
|
|
119
|
+
expect(provider.zoomAlignment).toBe('world')
|
|
120
|
+
expect(provider.customProp).toBe('hello')
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
describe('initMap', () => {
|
|
125
|
+
it('creates vector tile layer, OL objects, and emits MAP_READY by default', async () => {
|
|
126
|
+
const { provider, eventBus } = makeProvider()
|
|
127
|
+
await provider.initMap(defaultInitConfig)
|
|
128
|
+
expect(createVectorTileLayer).toHaveBeenCalledWith(defaultInitConfig.mapStyle.url, null)
|
|
129
|
+
expect(attachMapEvents).toHaveBeenCalled()
|
|
130
|
+
expect(attachAppEvents).toHaveBeenCalled()
|
|
131
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({
|
|
132
|
+
map: mockMapInstance,
|
|
133
|
+
mapStyleId: 'myStyle'
|
|
134
|
+
}))
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('calls map.once rendercomplete when bounds are provided', async () => {
|
|
138
|
+
const { provider } = makeProvider()
|
|
139
|
+
let renderCallback
|
|
140
|
+
mockMapOnce.mockImplementation((event, cb) => { renderCallback = cb })
|
|
141
|
+
const bounds = [100, 200, 300, 400]
|
|
142
|
+
await provider.initMap({ ...defaultInitConfig, bounds })
|
|
143
|
+
expect(mockMapOnce).toHaveBeenCalledWith('rendercomplete', expect.any(Function))
|
|
144
|
+
renderCallback()
|
|
145
|
+
expect(mockViewFit).toHaveBeenCalledWith(bounds, { size: [800, 600], duration: 0 })
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('does not call map.once when no bounds', async () => {
|
|
149
|
+
const { provider } = makeProvider()
|
|
150
|
+
await provider.initMap(defaultInitConfig)
|
|
151
|
+
expect(mockMapOnce).not.toHaveBeenCalled()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('calls createVectorTileLayer and skips createTileSource when mapStyle has no type', async () => {
|
|
155
|
+
const { provider } = makeProvider()
|
|
156
|
+
await provider.initMap(defaultInitConfig)
|
|
157
|
+
expect(createVectorTileLayer).toHaveBeenCalled()
|
|
158
|
+
expect(createTileSource).not.toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('calls createTileSource and skips createVectorTileLayer when mapStyle.type is "raster"', async () => {
|
|
162
|
+
const { provider, eventBus } = makeProvider()
|
|
163
|
+
const rasterConfig = {
|
|
164
|
+
...defaultInitConfig,
|
|
165
|
+
mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }
|
|
166
|
+
}
|
|
167
|
+
await provider.initMap(rasterConfig)
|
|
168
|
+
expect(createTileSource).toHaveBeenCalledWith('https://tiles.example.com/{z}/{x}/{y}', null)
|
|
169
|
+
expect(createVectorTileLayer).not.toHaveBeenCalled()
|
|
170
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_READY, expect.objectContaining({ mapStyleId: 'myStyle' }))
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('passes layerType "vector" and map to attachAppEvents by default', async () => {
|
|
174
|
+
const { provider } = makeProvider()
|
|
175
|
+
await provider.initMap(defaultInitConfig)
|
|
176
|
+
expect(attachAppEvents).toHaveBeenCalledWith(expect.objectContaining({
|
|
177
|
+
layerType: 'vector',
|
|
178
|
+
map: mockMapInstance
|
|
179
|
+
}))
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it('passes layerType "raster" to attachAppEvents when mapStyle.type is "raster"', async () => {
|
|
183
|
+
const { provider } = makeProvider()
|
|
184
|
+
const rasterConfig = {
|
|
185
|
+
...defaultInitConfig,
|
|
186
|
+
mapStyle: { id: 'myStyle', url: 'https://tiles.example.com/{z}/{x}/{y}', type: 'raster' }
|
|
187
|
+
}
|
|
188
|
+
await provider.initMap(rasterConfig)
|
|
189
|
+
expect(attachAppEvents).toHaveBeenCalledWith(expect.objectContaining({
|
|
190
|
+
layerType: 'raster'
|
|
191
|
+
}))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it('passes pixelRatio to OlMap constructor', async () => {
|
|
195
|
+
const OlMap = require('ol/Map.js').default
|
|
196
|
+
const { provider } = makeProvider()
|
|
197
|
+
await provider.initMap({ ...defaultInitConfig, pixelRatio: 2 })
|
|
198
|
+
expect(OlMap).toHaveBeenCalledWith(expect.objectContaining({ pixelRatio: 2 }))
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('passes mapProvider to attachAppEvents', async () => {
|
|
202
|
+
const { provider } = makeProvider()
|
|
203
|
+
await provider.initMap(defaultInitConfig)
|
|
204
|
+
expect(attachAppEvents).toHaveBeenCalledWith(expect.objectContaining({ mapProvider: provider }))
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
describe('destroyMap', () => {
|
|
209
|
+
it('removes event handles, nulls map target and all references', async () => {
|
|
210
|
+
const { provider } = makeProvider()
|
|
211
|
+
await provider.initMap(defaultInitConfig)
|
|
212
|
+
jest.clearAllMocks()
|
|
213
|
+
|
|
214
|
+
provider.destroyMap()
|
|
215
|
+
|
|
216
|
+
expect(mockMapEventHandlesRemove).toHaveBeenCalled()
|
|
217
|
+
expect(mockAppEventHandlesRemove).toHaveBeenCalled()
|
|
218
|
+
expect(mockMapSetTarget).toHaveBeenCalledWith(null)
|
|
219
|
+
expect(provider.map).toBeNull()
|
|
220
|
+
expect(provider.view).toBeNull()
|
|
221
|
+
expect(provider.tileLayer).toBeNull()
|
|
222
|
+
expect(provider.source).toBeNull()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('does not throw when called before initMap', () => {
|
|
226
|
+
const { provider } = makeProvider()
|
|
227
|
+
expect(() => provider.destroyMap()).not.toThrow()
|
|
228
|
+
})
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
describe('view control methods', () => {
|
|
232
|
+
let provider
|
|
233
|
+
|
|
234
|
+
beforeEach(async () => {
|
|
235
|
+
;({ provider } = makeProvider())
|
|
236
|
+
await provider.initMap(defaultInitConfig)
|
|
237
|
+
jest.clearAllMocks()
|
|
238
|
+
mockViewInstance.padding = null
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('setView animates to given center and zoom', () => {
|
|
242
|
+
provider.setView({ center: [500000, 400000], zoom: 8 })
|
|
243
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ center: [500000, 400000], zoom: 8 }))
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('setView falls back to current center and zoom when not supplied', () => {
|
|
247
|
+
mockGetCenter.mockReturnValue([400000, 300000])
|
|
248
|
+
mockGetZoom.mockReturnValue(7)
|
|
249
|
+
provider.setView({})
|
|
250
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ center: [400000, 300000], zoom: 7 }))
|
|
251
|
+
})
|
|
252
|
+
|
|
253
|
+
it('zoomIn increments zoom by delta', () => {
|
|
254
|
+
mockGetZoom.mockReturnValue(5)
|
|
255
|
+
provider.zoomIn(2)
|
|
256
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ zoom: 7 }))
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('zoomOut decrements zoom by delta', () => {
|
|
260
|
+
mockGetZoom.mockReturnValue(9)
|
|
261
|
+
provider.zoomOut(3)
|
|
262
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({ zoom: 6 }))
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it('panBy animates center offset scaled by resolution', () => {
|
|
266
|
+
mockGetCenter.mockReturnValue([400000, 300000])
|
|
267
|
+
mockGetResolution.mockReturnValue(56)
|
|
268
|
+
provider.panBy([10, 20])
|
|
269
|
+
expect(mockAnimate).toHaveBeenCalledWith(expect.objectContaining({
|
|
270
|
+
center: [400000 + 10 * 56, 300000 - 20 * 56]
|
|
271
|
+
}))
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('fitToBounds passes an array extent directly to view.fit', () => {
|
|
275
|
+
provider.fitToBounds([1, 2, 3, 4])
|
|
276
|
+
expect(getExtentFromGeoJSON).not.toHaveBeenCalled()
|
|
277
|
+
expect(mockViewFit).toHaveBeenCalledWith([1, 2, 3, 4], expect.any(Object))
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it('fitToBounds converts GeoJSON to extent via getExtentFromGeoJSON', () => {
|
|
281
|
+
const geojson = { type: 'Point', coordinates: [0, 51] }
|
|
282
|
+
provider.fitToBounds(geojson)
|
|
283
|
+
expect(getExtentFromGeoJSON).toHaveBeenCalledWith(geojson)
|
|
284
|
+
expect(mockViewFit).toHaveBeenCalledWith([10, 20, 30, 40], expect.any(Object))
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
it('setPadding converts object to OL [top, right, bottom, left] array', () => {
|
|
288
|
+
provider.setPadding({ top: 10, right: 20, bottom: 30, left: 40 })
|
|
289
|
+
expect(mockViewInstance.padding).toEqual([10, 20, 30, 40])
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('setPadding fills missing keys with zero', () => {
|
|
293
|
+
provider.setPadding({ top: 10 })
|
|
294
|
+
expect(mockViewInstance.padding).toEqual([10, 0, 0, 0])
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it('setPadding with null sets padding to undefined', () => {
|
|
298
|
+
provider.setPadding(null)
|
|
299
|
+
expect(mockViewInstance.padding).toBeUndefined()
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
describe('getter methods', () => {
|
|
304
|
+
let provider
|
|
305
|
+
|
|
306
|
+
beforeEach(async () => {
|
|
307
|
+
;({ provider } = makeProvider())
|
|
308
|
+
await provider.initMap(defaultInitConfig)
|
|
309
|
+
jest.clearAllMocks()
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('getCenter rounds coordinates to 2 decimal places', () => {
|
|
313
|
+
mockGetCenter.mockReturnValue([400000.126, 300000.455])
|
|
314
|
+
expect(provider.getCenter()).toEqual([400000.13, 300000.46])
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('getZoom delegates to view.getZoom', () => {
|
|
318
|
+
mockGetZoom.mockReturnValue(9)
|
|
319
|
+
expect(provider.getZoom()).toBe(9)
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
it('getBounds rounds extent values to 2 decimal places', () => {
|
|
323
|
+
mockCalculateExtent.mockReturnValue([1.126, 2.456, 3.994, 4.0])
|
|
324
|
+
expect(provider.getBounds()).toEqual([1.13, 2.46, 3.99, 4])
|
|
325
|
+
})
|
|
326
|
+
|
|
327
|
+
it('getFeaturesAtPoint returns empty array', () => {
|
|
328
|
+
expect(provider.getFeaturesAtPoint()).toEqual([])
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('getVisibleFeatures returns empty array', () => {
|
|
332
|
+
expect(provider.getVisibleFeatures()).toEqual([])
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
it('getResolution delegates to view.getResolution', () => {
|
|
336
|
+
mockGetResolution.mockReturnValue(28)
|
|
337
|
+
expect(provider.getResolution()).toBe(28)
|
|
338
|
+
})
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
describe('spatial helper methods', () => {
|
|
342
|
+
let provider
|
|
343
|
+
|
|
344
|
+
beforeEach(async () => {
|
|
345
|
+
;({ provider } = makeProvider())
|
|
346
|
+
await provider.initMap(defaultInitConfig)
|
|
347
|
+
jest.clearAllMocks()
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it('getAreaDimensions delegates to spatial utils', () => {
|
|
351
|
+
expect(provider.getAreaDimensions()).toBe('1 mile by 2 miles')
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
it('getCardinalMove delegates to spatial utils', () => {
|
|
355
|
+
expect(provider.getCardinalMove([0, 0], [100, 100])).toBe('north 100m')
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
it('mapToScreen returns pixel as {x, y}', () => {
|
|
359
|
+
mockMapGetPixel.mockReturnValue([150, 250])
|
|
360
|
+
expect(provider.mapToScreen([400000, 300000])).toEqual({ x: 150, y: 250 })
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('mapToScreen returns {0, 0} when getPixelFromCoordinate returns null', () => {
|
|
364
|
+
mockMapGetPixel.mockReturnValue(null)
|
|
365
|
+
expect(provider.mapToScreen([400000, 300000])).toEqual({ x: 0, y: 0 })
|
|
366
|
+
})
|
|
367
|
+
|
|
368
|
+
it('mapToScreen returns {0, 0} when map is not set', () => {
|
|
369
|
+
provider.map = null
|
|
370
|
+
expect(provider.mapToScreen([400000, 300000])).toEqual({ x: 0, y: 0 })
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
it('screenToMap delegates to map.getCoordinateFromPixel', () => {
|
|
374
|
+
mockMapGetCoord.mockReturnValue([400000, 300000])
|
|
375
|
+
expect(provider.screenToMap({ x: 100, y: 200 })).toEqual([400000, 300000])
|
|
376
|
+
expect(mockMapGetCoord).toHaveBeenCalledWith([100, 200])
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('isGeometryObscured delegates to spatial utils with map instance', () => {
|
|
380
|
+
const geojson = { type: 'Point', coordinates: [0, 51] }
|
|
381
|
+
const panelRect = { left: 0, top: 0, right: 100, bottom: 100 }
|
|
382
|
+
provider.isGeometryObscured(geojson, panelRect)
|
|
383
|
+
expect(isGeometryObscured).toHaveBeenCalledWith(geojson, panelRect, mockMapInstance)
|
|
384
|
+
})
|
|
385
|
+
})
|
|
386
|
+
})
|