@defra/interactive-map 0.0.9-alpha → 0.0.11-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/DOCS_README.md +39 -0
- package/README.md +1 -1
- package/dist/css/index.css +1 -1
- package/dist/esm/im-core.js +1 -1
- package/dist/esm/im-shell.js +1 -1
- package/dist/umd/im-core.js +1 -1
- package/dist/umd/index.js +1 -1
- package/docs/api/button-definition.md +21 -3
- package/docs/api/panel-definition.md +10 -12
- package/docs/api.md +81 -8
- package/docs/architecture/architecture-diagrams.md +1 -3
- package/docs/architecture/diagrams-viewer.mdx +12 -0
- package/docs/demo.mdx +70 -0
- package/docs/govuk-prototype.md +23 -0
- package/docs/index.md +19 -0
- package/docs/plugins/plugin-context.md +3 -3
- package/docs/plugins/plugin-manifest.md +1 -1
- package/docusaurus.config.cjs +136 -0
- package/mise.toml +2 -0
- package/package.json +27 -5
- package/plugins/beta/datasets/dist/esm/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/dist/umd/im-datasets-plugin.js +1 -1
- package/plugins/beta/datasets/src/manifest.js +3 -3
- package/plugins/beta/draw-es/dist/esm/im-draw-es-plugin.js +1 -1
- package/plugins/beta/draw-es/src/api/newPolygon.js +1 -3
- package/plugins/beta/draw-es/src/events.js +2 -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/newLine.js +2 -2
- package/plugins/beta/draw-ml/src/api/newPolygon.js +2 -2
- package/plugins/beta/draw-ml/src/events.js +18 -10
- package/plugins/beta/map-styles/dist/esm/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/dist/umd/im-map-styles-plugin.js +1 -1
- package/plugins/beta/map-styles/src/manifest.js +3 -3
- package/plugins/beta/use-location/dist/esm/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/dist/umd/im-use-location-plugin.js +1 -1
- package/plugins/beta/use-location/src/manifest.js +7 -7
- package/plugins/search/dist/css/index.css +1 -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 +9 -3
- package/plugins/search/src/Search.test.jsx +190 -0
- package/plugins/search/src/components/CloseButton/CloseButton.test.jsx +67 -0
- package/plugins/search/src/components/Form/Form.jsx +35 -7
- package/plugins/search/src/components/Form/Form.module.scss +27 -0
- package/plugins/search/src/components/Form/Form.test.jsx +255 -0
- package/plugins/search/src/components/OpenButton/OpenButton.test.jsx +47 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.jsx +28 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.module.scss +8 -0
- package/plugins/search/src/components/SubmitButton/SubmitButton.test.jsx +33 -0
- package/plugins/search/src/components/Suggestions/Suggestions.test.jsx +79 -0
- package/plugins/search/src/datasets.js +15 -11
- package/plugins/search/src/datasets.test.js +61 -0
- package/plugins/search/src/events/fetchSuggestions.js +1 -1
- package/plugins/search/src/events/fetchSuggestions.test.js +212 -0
- package/plugins/search/src/events/formHandlers.test.js +232 -0
- package/plugins/search/src/events/index.test.js +118 -0
- package/plugins/search/src/events/inputHandlers.test.js +104 -0
- package/plugins/search/src/events/suggestionHandlers.test.js +166 -0
- package/plugins/search/src/index.js +1 -1
- package/plugins/search/src/index.test.js +47 -0
- package/plugins/search/src/reducer.js +9 -4
- package/plugins/search/src/reducer.test.js +85 -0
- package/plugins/search/src/search.scss +5 -1
- package/plugins/search/src/utils/parseOsNamesResults.js +20 -3
- package/plugins/search/src/utils/parseOsNamesResults.test.js +158 -0
- package/plugins/search/src/utils/updateMap.test.js +52 -0
- package/providers/beta/esri/dist/esm/im-esri-provider.js +1 -1
- package/providers/beta/esri/src/appEvents.js +8 -2
- package/providers/beta/esri/src/esriProvider.js +6 -14
- package/providers/beta/esri/src/mapEvents.js +7 -1
- package/providers/beta/esri/src/utils/coords.js +33 -1
- package/providers/beta/esri/src/utils/coords.test.js +126 -0
- package/providers/maplibre/dist/esm/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/esm/index.js +1 -1
- package/providers/maplibre/dist/umd/im-maplibre-provider.js +1 -1
- package/providers/maplibre/dist/umd/index.js +1 -1
- package/providers/maplibre/src/appEvents.js +10 -1
- package/providers/maplibre/src/appEvents.test.js +13 -4
- package/providers/maplibre/src/index.js +5 -13
- package/providers/maplibre/src/index.test.js +34 -15
- package/providers/maplibre/src/mapEvents.js +9 -1
- package/providers/maplibre/src/maplibreProvider.js +14 -15
- package/providers/maplibre/src/maplibreProvider.test.js +14 -1
- package/providers/maplibre/src/utils/spatial.js +11 -0
- package/providers/maplibre/src/utils/spatial.test.js +12 -0
- package/src/App/components/Actions/Actions.module.scss +5 -4
- package/src/App/components/MapButton/MapButton.jsx +4 -16
- package/src/App/components/MapButton/MapButton.module.scss +12 -12
- package/src/App/components/MapButton/MapButton.test.jsx +0 -9
- package/src/App/components/Panel/Panel.jsx +6 -6
- package/src/App/components/Panel/Panel.test.jsx +14 -15
- package/src/App/components/Viewport/MapController.jsx +2 -1
- package/src/App/hooks/useLayoutMeasurements.js +1 -1
- package/src/App/hooks/useLayoutMeasurements.test.js +1 -1
- package/src/App/hooks/useMapProviderOverrides.js +21 -1
- package/src/App/hooks/useMapProviderOverrides.test.js +51 -2
- package/src/App/layout/Layout.jsx +4 -4
- package/src/App/layout/layout.module.scss +1 -0
- package/src/App/registry/panelRegistry.js +1 -10
- package/src/App/registry/panelRegistry.test.js +6 -11
- package/src/App/renderer/HtmlElementHost.jsx +11 -3
- package/src/App/renderer/HtmlElementHost.test.jsx +89 -0
- package/src/App/renderer/mapButtons.js +128 -28
- package/src/App/renderer/mapButtons.test.js +119 -19
- package/src/App/store/MapProvider.jsx +18 -5
- package/src/App/store/MapProvider.test.jsx +56 -1
- package/src/App/store/appActionsMap.js +17 -9
- package/src/App/store/appActionsMap.test.js +33 -7
- package/src/App/store/mapActionsMap.js +4 -7
- package/src/InteractiveMap/InteractiveMap.js +18 -0
- package/src/InteractiveMap/InteractiveMap.test.js +12 -0
- package/src/config/appConfig.js +17 -15
- package/src/config/events.js +41 -4
- package/src/config/getInitialOpenPanels.js +2 -2
- package/src/config/getInitialOpenPanels.test.js +7 -7
- package/src/types.js +13 -11
- package/src/utils/getValueForStyle.js +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { createSuggestionHandlers } from './suggestionHandlers.js'
|
|
5
|
+
import { updateMap } from '../utils/updateMap.js'
|
|
6
|
+
|
|
7
|
+
jest.mock('../utils/updateMap.js')
|
|
8
|
+
|
|
9
|
+
describe('createSuggestionHandlers', () => {
|
|
10
|
+
let dispatch
|
|
11
|
+
let services
|
|
12
|
+
let handlers
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
dispatch = jest.fn()
|
|
16
|
+
|
|
17
|
+
services = {
|
|
18
|
+
eventBus: { emit: jest.fn() },
|
|
19
|
+
announce: jest.fn()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
handlers = createSuggestionHandlers({
|
|
23
|
+
dispatch,
|
|
24
|
+
services,
|
|
25
|
+
mapProvider: 'map',
|
|
26
|
+
markers: 'markers',
|
|
27
|
+
showMarker: true,
|
|
28
|
+
markerColor: 'blue'
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
jest.clearAllMocks()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// ---------- Suggestion click ----------
|
|
35
|
+
|
|
36
|
+
test('handleSuggestionClick (desktop)', () => {
|
|
37
|
+
const suggestion = { text: 'Paris', bounds: 'b', point: 'p' }
|
|
38
|
+
|
|
39
|
+
handlers.handleSuggestionClick(suggestion, { breakpoint: 'desktop' })
|
|
40
|
+
|
|
41
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_VALUE', payload: 'Paris' })
|
|
42
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'HIDE_SUGGESTIONS' })
|
|
43
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
|
|
44
|
+
expect(updateMap).toHaveBeenCalledWith(expect.objectContaining({ bounds: 'b', point: 'p' }))
|
|
45
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith(
|
|
46
|
+
'search:match',
|
|
47
|
+
expect.objectContaining({ query: 'Paris' })
|
|
48
|
+
)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('handleSuggestionClick (mobile closes search)', () => {
|
|
52
|
+
const suggestion = { text: 'Berlin', bounds: 'b', point: 'p' }
|
|
53
|
+
|
|
54
|
+
handlers.handleSuggestionClick(suggestion, { breakpoint: 'mobile' })
|
|
55
|
+
|
|
56
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'TOGGLE_EXPANDED', payload: false })
|
|
57
|
+
expect(services.eventBus.emit).toHaveBeenCalledWith('search:close')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// ---------- ArrowDown ----------
|
|
61
|
+
|
|
62
|
+
test('ArrowDown selects next suggestion', () => {
|
|
63
|
+
const e = { key: 'ArrowDown', preventDefault: jest.fn() }
|
|
64
|
+
|
|
65
|
+
handlers.handleInputKeyDown(e, {
|
|
66
|
+
suggestions: [{ text: 'A' }, { text: 'B' }],
|
|
67
|
+
selectedIndex: 0
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
expect(e.preventDefault).toHaveBeenCalled()
|
|
71
|
+
expect(services.announce).toHaveBeenCalledWith('B. 2 of 2 is highlighted')
|
|
72
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: 1 })
|
|
73
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_KEYBOARD_FOCUS_WITHIN', payload: false })
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('ArrowDown does nothing when no suggestions', () => {
|
|
77
|
+
const e = { key: 'ArrowDown', preventDefault: jest.fn() }
|
|
78
|
+
|
|
79
|
+
handlers.handleInputKeyDown(e, { suggestions: [], selectedIndex: 0 })
|
|
80
|
+
|
|
81
|
+
expect(e.preventDefault).not.toHaveBeenCalled()
|
|
82
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
83
|
+
expect(services.announce).not.toHaveBeenCalled()
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('ArrowDown does nothing when at last suggestion', () => {
|
|
87
|
+
const e = { key: 'ArrowDown', preventDefault: jest.fn() }
|
|
88
|
+
|
|
89
|
+
handlers.handleInputKeyDown(e, {
|
|
90
|
+
suggestions: [{ text: 'A' }, { text: 'B' }],
|
|
91
|
+
selectedIndex: 1 // last index
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
expect(e.preventDefault).toHaveBeenCalled()
|
|
95
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
96
|
+
expect(services.announce).not.toHaveBeenCalled()
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// ---------- ArrowUp ----------
|
|
100
|
+
|
|
101
|
+
test('ArrowUp moves selection up and resets to -1', () => {
|
|
102
|
+
const e = { key: 'ArrowUp', preventDefault: jest.fn() }
|
|
103
|
+
|
|
104
|
+
handlers.handleInputKeyDown(e, {
|
|
105
|
+
suggestions: [{ text: 'A' }, { text: 'B' }],
|
|
106
|
+
selectedIndex: 0
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
expect(e.preventDefault).toHaveBeenCalled()
|
|
110
|
+
expect(services.announce).toHaveBeenCalledWith('2 suggestions available')
|
|
111
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
|
|
112
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_KEYBOARD_FOCUS_WITHIN', payload: true })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('ArrowUp moves selection up normally when selectedIndex > 0', () => {
|
|
116
|
+
const e = { key: 'ArrowUp', preventDefault: jest.fn() }
|
|
117
|
+
|
|
118
|
+
handlers.handleInputKeyDown(e, {
|
|
119
|
+
suggestions: [{ text: 'A' }, { text: 'B' }, { text: 'C' }],
|
|
120
|
+
selectedIndex: 2
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(e.preventDefault).toHaveBeenCalled()
|
|
124
|
+
expect(services.announce).toHaveBeenCalledWith('B. 2 of 3 is highlighted')
|
|
125
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: 1 })
|
|
126
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_KEYBOARD_FOCUS_WITHIN', payload: false })
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('ArrowUp does nothing when no suggestions', () => {
|
|
130
|
+
const e = { key: 'ArrowUp', preventDefault: jest.fn() }
|
|
131
|
+
|
|
132
|
+
handlers.handleInputKeyDown(e, { suggestions: [], selectedIndex: 0 })
|
|
133
|
+
|
|
134
|
+
expect(e.preventDefault).not.toHaveBeenCalled()
|
|
135
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
136
|
+
expect(services.announce).not.toHaveBeenCalled()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// ---------- Escape & default ----------
|
|
140
|
+
|
|
141
|
+
test('Escape hides suggestions and clears selection', () => {
|
|
142
|
+
const e = { key: 'Escape', preventDefault: jest.fn() }
|
|
143
|
+
|
|
144
|
+
handlers.handleInputKeyDown(e, {
|
|
145
|
+
suggestions: [{ text: 'A' }],
|
|
146
|
+
selectedIndex: 0
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
expect(e.preventDefault).toHaveBeenCalled()
|
|
150
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'HIDE_SUGGESTIONS' })
|
|
151
|
+
expect(dispatch).toHaveBeenCalledWith({ type: 'SET_SELECTED', payload: -1 })
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('Other keys do nothing', () => {
|
|
155
|
+
const e = { key: 'Enter', preventDefault: jest.fn() }
|
|
156
|
+
|
|
157
|
+
handlers.handleInputKeyDown(e, {
|
|
158
|
+
suggestions: [{ text: 'A' }],
|
|
159
|
+
selectedIndex: 0
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
expect(e.preventDefault).not.toHaveBeenCalled()
|
|
163
|
+
expect(dispatch).not.toHaveBeenCalled()
|
|
164
|
+
expect(services.announce).not.toHaveBeenCalled()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -3,7 +3,7 @@ import './search.scss'
|
|
|
3
3
|
|
|
4
4
|
export default function createPlugin (options = {}) {
|
|
5
5
|
// If search is open then we need to overirde the mobile slot
|
|
6
|
-
if (options.
|
|
6
|
+
if (options.expanded) {
|
|
7
7
|
options.manifest = {controls: [{ id: 'search', mobile: { slot: 'banner' }}]}
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// /plugins/search/index.test.js
|
|
2
|
+
|
|
3
|
+
// Mock SCSS import so Jest can run without parsing errors
|
|
4
|
+
jest.mock('./search.scss', () => {})
|
|
5
|
+
|
|
6
|
+
import createPlugin from './index'
|
|
7
|
+
|
|
8
|
+
describe('createPlugin', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
jest.resetModules()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('returns default plugin structure with showMarker and id', async () => {
|
|
14
|
+
const plugin = createPlugin()
|
|
15
|
+
expect(plugin.showMarker).toBe(true)
|
|
16
|
+
expect(plugin.id).toBe('search')
|
|
17
|
+
expect(typeof plugin.load).toBe('function')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('overrides manifest when expanded is true', () => {
|
|
21
|
+
const plugin = createPlugin({ expanded: true })
|
|
22
|
+
expect(plugin.expanded).toBe(true)
|
|
23
|
+
expect(plugin.manifest).toEqual({
|
|
24
|
+
controls: [{ id: 'search', mobile: { slot: 'banner' } }]
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('spreads custom options correctly', () => {
|
|
29
|
+
const custom = { foo: 'bar', expanded: false }
|
|
30
|
+
const plugin = createPlugin(custom)
|
|
31
|
+
expect(plugin.foo).toBe('bar')
|
|
32
|
+
expect(plugin.showMarker).toBe(true)
|
|
33
|
+
expect(plugin.id).toBe('search')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('load function dynamically imports the manifest and returns it', async () => {
|
|
37
|
+
// Mock the dynamic import for './manifest.js'
|
|
38
|
+
const manifestMock = { data: 'test-manifest' }
|
|
39
|
+
jest.mock('./manifest.js', () => ({
|
|
40
|
+
manifest: manifestMock
|
|
41
|
+
}), { virtual: true })
|
|
42
|
+
|
|
43
|
+
const plugin = createPlugin()
|
|
44
|
+
const result = await plugin.load()
|
|
45
|
+
expect(result).toEqual(manifestMock)
|
|
46
|
+
})
|
|
47
|
+
})
|
|
@@ -4,6 +4,7 @@ const initialState = {
|
|
|
4
4
|
value: '',
|
|
5
5
|
suggestions: [],
|
|
6
6
|
areSuggestionsVisible: false,
|
|
7
|
+
hasFetchedSuggestions: false,
|
|
7
8
|
selectedIndex: -1
|
|
8
9
|
}
|
|
9
10
|
|
|
@@ -12,7 +13,8 @@ const toggleExpanded = (state, payload) => {
|
|
|
12
13
|
return {
|
|
13
14
|
...state,
|
|
14
15
|
isExpanded: payload,
|
|
15
|
-
areSuggestionsVisible: payload
|
|
16
|
+
areSuggestionsVisible: payload,
|
|
17
|
+
hasFetchedSuggestions: false
|
|
16
18
|
}
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -43,21 +45,24 @@ const setValue = (state, payload) => {
|
|
|
43
45
|
const updateSuggestions = (state, payload) => {
|
|
44
46
|
return {
|
|
45
47
|
...state,
|
|
46
|
-
suggestions: payload
|
|
48
|
+
suggestions: payload,
|
|
49
|
+
hasFetchedSuggestions: true
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
const showSuggestions = (state) => {
|
|
51
54
|
return {
|
|
52
55
|
...state,
|
|
53
|
-
areSuggestionsVisible: true
|
|
56
|
+
areSuggestionsVisible: true,
|
|
57
|
+
hasFetchedSuggestions: false
|
|
54
58
|
}
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
const hideSuggestions = (state) => {
|
|
58
62
|
return {
|
|
59
63
|
...state,
|
|
60
|
-
areSuggestionsVisible: false
|
|
64
|
+
areSuggestionsVisible: false,
|
|
65
|
+
hasFetchedSuggestions: false
|
|
61
66
|
}
|
|
62
67
|
}
|
|
63
68
|
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// /plugins/search/reducer.test.js
|
|
2
|
+
|
|
3
|
+
import { initialState, actions } from './reducer'
|
|
4
|
+
|
|
5
|
+
describe('search state actions', () => {
|
|
6
|
+
it('TOGGLE_EXPANDED sets isExpanded and areSuggestionsVisible, and resets hasFetchedSuggestions', () => {
|
|
7
|
+
const state = { ...initialState, hasFetchedSuggestions: true }
|
|
8
|
+
const newState = actions.TOGGLE_EXPANDED(state, true)
|
|
9
|
+
expect(newState.isExpanded).toBe(true)
|
|
10
|
+
expect(newState.areSuggestionsVisible).toBe(true)
|
|
11
|
+
expect(newState.hasFetchedSuggestions).toBe(false)
|
|
12
|
+
|
|
13
|
+
const collapsed = actions.TOGGLE_EXPANDED(state, false)
|
|
14
|
+
expect(collapsed.isExpanded).toBe(false)
|
|
15
|
+
expect(collapsed.areSuggestionsVisible).toBe(false)
|
|
16
|
+
expect(collapsed.hasFetchedSuggestions).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('SET_KEYBOARD_FOCUS_WITHIN sets focus and shows suggestions', () => {
|
|
20
|
+
const state = { ...initialState }
|
|
21
|
+
const newState = actions.SET_KEYBOARD_FOCUS_WITHIN(state, true)
|
|
22
|
+
expect(newState.hasKeyboardFocusWithin).toBe(true)
|
|
23
|
+
expect(newState.areSuggestionsVisible).toBe(true)
|
|
24
|
+
|
|
25
|
+
const removedFocus = actions.SET_KEYBOARD_FOCUS_WITHIN(state, false)
|
|
26
|
+
expect(removedFocus.hasKeyboardFocusWithin).toBe(false)
|
|
27
|
+
expect(removedFocus.areSuggestionsVisible).toBe(true) // always true
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('INPUT_BLUR removes focus and updates areSuggestionsVisible correctly', () => {
|
|
31
|
+
const state = { ...initialState, areSuggestionsVisible: true, hasKeyboardFocusWithin: true }
|
|
32
|
+
|
|
33
|
+
// Non-keyboard blur
|
|
34
|
+
const newStateMouse = actions.INPUT_BLUR(state, 'mouse')
|
|
35
|
+
expect(newStateMouse.hasKeyboardFocusWithin).toBe(false)
|
|
36
|
+
expect(newStateMouse.areSuggestionsVisible).toBe(true) // still true
|
|
37
|
+
expect(newStateMouse.selectedIndex).toBe(-1)
|
|
38
|
+
|
|
39
|
+
// Keyboard blur hides suggestions
|
|
40
|
+
const newStateKeyboard = actions.INPUT_BLUR(state, 'keyboard')
|
|
41
|
+
expect(newStateKeyboard.hasKeyboardFocusWithin).toBe(false)
|
|
42
|
+
expect(newStateKeyboard.areSuggestionsVisible).toBe(false) // now false
|
|
43
|
+
expect(newStateKeyboard.selectedIndex).toBe(-1)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('SET_VALUE updates the input value', () => {
|
|
47
|
+
const state = { ...initialState }
|
|
48
|
+
const newState = actions.SET_VALUE(state, 'test')
|
|
49
|
+
expect(newState.value).toBe('test')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('UPDATE_SUGGESTIONS updates the suggestions array and sets hasFetchedSuggestions', () => {
|
|
53
|
+
const state = { ...initialState }
|
|
54
|
+
const suggestions = [{ id: 1 }, { id: 2 }]
|
|
55
|
+
const newState = actions.UPDATE_SUGGESTIONS(state, suggestions)
|
|
56
|
+
expect(newState.suggestions).toEqual(suggestions)
|
|
57
|
+
expect(newState.hasFetchedSuggestions).toBe(true)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('SHOW_SUGGESTIONS sets areSuggestionsVisible to true and resets hasFetchedSuggestions', () => {
|
|
61
|
+
const state = { ...initialState, areSuggestionsVisible: false, hasFetchedSuggestions: true }
|
|
62
|
+
const newState = actions.SHOW_SUGGESTIONS(state)
|
|
63
|
+
expect(newState.areSuggestionsVisible).toBe(true)
|
|
64
|
+
expect(newState.hasFetchedSuggestions).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('HIDE_SUGGESTIONS sets areSuggestionsVisible to false and resets hasFetchedSuggestions', () => {
|
|
68
|
+
const state = { ...initialState, areSuggestionsVisible: true, hasFetchedSuggestions: true }
|
|
69
|
+
const newState = actions.HIDE_SUGGESTIONS(state)
|
|
70
|
+
expect(newState.areSuggestionsVisible).toBe(false)
|
|
71
|
+
expect(newState.hasFetchedSuggestions).toBe(false)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('SET_SELECTED updates selectedIndex and visibility', () => {
|
|
75
|
+
const state = { ...initialState, areSuggestionsVisible: false }
|
|
76
|
+
|
|
77
|
+
const selected = actions.SET_SELECTED(state, 1)
|
|
78
|
+
expect(selected.selectedIndex).toBe(1)
|
|
79
|
+
expect(selected.areSuggestionsVisible).toBe(true)
|
|
80
|
+
|
|
81
|
+
const deselected = actions.SET_SELECTED(state, -1)
|
|
82
|
+
expect(deselected.selectedIndex).toBe(-1)
|
|
83
|
+
expect(deselected.areSuggestionsVisible).toBe(false)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
@use '../../../src/scss/tools/index' as tools;
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
// Components, using 'CSS in Component' pattern
|
|
5
4
|
@use './components/Form/Form.module';
|
|
6
5
|
@use './components/CloseButton/CloseButton.module';
|
|
6
|
+
@use './components/SubmitButton/SubmitButton.module';
|
|
7
7
|
@use './components/Suggestions/Suggestions.module';
|
|
8
8
|
|
|
9
|
+
:root {
|
|
10
|
+
--search-drop-shadow: drop-shadow(0 -2px 20px rgba(0,0,0,.1)) drop-shadow(0 0 12px rgba(0,0,0,.1));
|
|
11
|
+
}
|
|
12
|
+
|
|
9
13
|
// ===================================================
|
|
10
14
|
// Component: Search
|
|
11
15
|
// ===================================================
|
|
@@ -10,12 +10,27 @@ const isPostcode = (value) => {
|
|
|
10
10
|
return regex.test(value)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
const removeRegions = (results, regions) => results.filter(r => {
|
|
14
|
+
const country = r?.GAZETTEER_ENTRY?.COUNTRY?.toLowerCase()
|
|
15
|
+
return country && regions.includes(country)
|
|
16
|
+
})
|
|
17
|
+
|
|
13
18
|
const removeDuplicates = (results) =>
|
|
14
19
|
Array.from(new Map(results.map(r => [r.GAZETTEER_ENTRY.ID, r])).values())
|
|
15
20
|
|
|
21
|
+
const transformGazetteerResult = (result) => {
|
|
22
|
+
const gazetterEntry = result.GAZETTEER_ENTRY
|
|
23
|
+
// Sometimes the NAME1 value can be welsh, so we should use NAME2 instead
|
|
24
|
+
const name = gazetterEntry.NAME2_LANG === 'eng' ? gazetterEntry.NAME2 : gazetterEntry.NAME1
|
|
25
|
+
// Force NAME1 to be the english value
|
|
26
|
+
result.GAZETTEER_ENTRY.NAME1 = name
|
|
27
|
+
result.GAZETTEER_ENTRY.NAME1_LANG = 'eng'
|
|
28
|
+
return result
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
const removeTenuousResults = (results, query) => {
|
|
17
32
|
const words = query.toLowerCase().replace(/,/g, '').split(' ')
|
|
18
|
-
return results.filter(l => words.some(w => l.GAZETTEER_ENTRY.NAME1.toLowerCase().includes(w) || isPostcode(query)))
|
|
33
|
+
return results.map(transformGazetteerResult).filter(l => words.some(w => l.GAZETTEER_ENTRY.NAME1.toLowerCase().includes(w) || isPostcode(query)))
|
|
19
34
|
}
|
|
20
35
|
|
|
21
36
|
const markString = (string, find) => {
|
|
@@ -86,13 +101,14 @@ const label = (query, { NAME1, COUNTY_UNITARY, DISTRICT_BOROUGH, POSTCODE_DISTRI
|
|
|
86
101
|
}
|
|
87
102
|
}
|
|
88
103
|
|
|
89
|
-
const parseOsNamesResults = (json, query, crs) => {
|
|
104
|
+
const parseOsNamesResults = (json, query, regions, crs) => {
|
|
90
105
|
if (!json || json.error || json.header?.totalresults === 0) {
|
|
91
106
|
return []
|
|
92
107
|
}
|
|
93
108
|
let results = json.results
|
|
94
109
|
results = removeTenuousResults(results, query)
|
|
95
110
|
results = removeDuplicates(results)
|
|
111
|
+
results = removeRegions(results, regions)
|
|
96
112
|
results = results.slice(0, MAX_RESULTS)
|
|
97
113
|
|
|
98
114
|
return results.map(l => ({
|
|
@@ -105,5 +121,6 @@ const parseOsNamesResults = (json, query, crs) => {
|
|
|
105
121
|
}
|
|
106
122
|
|
|
107
123
|
export {
|
|
108
|
-
parseOsNamesResults
|
|
124
|
+
parseOsNamesResults,
|
|
125
|
+
point
|
|
109
126
|
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { parseOsNamesResults, point } from './parseOsNamesResults.js'
|
|
5
|
+
|
|
6
|
+
// Mock OsGridRef so we can control toLatLon outputs
|
|
7
|
+
jest.mock('geodesy/osgridref.js', () => {
|
|
8
|
+
return jest.fn().mockImplementation((x, y) => ({
|
|
9
|
+
x,
|
|
10
|
+
y,
|
|
11
|
+
toLatLon: () => ({ lat: y / 1e5, lon: x / 1e5 }) // deterministic lat/lon
|
|
12
|
+
}))
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('osNamesUtils', () => {
|
|
16
|
+
const regions = ['england', 'scotland', 'wales']
|
|
17
|
+
|
|
18
|
+
const sampleEntry = {
|
|
19
|
+
GAZETTEER_ENTRY: {
|
|
20
|
+
ID: 1,
|
|
21
|
+
NAME1: 'London',
|
|
22
|
+
COUNTY_UNITARY: 'Greater London',
|
|
23
|
+
DISTRICT_BOROUGH: 'Camden',
|
|
24
|
+
POSTCODE_DISTRICT: 'WC1',
|
|
25
|
+
LOCAL_TYPE: 'Town',
|
|
26
|
+
COUNTRY: 'England',
|
|
27
|
+
MBR_XMIN: 1000,
|
|
28
|
+
MBR_YMIN: 2000,
|
|
29
|
+
MBR_XMAX: 3000,
|
|
30
|
+
MBR_YMAX: 4000,
|
|
31
|
+
GEOMETRY_X: 1500,
|
|
32
|
+
GEOMETRY_Y: 2500
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('returns empty array for null/invalid/error results', () => {
|
|
37
|
+
expect(parseOsNamesResults(null, 'x', regions, 'EPSG:27700')).toEqual([])
|
|
38
|
+
expect(parseOsNamesResults({ error: true }, 'x', regions, 'EPSG:27700')).toEqual([])
|
|
39
|
+
expect(parseOsNamesResults({ header: { totalresults: 0 } }, 'x', regions, 'EPSG:27700')).toEqual([])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('removes tenuous results when query does not match', () => {
|
|
43
|
+
const results = [
|
|
44
|
+
{ GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, NAME1: 'Bristol', ID: 2 } }
|
|
45
|
+
]
|
|
46
|
+
const json = { results }
|
|
47
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
|
|
48
|
+
expect(output).toHaveLength(0)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test('removes duplicate IDs', () => {
|
|
52
|
+
const dup = { ...sampleEntry, GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY } }
|
|
53
|
+
const json = { results: [sampleEntry, dup] }
|
|
54
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
|
|
55
|
+
expect(output).toHaveLength(1)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('limits results to MAX_RESULTS', () => {
|
|
59
|
+
const manyResults = Array.from({ length: 10 }, (_, i) => ({
|
|
60
|
+
GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, ID: i }
|
|
61
|
+
}))
|
|
62
|
+
const json = { results: manyResults }
|
|
63
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
|
|
64
|
+
expect(output).toHaveLength(8)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('bounds returns raw OSGB values for EPSG:27700', () => {
|
|
68
|
+
const json = { results: [sampleEntry] }
|
|
69
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
|
|
70
|
+
expect(output[0].bounds).toEqual([
|
|
71
|
+
sampleEntry.GAZETTEER_ENTRY.MBR_XMIN,
|
|
72
|
+
sampleEntry.GAZETTEER_ENTRY.MBR_YMIN,
|
|
73
|
+
sampleEntry.GAZETTEER_ENTRY.MBR_XMAX,
|
|
74
|
+
sampleEntry.GAZETTEER_ENTRY.MBR_YMAX
|
|
75
|
+
])
|
|
76
|
+
expect(output[0].point).toEqual([
|
|
77
|
+
sampleEntry.GAZETTEER_ENTRY.GEOMETRY_X,
|
|
78
|
+
sampleEntry.GAZETTEER_ENTRY.GEOMETRY_Y
|
|
79
|
+
])
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('bounds converts to WGS84 for EPSG:4326', () => {
|
|
83
|
+
const json = { results: [sampleEntry] }
|
|
84
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:4326')
|
|
85
|
+
const expectedBounds = [
|
|
86
|
+
Math.round(1000 / 1e5 * 1e6) / 1e6,
|
|
87
|
+
Math.round(2000 / 1e5 * 1e6) / 1e6,
|
|
88
|
+
Math.round(3000 / 1e5 * 1e6) / 1e6,
|
|
89
|
+
Math.round(4000 / 1e5 * 1e6) / 1e6
|
|
90
|
+
]
|
|
91
|
+
expect(output[0].bounds).toEqual(expectedBounds)
|
|
92
|
+
const expectedPoint = [
|
|
93
|
+
Math.round(1500 / 1e5 * 1e6) / 1e6,
|
|
94
|
+
Math.round(2500 / 1e5 * 1e6) / 1e6
|
|
95
|
+
]
|
|
96
|
+
expect(output[0].point).toEqual(expectedPoint)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('label generates marked text', () => {
|
|
100
|
+
const json = { results: [sampleEntry] }
|
|
101
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
|
|
102
|
+
expect(output[0].text).toContain('London')
|
|
103
|
+
expect(output[0].marked).toContain('<mark>')
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test('handles MBR_XMIN null by using buffered GEOMETRY_X/Y', () => {
|
|
107
|
+
const entry = {
|
|
108
|
+
GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, MBR_XMIN: null, MBR_YMIN: null }
|
|
109
|
+
}
|
|
110
|
+
const json = { results: [entry] }
|
|
111
|
+
const output = parseOsNamesResults(json, 'London', regions, 'EPSG:27700')
|
|
112
|
+
expect(output[0].bounds).toEqual([
|
|
113
|
+
entry.GAZETTEER_ENTRY.GEOMETRY_X - 500,
|
|
114
|
+
entry.GAZETTEER_ENTRY.GEOMETRY_Y - 500,
|
|
115
|
+
entry.GAZETTEER_ENTRY.GEOMETRY_X + 500,
|
|
116
|
+
entry.GAZETTEER_ENTRY.GEOMETRY_Y + 500
|
|
117
|
+
])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('label falls back to DISTRICT_BOROUGH when COUNTY_UNITARY is absent', () => {
|
|
121
|
+
const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, COUNTY_UNITARY: null } }
|
|
122
|
+
const output = parseOsNamesResults({ results: [entry] }, 'London', regions, 'EPSG:27700')
|
|
123
|
+
expect(output[0].text).toContain(sampleEntry.GAZETTEER_ENTRY.DISTRICT_BOROUGH)
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
test('label omits qualifier for City type', () => {
|
|
127
|
+
const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, LOCAL_TYPE: 'City' } }
|
|
128
|
+
const output = parseOsNamesResults({ results: [entry] }, 'London', regions, 'EPSG:27700')
|
|
129
|
+
expect(output[0].text).toBe('London')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Cover transformGazetteerResult branches
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
test('uses NAME2 when NAME2_LANG is eng', () => {
|
|
137
|
+
const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, NAME1: 'Caerdydd', NAME2: 'Cardiff', NAME2_LANG: 'eng' }}
|
|
138
|
+
const output = parseOsNamesResults({ results: [entry] }, 'Cardiff', regions, 'EPSG:27700')
|
|
139
|
+
expect(output[0].text).toContain('Cardiff')
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test('falls back to NAME1 when NAME2_LANG is not eng', () => {
|
|
143
|
+
const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY, NAME1: 'London', NAME2: 'Londres', NAME2_LANG: 'fra' }}
|
|
144
|
+
const output = parseOsNamesResults({ results: [entry] }, 'London', regions, 'EPSG:27700')
|
|
145
|
+
expect(output[0].text).toContain('London')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('throws error for unsupported CRS', () => {
|
|
149
|
+
const entry = { GAZETTEER_ENTRY: { ...sampleEntry.GAZETTEER_ENTRY } }
|
|
150
|
+
const json = { results: [entry] }
|
|
151
|
+
expect(() => parseOsNamesResults(json, 'London', regions, 'EPSG:9999')).toThrow('Unsupported CRS')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
test('point function throws error for unsupported CRS', () => {
|
|
155
|
+
const coords = { GEOMETRY_X: 1500, GEOMETRY_Y: 2500 }
|
|
156
|
+
expect(() => point('EPSG:9999', coords)).toThrow('Unsupported CRS: EPSG:9999')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/plugins/search/utils/updateMap.test.js
|
|
2
|
+
|
|
3
|
+
import { updateMap } from './updateMap.js'
|
|
4
|
+
|
|
5
|
+
describe('updateMap', () => {
|
|
6
|
+
let mapProvider
|
|
7
|
+
let markers
|
|
8
|
+
const bounds = { north: 1, south: 0 }
|
|
9
|
+
const point = { lat: 10, lng: 20 }
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
mapProvider = {
|
|
13
|
+
fitToBounds: jest.fn(),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
markers = {
|
|
17
|
+
add: jest.fn(),
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('fits the map to bounds and adds a marker when showMarker is true', () => {
|
|
22
|
+
updateMap({
|
|
23
|
+
mapProvider,
|
|
24
|
+
bounds,
|
|
25
|
+
point,
|
|
26
|
+
markers,
|
|
27
|
+
showMarker: true,
|
|
28
|
+
markerColor: 'red',
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(bounds)
|
|
32
|
+
expect(markers.add).toHaveBeenCalledWith(
|
|
33
|
+
'search',
|
|
34
|
+
point,
|
|
35
|
+
{ color: 'red' }
|
|
36
|
+
)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('fits the map to bounds and does not add a marker when showMarker is false', () => {
|
|
40
|
+
updateMap({
|
|
41
|
+
mapProvider,
|
|
42
|
+
bounds,
|
|
43
|
+
point,
|
|
44
|
+
markers,
|
|
45
|
+
showMarker: false,
|
|
46
|
+
markerColor: 'blue',
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
expect(mapProvider.fitToBounds).toHaveBeenCalledWith(bounds)
|
|
50
|
+
expect(markers.add).not.toHaveBeenCalled()
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"@babel/runtime/helpers/objectWithoutProperties";import t from"@babel/runtime/helpers/asyncToGenerator";import
|
|
1
|
+
import e from"@babel/runtime/helpers/objectWithoutProperties";import t from"@babel/runtime/helpers/asyncToGenerator";import o from"@arcgis/core/config.js";import i from"@arcgis/core/Map.js";import r from"@arcgis/core/views/MapView.js";import n from"@arcgis/core/layers/VectorTileLayer.js";import{when as a,once as s,watch as m}from"@arcgis/core/core/reactiveUtils.js";import u from"@arcgis/core/geometry/Extent.js";import l from"@arcgis/core/geometry/Point.js";var v={animationDuration:200},c=["showKeyboardHelp","selectControl","moveLarge","nudgeMap","zoomLarge","nudgeZoom"];var p=(e,t)=>{var o=null,i=function(){for(var i=arguments.length,r=new Array(i),n=0;n<i;n++)r[n]=arguments[n];clearTimeout(o),o=setTimeout(()=>{e(...r)},t)};return i.cancel=()=>{o&&(clearTimeout(o),o=null)},i};function h(e){var{mapProvider:t,view:o,baseTileLayer:i,events:r,eventBus:n,getZoom:u,getCenter:l,getBounds:v,getResolution:c}=e,h=!1,d=[],y=[],g=e=>{var t=(()=>{if(h||!o||o.destroyed||!o.extent)return null;var{maxZoom:e,minZoom:t}=o.constraints;return{center:l(),bounds:v(),resolution:c(),zoom:u(),isAtMaxZoom:o.zoom+.01>=e,isAtMinZoom:o.zoom-.01<=t}})();t&&n.emit(e,t)};a(()=>i.loaded&&o.resolution>0,()=>g(r.MAP_LOADED)),s(()=>o.ready).then(()=>{h||n.emit(r.MAP_READY,{map:t.map,view:t.view,mapStyleId:t.mapStyleId,mapSize:t.mapSize,crs:t.crs})}),s(()=>o.stationary).then(()=>g(r.MAP_FIRST_IDLE));var f=p(()=>g(r.MAP_MOVE_END),500);y.push(f),d.push(m(()=>[o.interacting,o.animation],e=>{var[t,o]=e;(t||o)&&n.emit(r.MAP_MOVE_START),t||o||f()}));var x,w,M,b=(x=()=>g(r.MAP_MOVE),w=10,M=0,function(){var e=Date.now();e-M>=w&&(M=e,x(...arguments))});y.push(b),d.push(m(()=>o.zoom,b)),d.push(m(()=>o.extent,()=>n.emit(r.MAP_RENDER),{initial:!1}));var E=p(()=>g(r.MAP_DATA_CHANGE),500);return y.push(E),d.push(m(()=>o.updating,e=>!e&&E())),d.push(o.on("click",e=>{var t=e.mapPoint,o={x:e.x,y:e.y};n.emit(r.MAP_CLICK,{point:o,coords:[t.x,t.y]})})),{remove(){h=!0,y.forEach(e=>e.cancel()),d.forEach(e=>e.remove())}}}var d=e=>{var t=1609.344,o=e/t;if(o<.5/t)return"".concat(Math.round(e),"m");if(o<10){var i=Number.parseFloat(o.toFixed(1)),r=1===i?"mile":"miles";return"".concat(i," ").concat(r)}var n=Math.round(o),a=1===n?"mile":"miles";return"".concat(n," ").concat(a)};var y={top:0,right:0,bottom:0,left:0};function g(){return(g=t(function*(e,t){if(!e||!t)return[];var o=e.map.layers.filter(e=>e instanceof n);return o.length?(yield e.hitTest(t,{include:o.toArray()})).results.map(e=>({layerId:e.layer.id,layerTitle:e.layer.title||e.layer.id,type:e.layer.type,geometry:e.graphic.geometry,symbol:e.graphic.symbol})):[]})).apply(this,arguments)}var f=e=>e?new u({xmin:e[0],ymin:e[1],xmax:e[2],ymax:e[3],spatialReference:{wkid:27700}}):void 0,x=e=>e?new l({x:e[0],y:e[1],spatialReference:{wkid:27700}}):void 0,w=(e,t)=>{if(e)if("FeatureCollection"===e.type)e.features.forEach(e=>w(e,t));else if("Feature"===e.type)w(e.geometry,t);else if("GeometryCollection"===e.type)e.geometries.forEach(e=>w(e,t));else{var o=e=>{"number"==typeof e[0]?t.push(e):e.forEach(o)};o(e.coordinates)}},M=["container","padding","mapStyle","mapSize","maxExtent"];class b{constructor(e){var{mapProviderConfig:t={},events:o,eventBus:i}=e;this.events=o,this.eventBus=i,this.capabilities={supportedShortcuts:c,supportsMapSizes:!1},Object.assign(this,t),this.mapEventHandles=[],this.appEventHandles=[]}initMap(a){var s=this;return t(function*(){var{container:t,padding:m,mapStyle:u,mapSize:l,maxExtent:v}=a;e(a,M),s.mapStyleId=null==u?void 0:u.id,s.mapSize=l;var{events:c,eventBus:p}=s;s.setupConfig&&(yield s.setupConfig(o));var d=new n({id:"baselayer",url:u.url,visible:!0}),y=new i({layers:[d]}),g=v?f(v):null,w=new r({spatialReference:27700,container:t,map:y,zoom:a.zoom,center:x(a.center),maxExtent:v,constraints:{snapToZoom:!1,minZoom:a.minZoom,maxZoom:a.maxZoom,maxScale:0,geometry:g,rotationEnabled:!1},ui:{components:[]},popupEnabled:!1});(e=>{if(e){var t=e.querySelector(".esri-view-surface");t.removeAttribute("role"),t.tabIndex=-1,t.style["outline-color"]="transparent",t.style.touchAction="none"}})(w.container),w.padding=m,a.bounds&&w.when(()=>w.goTo(f(a.bounds),{duration:0})),s.mapEventHandles=h({mapProvider:s,view:w,baseTileLayer:d,events:c,eventBus:p,getZoom:s.getZoom.bind(s),getCenter:s.getCenter.bind(s),getBounds:s.getBounds.bind(s),getResolution:s.getResolution.bind(s)}),s.appEventHandles=function(e){var{baseTileLayer:t,events:o,eventBus:i}=e,r=e=>{t.loadStyle(e.url).then(()=>{i.emit(o.MAP_STYLE_CHANGE,{mapStyleId:e.id})})};return i.on(o.MAP_SET_STYLE,r),{remove(){i.off(o.MAP_SET_STYLE,r)}}}({baseTileLayer:d,events:c,eventBus:p})||[],s.map=y,s.view=w,s.baseTileLayer=d})()}destroyMap(){var e,t;null===(e=this.mapEvents)||void 0===e||e.remove(),null===(t=this.appEvents)||void 0===t||t.remove(),this.mapEvents=null,this.appEvents=null,this.view&&(this.view.container=null,this.view.destroy(),this.view=null),this.map&&(this.map.removeAll(),this.map=null)}setView(e){var t,{center:o,zoom:i}=e;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({center:o,zoom:i,duration:v.animationDuration})}zoomIn(e){var t;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({zoom:this.view.zoom+e,duration:v.animationDuration})}zoomOut(e){var t;null===(t=this.view.animation)||void 0===t||t.destroy(),this.view.goTo({zoom:this.view.zoom-e,duration:v.animationDuration})}panBy(e){var{x:t,y:o}=this.view.toScreen(this.view.center),i={x:t+e[0],y:o+e[1]},r=this.view.toMap(i);this.view.goTo({center:r,duration:v.animationDuration})}fitToBounds(e){var t=Array.isArray(e)?f(e):(e=>{var t=[];w(e,t);var o=t.map(e=>e[0]),i=t.map(e=>e[1]);return f([Math.min(...o),Math.min(...i),Math.max(...o),Math.max(...i)])})(e);this.view.goTo(t,{duration:v.DELAY})}setPadding(e){this.view.padding=e}getCenter(){var e=this.view.center;return[e.x,e.y].map(e=>Math.round(100*e)/100)}getZoom(){return this.view.zoom}getBounds(){var{xmin:e,ymin:t,xmax:o,ymax:i}=this.view.extent;return[e,t,o,i].map(e=>Math.round(100*e)/100)}getFeaturesAtPoint(e,t){return function(e,t){return g.apply(this,arguments)}(this.view,e)}getAreaDimensions(){return(e=>{if(!(e&&e instanceof u))return"";var t=e.xmin,o=e.ymin,i=e.xmax,r=e.ymax-o,n=d(i-t),a=d(r);return"".concat(a," by ").concat(n)})(function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:y;if(!e.container)return null;var{width:o,height:i}=e.container.getBoundingClientRect(),r={x:t.left,y:i-t.bottom},n={x:o-t.right,y:t.top},a=e.toMap(r),s=e.toMap(n);return new u({xmin:a.x,ymin:a.y,xmax:s.x,ymax:s.y,spatialReference:a.spatialReference})}(this.view))}getCardinalMove(e,t){return function(e,t){var[o,i]=e,[r,n]=t,a=r-o,s=n-i,m=[];return Math.abs(s)>.1&&m.push("".concat(s>0?"north":"south"," ").concat(d(Math.abs(s)))),Math.abs(a)>.1&&m.push("".concat(a>0?"east":"west"," ").concat(d(Math.abs(a)))),m.join(", ")}(e,t)}getResolution(){return this.view.resolution}mapToScreen(e){var t=x(e),o=this.view.toScreen(t);return{x:o.x,y:o.y}}screenToMap(e){var t=this.view.toMap(e);return[t.x,t.y]}}export{b as default};
|