@defra/forms-engine-plugin 4.0.40 → 4.0.42

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