@defra/forms-engine-plugin 4.0.41 → 4.0.43

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.
Files changed (110) hide show
  1. package/.public/javascripts/application.min.js +1 -1
  2. package/.public/javascripts/application.min.js.map +1 -1
  3. package/.public/javascripts/shared.min.js +1 -1
  4. package/.public/javascripts/shared.min.js.map +1 -1
  5. package/.public/stylesheets/application.min.css +2 -2
  6. package/.public/stylesheets/application.min.css.map +1 -1
  7. package/.server/client/javascripts/location-map.d.ts +93 -0
  8. package/.server/client/javascripts/location-map.js +749 -0
  9. package/.server/client/javascripts/location-map.js.map +1 -0
  10. package/.server/client/javascripts/shared.d.ts +4 -0
  11. package/.server/client/javascripts/shared.js +5 -0
  12. package/.server/client/javascripts/shared.js.map +1 -1
  13. package/.server/client/stylesheets/_location-fields.scss +11 -0
  14. package/.server/client/stylesheets/application.scss +0 -1
  15. package/.server/client/stylesheets/shared.scss +1 -0
  16. package/.server/config/index.js +1 -1
  17. package/.server/config/index.js.map +1 -1
  18. package/.server/server/plugins/engine/configureEnginePlugin.d.ts +1 -1
  19. package/.server/server/plugins/engine/configureEnginePlugin.js +4 -2
  20. package/.server/server/plugins/engine/configureEnginePlugin.js.map +1 -1
  21. package/.server/server/plugins/engine/options.js +2 -1
  22. package/.server/server/plugins/engine/options.js.map +1 -1
  23. package/.server/server/plugins/engine/plugin.js +14 -1
  24. package/.server/server/plugins/engine/plugin.js.map +1 -1
  25. package/.server/server/plugins/engine/types.d.ts +1 -0
  26. package/.server/server/plugins/engine/types.js.map +1 -1
  27. package/.server/server/plugins/engine/views/components/_location-field-base.html +1 -1
  28. package/.server/server/plugins/engine/views/components/osgridreffield.html +4 -2
  29. package/.server/server/plugins/map/index.d.ts +7 -0
  30. package/.server/server/plugins/map/index.js +20 -0
  31. package/.server/server/plugins/map/index.js.map +1 -0
  32. package/.server/server/plugins/map/routes/get-os-token.d.ts +6 -0
  33. package/.server/server/plugins/map/routes/get-os-token.js +41 -0
  34. package/.server/server/plugins/map/routes/get-os-token.js.map +1 -0
  35. package/.server/server/plugins/map/routes/get-os-token.test.js +49 -0
  36. package/.server/server/plugins/map/routes/get-os-token.test.js.map +1 -0
  37. package/.server/server/plugins/map/routes/index.d.ts +10 -0
  38. package/.server/server/plugins/map/routes/index.js +172 -0
  39. package/.server/server/plugins/map/routes/index.js.map +1 -0
  40. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark.json +690 -0
  41. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark.png +0 -0
  42. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark@2x.json +690 -0
  43. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark@2x.png +0 -0
  44. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale.json +690 -0
  45. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale.png +0 -0
  46. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale@2x.json +690 -0
  47. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale@2x.png +0 -0
  48. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite.json +690 -0
  49. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite.png +0 -0
  50. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite@2x.json +690 -0
  51. package/.server/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite@2x.png +0 -0
  52. package/.server/server/plugins/map/routes/vts/OS_VTS_3857_Black_and_White.json +7858 -0
  53. package/.server/server/plugins/map/routes/vts/OS_VTS_3857_Dark.json +7669 -0
  54. package/.server/server/plugins/map/routes/vts/OS_VTS_3857_Outdoor.json +7653 -0
  55. package/.server/server/plugins/map/routes/vts/README.md +5 -0
  56. package/.server/server/plugins/map/service.d.ts +14 -0
  57. package/.server/server/plugins/map/service.js +76 -0
  58. package/.server/server/plugins/map/service.js.map +1 -0
  59. package/.server/server/plugins/map/service.test.js +120 -0
  60. package/.server/server/plugins/map/service.test.js.map +1 -0
  61. package/.server/server/plugins/map/test/__stubs__/find.d.ts +3 -0
  62. package/.server/server/plugins/map/test/__stubs__/find.js +216 -0
  63. package/.server/server/plugins/map/test/__stubs__/find.js.map +1 -0
  64. package/.server/server/plugins/map/test/__stubs__/nearest.d.ts +37 -0
  65. package/.server/server/plugins/map/test/__stubs__/nearest.js +38 -0
  66. package/.server/server/plugins/map/test/__stubs__/nearest.js.map +1 -0
  67. package/.server/server/plugins/map/types.d.ts +233 -0
  68. package/.server/server/plugins/map/types.js +121 -0
  69. package/.server/server/plugins/map/types.js.map +1 -0
  70. package/.server/server/types.d.ts +1 -0
  71. package/.server/server/types.js.map +1 -1
  72. package/package.json +3 -1
  73. package/src/client/javascripts/location-map.js +774 -0
  74. package/src/client/javascripts/shared.js +4 -0
  75. package/src/client/stylesheets/_location-fields.scss +11 -0
  76. package/src/client/stylesheets/application.scss +0 -1
  77. package/src/client/stylesheets/shared.scss +1 -0
  78. package/src/config/index.ts +1 -1
  79. package/src/server/plugins/engine/configureEnginePlugin.ts +4 -2
  80. package/src/server/plugins/engine/options.js +2 -1
  81. package/src/server/plugins/engine/plugin.ts +14 -1
  82. package/src/server/plugins/engine/types.ts +1 -0
  83. package/src/server/plugins/engine/views/components/_location-field-base.html +1 -1
  84. package/src/server/plugins/engine/views/components/osgridreffield.html +4 -2
  85. package/src/server/plugins/map/index.js +19 -0
  86. package/src/server/plugins/map/routes/get-os-token.js +41 -0
  87. package/src/server/plugins/map/routes/get-os-token.test.js +55 -0
  88. package/src/server/plugins/map/routes/index.js +192 -0
  89. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark.json +690 -0
  90. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark.png +0 -0
  91. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark@2x.json +690 -0
  92. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/dark@2x.png +0 -0
  93. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale.json +690 -0
  94. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale.png +0 -0
  95. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale@2x.json +690 -0
  96. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/greyscale@2x.png +0 -0
  97. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite.json +690 -0
  98. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite.png +0 -0
  99. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite@2x.json +690 -0
  100. package/src/server/plugins/map/routes/vts/OS_VTS_3857/resources/sprites/sprite@2x.png +0 -0
  101. package/src/server/plugins/map/routes/vts/OS_VTS_3857_Black_and_White.json +7858 -0
  102. package/src/server/plugins/map/routes/vts/OS_VTS_3857_Dark.json +7669 -0
  103. package/src/server/plugins/map/routes/vts/OS_VTS_3857_Outdoor.json +7653 -0
  104. package/src/server/plugins/map/routes/vts/README.md +5 -0
  105. package/src/server/plugins/map/service.js +84 -0
  106. package/src/server/plugins/map/service.test.js +144 -0
  107. package/src/server/plugins/map/test/__stubs__/find.js +271 -0
  108. package/src/server/plugins/map/test/__stubs__/nearest.js +46 -0
  109. package/src/server/plugins/map/types.js +120 -0
  110. package/src/server/types.ts +1 -0
@@ -0,0 +1,774 @@
1
+ // @ts-expect-error - no types
2
+ import OsGridRef, { LatLon } from 'geodesy/osgridref.js'
3
+
4
+ /**
5
+ * Converts lat long to easting and northing
6
+ * @param {object} param
7
+ * @param {number} param.lat
8
+ * @param {number} param.long
9
+ * @returns {{ easting: number, northing: number }}
10
+ */
11
+ function latLongToEastingNorthing({ lat, long }) {
12
+ const point = new LatLon(lat, long)
13
+
14
+ return point.toOsGrid()
15
+ }
16
+
17
+ /**
18
+ * Converts easting and northing to lat long
19
+ * @param {object} param
20
+ * @param {number} param.easting
21
+ * @param {number} param.northing
22
+ * @returns {{ lat: number, long: number }}
23
+ */
24
+ function eastingNorthingToLatLong({ easting, northing }) {
25
+ const point = new OsGridRef(easting, northing)
26
+ const latLong = point.toLatLon()
27
+
28
+ return { lat: latLong.latitude, long: latLong.longitude }
29
+ }
30
+
31
+ /**
32
+ * Converts lat long to an ordnance survey grid reference
33
+ * @param {object} param
34
+ * @param {number} param.lat
35
+ * @param {number} param.long
36
+ * @returns {string}
37
+ */
38
+ function latLongToOsGridRef({ lat, long }) {
39
+ const point = new LatLon(lat, long)
40
+
41
+ return point.toOsGrid().toString()
42
+ }
43
+
44
+ /**
45
+ * Converts an ordnance survey grid reference to lat long
46
+ * @param {string} osGridRef
47
+ * @returns {{ lat: number, long: number }}
48
+ */
49
+ function osGridRefToLatLong(osGridRef) {
50
+ const point = OsGridRef.parse(osGridRef)
51
+ const latLong = point.toLatLon()
52
+
53
+ return { lat: latLong.latitude, long: latLong.longitude }
54
+ }
55
+
56
+ // Center of UK
57
+ const DEFAULT_LAT = 53.825564
58
+ const DEFAULT_LONG = -2.421975
59
+
60
+ /** @type {InteractiveMapInitConfig} */
61
+ const defaultConfig = {
62
+ zoom: '6',
63
+ center: [DEFAULT_LONG, DEFAULT_LAT]
64
+ }
65
+
66
+ const COMPANY_SYMBOL_CODE = 169
67
+ const LOCATION_FIELD_SELECTOR = 'input.govuk-input'
68
+ const EVENTS = {
69
+ interactMarkerChange: 'interact:markerchange'
70
+ }
71
+
72
+ const defaultData = {
73
+ VTS_OUTDOOR_URL: '/api/maps/vts/OS_VTS_3857_Outdoor.json',
74
+ VTS_DARK_URL: '/api/maps/vts/OS_VTS_3857_Dark.json',
75
+ VTS_BLACK_AND_WHITE_URL: '/api/maps/vts/OS_VTS_3857_Black_and_White.json'
76
+ }
77
+
78
+ /**
79
+ * Make a form submit handler that only allows submissions from allowed buttons
80
+ * @param {HTMLButtonElement[]} buttons - the form buttons to allow submissions
81
+ */
82
+ export function formSubmitFactory(buttons) {
83
+ /**
84
+ * The submit handler
85
+ * @param {SubmitEvent} e
86
+ */
87
+ const onFormSubmit = function (e) {
88
+ if (
89
+ !(e.submitter instanceof HTMLButtonElement) ||
90
+ !buttons.includes(e.submitter)
91
+ ) {
92
+ e.preventDefault()
93
+ }
94
+ }
95
+
96
+ return onFormSubmit
97
+ }
98
+
99
+ /**
100
+ * Initialise location maps
101
+ * @param {Partial<MapsEnvironmentConfig>} config - the map configuration
102
+ */
103
+ export function initMaps(config = {}) {
104
+ const {
105
+ assetPath = '/assets',
106
+ apiPath = '/form/api',
107
+ data = defaultData
108
+ } = config
109
+ const locations = document.querySelectorAll('.app-location-field')
110
+
111
+ // TODO: Fix this in `interactive-map`
112
+ // If there are location components on the page fix up the main form submit
113
+ // handler so it doesn't fire when using the integrated map search feature
114
+ if (locations.length) {
115
+ const form = document.querySelector('form')
116
+
117
+ if (form === null) {
118
+ return
119
+ }
120
+
121
+ const buttons = Array.from(form.querySelectorAll('button'))
122
+ form.addEventListener('submit', formSubmitFactory(buttons), false)
123
+ }
124
+
125
+ locations.forEach((location, index) => {
126
+ processLocation({ assetPath, apiPath, data }, location, index)
127
+ })
128
+ }
129
+
130
+ /**
131
+ * OS API request proxy factory
132
+ * @param {string} apiPath - the root API path
133
+ */
134
+ export function makeTileRequestTransformer(apiPath) {
135
+ /**
136
+ * Proxy OS API requests via our server
137
+ * @param {string} url - the request URL
138
+ * @param {string} resourceType - the resource type
139
+ */
140
+ return function transformTileRequest(url, resourceType) {
141
+ if (url.startsWith('https://api.os.uk')) {
142
+ if (resourceType === 'Tile') {
143
+ return {
144
+ url: url.replace(
145
+ 'https://api.os.uk/maps/vector/v1/vts',
146
+ `${window.location.origin}${apiPath}`
147
+ ),
148
+ headers: {}
149
+ }
150
+ }
151
+
152
+ if (resourceType !== 'Style') {
153
+ return {
154
+ url: `${apiPath}/map-proxy?url=${encodeURIComponent(url)}`,
155
+ headers: {}
156
+ }
157
+ }
158
+ }
159
+
160
+ const spritesPath =
161
+ 'https://raw.githubusercontent.com/OrdnanceSurvey/OS-Vector-Tile-API-Stylesheets/main'
162
+
163
+ // Proxy sprite requests
164
+ if (url.startsWith(spritesPath)) {
165
+ const path = url.substring(spritesPath.length)
166
+ return {
167
+ url: `${apiPath}/maps/vts${path}`,
168
+ headers: {}
169
+ }
170
+ }
171
+
172
+ return { url, headers: {} }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Processes a location field to add map capability
178
+ * @param {MapsEnvironmentConfig} config - the location field element
179
+ * @param {Element} location - the location field element
180
+ * @param {*} index - the 0-based index
181
+ */
182
+ function processLocation(config, location, index) {
183
+ if (!(location instanceof HTMLDivElement)) {
184
+ return
185
+ }
186
+
187
+ const locationInputs = location.querySelector('.app-location-field-inputs')
188
+ if (!(locationInputs instanceof HTMLDivElement)) {
189
+ return
190
+ }
191
+ const locationType = location.dataset.locationtype
192
+
193
+ // Check for support
194
+ const supportedLocations = [
195
+ 'latlongfield',
196
+ 'eastingnorthingfield',
197
+ 'osgridreffield'
198
+ ]
199
+ if (!locationType || !supportedLocations.includes(locationType)) {
200
+ return
201
+ }
202
+
203
+ const mapContainer = document.createElement('div')
204
+ const mapId = `map_${index}`
205
+
206
+ mapContainer.setAttribute('id', mapId)
207
+ mapContainer.setAttribute('class', 'map-container')
208
+
209
+ const initConfig = getInitMapConfig(location) ?? defaultConfig
210
+
211
+ locationInputs.after(mapContainer)
212
+
213
+ const { map, interactPlugin } = createMap(mapId, initConfig, config)
214
+
215
+ map.on(
216
+ 'map:ready',
217
+ /**
218
+ * Callback function which fires when the map is ready
219
+ * @param {object} e - the event
220
+ * @param {MapLibreMap} e.map - the map provider instance
221
+ */
222
+ function onMapReady(e) {
223
+ switch (locationType) {
224
+ case 'latlongfield':
225
+ bindLatLongField(location, map, e.map)
226
+ break
227
+ case 'eastingnorthingfield':
228
+ bindEastingNorthingField(location, map, e.map)
229
+ break
230
+ case 'osgridreffield':
231
+ bindOsGridRefField(location, map, e.map)
232
+ break
233
+ default:
234
+ throw new Error('Not implemented')
235
+ }
236
+
237
+ // Add info panel
238
+ map.addPanel('info', {
239
+ showLabel: true,
240
+ label: 'How to use the map',
241
+ mobile: {
242
+ slot: 'bottom',
243
+ initiallyOpen: true,
244
+ dismissable: true,
245
+ modal: false
246
+ },
247
+ tablet: {
248
+ slot: 'bottom',
249
+ initiallyOpen: true,
250
+ dismissable: true,
251
+ modal: false
252
+ },
253
+ desktop: {
254
+ slot: 'bottom',
255
+ initiallyOpen: true,
256
+ dismissable: true,
257
+ modal: false
258
+ },
259
+ html: 'If using a map click on a point to update the location.<br><br>If using a keyboard, navigate to the point, centering the crosshair at the location and press enter.'
260
+ })
261
+
262
+ // Enable the interact plugin
263
+ interactPlugin.enable()
264
+ }
265
+ )
266
+ }
267
+
268
+ /**
269
+ * Create a Defra map instance
270
+ * @param {string} mapId - the map id
271
+ * @param {InteractiveMapInitConfig} initConfig - the map initial configuration
272
+ * @param {MapsEnvironmentConfig} mapsConfig - the map environment params
273
+ */
274
+ function createMap(mapId, initConfig, mapsConfig) {
275
+ const { assetPath, apiPath, data = defaultData } = mapsConfig
276
+ const logoAltText = 'Ordnance survey logo'
277
+
278
+ // @ts-expect-error - Defra namespace currently comes from UMD support files
279
+ const defra = window.defra
280
+
281
+ const interactPlugin = defra.interactPlugin({
282
+ dataLayers: [],
283
+ markerColor: { outdoor: '#ff0000', dark: '#00ff00' },
284
+ interactionMode: 'marker',
285
+ multiSelect: false
286
+ })
287
+
288
+ /** @type {InteractiveMap} */
289
+ const map = new defra.InteractiveMap(mapId, {
290
+ ...initConfig,
291
+ mapProvider: defra.maplibreProvider(),
292
+ reverseGeocodeProvider: defra.openNamesProvider({
293
+ url: `${apiPath}/reverse-geocode-proxy?easting={easting}&northing={northing}`
294
+ }),
295
+ behaviour: 'inline',
296
+ minZoom: 6,
297
+ maxZoom: 18,
298
+ containerHeight: '400px',
299
+ enableZoomControls: true,
300
+ transformRequest: makeTileRequestTransformer(apiPath),
301
+ plugins: [
302
+ defra.mapStylesPlugin({
303
+ mapStyles: [
304
+ {
305
+ id: 'outdoor',
306
+ label: 'Outdoor',
307
+ url: data.VTS_OUTDOOR_URL,
308
+ thumbnail: `${assetPath}/interactive-map/assets/images/outdoor-map-thumb.jpg`,
309
+ logo: `${assetPath}/interactive-map/assets/images/os-logo.svg`,
310
+ logoAltText,
311
+ attribution: `Contains OS data ${String.fromCodePoint(COMPANY_SYMBOL_CODE)} Crown copyright and database rights ${new Date().getFullYear()}`,
312
+ backgroundColor: '#f5f5f0'
313
+ },
314
+ {
315
+ id: 'dark',
316
+ label: 'Dark',
317
+ url: data.VTS_DARK_URL,
318
+ mapColorScheme: 'dark',
319
+ appColorScheme: 'dark',
320
+ thumbnail: `${assetPath}/interactive-map/assets/images/dark-map-thumb.jpg`,
321
+ logo: `${assetPath}/interactive-map/assets/images/os-logo-white.svg`,
322
+ logoAltText,
323
+ attribution: `Contains OS data ${String.fromCodePoint(COMPANY_SYMBOL_CODE)} Crown copyright and database rights ${new Date().getFullYear()}`
324
+ },
325
+ {
326
+ id: 'black-and-white',
327
+ label: 'Black/White',
328
+ url: data.VTS_BLACK_AND_WHITE_URL,
329
+ thumbnail: `${assetPath}/interactive-map/assets/images/black-and-white-map-thumb.jpg`,
330
+ logo: `${assetPath}/interactive-map/assets/images/os-logo-black.svg`,
331
+ logoAltText,
332
+ attribution: `Contains OS data ${String.fromCodePoint(COMPANY_SYMBOL_CODE)} Crown copyright and database rights ${new Date().getFullYear()}`
333
+ }
334
+ ]
335
+ }),
336
+ interactPlugin,
337
+ defra.searchPlugin({
338
+ osNamesURL: `${apiPath}/geocode-proxy?query={query}`,
339
+ width: '300px',
340
+ showMarker: false
341
+ }),
342
+ defra.scaleBarPlugin({
343
+ units: 'metric'
344
+ })
345
+ ]
346
+ })
347
+
348
+ return { map, interactPlugin }
349
+ }
350
+
351
+ /**
352
+ * Gets initial map config for a location field
353
+ * @param {HTMLDivElement} locationField - the location field element
354
+ */
355
+ function getInitMapConfig(locationField) {
356
+ const locationType = locationField.dataset.locationtype
357
+
358
+ switch (locationType) {
359
+ case 'latlongfield':
360
+ return getInitLatLongMapConfig(locationField)
361
+ case 'eastingnorthingfield':
362
+ return getInitEastingNorthingMapConfig(locationField)
363
+ case 'osgridreffield':
364
+ return getInitOsGridRefMapConfig(locationField)
365
+ default:
366
+ throw new Error('Not implemented')
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Validates lat and long is numeric and within UK bounds
372
+ * @param {string} strLat - the latitude string
373
+ * @param {string} strLong - the longitude string
374
+ * @returns {{ valid: false } | { valid: true, value: { lat: number, long: number } }}
375
+ */
376
+ function validateLatLong(strLat, strLong) {
377
+ const lat = strLat.trim() && Number(strLat.trim())
378
+ const long = strLong.trim() && Number(strLong.trim())
379
+
380
+ if (!lat || !long) {
381
+ return { valid: false }
382
+ }
383
+
384
+ const latMin = 49.85
385
+ const latMax = 60.859
386
+ const longMin = -13.687
387
+ const longMax = 1.767
388
+
389
+ const latInBounds = lat >= latMin && lat <= latMax
390
+ const longInBounds = long >= longMin && long <= longMax
391
+
392
+ if (!latInBounds || !longInBounds) {
393
+ return { valid: false }
394
+ }
395
+
396
+ return { valid: true, value: { lat, long } }
397
+ }
398
+
399
+ /**
400
+ * Validates easting and northing is numeric and within UK bounds
401
+ * @param {string} strEasting - the easting string
402
+ * @param {string} strNorthing - the northing string
403
+ * @returns {{ valid: false } | { valid: true, value: { easting: number, northing: number } }}
404
+ */
405
+ function validateEastingNorthing(strEasting, strNorthing) {
406
+ const easting = strEasting.trim() && Number(strEasting.trim())
407
+ const northing = strNorthing.trim() && Number(strNorthing.trim())
408
+
409
+ if (!easting || !northing) {
410
+ return { valid: false }
411
+ }
412
+
413
+ const eastingMin = 0
414
+ const eastingMax = 700000
415
+ const northingMin = 0
416
+ const northingMax = 1300000
417
+
418
+ const latInBounds = easting >= eastingMin && easting <= eastingMax
419
+ const longInBounds = northing >= northingMin && northing <= northingMax
420
+
421
+ if (!latInBounds || !longInBounds) {
422
+ return { valid: false }
423
+ }
424
+
425
+ return { valid: true, value: { easting, northing } }
426
+ }
427
+
428
+ /**
429
+ * Validates OS grid reference is correct
430
+ * @param {string} osGridRef - the OsGridRef
431
+ * @returns {{ valid: false } | { valid: true, value: string }}
432
+ */
433
+ function validateOsGridRef(osGridRef) {
434
+ if (!osGridRef) {
435
+ return { valid: false }
436
+ }
437
+
438
+ const pattern =
439
+ /^((([sS]|[nN])[a-hA-Hj-zJ-Z])|(([tT]|[oO])[abfglmqrvwABFGLMQRVW])|([hH][l-zL-Z])|([jJ][lmqrvwLMQRVW]))\s?(([0-9]{3})\s?([0-9]{3})|([0-9]{4})\s?([0-9]{4})|([0-9]{5})\s?([0-9]{5}))$/
440
+
441
+ const match = pattern.exec(osGridRef)
442
+
443
+ if (match === null) {
444
+ return { valid: false }
445
+ }
446
+
447
+ return { valid: true, value: match[0] }
448
+ }
449
+
450
+ /**
451
+ * Gets the inputs for a latlong location field
452
+ * @param {HTMLDivElement} locationField - the latlong location field element
453
+ */
454
+ function getLatLongInputs(locationField) {
455
+ const inputs = locationField.querySelectorAll(LOCATION_FIELD_SELECTOR)
456
+
457
+ if (inputs.length !== 2) {
458
+ throw new Error('Expected 2 inputs for lat and long')
459
+ }
460
+
461
+ const latInput = /** @type {HTMLInputElement} */ (inputs[0])
462
+ const longInput = /** @type {HTMLInputElement} */ (inputs[1])
463
+
464
+ return { latInput, longInput }
465
+ }
466
+
467
+ /**
468
+ * Gets the inputs for a easting/northing location field
469
+ * @param {HTMLDivElement} locationField - the eastingnorthing location field element
470
+ */
471
+ function getEastingNorthingInputs(locationField) {
472
+ const inputs = locationField.querySelectorAll(LOCATION_FIELD_SELECTOR)
473
+
474
+ if (inputs.length !== 2) {
475
+ throw new Error('Expected 2 inputs for easting and northing')
476
+ }
477
+
478
+ const eastingInput = /** @type {HTMLInputElement} */ (inputs[0])
479
+ const northingInput = /** @type {HTMLInputElement} */ (inputs[1])
480
+
481
+ return { eastingInput, northingInput }
482
+ }
483
+
484
+ /**
485
+ * Gets the input for a OS grid reference location field
486
+ * @param {HTMLDivElement} locationField - the osgridref location field element
487
+ */
488
+ function getOsGridRefInput(locationField) {
489
+ const input = locationField.querySelector(LOCATION_FIELD_SELECTOR)
490
+
491
+ if (input === null) {
492
+ throw new Error('Expected 1 input for osgridref')
493
+ }
494
+
495
+ return /** @type {HTMLInputElement} */ (input)
496
+ }
497
+
498
+ /**
499
+ * Get the initial map config for a center point
500
+ * @param {MapCenter} center - the point
501
+ */
502
+ function getInitMapCenterConfig(center) {
503
+ return {
504
+ zoom: '16',
505
+ center,
506
+ markers: [
507
+ {
508
+ id: 'location',
509
+ coords: center
510
+ }
511
+ ]
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Gets initial map config for a latlong location field
517
+ * @param {HTMLDivElement} locationField - the latlong location field element
518
+ * @returns {InteractiveMapInitConfig | undefined}
519
+ */
520
+ function getInitLatLongMapConfig(locationField) {
521
+ const { latInput, longInput } = getLatLongInputs(locationField)
522
+ const result = validateLatLong(latInput.value, longInput.value)
523
+
524
+ if (!result.valid) {
525
+ return undefined
526
+ }
527
+
528
+ /** @type {MapCenter} */
529
+ const center = [result.value.long, result.value.lat]
530
+
531
+ return getInitMapCenterConfig(center)
532
+ }
533
+
534
+ /**
535
+ * Gets initial map config for a easting/northing location field
536
+ * @param {HTMLDivElement} locationField - the eastingnorthing location field element
537
+ * @returns {InteractiveMapInitConfig | undefined}
538
+ */
539
+ function getInitEastingNorthingMapConfig(locationField) {
540
+ const { eastingInput, northingInput } =
541
+ getEastingNorthingInputs(locationField)
542
+ const result = validateEastingNorthing(
543
+ eastingInput.value,
544
+ northingInput.value
545
+ )
546
+
547
+ if (!result.valid) {
548
+ return undefined
549
+ }
550
+
551
+ const latlong = eastingNorthingToLatLong(result.value)
552
+
553
+ /** @type {MapCenter} */
554
+ const center = [latlong.long, latlong.lat]
555
+
556
+ return getInitMapCenterConfig(center)
557
+ }
558
+
559
+ /**
560
+ * Gets initial map config for an OS grid reference location field
561
+ * @param {HTMLDivElement} locationField - the osgridref location field element
562
+ * @returns {InteractiveMapInitConfig | undefined}
563
+ */
564
+ function getInitOsGridRefMapConfig(locationField) {
565
+ const osGridRefInput = getOsGridRefInput(locationField)
566
+ const result = validateOsGridRef(osGridRefInput.value)
567
+
568
+ if (!result.valid) {
569
+ return undefined
570
+ }
571
+
572
+ const latlong = osGridRefToLatLong(result.value)
573
+
574
+ /** @type {MapCenter} */
575
+ const center = [latlong.long, latlong.lat]
576
+
577
+ return getInitMapCenterConfig(center)
578
+ }
579
+
580
+ /**
581
+ * Bind a latlong field to the map
582
+ * @param {HTMLDivElement} locationField - the latlong location field
583
+ * @param {InteractiveMap} map - the map component instance (of InteractiveMap)
584
+ * @param {MapLibreMap} mapProvider - the map provider instance (of MapLibreMap)
585
+ */
586
+ function bindLatLongField(locationField, map, mapProvider) {
587
+ const { latInput, longInput } = getLatLongInputs(locationField)
588
+
589
+ map.on(
590
+ EVENTS.interactMarkerChange,
591
+ /**
592
+ * Callback function which fires when the map marker changes
593
+ * @param {object} e - the event
594
+ * @param {[number, number]} e.coords - the map marker coordinates
595
+ */
596
+ function onInteractMarkerChange(e) {
597
+ const maxPrecision = 7
598
+ latInput.value = e.coords[1].toFixed(maxPrecision)
599
+ longInput.value = e.coords[0].toFixed(maxPrecision)
600
+ }
601
+ )
602
+
603
+ /**
604
+ * Lat & long input change event listener
605
+ * Update the map view location when the inputs are changed
606
+ */
607
+ function onUpdateInputs() {
608
+ const result = validateLatLong(latInput.value, longInput.value)
609
+
610
+ if (result.valid) {
611
+ /** @type {MapCenter} */
612
+ const center = [result.value.long, result.value.lat]
613
+
614
+ centerMap(map, mapProvider, center)
615
+ }
616
+ }
617
+
618
+ latInput.addEventListener('change', onUpdateInputs, false)
619
+ longInput.addEventListener('change', onUpdateInputs, false)
620
+ }
621
+
622
+ /**
623
+ * Bind an eastingnorthing field to the map
624
+ * @param {HTMLDivElement} locationField - the eastingnorthing location field
625
+ * @param {InteractiveMap} map - the map component instance (of InteractiveMap)
626
+ * @param {MapLibreMap} mapProvider - the map provider instance (of MapLibreMap)
627
+ */
628
+ function bindEastingNorthingField(locationField, map, mapProvider) {
629
+ const { eastingInput, northingInput } =
630
+ getEastingNorthingInputs(locationField)
631
+
632
+ map.on(
633
+ EVENTS.interactMarkerChange,
634
+ /**
635
+ * Callback function which fires when the map marker changes
636
+ * @param {object} e - the event
637
+ * @param {[number, number]} e.coords - the map marker coordinates
638
+ */
639
+ function onInteractMarkerChange(e) {
640
+ const maxPrecision = 0
641
+ const point = latLongToEastingNorthing({
642
+ lat: e.coords[1],
643
+ long: e.coords[0]
644
+ })
645
+
646
+ eastingInput.value = point.easting.toFixed(maxPrecision)
647
+ northingInput.value = point.northing.toFixed(maxPrecision)
648
+ }
649
+ )
650
+
651
+ /**
652
+ * Easting & northing input change event listener
653
+ * Update the map view location when the inputs are changed
654
+ */
655
+ function onUpdateInputs() {
656
+ const result = validateEastingNorthing(
657
+ eastingInput.value,
658
+ northingInput.value
659
+ )
660
+
661
+ if (result.valid) {
662
+ const latlong = eastingNorthingToLatLong(result.value)
663
+
664
+ /** @type {MapCenter} */
665
+ const center = [latlong.long, latlong.lat]
666
+
667
+ centerMap(map, mapProvider, center)
668
+ }
669
+ }
670
+
671
+ eastingInput.addEventListener('change', onUpdateInputs, false)
672
+ northingInput.addEventListener('change', onUpdateInputs, false)
673
+ }
674
+
675
+ /**
676
+ * Bind an OS grid reference field to the map
677
+ * @param {HTMLDivElement} locationField - the osgridref location field
678
+ * @param {InteractiveMap} map - the map component instance (of InteractiveMap)
679
+ * @param {MapLibreMap} mapProvider - the map provider instance (of MapLibreMap)
680
+ */
681
+ function bindOsGridRefField(locationField, map, mapProvider) {
682
+ const osGridRefInput = getOsGridRefInput(locationField)
683
+
684
+ map.on(
685
+ EVENTS.interactMarkerChange,
686
+ /**
687
+ * Callback function which fires when the map marker changes
688
+ * @param {object} e - the event
689
+ * @param {[number, number]} e.coords - the map marker coordinates
690
+ */
691
+ function onInteractMarkerChange(e) {
692
+ const point = latLongToOsGridRef({
693
+ lat: e.coords[1],
694
+ long: e.coords[0]
695
+ })
696
+
697
+ osGridRefInput.value = point
698
+ }
699
+ )
700
+
701
+ /**
702
+ * OS grid reference input change event listener
703
+ * Update the map view location when the input is changed
704
+ */
705
+ function onUpdateInput() {
706
+ const result = validateOsGridRef(osGridRefInput.value)
707
+
708
+ if (result.valid) {
709
+ const latlong = osGridRefToLatLong(result.value)
710
+
711
+ /** @type {MapCenter} */
712
+ const center = [latlong.long, latlong.lat]
713
+
714
+ centerMap(map, mapProvider, center)
715
+ }
716
+ }
717
+
718
+ osGridRefInput.addEventListener('change', onUpdateInput, false)
719
+ }
720
+
721
+ /**
722
+ * Updates the marker position and moves the map view port the new location
723
+ * @param {InteractiveMap} map - the map component instance (of InteractiveMap)
724
+ * @param {MapLibreMap} mapProvider - the map provider instance (of MapLibreMap)
725
+ * @param {MapCenter} center - the point
726
+ */
727
+ function centerMap(map, mapProvider, center) {
728
+ // Move the 'location' marker to the new point
729
+ map.addMarker('location', center)
730
+
731
+ // Pan & zoom the map to the new valid location
732
+ mapProvider.flyTo({
733
+ center,
734
+ zoom: 14,
735
+ essential: true
736
+ })
737
+ }
738
+
739
+ /**
740
+ * @typedef {object} InteractiveMap - an instance of a InteractiveMap
741
+ * @property {Function} on - register callback listeners to map events
742
+ * @property {Function} addPanel - adds a new panel to the map
743
+ * @property {Function} addMarker - adds/updates a marker
744
+ */
745
+
746
+ /**
747
+ * @typedef {object} MapLibreMap
748
+ * @property {Function} flyTo - pans/zooms to a new location
749
+ */
750
+
751
+ /**
752
+ * @typedef {[number, number]} MapCenter - Map center point as [long, lat]
753
+ */
754
+
755
+ /**
756
+ * @typedef {object} InteractiveMapInitConfig - additional config that can be provided to InteractiveMap
757
+ * @property {string} zoom - the zoom level of the map
758
+ * @property {MapCenter} center - the center point of the map
759
+ * @property {{ id: string, coords: MapCenter}[]} [markers] - the markers to add to the map
760
+ */
761
+
762
+ /**
763
+ * @typedef {object} TileData
764
+ * @property {string} VTS_OUTDOOR_URL - the outdoor tile URL
765
+ * @property {string} VTS_DARK_URL - the dark tile URL
766
+ * @property {string} VTS_BLACK_AND_WHITE_URL - the black and white tile URL
767
+ */
768
+
769
+ /**
770
+ * @typedef {object} MapsEnvironmentConfig
771
+ * @property {string} assetPath - the root asset path
772
+ * @property {string} apiPath - the root API path
773
+ * @property {TileData} data - the tile data config
774
+ */