@defra/forms-engine-plugin 4.10.0 → 4.11.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 (32) hide show
  1. package/.public/javascripts/shared.min.js +1 -1
  2. package/.public/javascripts/shared.min.js.map +1 -1
  3. package/.server/client/javascripts/geospatial-map.js +34 -3
  4. package/.server/client/javascripts/geospatial-map.js.map +1 -1
  5. package/.server/client/javascripts/map.d.ts +0 -6
  6. package/.server/client/javascripts/map.js +1 -14
  7. package/.server/client/javascripts/map.js.map +1 -1
  8. package/.server/server/plugins/engine/components/GeospatialField.d.ts +1 -0
  9. package/.server/server/plugins/engine/components/GeospatialField.js +6 -2
  10. package/.server/server/plugins/engine/components/GeospatialField.js.map +1 -1
  11. package/.server/server/plugins/engine/components/helpers/geospatial.d.ts +2 -1
  12. package/.server/server/plugins/engine/components/helpers/geospatial.js +32 -1
  13. package/.server/server/plugins/engine/components/helpers/geospatial.js.map +1 -1
  14. package/.server/server/plugins/engine/components/helpers/geospatial.test.js +21 -1
  15. package/.server/server/plugins/engine/components/helpers/geospatial.test.js.map +1 -1
  16. package/.server/server/plugins/engine/views/components/geospatialfield.html +1 -1
  17. package/.server/server/plugins/map/routes/index.d.ts +9 -1
  18. package/.server/server/plugins/map/routes/index.js +51 -2
  19. package/.server/server/plugins/map/routes/index.js.map +1 -1
  20. package/.server/server/plugins/map/routes/vts/countries.geojson +39285 -0
  21. package/.server/server/plugins/map/types.d.ts +23 -1
  22. package/.server/server/plugins/map/types.js +14 -1
  23. package/.server/server/plugins/map/types.js.map +1 -1
  24. package/package.json +10 -8
  25. package/src/client/javascripts/geospatial-map.js +39 -3
  26. package/src/client/javascripts/map.js +1 -14
  27. package/src/server/plugins/engine/components/GeospatialField.ts +8 -2
  28. package/src/server/plugins/engine/components/helpers/geospatial.ts +48 -2
  29. package/src/server/plugins/engine/views/components/geospatialfield.html +1 -1
  30. package/src/server/plugins/map/routes/index.js +58 -2
  31. package/src/server/plugins/map/routes/vts/countries.geojson +39285 -0
  32. package/src/server/plugins/map/types.js +14 -1
@@ -33,6 +33,19 @@ export type MapReverseGeocodeQuery = {
33
33
  */
34
34
  northing: number;
35
35
  };
36
+ /**
37
+ * Geospatial countries query params
38
+ */
39
+ export type GeospatialCountriesQuery = {
40
+ /**
41
+ * - the country to omit
42
+ */
43
+ omit: string;
44
+ /**
45
+ * - the country to include
46
+ */
47
+ only: string;
48
+ };
36
49
  /**
37
50
  * Map geocode get request
38
51
  */
@@ -52,7 +65,7 @@ export type MapGeocodeGetRequestRefs = {
52
65
  Query: MapGeocodeQuery;
53
66
  };
54
67
  /**
55
- * Map reverst geocode get request
68
+ * Map reverse geocode get request
56
69
  */
57
70
  export type MapReverseGeocodeGetRequestRefs = {
58
71
  /**
@@ -60,6 +73,15 @@ export type MapReverseGeocodeGetRequestRefs = {
60
73
  */
61
74
  Query: MapReverseGeocodeQuery;
62
75
  };
76
+ /**
77
+ * Map countries geojson get request
78
+ */
79
+ export type GeospatialCountriesGetRequestRefs = {
80
+ /**
81
+ * - Request query
82
+ */
83
+ Query: GeospatialCountriesQuery;
84
+ };
63
85
  export type MapProxyRequestRefs = MapProxyGetRequestRefs;
64
86
  export type MapGeocodeRequestRefs = MapGeocodeGetRequestRefs;
65
87
  export type MapReverseGeocodeRequestRefs = MapReverseGeocodeGetRequestRefs;
@@ -28,6 +28,13 @@
28
28
  * @property {number} northing - the Northing point
29
29
  */
30
30
 
31
+ /**
32
+ * Geospatial countries query params
33
+ * @typedef {object} GeospatialCountriesQuery
34
+ * @property {string} omit - the country to omit
35
+ * @property {string} only - the country to include
36
+ */
37
+
31
38
  /**
32
39
  * Map geocode get request
33
40
  * @typedef {object} MapProxyGetRequestRefs
@@ -41,11 +48,17 @@
41
48
  */
42
49
 
43
50
  /**
44
- * Map reverst geocode get request
51
+ * Map reverse geocode get request
45
52
  * @typedef {object} MapReverseGeocodeGetRequestRefs
46
53
  * @property {MapReverseGeocodeQuery} Query - Request query
47
54
  */
48
55
 
56
+ /**
57
+ * Map countries geojson get request
58
+ * @typedef {object} GeospatialCountriesGetRequestRefs
59
+ * @property {GeospatialCountriesQuery} Query - Request query
60
+ */
61
+
49
62
  /**
50
63
  * @typedef {MapProxyGetRequestRefs} MapProxyRequestRefs
51
64
  * @typedef {MapGeocodeGetRequestRefs} MapGeocodeRequestRefs
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","names":[],"sources":["../../../../src/server/plugins/map/types.js"],"sourcesContent":["/**\n * @typedef {{\n * ordnanceSurveyApiKey: string\n * ordnanceSurveyApiSecret: string\n * }} MapConfiguration\n */\n\n//\n// Route types\n//\n\n/**\n * Map proxy query params\n * @typedef {object} MapProxyQuery\n * @property {string} url - the proxied url\n */\n\n/**\n * Map geocode query params\n * @typedef {object} MapGeocodeQuery\n * @property {string} query - name query\n */\n\n/**\n * Map reverse geocode query params\n * @typedef {object} MapReverseGeocodeQuery\n * @property {number} easting - the Easting point\n * @property {number} northing - the Northing point\n */\n\n/**\n * Map geocode get request\n * @typedef {object} MapProxyGetRequestRefs\n * @property {MapProxyQuery} Query - Request query\n */\n\n/**\n * Map geocode get request\n * @typedef {object} MapGeocodeGetRequestRefs\n * @property {MapGeocodeQuery} Query - Request query\n */\n\n/**\n * Map reverst geocode get request\n * @typedef {object} MapReverseGeocodeGetRequestRefs\n * @property {MapReverseGeocodeQuery} Query - Request query\n */\n\n/**\n * @typedef {MapProxyGetRequestRefs} MapProxyRequestRefs\n * @typedef {MapGeocodeGetRequestRefs} MapGeocodeRequestRefs\n * @typedef {MapReverseGeocodeGetRequestRefs} MapReverseGeocodeRequestRefs\n * @typedef {Request<MapGeocodeGetRequestRefs>} MapProxyGetRequest\n * @typedef {Request<MapGeocodeGetRequestRefs>} MapGeocodeGetRequest\n * @typedef {Request<MapReverseGeocodeGetRequestRefs>} MapReverseGeocodeGetRequest\n * @typedef {MapProxyGetRequest | MapGeocodeGetRequest | MapReverseGeocodeGetRequest} MapRequest\n */\n\n//\n// Service types\n//\n\n/**\n * @typedef {object} OsNamesFindResponse\n * @property {OsNamesFindHeader} header - Metadata about the search request and results.\n * @property {OsNamesFindResult[]} results - An array of matched place records from the search.\n */\n\n/**\n * @typedef {object} OsNamesFindHeader\n * @property {string} uri - The query URI (usually same as search text).\n * @property {string} query - The original text query string passed to the API.\n * @property {string} format - The response format returned (e.g., \"JSON\").\n * @property {number} maxresults - The maximum number of results requested.\n * @property {number} offset - The offset used in the search results.\n * @property {number} totalresults - The total number of results that matched the query.\n * @property {string} filter - The original filter string passed to the API.\n */\n\n/**\n * @typedef {object} OsNamesFindGazetteerEntry\n * @property {string} ID - Unique identifier for the place/feature.\n * @property {string} NAMES_URI - A URI (identifier) for this named feature.\n * @property {string} NAME1 - Primary name of the feature.\n * @property {string} TYPE - General type classification of the feature.\n * @property {string} LOCAL_TYPE - Local or more specific type classification.\n * @property {number} GEOMETRY_X - Easting coordinate (British National Grid).\n * @property {number} GEOMETRY_Y - Northing coordinate (British National Grid).\n * @property {number} MOST_DETAIL_VIEW_RES - Most detailed resolution available.\n * @property {number} LEAST_DETAIL_VIEW_RES - Least detailed resolution available.\n * @property {number} [MBR_XMIN] - Minimum bounding box X (easting).\n * @property {number} [MBR_YMIN] - Minimum bounding box Y (northing).\n * @property {number} [MBR_XMAX] - Maximum bounding box X (easting).\n * @property {number} [MBR_YMAX] - Maximum bounding box Y (northing).\n * @property {string} [DISTRICT_BOROUGH] - (Optional) District borough.\n * @property {string} [DISTRICT_BOROUGH_URI] - (Optional) URI for the district borough.\n * @property {string} [DISTRICT_BOROUGH_TYPE] - (Optional) Type of the district borough.\n * @property {string} [POSTCODE_DISTRICT] - (Optional) Postcode district.\n * @property {string} [POSTCODE_DISTRICT_URI] - (Optional) URI for the postcode district.\n * @property {string} [POPULATED_PLACE] - (Optional) Name of associated populated place.\n * @property {string} [POPULATED_PLACE_URI] - (Optional) URI of populated place.\n * @property {string} [POPULATED_PLACE_TYPE] - (Optional) Type of populated place.\n * @property {string} [COUNTY_UNITARY] - (Optional) County or unitary authority name.\n * @property {string} [COUNTY_UNITARY_URI] - (Optional) URI for county/unitary authority.\n * @property {string} [COUNTY_UNITARY_TYPE] - (Optional) Classification of county/unitary authority.\n * @property {string} [REGION] - (Optional) Region name.\n * @property {string} [REGION_URI] - (Optional) URI for region.\n * @property {string} [COUNTRY] - (Optional) Country name.\n * @property {string} [COUNTRY_URI] - (Optional) URI for country.\n */\n\n/**\n * OS names GAZETTEER_ENTRY response\n * @typedef {object} OsNamesFindResult\n * @property {OsNamesFindGazetteerEntry} GAZETTEER_ENTRY - Gazetteer entry\n */\n\n/**\n * @import { Request } from '@hapi/hapi'\n */\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA","ignoreList":[]}
1
+ {"version":3,"file":"types.js","names":[],"sources":["../../../../src/server/plugins/map/types.js"],"sourcesContent":["/**\n * @typedef {{\n * ordnanceSurveyApiKey: string\n * ordnanceSurveyApiSecret: string\n * }} MapConfiguration\n */\n\n//\n// Route types\n//\n\n/**\n * Map proxy query params\n * @typedef {object} MapProxyQuery\n * @property {string} url - the proxied url\n */\n\n/**\n * Map geocode query params\n * @typedef {object} MapGeocodeQuery\n * @property {string} query - name query\n */\n\n/**\n * Map reverse geocode query params\n * @typedef {object} MapReverseGeocodeQuery\n * @property {number} easting - the Easting point\n * @property {number} northing - the Northing point\n */\n\n/**\n * Geospatial countries query params\n * @typedef {object} GeospatialCountriesQuery\n * @property {string} omit - the country to omit\n * @property {string} only - the country to include\n */\n\n/**\n * Map geocode get request\n * @typedef {object} MapProxyGetRequestRefs\n * @property {MapProxyQuery} Query - Request query\n */\n\n/**\n * Map geocode get request\n * @typedef {object} MapGeocodeGetRequestRefs\n * @property {MapGeocodeQuery} Query - Request query\n */\n\n/**\n * Map reverse geocode get request\n * @typedef {object} MapReverseGeocodeGetRequestRefs\n * @property {MapReverseGeocodeQuery} Query - Request query\n */\n\n/**\n * Map countries geojson get request\n * @typedef {object} GeospatialCountriesGetRequestRefs\n * @property {GeospatialCountriesQuery} Query - Request query\n */\n\n/**\n * @typedef {MapProxyGetRequestRefs} MapProxyRequestRefs\n * @typedef {MapGeocodeGetRequestRefs} MapGeocodeRequestRefs\n * @typedef {MapReverseGeocodeGetRequestRefs} MapReverseGeocodeRequestRefs\n * @typedef {Request<MapGeocodeGetRequestRefs>} MapProxyGetRequest\n * @typedef {Request<MapGeocodeGetRequestRefs>} MapGeocodeGetRequest\n * @typedef {Request<MapReverseGeocodeGetRequestRefs>} MapReverseGeocodeGetRequest\n * @typedef {MapProxyGetRequest | MapGeocodeGetRequest | MapReverseGeocodeGetRequest} MapRequest\n */\n\n//\n// Service types\n//\n\n/**\n * @typedef {object} OsNamesFindResponse\n * @property {OsNamesFindHeader} header - Metadata about the search request and results.\n * @property {OsNamesFindResult[]} results - An array of matched place records from the search.\n */\n\n/**\n * @typedef {object} OsNamesFindHeader\n * @property {string} uri - The query URI (usually same as search text).\n * @property {string} query - The original text query string passed to the API.\n * @property {string} format - The response format returned (e.g., \"JSON\").\n * @property {number} maxresults - The maximum number of results requested.\n * @property {number} offset - The offset used in the search results.\n * @property {number} totalresults - The total number of results that matched the query.\n * @property {string} filter - The original filter string passed to the API.\n */\n\n/**\n * @typedef {object} OsNamesFindGazetteerEntry\n * @property {string} ID - Unique identifier for the place/feature.\n * @property {string} NAMES_URI - A URI (identifier) for this named feature.\n * @property {string} NAME1 - Primary name of the feature.\n * @property {string} TYPE - General type classification of the feature.\n * @property {string} LOCAL_TYPE - Local or more specific type classification.\n * @property {number} GEOMETRY_X - Easting coordinate (British National Grid).\n * @property {number} GEOMETRY_Y - Northing coordinate (British National Grid).\n * @property {number} MOST_DETAIL_VIEW_RES - Most detailed resolution available.\n * @property {number} LEAST_DETAIL_VIEW_RES - Least detailed resolution available.\n * @property {number} [MBR_XMIN] - Minimum bounding box X (easting).\n * @property {number} [MBR_YMIN] - Minimum bounding box Y (northing).\n * @property {number} [MBR_XMAX] - Maximum bounding box X (easting).\n * @property {number} [MBR_YMAX] - Maximum bounding box Y (northing).\n * @property {string} [DISTRICT_BOROUGH] - (Optional) District borough.\n * @property {string} [DISTRICT_BOROUGH_URI] - (Optional) URI for the district borough.\n * @property {string} [DISTRICT_BOROUGH_TYPE] - (Optional) Type of the district borough.\n * @property {string} [POSTCODE_DISTRICT] - (Optional) Postcode district.\n * @property {string} [POSTCODE_DISTRICT_URI] - (Optional) URI for the postcode district.\n * @property {string} [POPULATED_PLACE] - (Optional) Name of associated populated place.\n * @property {string} [POPULATED_PLACE_URI] - (Optional) URI of populated place.\n * @property {string} [POPULATED_PLACE_TYPE] - (Optional) Type of populated place.\n * @property {string} [COUNTY_UNITARY] - (Optional) County or unitary authority name.\n * @property {string} [COUNTY_UNITARY_URI] - (Optional) URI for county/unitary authority.\n * @property {string} [COUNTY_UNITARY_TYPE] - (Optional) Classification of county/unitary authority.\n * @property {string} [REGION] - (Optional) Region name.\n * @property {string} [REGION_URI] - (Optional) URI for region.\n * @property {string} [COUNTRY] - (Optional) Country name.\n * @property {string} [COUNTRY_URI] - (Optional) URI for country.\n */\n\n/**\n * OS names GAZETTEER_ENTRY response\n * @typedef {object} OsNamesFindResult\n * @property {OsNamesFindGazetteerEntry} GAZETTEER_ENTRY - Gazetteer entry\n */\n\n/**\n * @import { Request } from '@hapi/hapi'\n */\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@defra/forms-engine-plugin",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "description": "Defra forms engine",
5
5
  "type": "module",
6
6
  "files": [
@@ -44,9 +44,10 @@
44
44
  "dev:debug": "concurrently \"npm run client:watch\" \"npm run server:watch:debug\" --kill-others --names \"client,server\" --prefix-colors \"red.dim,blue.dim\"",
45
45
  "format": "npm run format:check -- --write",
46
46
  "format:check": "prettier --cache --cache-location .cache/prettier --cache-strategy content --check \"**/*.{cjs,js,json,md,mjs,scss,ts}\"",
47
+ "predocs:dev": "node scripts/generate-schema-docs.js && node scripts/generate-component-docs.js",
47
48
  "docs:dev": "BROWSERSLIST_ENV=javascripts docusaurus start --host 0.0.0.0",
48
49
  "docs:build": "BROWSERSLIST_ENV=javascripts docusaurus build",
49
- "docs:build:all": "node scripts/generate-schema-docs.js && npm run docs:build",
50
+ "docs:build:all": "node scripts/generate-schema-docs.js && node scripts/generate-component-docs.js && npm run docs:build",
50
51
  "docs:serve": "docusaurus serve --host 0.0.0.0",
51
52
  "docs:clear": "docusaurus clear",
52
53
  "generate-schema-docs": "node scripts/generate-schema-docs.js",
@@ -78,7 +79,7 @@
78
79
  "minimatch": "3.1.5"
79
80
  },
80
81
  "serialize-javascript": ">=7.0.3",
81
- "react-router": "^7.13.1"
82
+ "react-router": "^5.3.4"
82
83
  },
83
84
  "engines": {
84
85
  "node": ">=22.11.0 <25.0.0",
@@ -86,9 +87,9 @@
86
87
  },
87
88
  "license": "SEE LICENSE IN LICENSE",
88
89
  "dependencies": {
89
- "@defra/forms-model": "^3.0.648",
90
+ "@defra/forms-model": "^3.0.655",
90
91
  "@defra/hapi-tracing": "^1.29.0",
91
- "@defra/interactive-map": "^0.0.17-alpha",
92
+ "@defra/interactive-map": "^0.0.22-alpha",
92
93
  "@elastic/ecs-pino-format": "^1.5.0",
93
94
  "@hapi/boom": "^10.0.1",
94
95
  "@hapi/bourne": "^3.0.0",
@@ -104,6 +105,7 @@
104
105
  "@hapi/wreck": "^18.1.0",
105
106
  "@hapi/yar": "^11.0.3",
106
107
  "@turf/bbox": "^7.3.4",
108
+ "@turf/boolean-within": "^7.3.5",
107
109
  "@turf/centroid": "^7.3.4",
108
110
  "@types/humanize-duration": "^3.27.4",
109
111
  "accessible-autocomplete": "^3.0.1",
@@ -143,9 +145,9 @@
143
145
  "@babel/plugin-syntax-import-attributes": "^7.27.1",
144
146
  "@babel/preset-env": "^7.28.5",
145
147
  "@babel/preset-typescript": "^7.28.5",
146
- "@defra/docusaurus-theme-govuk": "^0.0.13-alpha",
147
- "@docusaurus/core": "^3.9.2",
148
- "@docusaurus/plugin-content-docs": "^3.9.2",
148
+ "@defra/docusaurus-theme-govuk": "^0.0.22-alpha",
149
+ "@docusaurus/core": "^3.10.1",
150
+ "@docusaurus/plugin-content-docs": "^3.10.1",
149
151
  "@easyops-cn/docusaurus-search-local": "^0.55.0",
150
152
  "@hapi/basic": "^7.0.2",
151
153
  "@mdx-js/react": "^3.1.1",
@@ -39,8 +39,8 @@ const lineFeatureProperties = {
39
39
  }
40
40
 
41
41
  const polygonFeatureProperties = {
42
- stroke: 'rgba(0,112,60,1)',
43
- fill: 'rgba(0,112,60,0.2)',
42
+ stroke: 'rgb(0, 0, 0)',
43
+ fill: 'rgba(255, 221, 0, 0.2)',
44
44
  strokeWidth: 2
45
45
  }
46
46
 
@@ -111,11 +111,47 @@ export function processGeospatial(config, geospatial, index) {
111
111
  const geojson = getGeoJSON(geospatialInput)
112
112
  const bounds = geojson.features.length ? getBoundingBox(geojson) : undefined
113
113
  const drawPlugin = defra.drawMLPlugin()
114
+ const plugins = [drawPlugin]
115
+ const country = geospatial.dataset.country
116
+
117
+ if (country) {
118
+ // Add the country bounds as a dataset plugin to show the valid area on the map
119
+ // and provide feedback to the user when they add features outside of the bounds.
120
+ const datasetsPlugin = defra.datasetsMaplibrePlugin({
121
+ datasets: [
122
+ {
123
+ id: 'invalid-area',
124
+ label: 'Invalid areas',
125
+ geojson: `${config.apiPath}/maps/countries.geojson?omit=${country}`,
126
+ showInKey: false,
127
+ showInMenu: false,
128
+ style: {
129
+ stroke: 'gray',
130
+ strokeWidth: 1,
131
+ fill: 'rgba(211,211,211,0.8)'
132
+ }
133
+ },
134
+ {
135
+ id: 'valid-area',
136
+ label: 'Valid areas',
137
+ geojson: `${config.apiPath}/maps/countries.geojson?only=${country}`,
138
+ showInKey: false,
139
+ showInMenu: false,
140
+ style: {
141
+ stroke: 'rgba(0,112,60,1)',
142
+ strokeWidth: 1
143
+ }
144
+ }
145
+ ]
146
+ })
147
+
148
+ plugins.push(datasetsPlugin)
149
+ }
114
150
 
115
151
  const initConfig = {
116
152
  ...defaultConfig,
117
153
  bounds,
118
- plugins: [drawPlugin]
154
+ plugins
119
155
  }
120
156
 
121
157
  const { map, interactPlugin } = createMap(mapId, initConfig, config)
@@ -240,18 +240,6 @@ export function makeTileRequestTransformer(apiPath) {
240
240
  }
241
241
  }
242
242
 
243
- /**
244
- * Temporary transform request function to transform geocode requests. Fixed in v0.0.18 of interactive map so this is not needed when we upgrade.
245
- * @param {object} request
246
- * @param {string} request.url
247
- * @param {{ method: 'get' }} request.options
248
- * @returns {Request}
249
- */
250
- export const transformGeocodeRequest = (request) => {
251
- const url = new URL(request.url, window.location.origin)
252
- return new Request(url.toString(), request.options)
253
- }
254
-
255
243
  /**
256
244
  * Create a Defra map instance
257
245
  * @param {string} mapId - the map id
@@ -267,7 +255,7 @@ export function createMap(mapId, initConfig, mapsConfig) {
267
255
 
268
256
  const interactPlugin = defra.interactPlugin({
269
257
  markerColor: { outdoor: '#ff0000', dark: '#00ff00' },
270
- interactionMode: 'marker',
258
+ interactionModes: ['placeMarker'],
271
259
  multiSelect: false
272
260
  })
273
261
 
@@ -332,7 +320,6 @@ export function createMap(mapId, initConfig, mapsConfig) {
332
320
  }),
333
321
  interactPlugin,
334
322
  defra.searchPlugin({
335
- transformRequest: transformGeocodeRequest,
336
323
  osNamesURL: `${apiPath}/geocode-proxy?query={query}`,
337
324
  width: '300px',
338
325
  showMarker: false
@@ -6,7 +6,7 @@ import {
6
6
  FormComponent,
7
7
  isGeospatialState
8
8
  } from './FormComponent.js'
9
- import { geospatialSchema } from './helpers/geospatial.js'
9
+ import { getGeospatialSchema } from './helpers/geospatial.js'
10
10
  import { messageTemplate } from '../pageControllers/validationOptions.js'
11
11
  import {
12
12
  type ErrorMessageTemplateList,
@@ -31,7 +31,9 @@ export class GeospatialField extends FormComponent {
31
31
 
32
32
  const { options } = def
33
33
 
34
- let formSchema = geospatialSchema.label(this.label).required()
34
+ let formSchema = getGeospatialSchema(options.countries?.at(0))
35
+ .label(this.label)
36
+ .required()
35
37
 
36
38
  formSchema = formSchema.max(50)
37
39
 
@@ -90,6 +92,7 @@ export class GeospatialField extends FormComponent {
90
92
 
91
93
  return {
92
94
  ...viewModel,
95
+ country: this.options.countries?.at(0),
93
96
  value
94
97
  }
95
98
  }
@@ -101,6 +104,9 @@ export class GeospatialField extends FormComponent {
101
104
  if (err.name === 'description') {
102
105
  err.href = `#description_${err.path[1]}`
103
106
  err.text = `Enter description for location ${Number(err.path[1]) + 1}`
107
+ } else if (typeof err.name === 'number' && err.context?.country) {
108
+ err.href = `#description_${err.path[1]}`
109
+ err.text = `Location ${Number(err.path[1]) + 1} must be in ${err.context.country}`
104
110
  }
105
111
  })
106
112
 
@@ -1,5 +1,10 @@
1
+ import {
2
+ GeospatialFieldOptionsCountryEnum,
3
+ type GeospatialFieldOptionsCountry
4
+ } from '@defra/forms-model'
1
5
  import Bourne from '@hapi/bourne'
2
- import JoiBase from 'joi'
6
+ import { booleanWithin } from '@turf/boolean-within'
7
+ import JoiBase, { type CustomValidator } from 'joi'
3
8
 
4
9
  import {
5
10
  type Coordinates,
@@ -7,6 +12,14 @@ import {
7
12
  type FeatureProperties,
8
13
  type Geometry
9
14
  } from '../../types.js'
15
+ import { countries } from '../../../map/routes/index.js'
16
+
17
+ const countriesDesc: Record<GeospatialFieldOptionsCountryEnum, string> = {
18
+ [GeospatialFieldOptionsCountryEnum.England]: 'England',
19
+ [GeospatialFieldOptionsCountryEnum.NorthernIreland]: 'Northern Ireland',
20
+ [GeospatialFieldOptionsCountryEnum.Scotland]: 'Scotland',
21
+ [GeospatialFieldOptionsCountryEnum.Wales]: 'Wales'
22
+ }
10
23
 
11
24
  const Joi = JoiBase.extend({
12
25
  type: 'array',
@@ -83,11 +96,44 @@ const featureSchema = Joi.object<Feature>().keys({
83
96
  geometry: featureGeometrySchema
84
97
  })
85
98
 
86
- export const geospatialSchema = Joi.array<Feature[]>()
99
+ const geospatialSchema = Joi.array<Feature[]>()
87
100
  .items(featureSchema)
88
101
  .unique('id')
89
102
  .required()
90
103
 
104
+ export function getGeospatialSchema(country?: GeospatialFieldOptionsCountry) {
105
+ if (!country) {
106
+ return geospatialSchema
107
+ }
108
+
109
+ const validateCountryBounds: CustomValidator = (value, helpers) => {
110
+ const countryFeature = countries.features.find(
111
+ (feature) => feature.id === country
112
+ )
113
+
114
+ if (!countryFeature) {
115
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
116
+ return value
117
+ }
118
+
119
+ const result = booleanWithin(value, countryFeature)
120
+
121
+ if (!result) {
122
+ return helpers.error('any.custom', {
123
+ country: countriesDesc[country as GeospatialFieldOptionsCountryEnum]
124
+ })
125
+ }
126
+
127
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
128
+ return value
129
+ }
130
+
131
+ return Joi.array<Feature[]>()
132
+ .items(featureSchema.custom(validateCountryBounds))
133
+ .unique('id')
134
+ .required()
135
+ }
136
+
91
137
  /**
92
138
  * @import { CustomHelpers } from 'joi'
93
139
  */
@@ -1,7 +1,7 @@
1
1
  {% from "govuk/components/textarea/macro.njk" import govukTextarea %}
2
2
 
3
3
  {% macro GeospatialField(component) %}
4
- <div class="app-geospatial-field">
4
+ <div class="app-geospatial-field" data-country="{{component.model.country}}">
5
5
  {{ govukTextarea(component.model) }}
6
6
  </div>
7
7
  {% endmacro %}
@@ -1,5 +1,7 @@
1
+ import fs from 'node:fs'
1
2
  import { resolve } from 'node:path'
2
3
 
4
+ import { GeospatialFieldOptionsCountryEnum } from '@defra/forms-model'
3
5
  import { StatusCodes } from 'http-status-codes'
4
6
  import Joi from 'joi'
5
7
 
@@ -10,6 +12,18 @@ import {
10
12
  request as httpRequest
11
13
  } from '../../../services/httpService.js'
12
14
 
15
+ const filePath = resolve(import.meta.dirname, './vts/countries.geojson')
16
+
17
+ /**
18
+ * @type {FeatureCollection}
19
+ */
20
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
21
+ export const countries = JSON.parse(fs.readFileSync(filePath, 'utf8'))
22
+
23
+ export const countrySchema = Joi.string().valid(
24
+ ...Object.values(GeospatialFieldOptionsCountryEnum)
25
+ )
26
+
13
27
  /**
14
28
  * Gets the map support routes
15
29
  * @param {MapConfiguration} options - ordnance survey names api key
@@ -20,7 +34,8 @@ export function getRoutes(options) {
20
34
  mapProxyRoute(options),
21
35
  tileProxyRoute(options),
22
36
  geocodeProxyRoute(options),
23
- reverseGeocodeProxyRoute(options)
37
+ reverseGeocodeProxyRoute(options),
38
+ getGeospatialCountries()
24
39
  ]
25
40
  }
26
41
 
@@ -188,7 +203,48 @@ function mapStyleResourceRoutes() {
188
203
  }
189
204
  }
190
205
 
206
+ /**
207
+ * Resource routes to return sprites and glyphs
208
+ * @returns {ServerRoute<GeospatialCountriesGetRequestRefs>}
209
+ */
210
+ function getGeospatialCountries() {
211
+ return {
212
+ method: 'GET',
213
+ path: '/api/maps/countries.geojson',
214
+ handler: (request) => {
215
+ const { omit, only } = request.query
216
+
217
+ if (omit) {
218
+ return {
219
+ ...countries,
220
+ features: countries.features.filter((feature) => feature.id !== omit)
221
+ }
222
+ }
223
+
224
+ if (only) {
225
+ return {
226
+ ...countries,
227
+ features: countries.features.filter((feature) => feature.id === only)
228
+ }
229
+ }
230
+
231
+ return countries
232
+ },
233
+ options: {
234
+ validate: {
235
+ query: Joi.object()
236
+ .keys({
237
+ omit: countrySchema.optional(),
238
+ only: countrySchema.optional()
239
+ })
240
+ .optional()
241
+ }
242
+ }
243
+ }
244
+ }
245
+
191
246
  /**
192
247
  * @import { ServerRoute } from '@hapi/hapi'
193
- * @import { MapConfiguration, MapProxyGetRequestRefs, MapGeocodeGetRequestRefs, MapReverseGeocodeGetRequestRefs } from '../types.js'
248
+ * @import { FeatureCollection } from 'geojson'
249
+ * @import { MapConfiguration, MapProxyGetRequestRefs, MapGeocodeGetRequestRefs, MapReverseGeocodeGetRequestRefs, GeospatialCountriesGetRequestRefs } from '../types.js'
194
250
  */