@defra/interactive-map 0.0.7-alpha → 0.0.9-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/templates/map.njk +2 -2
- package/dist/esm/im-core.js +1 -2
- package/dist/esm/im-shell.js +1 -0
- package/dist/esm/index.js +1 -2
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +104 -3
- package/docs/api.md +21 -1
- package/docs/getting-started.md +78 -8
- package/package.json +31 -24
- package/plugins/beta/datasets/dist/css/index.css +50 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -2
- package/plugins/beta/datasets/dist/esm/index.js +1 -2
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -2
- package/plugins/beta/draw-es/dist/esm/index.js +1 -2
- package/plugins/beta/draw-ml/dist/esm/im-draw-ml-plugin.js +1 -1
- package/plugins/beta/draw-ml/dist/esm/index.js +1 -2
- package/plugins/beta/frame/dist/css/index.css +11 -1
- package/plugins/beta/frame/dist/esm/im-frame-plugin.js +1 -1
- package/plugins/beta/frame/dist/esm/index.js +1 -2
- package/plugins/beta/map-styles/dist/css/index.css +79 -1
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/esm/index.js +1 -2
- package/plugins/beta/scale-bar/dist/esm/im-scale-bar-plugin.js +1 -1
- package/plugins/beta/scale-bar/dist/esm/index.js +1 -2
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/esm/index.js +1 -2
- package/plugins/beta/use-location/dist/umd/index.js +1 -1
- package/plugins/interact/dist/esm/im-interact-plugin.js +1 -1
- package/plugins/interact/dist/esm/index.js +1 -2
- package/plugins/search/dist/esm/im-search-plugin.js +1 -2
- package/plugins/search/dist/esm/index.js +1 -2
- package/plugins/search/src/components/Suggestions/Suggestions.module.scss +1 -1
- package/plugins/search/src/search.scss +1 -1
- package/providers/beta/esri/dist/css/index.css +30 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -2
- package/providers/beta/esri/dist/esm/index.js +1 -2
- package/providers/beta/open-names/dist/esm/im-reverse-geocode.js +1 -2
- package/providers/beta/open-names/dist/esm/index.js +1 -2
- package/providers/beta/open-names/src/utils/mapToLocationModel.test.js +61 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -2
- package/providers/maplibre/dist/esm/index.js +1 -2
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/src/appEvents.test.js +44 -0
- package/providers/maplibre/src/index.test.js +60 -0
- package/providers/maplibre/src/mapEvents.test.js +115 -0
- package/providers/maplibre/src/maplibreProvider.test.js +205 -0
- package/providers/maplibre/src/utils/calculateLinearTextSize.test.js +31 -0
- package/providers/maplibre/src/utils/detectWebgl.test.js +63 -0
- package/providers/maplibre/src/utils/highlightFeatures.test.js +126 -0
- package/providers/maplibre/src/utils/labels.js +1 -3
- package/providers/maplibre/src/utils/labels.test.js +231 -0
- package/providers/maplibre/src/utils/maplibreFixes.test.js +66 -0
- package/providers/maplibre/src/utils/queryFeatures.test.js +60 -0
- package/providers/maplibre/src/utils/spatial.js +5 -4
- package/providers/maplibre/src/utils/spatial.test.js +96 -0
- package/rollup.esm.mjs +288 -0
- package/src/App/store/appActionsMap.js +1 -1
- package/src/InteractiveMap/InteractiveMap.js +3 -2
- package/webpack.dev.mjs +9 -1
- package/webpack.prod.mjs +8 -1
- package/webpack.umd.mjs +1 -3
- package/dist/esm/im-core.js.LICENSE.txt +0 -1
- package/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js.LICENSE.txt +0 -1
- package/plugins/beta/datasets/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js.LICENSE.txt +0 -1
- package/plugins/beta/draw-es/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/draw-ml/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/frame/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/map-styles/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/scale-bar/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/beta/use-location/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/interact/dist/esm/index.js.LICENSE.txt +0 -1
- package/plugins/search/dist/esm/im-search-plugin.js.LICENSE.txt +0 -1
- package/plugins/search/dist/esm/index.js.LICENSE.txt +0 -1
- package/providers/beta/esri/dist/css/im-esri-provider.css +0 -1
- package/providers/beta/esri/dist/esm/im-esri-provider.js.LICENSE.txt +0 -1
- package/providers/beta/esri/dist/esm/index.js.LICENSE.txt +0 -1
- package/providers/beta/open-names/dist/esm/im-reverse-geocode.js.LICENSE.txt +0 -1
- package/providers/beta/open-names/dist/esm/index.js.LICENSE.txt +0 -1
- package/providers/maplibre/dist/esm/im-maplibre-framework.js +0 -2
- package/providers/maplibre/dist/esm/im-maplibre-framework.js.LICENSE.txt +0 -4
- package/providers/maplibre/dist/esm/im-maplibre-provider.js.LICENSE.txt +0 -1
- package/providers/maplibre/dist/esm/index.js.LICENSE.txt +0 -1
- package/webpack.esm.mjs +0 -153
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
2
|
+
|
|
3
|
+
jest.mock('../../../src/utils/debounce.js', () => ({
|
|
4
|
+
debounce: jest.fn(fn => {
|
|
5
|
+
const d = jest.fn((...args) => fn(...args))
|
|
6
|
+
d.cancel = jest.fn()
|
|
7
|
+
return d
|
|
8
|
+
})
|
|
9
|
+
}))
|
|
10
|
+
|
|
11
|
+
jest.mock('../../../src/utils/throttle.js', () => ({
|
|
12
|
+
throttle: jest.fn(fn => {
|
|
13
|
+
const t = jest.fn((...args) => fn(...args))
|
|
14
|
+
t.cancel = jest.fn()
|
|
15
|
+
return t
|
|
16
|
+
})
|
|
17
|
+
}))
|
|
18
|
+
|
|
19
|
+
describe('attachMapEvents', () => {
|
|
20
|
+
let map, eventBus, events, getCenter, getZoom, getBounds, getResolution, result
|
|
21
|
+
|
|
22
|
+
const handler = (evt) => map.on.mock.calls.find(([e]) => e === evt)[1]
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
map = {
|
|
26
|
+
on: jest.fn(),
|
|
27
|
+
once: jest.fn(),
|
|
28
|
+
off: jest.fn(),
|
|
29
|
+
getMaxZoom: jest.fn(() => 20),
|
|
30
|
+
getMinZoom: jest.fn(() => 0)
|
|
31
|
+
}
|
|
32
|
+
eventBus = { emit: jest.fn() }
|
|
33
|
+
events = {
|
|
34
|
+
MAP_LOADED: 'loaded', MAP_FIRST_IDLE: 'firstIdle', MAP_MOVE_START: 'moveStart',
|
|
35
|
+
MAP_MOVE_END: 'moveEnd', MAP_MOVE: 'move', MAP_RENDER: 'render',
|
|
36
|
+
MAP_DATA_CHANGE: 'dataChange', MAP_STYLE_CHANGE: 'styleChange', MAP_CLICK: 'click'
|
|
37
|
+
}
|
|
38
|
+
getCenter = jest.fn(() => [0, 0])
|
|
39
|
+
getZoom = jest.fn(() => 10)
|
|
40
|
+
getBounds = jest.fn(() => [[0, 0], [1, 1]])
|
|
41
|
+
getResolution = jest.fn(() => 100)
|
|
42
|
+
result = attachMapEvents({ map, events, eventBus, getCenter, getZoom, getBounds, getResolution })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('registers listeners for all map events', () => {
|
|
46
|
+
;['load', 'movestart', 'moveend', 'zoom', 'render', 'styledata', 'style.load', 'click'].forEach(e =>
|
|
47
|
+
expect(map.on).toHaveBeenCalledWith(e, expect.any(Function))
|
|
48
|
+
)
|
|
49
|
+
expect(map.once).toHaveBeenCalledWith('idle', expect.any(Function))
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('stateless events emit correct event names', () => {
|
|
53
|
+
handler('load')()
|
|
54
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_LOADED, undefined)
|
|
55
|
+
handler('movestart')()
|
|
56
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_START, undefined)
|
|
57
|
+
handler('render')()
|
|
58
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_RENDER, undefined)
|
|
59
|
+
handler('style.load')()
|
|
60
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_STYLE_CHANGE, undefined)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
test('stateful events emit map state', () => {
|
|
64
|
+
const state = {
|
|
65
|
+
center: [0, 0], bounds: [[0, 0], [1, 1]], resolution: 100,
|
|
66
|
+
zoom: 10, isAtMaxZoom: false, isAtMinZoom: false
|
|
67
|
+
}
|
|
68
|
+
map.once.mock.calls.find(([e]) => e === 'idle')[1]()
|
|
69
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_FIRST_IDLE, state)
|
|
70
|
+
handler('moveend')()
|
|
71
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE_END, state)
|
|
72
|
+
handler('zoom')()
|
|
73
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_MOVE, state)
|
|
74
|
+
handler('styledata')()
|
|
75
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_DATA_CHANGE, state)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('getMapState: isAtMaxZoom true at max zoom; isAtMinZoom true at min zoom', () => {
|
|
79
|
+
getZoom.mockReturnValue(20)
|
|
80
|
+
handler('moveend')()
|
|
81
|
+
expect(eventBus.emit).toHaveBeenLastCalledWith(events.MAP_MOVE_END,
|
|
82
|
+
expect.objectContaining({ isAtMaxZoom: true, isAtMinZoom: false })
|
|
83
|
+
)
|
|
84
|
+
eventBus.emit.mockClear()
|
|
85
|
+
getZoom.mockReturnValue(0)
|
|
86
|
+
handler('moveend')()
|
|
87
|
+
expect(eventBus.emit).toHaveBeenLastCalledWith(events.MAP_MOVE_END,
|
|
88
|
+
expect.objectContaining({ isAtMaxZoom: false, isAtMinZoom: true })
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('click emits MAP_CLICK with point and coords', () => {
|
|
93
|
+
handler('click')({ point: { x: 10, y: 20 }, lngLat: { lng: -1.5, lat: 52.3 } })
|
|
94
|
+
expect(eventBus.emit).toHaveBeenCalledWith(events.MAP_CLICK, {
|
|
95
|
+
point: { x: 10, y: 20 },
|
|
96
|
+
coords: [-1.5, 52.3]
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('remove() cancels debouncers and unregisters all handlers', () => {
|
|
101
|
+
const moveEndFn = handler('moveend')
|
|
102
|
+
const moveFn = handler('zoom')
|
|
103
|
+
const dataChangeFn = handler('styledata')
|
|
104
|
+
|
|
105
|
+
result.remove()
|
|
106
|
+
|
|
107
|
+
expect(moveEndFn.cancel).toHaveBeenCalled()
|
|
108
|
+
expect(moveFn.cancel).toHaveBeenCalled()
|
|
109
|
+
expect(dataChangeFn.cancel).toHaveBeenCalled()
|
|
110
|
+
expect(map.off).toHaveBeenCalledTimes(8)
|
|
111
|
+
;['load', 'movestart', 'moveend', 'zoom', 'render', 'styledata', 'style.load', 'click'].forEach(e =>
|
|
112
|
+
expect(map.off).toHaveBeenCalledWith(e, expect.any(Function))
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import MapLibreProvider from './maplibreProvider.js'
|
|
2
|
+
import { attachMapEvents } from './mapEvents.js'
|
|
3
|
+
import { attachAppEvents } from './appEvents.js'
|
|
4
|
+
import { createMapLabelNavigator } from './utils/labels.js'
|
|
5
|
+
import { updateHighlightedFeatures } from './utils/highlightFeatures.js'
|
|
6
|
+
import { queryFeatures } from './utils/queryFeatures.js'
|
|
7
|
+
import { getAreaDimensions, getCardinalMove, getResolution, getPaddedBounds } from './utils/spatial.js'
|
|
8
|
+
|
|
9
|
+
jest.mock('./defaults.js', () => ({
|
|
10
|
+
DEFAULTS: { animationDuration: 400, coordinatePrecision: 7 },
|
|
11
|
+
supportedShortcuts: []
|
|
12
|
+
}))
|
|
13
|
+
jest.mock('./utils/maplibreFixes.js', () => ({
|
|
14
|
+
cleanCanvas: jest.fn(),
|
|
15
|
+
applyPreventDefaultFix: jest.fn()
|
|
16
|
+
}))
|
|
17
|
+
jest.mock('./mapEvents.js', () => ({ attachMapEvents: jest.fn() }))
|
|
18
|
+
jest.mock('./appEvents.js', () => ({ attachAppEvents: jest.fn() }))
|
|
19
|
+
jest.mock('./utils/spatial.js', () => ({
|
|
20
|
+
getAreaDimensions: jest.fn(() => '400m by 750m'),
|
|
21
|
+
getCardinalMove: jest.fn(() => 'north'),
|
|
22
|
+
getResolution: jest.fn(() => 10),
|
|
23
|
+
getPaddedBounds: jest.fn(() => [[0, 0], [1, 1]])
|
|
24
|
+
}))
|
|
25
|
+
jest.mock('./utils/labels.js', () => ({
|
|
26
|
+
createMapLabelNavigator: jest.fn(() => ({
|
|
27
|
+
highlightNextLabel: jest.fn(() => 'next'),
|
|
28
|
+
highlightLabelAtCenter: jest.fn(() => 'center'),
|
|
29
|
+
clearHighlightedLabel: jest.fn(() => 'cleared')
|
|
30
|
+
}))
|
|
31
|
+
}))
|
|
32
|
+
jest.mock('./utils/highlightFeatures.js', () => ({ updateHighlightedFeatures: jest.fn(() => []) }))
|
|
33
|
+
jest.mock('./utils/queryFeatures.js', () => ({ queryFeatures: jest.fn(() => []) }))
|
|
34
|
+
|
|
35
|
+
describe('MapLibreProvider', () => {
|
|
36
|
+
let map, eventBus, maplibreModule, loadCallback
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
loadCallback = null
|
|
40
|
+
map = {
|
|
41
|
+
touchZoomRotate: { disableRotation: jest.fn() },
|
|
42
|
+
on: jest.fn((e, fn) => { if (e === 'load') loadCallback = fn }),
|
|
43
|
+
off: jest.fn(),
|
|
44
|
+
remove: jest.fn(),
|
|
45
|
+
setPadding: jest.fn(),
|
|
46
|
+
fitBounds: jest.fn(),
|
|
47
|
+
flyTo: jest.fn(),
|
|
48
|
+
easeTo: jest.fn(),
|
|
49
|
+
panBy: jest.fn(),
|
|
50
|
+
project: jest.fn(() => ({ x: 100, y: 200 })),
|
|
51
|
+
unproject: jest.fn(() => ({ lng: 1, lat: 2 })),
|
|
52
|
+
getCenter: jest.fn(() => ({ lng: 1.2345678, lat: 2.3456789 })),
|
|
53
|
+
getZoom: jest.fn(() => 10),
|
|
54
|
+
getBounds: jest.fn(() => ({ toArray: jest.fn(() => [[0, 0], [1, 1]]) }))
|
|
55
|
+
}
|
|
56
|
+
eventBus = { emit: jest.fn() }
|
|
57
|
+
maplibreModule = { Map: jest.fn(() => map), LngLatBounds: jest.fn() }
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const makeProvider = (config = {}) => new MapLibreProvider({
|
|
61
|
+
mapFramework: maplibreModule,
|
|
62
|
+
events: { MAP_READY: 'map:ready' },
|
|
63
|
+
eventBus,
|
|
64
|
+
...config
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const doInitMap = (p, extra = {}) => p.initMap({
|
|
68
|
+
container: 'div', padding: {}, mapStyle: { url: 'style.json', mapColorScheme: 'dark' },
|
|
69
|
+
center: [0, 0], zoom: 5, ...extra
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('constructor spreads mapProviderConfig and sets capabilities', () => {
|
|
73
|
+
const p = makeProvider({ mapProviderConfig: { crs: 'EPSG:4326', tileSize: 512 } })
|
|
74
|
+
expect(p.crs).toBe('EPSG:4326')
|
|
75
|
+
expect(p.tileSize).toBe(512)
|
|
76
|
+
expect(p.capabilities.supportsMapSizes).toBe(true)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('initMap: creates map, disables rotation, attaches events, emits MAP_READY', async () => {
|
|
80
|
+
const p = makeProvider()
|
|
81
|
+
await doInitMap(p)
|
|
82
|
+
expect(maplibreModule.Map).toHaveBeenCalledWith(expect.objectContaining({ style: 'style.json' }))
|
|
83
|
+
expect(map.touchZoomRotate.disableRotation).toHaveBeenCalled()
|
|
84
|
+
expect(map.setPadding).toHaveBeenCalled()
|
|
85
|
+
expect(attachMapEvents).toHaveBeenCalled()
|
|
86
|
+
expect(attachAppEvents).toHaveBeenCalled()
|
|
87
|
+
expect(eventBus.emit).toHaveBeenCalledWith('map:ready', expect.any(Object))
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('initMap: fitBounds called when bounds provided; skipped when absent; null mapStyle → style undefined', async () => {
|
|
91
|
+
const p = makeProvider()
|
|
92
|
+
await doInitMap(p, { bounds: [[0, 0], [1, 1]] })
|
|
93
|
+
expect(map.fitBounds).toHaveBeenCalledWith([[0, 0], [1, 1]], { duration: 0 })
|
|
94
|
+
|
|
95
|
+
map.fitBounds.mockClear()
|
|
96
|
+
const p2 = makeProvider()
|
|
97
|
+
await p2.initMap({ container: 'div', padding: {}, mapStyle: null, center: [0, 0], zoom: 5 })
|
|
98
|
+
expect(map.fitBounds).not.toHaveBeenCalled()
|
|
99
|
+
expect(maplibreModule.Map).toHaveBeenLastCalledWith(expect.objectContaining({ style: undefined }))
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
test('load callback: creates labelNavigator with mapColorScheme; undefined when mapStyle null', async () => {
|
|
103
|
+
const p = makeProvider()
|
|
104
|
+
await doInitMap(p)
|
|
105
|
+
loadCallback()
|
|
106
|
+
expect(createMapLabelNavigator).toHaveBeenCalledWith(map, 'dark', expect.anything(), eventBus)
|
|
107
|
+
expect(p.labelNavigator).toBeDefined()
|
|
108
|
+
|
|
109
|
+
createMapLabelNavigator.mockClear()
|
|
110
|
+
const p2 = makeProvider()
|
|
111
|
+
await p2.initMap({ container: 'div', padding: {}, mapStyle: null, center: [0, 0], zoom: 5 })
|
|
112
|
+
loadCallback()
|
|
113
|
+
expect(createMapLabelNavigator).toHaveBeenCalledWith(map, undefined, expect.anything(), eventBus)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
test('destroyMap: calls remove on mapEvents/appEvents if set; skips if absent', async () => {
|
|
117
|
+
const p = makeProvider()
|
|
118
|
+
await doInitMap(p)
|
|
119
|
+
const mockRemove = jest.fn()
|
|
120
|
+
p.mapEvents = { remove: mockRemove }
|
|
121
|
+
p.appEvents = { remove: mockRemove }
|
|
122
|
+
p.destroyMap()
|
|
123
|
+
expect(mockRemove).toHaveBeenCalledTimes(2)
|
|
124
|
+
expect(map.remove).toHaveBeenCalled()
|
|
125
|
+
|
|
126
|
+
const p2 = makeProvider()
|
|
127
|
+
await doInitMap(p2)
|
|
128
|
+
expect(() => p2.destroyMap()).not.toThrow()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('setView: uses provided center/zoom; falls back to getCenter/getZoom when omitted', async () => {
|
|
132
|
+
const p = makeProvider()
|
|
133
|
+
await doInitMap(p)
|
|
134
|
+
p.setView({ center: [1, 2], zoom: 8 })
|
|
135
|
+
expect(map.flyTo).toHaveBeenCalledWith({ center: [1, 2], zoom: 8, duration: 400 })
|
|
136
|
+
|
|
137
|
+
map.flyTo.mockClear()
|
|
138
|
+
p.setView({})
|
|
139
|
+
const call = map.flyTo.mock.calls[0][0]
|
|
140
|
+
expect(call.center).toBeDefined()
|
|
141
|
+
expect(call.zoom).toBe(10)
|
|
142
|
+
expect(call.duration).toBe(400)
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
test('zoomIn, zoomOut, panBy, fitToBounds, setPadding delegate to map', async () => {
|
|
146
|
+
const p = makeProvider()
|
|
147
|
+
await doInitMap(p)
|
|
148
|
+
p.zoomIn(1)
|
|
149
|
+
expect(map.easeTo).toHaveBeenCalledWith({ zoom: 11, duration: 400 })
|
|
150
|
+
p.zoomOut(2)
|
|
151
|
+
expect(map.easeTo).toHaveBeenCalledWith({ zoom: 8, duration: 400 })
|
|
152
|
+
p.panBy([10, 20])
|
|
153
|
+
expect(map.panBy).toHaveBeenCalledWith([10, 20], { duration: 400 })
|
|
154
|
+
p.fitToBounds([[0, 0], [1, 1]])
|
|
155
|
+
expect(map.fitBounds).toHaveBeenCalledWith([[0, 0], [1, 1]], { duration: 400 })
|
|
156
|
+
p.setPadding({ top: 5 })
|
|
157
|
+
expect(map.setPadding).toHaveBeenCalledWith({ top: 5 })
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('getCenter, getZoom, getBounds return formatted values', async () => {
|
|
161
|
+
const p = makeProvider()
|
|
162
|
+
await doInitMap(p)
|
|
163
|
+
expect(p.getCenter()).toEqual([1.2345678, 2.3456789])
|
|
164
|
+
expect(p.getZoom()).toBe(10)
|
|
165
|
+
expect(p.getBounds()).toEqual([0, 0, 1, 1])
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
test('spatial helpers delegate correctly', async () => {
|
|
169
|
+
const p = makeProvider()
|
|
170
|
+
await doInitMap(p)
|
|
171
|
+
expect(p.getAreaDimensions()).toBe('400m by 750m')
|
|
172
|
+
expect(getPaddedBounds).toHaveBeenCalled()
|
|
173
|
+
expect(getAreaDimensions).toHaveBeenCalled()
|
|
174
|
+
expect(p.getCardinalMove([0, 0], [1, 1])).toBe('north')
|
|
175
|
+
expect(getCardinalMove).toHaveBeenCalledWith([0, 0], [1, 1])
|
|
176
|
+
p.getResolution()
|
|
177
|
+
expect(getResolution).toHaveBeenCalled()
|
|
178
|
+
expect(p.mapToScreen([1, 2])).toEqual({ x: 100, y: 200 })
|
|
179
|
+
expect(p.screenToMap({ x: 100, y: 200 })).toEqual([1, 2])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
test('getFeaturesAtPoint and updateHighlightedFeatures delegate correctly', async () => {
|
|
183
|
+
const p = makeProvider()
|
|
184
|
+
await doInitMap(p)
|
|
185
|
+
p.getFeaturesAtPoint({ x: 10, y: 20 }, { radius: 5 })
|
|
186
|
+
expect(queryFeatures).toHaveBeenCalledWith(map, { x: 10, y: 20 }, { radius: 5 })
|
|
187
|
+
p.updateHighlightedFeatures(['feat'], { style: 1 })
|
|
188
|
+
expect(updateHighlightedFeatures).toHaveBeenCalledWith({
|
|
189
|
+
LngLatBounds: maplibreModule.LngLatBounds, map, selectedFeatures: ['feat'], stylesMap: { style: 1 }
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
test('label methods return null without labelNavigator; delegate when set', async () => {
|
|
194
|
+
const p = makeProvider()
|
|
195
|
+
await doInitMap(p)
|
|
196
|
+
expect(p.highlightNextLabel('ArrowRight')).toBeNull()
|
|
197
|
+
expect(p.highlightLabelAtCenter()).toBeNull()
|
|
198
|
+
expect(p.clearHighlightedLabel()).toBeNull()
|
|
199
|
+
|
|
200
|
+
loadCallback()
|
|
201
|
+
expect(p.highlightNextLabel('ArrowRight')).toBe('next')
|
|
202
|
+
expect(p.highlightLabelAtCenter()).toBe('center')
|
|
203
|
+
expect(p.clearHighlightedLabel()).toBe('cleared')
|
|
204
|
+
})
|
|
205
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { calculateLinearTextSize } from './calculateLinearTextSize.js'
|
|
2
|
+
|
|
3
|
+
describe('calculateLinearTextSize', () => {
|
|
4
|
+
it('returns 0 if stops is empty', () => {
|
|
5
|
+
expect(calculateLinearTextSize({ stops: [] }, 5)).toBe(0)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
it('returns single stop value if stops has one entry', () => {
|
|
9
|
+
expect(calculateLinearTextSize({ stops: [[3, 10]] }, 5)).toBe(10)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('returns lower stop if zoom below first stop', () => {
|
|
13
|
+
expect(calculateLinearTextSize({ stops: [[3, 10], [6, 20]] }, 2)).toBe(10)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('returns upper stop if zoom above last stop', () => {
|
|
17
|
+
expect(calculateLinearTextSize({ stops: [[3, 10], [6, 20]] }, 7)).toBe(20)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('interpolates between stops for zoom in range', () => {
|
|
21
|
+
expect(calculateLinearTextSize({ stops: [[3, 10], [6, 20]] }, 4.5)).toBe(15)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('works with multiple stops', () => {
|
|
25
|
+
const expr = { stops: [[0, 5], [5, 15], [10, 25]] }
|
|
26
|
+
expect(calculateLinearTextSize(expr, -1)).toBe(5) // below first
|
|
27
|
+
expect(calculateLinearTextSize(expr, 12)).toBe(25) // above last
|
|
28
|
+
expect(calculateLinearTextSize(expr, 2.5)).toBe(10) // between first two
|
|
29
|
+
expect(calculateLinearTextSize(expr, 7.5)).toBe(20) // between last two
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { getWebGL } from './detectWebgl.js'
|
|
2
|
+
|
|
3
|
+
describe('getWebGL', () => {
|
|
4
|
+
const originalWebGL = window.WebGLRenderingContext
|
|
5
|
+
const originalCreateElement = document.createElement
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
window.WebGLRenderingContext = originalWebGL
|
|
9
|
+
document.createElement = originalCreateElement
|
|
10
|
+
jest.restoreAllMocks()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns disabled if WebGL is not supported', () => {
|
|
14
|
+
window.WebGLRenderingContext = undefined
|
|
15
|
+
expect(getWebGL(['webgl'])).toEqual({
|
|
16
|
+
isEnabled: false,
|
|
17
|
+
error: 'WebGL is not supported'
|
|
18
|
+
})
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('returns enabled if WebGL context is created successfully', () => {
|
|
22
|
+
window.WebGLRenderingContext = class {}
|
|
23
|
+
const fakeContext = { getParameter: () => true }
|
|
24
|
+
document.createElement = jest.fn(() => ({
|
|
25
|
+
getContext: jest.fn(() => fakeContext)
|
|
26
|
+
}))
|
|
27
|
+
expect(getWebGL(['webgl'])).toEqual({ isEnabled: true })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('returns disabled if WebGL is supported but context fails', () => {
|
|
31
|
+
window.WebGLRenderingContext = class {}
|
|
32
|
+
document.createElement = jest.fn(() => ({
|
|
33
|
+
getContext: jest.fn(() => null)
|
|
34
|
+
}))
|
|
35
|
+
expect(getWebGL(['webgl'])).toEqual({
|
|
36
|
+
isEnabled: false,
|
|
37
|
+
error: 'WebGL is supported, but disabled'
|
|
38
|
+
})
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('tries multiple context names and succeeds on second', () => {
|
|
42
|
+
window.WebGLRenderingContext = class {}
|
|
43
|
+
let call = 0
|
|
44
|
+
document.createElement = jest.fn(() => ({
|
|
45
|
+
getContext: jest.fn(() => {
|
|
46
|
+
call++
|
|
47
|
+
return call === 2 ? { getParameter: () => true } : null
|
|
48
|
+
})
|
|
49
|
+
}))
|
|
50
|
+
expect(getWebGL(['webgl1', 'webgl2'])).toEqual({ isEnabled: true })
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('catches errors from getContext and continues', () => {
|
|
54
|
+
window.WebGLRenderingContext = class {}
|
|
55
|
+
document.createElement = jest.fn(() => ({
|
|
56
|
+
getContext: jest.fn(() => { throw new Error('fail') })
|
|
57
|
+
}))
|
|
58
|
+
expect(getWebGL(['webgl', 'webgl2'])).toEqual({
|
|
59
|
+
isEnabled: false,
|
|
60
|
+
error: 'WebGL is supported, but disabled'
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { updateHighlightedFeatures } from './highlightFeatures.js'
|
|
2
|
+
|
|
3
|
+
describe('Highlighting Utils', () => {
|
|
4
|
+
let map
|
|
5
|
+
const LngLatBounds = function() {
|
|
6
|
+
this.coords = []
|
|
7
|
+
this.extend = (c) => this.coords.push(c)
|
|
8
|
+
this.getWest = () => Math.min(...this.coords.map(c => c[0]))
|
|
9
|
+
this.getSouth = () => Math.min(...this.coords.map(c => c[1]))
|
|
10
|
+
this.getEast = () => Math.max(...this.coords.map(c => c[0]))
|
|
11
|
+
this.getNorth = () => Math.max(...this.coords.map(c => c[1]))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
map = {
|
|
16
|
+
_highlightedSources: new Set(['stale']),
|
|
17
|
+
getLayer: jest.fn(),
|
|
18
|
+
addLayer: jest.fn(),
|
|
19
|
+
setFilter: jest.fn(),
|
|
20
|
+
setPaintProperty: jest.fn(),
|
|
21
|
+
queryRenderedFeatures: jest.fn()
|
|
22
|
+
}
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('All branches', () => {
|
|
26
|
+
// Coverage for Line 93: Null map check
|
|
27
|
+
expect(updateHighlightedFeatures({ map: null })).toBeNull()
|
|
28
|
+
|
|
29
|
+
map.getLayer.mockImplementation((id) => {
|
|
30
|
+
if (id.includes('stale')) return true // Coverage for Line 49
|
|
31
|
+
if (id === 'l1') return { source: 's1', type: 'fill' }
|
|
32
|
+
if (id === 'l2') return { source: 's2', type: 'line' }
|
|
33
|
+
if (id === 'highlight-s2-fill') return true // Coverage for Line 124
|
|
34
|
+
return null // Coverage for Line 13
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const selectedFeatures = [
|
|
38
|
+
// Coverage for Lines 37-40: Polygon & MultiPolygon checks
|
|
39
|
+
{ featureId: 1, layerId: 'l1', geometry: { type: 'Polygon' } },
|
|
40
|
+
{ featureId: 2, layerId: 'l1', geometry: { type: 'MultiPolygon' } },
|
|
41
|
+
// Coverage for Line 13: Invalid layer
|
|
42
|
+
{ featureId: 3, layerId: 'invalid' },
|
|
43
|
+
// Coverage for Line 116: idProperty exists
|
|
44
|
+
{ featureId: 4, layerId: 'l2', idProperty: 'customId', geometry: { type: 'Point' } }
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
const stylesMap = {
|
|
48
|
+
l1: { stroke: 'red', fill: 'blue' },
|
|
49
|
+
l2: { stroke: 'green' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Coverage for Lines 78-80: Recursive coordinate handling (numbers vs arrays)
|
|
53
|
+
map.queryRenderedFeatures.mockReturnValue([
|
|
54
|
+
{ id: 1, geometry: { coordinates: [10, 10] } }, // Simple point
|
|
55
|
+
{ id: 2, properties: { customId: 4 }, geometry: { coordinates: [[0, 0], [5, 5]] } } // Nested
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const bounds = updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures, stylesMap })
|
|
59
|
+
|
|
60
|
+
// Line 13 verify: map.getLayer returned null and function returned early
|
|
61
|
+
// Line 49-50 verify: Stale sources filtered out
|
|
62
|
+
expect(map.setFilter).toHaveBeenCalledWith('highlight-stale-fill', ['==', 'id', ''])
|
|
63
|
+
|
|
64
|
+
// Line 124 verify: Clear fill highlight when switching to line geometry
|
|
65
|
+
expect(map.setFilter).toHaveBeenCalledWith('highlight-s2-fill', ['==', 'id', ''])
|
|
66
|
+
|
|
67
|
+
// Line 116 verify: Using ['get', idProperty]
|
|
68
|
+
expect(map.setFilter).toHaveBeenCalledWith('highlight-s2-line', expect.arrayContaining([['get', 'customId']]))
|
|
69
|
+
|
|
70
|
+
// Line 80-82 verify: Recursive LngLatBounds logic
|
|
71
|
+
expect(bounds).toEqual([0, 0, 10, 10])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('undefined _highlightedSources falls back to empty set; line geom skips absent fill layer', () => {
|
|
75
|
+
// line 93: || new Set() fallback; line 124 false: no pre-existing fill to clear
|
|
76
|
+
map._highlightedSources = undefined
|
|
77
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null)
|
|
78
|
+
map.queryRenderedFeatures.mockReturnValue([])
|
|
79
|
+
updateHighlightedFeatures({ LngLatBounds, map,
|
|
80
|
+
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
81
|
+
stylesMap: { l1: { stroke: 'red' } }
|
|
82
|
+
})
|
|
83
|
+
expect(map.setFilter).not.toHaveBeenCalledWith('highlight-s1-fill', expect.anything())
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('persistent source skips cleanup; missing stale layers skip setFilter', () => {
|
|
87
|
+
// line 37 false: src IS in currentSources; line 41 false: getLayer returns null for stale layers
|
|
88
|
+
map._highlightedSources = new Set(['stale', 's1'])
|
|
89
|
+
map.getLayer.mockImplementation(id => id === 'l1' ? { source: 's1', type: 'line' } : null)
|
|
90
|
+
map.queryRenderedFeatures.mockReturnValue([])
|
|
91
|
+
updateHighlightedFeatures({ LngLatBounds, map,
|
|
92
|
+
selectedFeatures: [{ featureId: 1, layerId: 'l1' }],
|
|
93
|
+
stylesMap: { l1: { stroke: 'red' } }
|
|
94
|
+
})
|
|
95
|
+
expect(map.setFilter).not.toHaveBeenCalledWith(expect.stringContaining('stale'), expect.anything())
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('reuses existing highlight layer; new layer spreads sourceLayer', () => {
|
|
99
|
+
// line 50 false: getLayer truthy → skip addLayer for s1
|
|
100
|
+
// line 55: srcLayer truthy → 'source-layer' spread in addLayer for s2
|
|
101
|
+
map.getLayer.mockImplementation(id => {
|
|
102
|
+
if (id === 'l1') return { source: 's1', type: 'line' }
|
|
103
|
+
if (id === 'l2') return { source: 's2', type: 'line', sourceLayer: 'tiles' }
|
|
104
|
+
if (id === 'highlight-s1-line') return true
|
|
105
|
+
return null
|
|
106
|
+
})
|
|
107
|
+
map.queryRenderedFeatures.mockReturnValue([])
|
|
108
|
+
updateHighlightedFeatures({ LngLatBounds, map,
|
|
109
|
+
selectedFeatures: [
|
|
110
|
+
{ featureId: 1, layerId: 'l1' },
|
|
111
|
+
{ featureId: 2, layerId: 'l2' }
|
|
112
|
+
],
|
|
113
|
+
stylesMap: { l1: { stroke: 'blue' }, l2: { stroke: 'green' } }
|
|
114
|
+
})
|
|
115
|
+
expect(map.addLayer).toHaveBeenCalledTimes(1)
|
|
116
|
+
expect(map.addLayer).toHaveBeenCalledWith(expect.objectContaining({ 'source-layer': 'tiles' }))
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test('Empty features coverage', () => {
|
|
120
|
+
// Coverage for Line 72: empty renderedFeatures
|
|
121
|
+
map.getLayer.mockReturnValue({ source: 's1', type: 'line' })
|
|
122
|
+
map.queryRenderedFeatures.mockReturnValue([])
|
|
123
|
+
const res = updateHighlightedFeatures({ LngLatBounds, map, selectedFeatures: [], stylesMap: {} })
|
|
124
|
+
expect(res).toBeNull()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -234,9 +234,7 @@ export function createMapLabelNavigator(map, mapColorScheme, events, eventBus) {
|
|
|
234
234
|
}
|
|
235
235
|
const centerPoint = map.project(map.getCenter())
|
|
236
236
|
const closest = findClosestLabel(state.labels, centerPoint)
|
|
237
|
-
|
|
238
|
-
state.currentPixel = { x: closest.x, y: closest.y }
|
|
239
|
-
}
|
|
237
|
+
state.currentPixel = { x: closest.x, y: closest.y }
|
|
240
238
|
applyHighlight(map, closest, state)
|
|
241
239
|
return `${closest.text} (${closest.layer.id})`
|
|
242
240
|
}
|