@defra/forms-engine-plugin 4.3.0 → 4.4.0

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 (83) hide show
  1. package/.public/javascripts/application.min.js.map +1 -1
  2. package/.public/javascripts/shared.min.js +1 -1
  3. package/.public/javascripts/shared.min.js.map +1 -1
  4. package/.public/javascripts/vendor/accessible-autocomplete.min.js.map +1 -1
  5. package/.public/stylesheets/application.min.css +1 -1
  6. package/.public/stylesheets/application.min.css.map +1 -1
  7. package/.server/client/javascripts/geospatial-map.d.ts +189 -0
  8. package/.server/client/javascripts/geospatial-map.js +1068 -0
  9. package/.server/client/javascripts/geospatial-map.js.map +1 -0
  10. package/.server/client/javascripts/location-map.d.ts +6 -91
  11. package/.server/client/javascripts/location-map.js +78 -385
  12. package/.server/client/javascripts/location-map.js.map +1 -1
  13. package/.server/client/javascripts/map.d.ts +199 -0
  14. package/.server/client/javascripts/map.js +384 -0
  15. package/.server/client/javascripts/map.js.map +1 -0
  16. package/.server/client/javascripts/shared.d.ts +3 -1
  17. package/.server/client/javascripts/shared.js +3 -1
  18. package/.server/client/javascripts/shared.js.map +1 -1
  19. package/.server/client/stylesheets/shared.scss +7 -0
  20. package/.server/server/plugins/engine/components/ComponentBase.d.ts +1 -0
  21. package/.server/server/plugins/engine/components/ComponentBase.js +2 -0
  22. package/.server/server/plugins/engine/components/ComponentBase.js.map +1 -1
  23. package/.server/server/plugins/engine/components/FormComponent.d.ts +9 -1
  24. package/.server/server/plugins/engine/components/FormComponent.js +22 -0
  25. package/.server/server/plugins/engine/components/FormComponent.js.map +1 -1
  26. package/.server/server/plugins/engine/components/GeospatialField.d.ts +77 -0
  27. package/.server/server/plugins/engine/components/GeospatialField.js +102 -0
  28. package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -0
  29. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.d.ts +3 -0
  30. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js +63 -0
  31. package/.server/server/plugins/engine/components/helpers/__stubs__/geospatial.js.map +1 -0
  32. package/.server/server/plugins/engine/components/helpers/components.d.ts +1 -1
  33. package/.server/server/plugins/engine/components/helpers/components.js +7 -0
  34. package/.server/server/plugins/engine/components/helpers/components.js.map +1 -1
  35. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +6 -0
  36. package/.server/server/plugins/engine/components/helpers/geospatial.js +71 -0
  37. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -0
  38. package/.server/server/plugins/engine/components/helpers/geospatial.test.js +42 -0
  39. package/.server/server/plugins/engine/components/helpers/geospatial.test.js.map +1 -0
  40. package/.server/server/plugins/engine/components/index.d.ts +1 -0
  41. package/.server/server/plugins/engine/components/index.js +1 -0
  42. package/.server/server/plugins/engine/components/index.js.map +1 -1
  43. package/.server/server/plugins/engine/pageControllers/PageController.d.ts +1 -0
  44. package/.server/server/plugins/engine/pageControllers/PageController.js +2 -0
  45. package/.server/server/plugins/engine/pageControllers/PageController.js.map +1 -1
  46. package/.server/server/plugins/engine/pageControllers/helpers/submission.js +13 -1
  47. package/.server/server/plugins/engine/pageControllers/helpers/submission.js.map +1 -1
  48. package/.server/server/plugins/engine/pageControllers/validationOptions.js +2 -1
  49. package/.server/server/plugins/engine/pageControllers/validationOptions.js.map +1 -1
  50. package/.server/server/plugins/engine/types.d.ts +63 -2
  51. package/.server/server/plugins/engine/types.js +33 -0
  52. package/.server/server/plugins/engine/types.js.map +1 -1
  53. package/.server/server/plugins/engine/views/components/geospatialfield.html +7 -0
  54. package/.server/server/plugins/nunjucks/context.test.js.map +1 -1
  55. package/.server/server/plugins/nunjucks/filters/field.d.ts +1 -1
  56. package/.server/server/routes/types.js.map +1 -1
  57. package/.server/server/services/cacheService.js +3 -0
  58. package/.server/server/services/cacheService.js.map +1 -1
  59. package/package.json +9 -5
  60. package/src/client/javascripts/geospatial-map.js +1023 -0
  61. package/src/client/javascripts/location-map.js +94 -390
  62. package/src/client/javascripts/map.js +389 -0
  63. package/src/client/javascripts/shared.js +3 -1
  64. package/src/client/stylesheets/shared.scss +7 -0
  65. package/src/server/plugins/engine/components/ComponentBase.ts +2 -0
  66. package/src/server/plugins/engine/components/FormComponent.ts +29 -0
  67. package/src/server/plugins/engine/components/GeospatialField.test.ts +380 -0
  68. package/src/server/plugins/engine/components/GeospatialField.ts +145 -0
  69. package/src/server/plugins/engine/components/helpers/__stubs__/geospatial.ts +85 -0
  70. package/src/server/plugins/engine/components/helpers/components.test.ts +44 -0
  71. package/src/server/plugins/engine/components/helpers/components.ts +10 -0
  72. package/src/server/plugins/engine/components/helpers/geospatial.test.js +55 -0
  73. package/src/server/plugins/engine/components/helpers/geospatial.ts +93 -0
  74. package/src/server/plugins/engine/components/index.ts +1 -0
  75. package/src/server/plugins/engine/pageControllers/PageController.ts +2 -0
  76. package/src/server/plugins/engine/pageControllers/helpers/submission.test.ts +74 -0
  77. package/src/server/plugins/engine/pageControllers/helpers/submission.ts +17 -1
  78. package/src/server/plugins/engine/pageControllers/validationOptions.ts +3 -1
  79. package/src/server/plugins/engine/types.ts +77 -4
  80. package/src/server/plugins/engine/views/components/geospatialfield.html +7 -0
  81. package/src/server/plugins/nunjucks/context.test.js +2 -3
  82. package/src/server/routes/types.ts +4 -2
  83. package/src/server/services/cacheService.ts +2 -0
@@ -0,0 +1,389 @@
1
+ import { centroid } from '@turf/centroid'
2
+ // @ts-expect-error - no types
3
+ import OsGridRef, { LatLon } from 'geodesy/osgridref.js'
4
+
5
+ import { processGeospatial } from '~/src/client/javascripts/geospatial-map.js'
6
+ import { processLocation } from '~/src/client/javascripts/location-map.js'
7
+
8
+ // Center of UK
9
+ const DEFAULT_LAT = 53.825564
10
+ const DEFAULT_LONG = -2.421975
11
+ const COMPANY_SYMBOL_CODE = 169
12
+
13
+ const defaultData = {
14
+ VTS_OUTDOOR_URL: '/api/maps/vts/OS_VTS_3857_Outdoor.json',
15
+ VTS_DARK_URL: '/api/maps/vts/OS_VTS_3857_Dark.json',
16
+ VTS_BLACK_AND_WHITE_URL: '/api/maps/vts/OS_VTS_3857_Black_and_White.json'
17
+ }
18
+
19
+ /**
20
+ * Converts lat long to easting and northing
21
+ * @param {object} param
22
+ * @param {number} param.lat
23
+ * @param {number} param.long
24
+ * @returns {{ easting: number, northing: number }}
25
+ */
26
+ export function latLongToEastingNorthing({ lat, long }) {
27
+ const point = new LatLon(lat, long)
28
+
29
+ return point.toOsGrid()
30
+ }
31
+
32
+ /**
33
+ * Converts easting and northing to lat long
34
+ * @param {object} param
35
+ * @param {number} param.easting
36
+ * @param {number} param.northing
37
+ * @returns {{ lat: number, long: number }}
38
+ */
39
+ export function eastingNorthingToLatLong({ easting, northing }) {
40
+ const point = new OsGridRef(easting, northing)
41
+ const latLong = point.toLatLon()
42
+
43
+ return { lat: latLong.latitude, long: latLong.longitude }
44
+ }
45
+
46
+ /**
47
+ * Converts lat long to an ordnance survey grid reference
48
+ * @param {object} param
49
+ * @param {number} param.lat
50
+ * @param {number} param.long
51
+ * @returns {string}
52
+ */
53
+ export function latLongToOsGridRef({ lat, long }) {
54
+ const point = new LatLon(lat, long)
55
+
56
+ return point.toOsGrid().toString()
57
+ }
58
+
59
+ /**
60
+ * Converts an ordnance survey grid reference to lat long
61
+ * @param {string} osGridRef
62
+ * @returns {{ lat: number, long: number }}
63
+ */
64
+ export function osGridRefToLatLong(osGridRef) {
65
+ const point = OsGridRef.parse(osGridRef)
66
+ const latLong = point.toLatLon()
67
+
68
+ return { lat: latLong.latitude, long: latLong.longitude }
69
+ }
70
+
71
+ /**
72
+ * Get the grid ref from the first coordinate of a long/lat feature
73
+ * @param {Feature} feature
74
+ */
75
+ export function getCoordinateGridRef(feature) {
76
+ if (feature.geometry.type === 'Point') {
77
+ const [long, lat] = feature.geometry.coordinates
78
+ const point = new LatLon(lat, long)
79
+
80
+ return point.toOsGrid().toString()
81
+ } else if (feature.geometry.type === 'LineString') {
82
+ const [long, lat] = feature.geometry.coordinates[0]
83
+ const point = new LatLon(lat, long)
84
+
85
+ return point.toOsGrid().toString()
86
+ } else {
87
+ const [long, lat] = feature.geometry.coordinates[0][0]
88
+ const point = new LatLon(lat, long)
89
+
90
+ return point.toOsGrid().toString()
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Get the centroid grid ref from a long/lat feature
96
+ * @param {Feature} feature
97
+ */
98
+ export function getCentroidGridRef(feature) {
99
+ if (feature.geometry.type === 'Point') {
100
+ const [long, lat] = feature.geometry.coordinates
101
+ const point = new LatLon(lat, long)
102
+
103
+ return point.toOsGrid().toString()
104
+ } else {
105
+ const centre = centroid(feature)
106
+ const [long, lat] = centre.geometry.coordinates
107
+ const point = new LatLon(lat, long)
108
+
109
+ return point.toOsGrid().toString()
110
+ }
111
+ }
112
+
113
+ /** @type {InteractiveMapInitConfig} */
114
+ export const defaultConfig = {
115
+ zoom: '6',
116
+ center: [DEFAULT_LONG, DEFAULT_LAT]
117
+ }
118
+
119
+ export const EVENTS = {
120
+ mapReady: 'map:ready',
121
+ interactMarkerChange: 'interact:markerchange',
122
+ drawReady: 'draw:ready',
123
+ drawCreated: 'draw:created',
124
+ drawEdited: 'draw:edited',
125
+ drawCancelled: 'draw:cancelled'
126
+ }
127
+
128
+ /**
129
+ * Make a form submit handler that only allows submissions from allowed buttons
130
+ * @param {HTMLButtonElement[]} buttons - the form buttons to allow submissions
131
+ */
132
+ export function formSubmitFactory(buttons) {
133
+ /**
134
+ * The submit handler
135
+ * @param {SubmitEvent} e
136
+ */
137
+ const onFormSubmit = function (e) {
138
+ if (
139
+ !(e.submitter instanceof HTMLButtonElement) ||
140
+ !buttons.includes(e.submitter)
141
+ ) {
142
+ e.preventDefault()
143
+ }
144
+ }
145
+
146
+ return onFormSubmit
147
+ }
148
+
149
+ /**
150
+ * Initialise location maps
151
+ * @param {Partial<MapsEnvironmentConfig>} config - the map configuration
152
+ */
153
+ export function initMaps(config = {}) {
154
+ const {
155
+ assetPath = '/assets',
156
+ apiPath = '/form/api',
157
+ data = defaultData
158
+ } = config
159
+ const locations = document.querySelectorAll('.app-location-field')
160
+ const geospatials = document.querySelectorAll('.app-geospatial-field')
161
+
162
+ // TODO: Fix this in `interactive-map`
163
+ // If there are location components on the page fix up the main form submit
164
+ // handler so it doesn't fire when using the integrated map search feature
165
+ if (locations.length) {
166
+ const form = locations[0].closest('form')
167
+
168
+ if (form === null) {
169
+ return
170
+ }
171
+
172
+ const buttons = Array.from(form.querySelectorAll('button'))
173
+ form.addEventListener('submit', formSubmitFactory(buttons), false)
174
+ }
175
+
176
+ if (geospatials.length) {
177
+ const form = geospatials[0].closest('form')
178
+
179
+ if (form === null) {
180
+ return
181
+ }
182
+
183
+ const buttons = Array.from(form.querySelectorAll('button'))
184
+ form.addEventListener('submit', formSubmitFactory(buttons), false)
185
+ }
186
+
187
+ locations.forEach((location, index) => {
188
+ processLocation({ assetPath, apiPath, data }, location, index)
189
+ })
190
+
191
+ geospatials.forEach((geospatial, index) => {
192
+ processGeospatial({ assetPath, apiPath, data }, geospatial, index)
193
+ })
194
+ }
195
+
196
+ /**
197
+ * OS API request proxy factory
198
+ * @param {string} apiPath - the root API path
199
+ */
200
+ export function makeTileRequestTransformer(apiPath) {
201
+ /**
202
+ * Proxy OS API requests via our server
203
+ * @param {string} url - the request URL
204
+ * @param {string} resourceType - the resource type
205
+ */
206
+ return function transformTileRequest(url, resourceType) {
207
+ if (url.startsWith('https://api.os.uk')) {
208
+ if (resourceType === 'Tile') {
209
+ return {
210
+ url: url.replace(
211
+ 'https://api.os.uk/maps/vector/v1/vts',
212
+ `${window.location.origin}${apiPath}`
213
+ ),
214
+ headers: {}
215
+ }
216
+ }
217
+
218
+ if (resourceType !== 'Style') {
219
+ return {
220
+ url: `${apiPath}/map-proxy?url=${encodeURIComponent(url)}`,
221
+ headers: {}
222
+ }
223
+ }
224
+ }
225
+
226
+ const spritesPath =
227
+ 'https://raw.githubusercontent.com/OrdnanceSurvey/OS-Vector-Tile-API-Stylesheets/main'
228
+
229
+ // Proxy sprite requests
230
+ if (url.startsWith(spritesPath)) {
231
+ const path = url.substring(spritesPath.length)
232
+ return {
233
+ url: `${apiPath}/maps/vts${path}`,
234
+ headers: {}
235
+ }
236
+ }
237
+
238
+ return { url, headers: {} }
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Create a Defra map instance
244
+ * @param {string} mapId - the map id
245
+ * @param {InteractiveMapInitConfig} initConfig - the map initial configuration
246
+ * @param {MapsEnvironmentConfig} mapsConfig - the map environment params
247
+ */
248
+ export function createMap(mapId, initConfig, mapsConfig) {
249
+ const { assetPath, apiPath, data = defaultData } = mapsConfig
250
+ const logoAltText = 'Ordnance survey logo'
251
+
252
+ // @ts-expect-error - Defra namespace currently comes from UMD support files
253
+ const defra = window.defra
254
+
255
+ const interactPlugin = defra.interactPlugin({
256
+ markerColor: { outdoor: '#ff0000', dark: '#00ff00' },
257
+ interactionMode: 'marker',
258
+ multiSelect: false
259
+ })
260
+
261
+ /** @type {InteractiveMap} */
262
+ const map = new defra.InteractiveMap(mapId, {
263
+ enableFullscreen: true,
264
+ autoColorScheme: false,
265
+ mapProvider: defra.maplibreProvider(),
266
+ reverseGeocodeProvider: defra.openNamesProvider({
267
+ url: `${apiPath}/reverse-geocode-proxy?easting={easting}&northing={northing}`
268
+ }),
269
+ behaviour: 'inline',
270
+ minZoom: 6,
271
+ maxZoom: 18,
272
+ containerHeight: '400px',
273
+ enableZoomControls: true,
274
+ transformRequest: makeTileRequestTransformer(apiPath),
275
+ ...initConfig,
276
+ plugins: [
277
+ defra.mapStylesPlugin({
278
+ mapStyles: [
279
+ {
280
+ id: 'outdoor',
281
+ label: 'Outdoor',
282
+ url: data.VTS_OUTDOOR_URL,
283
+ thumbnail: `${assetPath}/interactive-map/assets/images/outdoor-map-thumb.jpg`,
284
+ logo: `${assetPath}/interactive-map/assets/images/os-logo.svg`,
285
+ logoAltText,
286
+ attribution: `Contains OS data ${String.fromCodePoint(COMPANY_SYMBOL_CODE)} Crown copyright and database rights ${new Date().getFullYear()}`,
287
+ backgroundColor: '#f5f5f0'
288
+ },
289
+ {
290
+ id: 'dark',
291
+ label: 'Dark',
292
+ url: data.VTS_DARK_URL,
293
+ mapColorScheme: 'dark',
294
+ appColorScheme: 'dark',
295
+ thumbnail: `${assetPath}/interactive-map/assets/images/dark-map-thumb.jpg`,
296
+ logo: `${assetPath}/interactive-map/assets/images/os-logo-white.svg`,
297
+ logoAltText,
298
+ attribution: `Contains OS data ${String.fromCodePoint(COMPANY_SYMBOL_CODE)} Crown copyright and database rights ${new Date().getFullYear()}`
299
+ },
300
+ {
301
+ id: 'black-and-white',
302
+ label: 'Black/White',
303
+ url: data.VTS_BLACK_AND_WHITE_URL,
304
+ thumbnail: `${assetPath}/interactive-map/assets/images/black-and-white-map-thumb.jpg`,
305
+ logo: `${assetPath}/interactive-map/assets/images/os-logo-black.svg`,
306
+ logoAltText,
307
+ attribution: `Contains OS data ${String.fromCodePoint(COMPANY_SYMBOL_CODE)} Crown copyright and database rights ${new Date().getFullYear()}`
308
+ }
309
+ ]
310
+ }),
311
+ interactPlugin,
312
+ defra.searchPlugin({
313
+ osNamesURL: `${apiPath}/geocode-proxy?query={query}`,
314
+ width: '300px',
315
+ showMarker: false
316
+ }),
317
+ defra.scaleBarPlugin({
318
+ units: 'metric'
319
+ }),
320
+ ...(initConfig.plugins ?? [])
321
+ ]
322
+ })
323
+
324
+ return { map, interactPlugin }
325
+ }
326
+
327
+ /**
328
+ * Updates the marker position and moves the map view port the new location
329
+ * @param {InteractiveMap} map - the map component instance (of InteractiveMap)
330
+ * @param {MapLibreMap} mapProvider - the map provider instance (of MapLibreMap)
331
+ * @param {MapCenter} center - the point
332
+ */
333
+ export function centerMap(map, mapProvider, center) {
334
+ // Move the 'location' marker to the new point
335
+ map.addMarker('location', center)
336
+
337
+ // Pan & zoom the map to the new valid location
338
+ mapProvider.flyTo({
339
+ center,
340
+ zoom: 14,
341
+ essential: true
342
+ })
343
+ }
344
+
345
+ /**
346
+ * @typedef {object} InteractiveMap - an instance of a InteractiveMap
347
+ * @property {Function} on - register callback listeners to map events
348
+ * @property {Function} addPanel - adds a new panel to the map
349
+ * @property {Function} addMarker - adds/updates a marker
350
+ * @property {Function} removeMarker - removes a marker
351
+ * @property {Function} addButton - adds/updates a button
352
+ * @property {Function} toggleButtonState - toggle the state of a button
353
+ */
354
+
355
+ /**
356
+ * @typedef {object} MapLibreMap
357
+ * @property {Function} flyTo - pans/zooms to a new location
358
+ * @property {Function} fitBounds - fits the my to the new bounds
359
+ */
360
+
361
+ /**
362
+ * @typedef {[number, number]} MapCenter - Map center point as [long, lat]
363
+ */
364
+
365
+ /**
366
+ * @typedef {object} InteractiveMapInitConfig - additional config that can be provided to InteractiveMap
367
+ * @property {string} zoom - the zoom level of the map
368
+ * @property {MapCenter} center - the center point of the map
369
+ * @property {{ id: string, coords: MapCenter }[]} [markers] - the markers to add to the map
370
+ * @property {any[]} [plugins] - additional plugins
371
+ */
372
+
373
+ /**
374
+ * @typedef {object} TileData
375
+ * @property {string} VTS_OUTDOOR_URL - the outdoor tile URL
376
+ * @property {string} VTS_DARK_URL - the dark tile URL
377
+ * @property {string} VTS_BLACK_AND_WHITE_URL - the black and white tile URL
378
+ */
379
+
380
+ /**
381
+ * @typedef {object} MapsEnvironmentConfig
382
+ * @property {string} assetPath - the root asset path
383
+ * @property {string} apiPath - the root API path
384
+ * @property {TileData} data - the tile data config
385
+ */
386
+
387
+ /**
388
+ * @import { Feature } from '~/src/server/plugins/engine/types.js'
389
+ */
@@ -2,7 +2,9 @@ import { initAllAutocomplete as initAllAutocompleteImp } from '~/src/client/java
2
2
  import { initFileUpload as initFileUploadImp } from '~/src/client/javascripts/file-upload.js'
3
3
  import { initAllGovuk as initAllGovukImp } from '~/src/client/javascripts/govuk.js'
4
4
  import { initPreviewCloseLink as initPreviewCloseLinkImp } from '~/src/client/javascripts/preview-close-link.js'
5
- export { initMaps } from '~/src/client/javascripts/location-map.js'
5
+ export { initMaps } from '~/src/client/javascripts/map.js'
6
+ export * as map from '~/src/client/javascripts/map.js'
7
+ export * as geospatialMap from '~/src/client/javascripts/geospatial-map.js'
6
8
 
7
9
  export const initAllGovuk = initAllGovukImp
8
10
  export const initAllAutocomplete = initAllAutocompleteImp
@@ -16,6 +16,13 @@
16
16
  visibility: hidden;
17
17
  }
18
18
 
19
+ .js-enabled {
20
+ .js-hidden {
21
+ display: none;
22
+ visibility: hidden;
23
+ }
24
+ }
25
+
19
26
  // Used in postcode lookup
20
27
  .govuk-button--link {
21
28
  @extend %govuk-link;
@@ -15,6 +15,7 @@ import { type FormModel } from '~/src/server/plugins/engine/models/index.js'
15
15
  import { type PageControllerClass } from '~/src/server/plugins/engine/pageControllers/helpers/pages.js'
16
16
 
17
17
  export class ComponentBase {
18
+ id?: string
18
19
  page?: PageControllerClass
19
20
  parent: Component | undefined
20
21
  collection: ComponentCollection | undefined
@@ -40,6 +41,7 @@ export class ComponentBase {
40
41
  model: FormModel
41
42
  }
42
43
  ) {
44
+ this.id = def.id
43
45
  this.type = def.type
44
46
  this.name = def.name
45
47
  this.title = def.title
@@ -12,6 +12,7 @@ import {
12
12
  } from '~/src/server/plugins/engine/types/index.js'
13
13
  import {
14
14
  type ErrorMessageTemplateList,
15
+ type Feature,
15
16
  type FileState,
16
17
  type FormPayload,
17
18
  type FormState,
@@ -19,6 +20,7 @@ import {
19
20
  type FormSubmissionError,
20
21
  type FormSubmissionState,
21
22
  type FormValue,
23
+ type GeospatialState,
22
24
  type RepeatItemState,
23
25
  type RepeatListState,
24
26
  type UploadState
@@ -302,9 +304,36 @@ export function isUploadState(value?: unknown): value is UploadState {
302
304
  return value.every(isUploadValue)
303
305
  }
304
306
 
307
+ /**
308
+ * Check for geospatial state
309
+ */
310
+ export function isGeospatialState(value?: unknown): value is GeospatialState {
311
+ if (!Array.isArray(value)) {
312
+ return false
313
+ }
314
+
315
+ // Skip checks when empty
316
+ if (!value.length) {
317
+ return true
318
+ }
319
+
320
+ return value.every(isGeospatialValue)
321
+ }
322
+
305
323
  /**
306
324
  * Check for upload state value
307
325
  */
308
326
  export function isUploadValue(value?: unknown): value is FileState {
309
327
  return isFormState(value) && typeof value.uploadId === 'string'
310
328
  }
329
+
330
+ /**
331
+ * Check for geospatial state value
332
+ */
333
+ export function isGeospatialValue(value?: unknown): value is Feature {
334
+ return (
335
+ isFormState(value) &&
336
+ typeof value.type === 'string' &&
337
+ value.type === 'Feature'
338
+ )
339
+ }