Skip to content

Commit ee85669

Browse files
Add OS grid reference to the location map
1 parent c3d788c commit ee85669

2 files changed

Lines changed: 161 additions & 5 deletions

File tree

src/client/javascripts/location-map.js

Lines changed: 157 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ function eastingNorthingToLatLong({ easting, northing }) {
2828
return { lat: latLong.latitude, long: latLong.longitude }
2929
}
3030

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+
3156
// Center of UK
3257
const DEFAULT_LAT = 53.825564
3358
const DEFAULT_LONG = -2.421975
@@ -145,7 +170,11 @@ function processLocation(config, location, index) {
145170
const locationType = location.dataset.locationtype
146171

147172
// Check for support
148-
const supportedLocations = ['latlongfield', 'eastingnorthingfield']
173+
const supportedLocations = [
174+
'latlongfield',
175+
'eastingnorthingfield',
176+
'osgridreffield'
177+
]
149178
if (!locationType || !supportedLocations.includes(locationType)) {
150179
return
151180
}
@@ -177,6 +206,9 @@ function processLocation(config, location, index) {
177206
case 'eastingnorthingfield':
178207
bindEastingNorthingField(location, map, e.map)
179208
break
209+
case 'osgridreffield':
210+
bindOsGridRefField(location, map, e.map)
211+
break
180212
default:
181213
throw new Error('Not implemented')
182214
}
@@ -302,6 +334,8 @@ function getInitMapConfig(locationField) {
302334
return getInitLatLongMapConfig(locationField)
303335
case 'eastingnorthingfield':
304336
return getInitEastingNorthingMapConfig(locationField)
337+
case 'osgridreffield':
338+
return getInitOsGridRefMapConfig(locationField)
305339
default:
306340
throw new Error('Not implemented')
307341
}
@@ -366,7 +400,29 @@ function validateEastingNorthing(strEasting, strNorthing) {
366400
}
367401

368402
/**
369-
* Gets initial map config for a latlong location field
403+
* Validates OS grid reference is correct
404+
* @param {string} osGridRef - the OsGridRef
405+
* @returns {{ valid: false } | { valid: true, value: string }}
406+
*/
407+
function validateOsGridRef(osGridRef) {
408+
if (!osGridRef) {
409+
return { valid: false }
410+
}
411+
412+
const pattern =
413+
/^((([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}))$/
414+
415+
const match = pattern.exec(osGridRef)
416+
417+
if (match === null) {
418+
return { valid: false }
419+
}
420+
421+
return { valid: true, value: match[0] }
422+
}
423+
424+
/**
425+
* Gets the inputs for a latlong location field
370426
* @param {HTMLDivElement} locationField - the latlong location field element
371427
*/
372428
function getLatLongInputs(locationField) {
@@ -383,7 +439,7 @@ function getLatLongInputs(locationField) {
383439
}
384440

385441
/**
386-
* Gets initial map config for a easting/northing location field
442+
* Gets the inputs for a easting/northing location field
387443
* @param {HTMLDivElement} locationField - the eastingnorthing location field element
388444
*/
389445
function getEastingNorthingInputs(locationField) {
@@ -399,6 +455,20 @@ function getEastingNorthingInputs(locationField) {
399455
return { eastingInput, northingInput }
400456
}
401457

458+
/**
459+
* Gets the input for a OS grid reference location field
460+
* @param {HTMLDivElement} locationField - the osgridref location field element
461+
*/
462+
function getOsGridRefInput(locationField) {
463+
const input = locationField.querySelector('input.govuk-input')
464+
465+
if (input === null) {
466+
throw new Error('Expected 1 input for osgridref')
467+
}
468+
469+
return /** @type {HTMLInputElement} */ (input)
470+
}
471+
402472
/**
403473
* Gets initial map config for a latlong location field
404474
* @param {HTMLDivElement} locationField - the latlong location field element
@@ -461,6 +531,36 @@ function getInitEastingNorthingMapConfig(locationField) {
461531
}
462532
}
463533

534+
/**
535+
* Gets initial map config for an OS grid reference location field
536+
* @param {HTMLDivElement} locationField - the osgridref location field element
537+
* @returns {DefraMapInitConfig | undefined}
538+
*/
539+
function getInitOsGridRefMapConfig(locationField) {
540+
const osGridRefInput = getOsGridRefInput(locationField)
541+
const result = validateOsGridRef(osGridRefInput.value)
542+
543+
if (!result.valid) {
544+
return undefined
545+
}
546+
547+
const latlong = osGridRefToLatLong(result.value)
548+
549+
/** @type {MapCenter} */
550+
const center = [latlong.long, latlong.lat]
551+
552+
return {
553+
zoom: '16',
554+
center,
555+
markers: [
556+
{
557+
id: 'location',
558+
coords: center
559+
}
560+
]
561+
}
562+
}
563+
464564
/**
465565
* Bind a latlong field to the map
466566
* @param {HTMLDivElement} locationField - the latlong location field
@@ -572,6 +672,60 @@ function bindEastingNorthingField(locationField, map, mapProvider) {
572672
northingInput.addEventListener('change', onUpdateInputs, false)
573673
}
574674

675+
/**
676+
* Bind an OS grid reference field to the map
677+
* @param {HTMLDivElement} locationField - the osgridref location field
678+
* @param {DefraMap} map - the map component instance (of DefraMap)
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+
'interact:markerchange',
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+
// Move the 'location' marker to the new point
715+
map.addMarker('location', center)
716+
717+
// Pan & zoom the map to the new valid location
718+
mapProvider.flyTo({
719+
center,
720+
zoom: 14,
721+
essential: true
722+
})
723+
}
724+
}
725+
726+
osGridRefInput.addEventListener('change', onUpdateInput, false)
727+
}
728+
575729
/**
576730
* @typedef {object} DefraMap - an instance of a DefraMap
577731
* @property {Function} on - register callback listeners to map events

src/server/plugins/engine/views/components/osgridreffield.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33

44
{% macro OsGridRefField(component) %}
55
{% set hasErrors = component.model.errorMessage %}
6-
<div class="govuk-form-group {{ "govuk-form-group--error" if hasErrors }}">
7-
{{ TextField(component) }}
6+
<div class="govuk-form-group app-location-field {{ "govuk-form-group--error" if hasErrors }}" data-locationtype="{{component.type | lower}}">
7+
<div class="app-location-field-inputs">
8+
{{ TextField(component) }}
9+
</div>
810

911
{% if component.model.instructionText %}
1012
{{ govukDetails({

0 commit comments

Comments
 (0)