diff --git a/sonar-project.properties b/sonar-project.properties index a4f3fc6..9140163 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -13,7 +13,7 @@ sonar.links.scm=https://github.com/DEFRA/nrf-frontend sonar.links.issue=https://github.com/DEFRA/nrf-frontend/issues sonar.sources=src/ -sonar.exclusions=src/**/*.test.js,src/test-utils/** +sonar.exclusions=src/**/*.test.js,src/test-utils/**,src/**/__fixtures__/** sonar.tests=src/ sonar.test.inclusions=src/**/*.test.js diff --git a/src/client/javascripts/__fixtures__/boundary-map-fixtures.js b/src/client/javascripts/__fixtures__/boundary-map-fixtures.js new file mode 100644 index 0000000..f00e9c0 --- /dev/null +++ b/src/client/javascripts/__fixtures__/boundary-map-fixtures.js @@ -0,0 +1,101 @@ +export const validGeojson = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-1.5, 52.0], + [-1.4, 52.0], + [-1.4, 52.1], + [-1.5, 52.1], + [-1.5, 52.0] + ] + ] + }, + properties: {} + } + ] +} + +export const validEdpBoundaryGeojson = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-1.6, 51.9], + [-1.3, 51.9], + [-1.3, 52.2], + [-1.6, 52.2], + [-1.6, 51.9] + ] + ] + }, + properties: { label: 'EDP 1' } + } + ] +} + +export const validEdpIntersectionGeojson = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [ + [ + [-1.5, 52.0], + [-1.45, 52.0], + [-1.45, 52.05], + [-1.5, 52.05], + [-1.5, 52.0] + ] + ] + }, + properties: { + label: 'EDP 1', + overlap_area_ha: 0.5, + overlap_percentage: 25.0 + } + } + ] +} + +export const singleGeometry = { + type: 'Feature', + geometry: { type: 'Point', coordinates: [-1.5, 52.0] }, + properties: {} +} + +export const multiPolygon = { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + geometry: { + type: 'MultiPolygon', + coordinates: [ + [ + [ + [-2.0, 51.0], + [-1.0, 51.0], + [-1.0, 53.0], + [-2.0, 53.0], + [-2.0, 51.0] + ] + ] + ] + }, + properties: {} + } + ] +} + +export const emptyGeojson = { type: 'FeatureCollection', features: [] } diff --git a/src/client/javascripts/boundary-map-geo.js b/src/client/javascripts/boundary-map-geo.js new file mode 100644 index 0000000..7431263 --- /dev/null +++ b/src/client/javascripts/boundary-map-geo.js @@ -0,0 +1,48 @@ +/** + * Geometry helpers for the boundary map. + */ + +export function collectCoords(c, coords) { + if (typeof c[0] === 'number') { + coords.push(c) + } else { + c.forEach(function (inner) { + collectCoords(inner, coords) + }) + } +} + +export function fitMapToBounds(mapInstance, geojson) { + const coords = [] + ;(geojson.features || [geojson]).forEach(function (f) { + collectCoords(f.geometry ? f.geometry.coordinates : f.coordinates, coords) + }) + + if (coords.length) { + let west = coords[0][0] + let south = coords[0][1] + let east = coords[0][0] + let north = coords[0][1] + coords.forEach(function (c) { + if (c[0] < west) { + west = c[0] + } + if (c[0] > east) { + east = c[0] + } + if (c[1] < south) { + south = c[1] + } + if (c[1] > north) { + north = c[1] + } + }) + mapInstance.fitBounds( + [ + [west, south], + [east, north] + ], + { padding: 40, maxZoom: 15 } + ) + } +} diff --git a/src/client/javascripts/boundary-map-geo.test.js b/src/client/javascripts/boundary-map-geo.test.js new file mode 100644 index 0000000..2b30567 --- /dev/null +++ b/src/client/javascripts/boundary-map-geo.test.js @@ -0,0 +1,82 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi } from 'vitest' +import { collectCoords, fitMapToBounds } from './boundary-map-geo.js' +import { + validGeojson, + singleGeometry, + multiPolygon, + emptyGeojson +} from './__fixtures__/boundary-map-fixtures.js' + +describe('collectCoords', () => { + it('collects flat coordinate pairs', () => { + const coords = [] + collectCoords([-1.5, 52.0], coords) + expect(coords).toEqual([[-1.5, 52.0]]) + }) + + it('collects nested coordinate arrays', () => { + const coords = [] + collectCoords( + [ + [ + [-2.0, 51.0], + [-1.0, 53.0] + ] + ], + coords + ) + expect(coords).toEqual([ + [-2.0, 51.0], + [-1.0, 53.0] + ]) + }) +}) + +describe('fitMapToBounds', () => { + it('calculates bounds from a FeatureCollection', () => { + const mapInstance = { fitBounds: vi.fn() } + fitMapToBounds(mapInstance, validGeojson) + + expect(mapInstance.fitBounds).toHaveBeenCalledWith( + [ + [-1.5, 52.0], + [-1.4, 52.1] + ], + { padding: 40, maxZoom: 15 } + ) + }) + + it('handles a single geometry (no features array)', () => { + const mapInstance = { fitBounds: vi.fn() } + fitMapToBounds(mapInstance, singleGeometry) + + expect(mapInstance.fitBounds).toHaveBeenCalledWith( + [ + [-1.5, 52.0], + [-1.5, 52.0] + ], + { padding: 40, maxZoom: 15 } + ) + }) + + it('handles nested coordinate arrays (MultiPolygon)', () => { + const mapInstance = { fitBounds: vi.fn() } + fitMapToBounds(mapInstance, multiPolygon) + + expect(mapInstance.fitBounds).toHaveBeenCalledWith( + [ + [-2.0, 51.0], + [-1.0, 53.0] + ], + { padding: 40, maxZoom: 15 } + ) + }) + + it('does not call fitBounds for empty features', () => { + const mapInstance = { fitBounds: vi.fn() } + fitMapToBounds(mapInstance, emptyGeojson) + + expect(mapInstance.fitBounds).not.toHaveBeenCalled() + }) +}) diff --git a/src/client/javascripts/boundary-map-layers.js b/src/client/javascripts/boundary-map-layers.js new file mode 100644 index 0000000..9be7d69 --- /dev/null +++ b/src/client/javascripts/boundary-map-layers.js @@ -0,0 +1,116 @@ +/** + * Map layer functions for the boundary map. + */ + +import { fitMapToBounds } from './boundary-map-geo.js' + +// MapLibre source IDs – each source holds GeoJSON data rendered by one or more layers +const MAP_SOURCE_BOUNDARY = 'boundary' +const MAP_SOURCE_EDP_BOUNDARY = 'edp-boundary' +const MAP_SOURCE_EDP_INTERSECTION = 'edp-intersection' + +const BOUNDARY_COLOR = '#d4351c' + +export function addBoundaryLayer(mapInstance, geojson) { + if (mapInstance.getSource(MAP_SOURCE_BOUNDARY)) { + return + } + + mapInstance.addSource(MAP_SOURCE_BOUNDARY, { + type: 'geojson', + data: geojson + }) + + mapInstance.addLayer({ + id: 'boundary-fill', + type: 'fill', + source: MAP_SOURCE_BOUNDARY, + paint: { + 'fill-color': BOUNDARY_COLOR, + 'fill-opacity': 0.1 + } + }) + + mapInstance.addLayer({ + id: 'boundary-line', + type: 'line', + source: MAP_SOURCE_BOUNDARY, + paint: { + 'line-color': BOUNDARY_COLOR, + 'line-width': 3 + } + }) + + fitMapToBounds(mapInstance, geojson) +} + +export function addEdpBoundaryLayer(mapInstance, edpBoundaryGeojson) { + if (!edpBoundaryGeojson?.features?.length) { + return + } + + if (mapInstance.getSource(MAP_SOURCE_EDP_BOUNDARY)) { + return + } + + mapInstance.addSource(MAP_SOURCE_EDP_BOUNDARY, { + type: 'geojson', + data: edpBoundaryGeojson + }) + + mapInstance.addLayer({ + id: 'edp-boundary-fill', + type: 'fill', + source: MAP_SOURCE_EDP_BOUNDARY, + paint: { + 'fill-color': '#00703c', + 'fill-opacity': 0.08 + } + }) + + mapInstance.addLayer({ + id: 'edp-boundary-line', + type: 'line', + source: MAP_SOURCE_EDP_BOUNDARY, + paint: { + 'line-color': '#00703c', + 'line-width': 2 + } + }) +} + +export function addEdpIntersectionLayer(mapInstance, edpGeojson) { + if (!edpGeojson?.features?.length) { + return + } + + if (mapInstance.getSource(MAP_SOURCE_EDP_INTERSECTION)) { + return + } + + mapInstance.addSource(MAP_SOURCE_EDP_INTERSECTION, { + type: 'geojson', + data: edpGeojson + }) + + mapInstance.addLayer({ + id: 'edp-intersection-fill', + type: 'fill', + source: MAP_SOURCE_EDP_INTERSECTION, + paint: { + 'fill-color': '#1d70b8', + 'fill-opacity': 0.3 + } + }) + + mapInstance.addLayer({ + id: 'edp-intersection-line', + type: 'line', + source: MAP_SOURCE_EDP_INTERSECTION, + paint: { + 'line-color': '#1d70b8', + 'line-width': 2, + 'line-dasharray': [4, 2] + } + }) +} diff --git a/src/client/javascripts/boundary-map-layers.test.js b/src/client/javascripts/boundary-map-layers.test.js new file mode 100644 index 0000000..48463b9 --- /dev/null +++ b/src/client/javascripts/boundary-map-layers.test.js @@ -0,0 +1,139 @@ +// @vitest-environment jsdom +import { describe, it, expect, vi } from 'vitest' +import { + addBoundaryLayer, + addEdpBoundaryLayer, + addEdpIntersectionLayer +} from './boundary-map-layers.js' +import { + validGeojson, + validEdpBoundaryGeojson, + validEdpIntersectionGeojson, + emptyGeojson +} from './__fixtures__/boundary-map-fixtures.js' + +function createMockMapInstance() { + return { + getSource: vi.fn().mockReturnValue(null), + addSource: vi.fn(), + addLayer: vi.fn(), + fitBounds: vi.fn() + } +} + +describe('addBoundaryLayer', () => { + it('adds source and two layers', () => { + const mapInstance = createMockMapInstance() + addBoundaryLayer(mapInstance, validGeojson) + + expect(mapInstance.addSource).toHaveBeenCalledWith('boundary', { + type: 'geojson', + data: validGeojson + }) + expect(mapInstance.addLayer).toHaveBeenCalledTimes(2) + expect(mapInstance.fitBounds).toHaveBeenCalled() + }) + + it('skips if source already exists', () => { + const mapInstance = createMockMapInstance() + mapInstance.getSource.mockReturnValue({}) + addBoundaryLayer(mapInstance, validGeojson) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + expect(mapInstance.addLayer).not.toHaveBeenCalled() + }) +}) + +describe('addEdpBoundaryLayer', () => { + it('adds source and two layers', () => { + const mapInstance = createMockMapInstance() + addEdpBoundaryLayer(mapInstance, validEdpBoundaryGeojson) + + expect(mapInstance.addSource).toHaveBeenCalledWith('edp-boundary', { + type: 'geojson', + data: validEdpBoundaryGeojson + }) + expect(mapInstance.addLayer).toHaveBeenCalledTimes(2) + expect(mapInstance.addLayer).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'edp-boundary-fill', + source: 'edp-boundary' + }) + ) + expect(mapInstance.addLayer).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'edp-boundary-line', + source: 'edp-boundary' + }) + ) + }) + + it('skips when features array is empty', () => { + const mapInstance = createMockMapInstance() + addEdpBoundaryLayer(mapInstance, emptyGeojson) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + }) + + it('skips when geojson is null', () => { + const mapInstance = createMockMapInstance() + addEdpBoundaryLayer(mapInstance, null) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + }) + + it('skips if source already exists', () => { + const mapInstance = createMockMapInstance() + mapInstance.getSource.mockReturnValue({}) + addEdpBoundaryLayer(mapInstance, validEdpBoundaryGeojson) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + }) +}) + +describe('addEdpIntersectionLayer', () => { + it('adds source and two layers', () => { + const mapInstance = createMockMapInstance() + addEdpIntersectionLayer(mapInstance, validEdpIntersectionGeojson) + + expect(mapInstance.addSource).toHaveBeenCalledWith('edp-intersection', { + type: 'geojson', + data: validEdpIntersectionGeojson + }) + expect(mapInstance.addLayer).toHaveBeenCalledTimes(2) + expect(mapInstance.addLayer).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'edp-intersection-fill', + source: 'edp-intersection' + }) + ) + expect(mapInstance.addLayer).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'edp-intersection-line', + source: 'edp-intersection' + }) + ) + }) + + it('skips when features array is empty', () => { + const mapInstance = createMockMapInstance() + addEdpIntersectionLayer(mapInstance, emptyGeojson) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + }) + + it('skips when geojson is null', () => { + const mapInstance = createMockMapInstance() + addEdpIntersectionLayer(mapInstance, null) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + }) + + it('skips if source already exists', () => { + const mapInstance = createMockMapInstance() + mapInstance.getSource.mockReturnValue({}) + addEdpIntersectionLayer(mapInstance, validEdpIntersectionGeojson) + + expect(mapInstance.addSource).not.toHaveBeenCalled() + }) +}) diff --git a/src/client/javascripts/boundary-map.js b/src/client/javascripts/boundary-map.js index b8b137f..2df440f 100644 --- a/src/client/javascripts/boundary-map.js +++ b/src/client/javascripts/boundary-map.js @@ -8,8 +8,13 @@ /* global defra */ +import { + addBoundaryLayer, + addEdpBoundaryLayer, + addEdpIntersectionLayer +} from './boundary-map-layers.js' + const MAP_ELEMENT_ID = 'boundary-map' -const BOUNDARY_COLOR = '#d4351c' // TODO: send warnings to the server once server-side logging is available function logWarning(message, error) { @@ -25,82 +30,22 @@ function parseGeojson(mapEl) { } } -function collectCoords(c, coords) { - if (typeof c[0] === 'number') { - coords.push(c) - } else { - c.forEach(function (inner) { - collectCoords(inner, coords) - }) - } -} - -function fitMapToBounds(mapInstance, geojson) { - const coords = [] - ;(geojson.features || [geojson]).forEach(function (f) { - collectCoords(f.geometry ? f.geometry.coordinates : f.coordinates, coords) - }) - - if (coords.length) { - let west = coords[0][0] - let south = coords[0][1] - let east = coords[0][0] - let north = coords[0][1] - coords.forEach(function (c) { - if (c[0] < west) { - west = c[0] - } - if (c[0] > east) { - east = c[0] - } - if (c[1] < south) { - south = c[1] - } - if (c[1] > north) { - north = c[1] - } - }) - mapInstance.fitBounds( - [ - [west, south], - [east, north] - ], - { padding: 40, maxZoom: 15 } - ) +function parseEdpBoundaryGeojson(mapEl) { + try { + return JSON.parse(mapEl.dataset.edpBoundaryGeojson) + } catch { + logWarning('Failed to parse EDP boundary GeoJSON') + return null } } -function addBoundaryLayer(mapInstance, geojson) { - if (mapInstance.getSource('boundary')) { - return +function parseEdpIntersectionGeojson(mapEl) { + try { + return JSON.parse(mapEl.dataset.edpIntersectionGeojson) + } catch { + logWarning('Failed to parse EDP intersection GeoJSON') + return null } - - mapInstance.addSource('boundary', { - type: 'geojson', - data: geojson - }) - - mapInstance.addLayer({ - id: 'boundary-fill', - type: 'fill', - source: 'boundary', - paint: { - 'fill-color': BOUNDARY_COLOR, - 'fill-opacity': 0.1 - } - }) - - mapInstance.addLayer({ - id: 'boundary-line', - type: 'line', - source: 'boundary', - paint: { - 'line-color': BOUNDARY_COLOR, - 'line-width': 3 - } - }) - - fitMapToBounds(mapInstance, geojson) } function initBoundaryMap() { @@ -116,6 +61,9 @@ function initBoundaryMap() { return } + const edpBoundaryGeojson = parseEdpBoundaryGeojson(mapEl) + const edpIntersectionGeojson = parseEdpIntersectionGeojson(mapEl) + if ( typeof defra === 'undefined' || !defra.InteractiveMap || @@ -148,9 +96,13 @@ function initBoundaryMap() { if (mapInstance.isStyleLoaded()) { addBoundaryLayer(mapInstance, geojson) + addEdpBoundaryLayer(mapInstance, edpBoundaryGeojson) + addEdpIntersectionLayer(mapInstance, edpIntersectionGeojson) } else { mapInstance.once('style.load', function () { addBoundaryLayer(mapInstance, geojson) + addEdpBoundaryLayer(mapInstance, edpBoundaryGeojson) + addEdpIntersectionLayer(mapInstance, edpIntersectionGeojson) }) } }) diff --git a/src/client/javascripts/boundary-map.test.js b/src/client/javascripts/boundary-map.test.js index c2e439d..a2084e6 100644 --- a/src/client/javascripts/boundary-map.test.js +++ b/src/client/javascripts/boundary-map.test.js @@ -1,31 +1,15 @@ // @vitest-environment jsdom import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' - -const validGeojson = { - type: 'FeatureCollection', - features: [ - { - type: 'Feature', - geometry: { - type: 'Polygon', - coordinates: [ - [ - [-1.5, 52.0], - [-1.4, 52.0], - [-1.4, 52.1], - [-1.5, 52.1], - [-1.5, 52.0] - ] - ] - }, - properties: {} - } - ] -} +import { + validGeojson, + validEdpBoundaryGeojson, + validEdpIntersectionGeojson +} from './__fixtures__/boundary-map-fixtures.js' function createMapElement( geojson, - styleUrl = 'https://example.com/style.json' + styleUrl = 'https://example.com/style.json', + { edpBoundary, edpIntersection } = {} ) { const el = document.createElement('div') el.id = 'boundary-map' @@ -33,6 +17,18 @@ function createMapElement( el.dataset.geojson = typeof geojson === 'string' ? geojson : JSON.stringify(geojson) } + if (edpBoundary !== undefined) { + el.dataset.edpBoundaryGeojson = + typeof edpBoundary === 'string' + ? edpBoundary + : JSON.stringify(edpBoundary) + } + if (edpIntersection !== undefined) { + el.dataset.edpIntersectionGeojson = + typeof edpIntersection === 'string' + ? edpIntersection + : JSON.stringify(edpIntersection) + } el.dataset.mapStyleUrl = styleUrl document.body.appendChild(el) return el @@ -52,7 +48,6 @@ function createMockMapInstance(styleLoaded = true) { function createMockDefra(mapInstance) { const mockMap = { on: vi.fn() } - // Use a real function so it works with `new` function MockInteractiveMap() { return mockMap } @@ -87,7 +82,7 @@ function createMockDefra(mapInstance) { let initFn = null const originalAddEventListener = document.addEventListener.bind(document) -describe('boundary-map', () => { +describe('boundary-map init', () => { let warnSpy beforeEach(() => { @@ -96,7 +91,6 @@ describe('boundary-map', () => { vi.resetModules() warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}) - // Intercept addEventListener to capture the init function vi.spyOn(document, 'addEventListener').mockImplementation( (event, fn, ...rest) => { if (event === 'DOMContentLoaded') { @@ -115,7 +109,6 @@ describe('boundary-map', () => { async function loadModule() { await import('./boundary-map.js') - // Call the captured init function directly if (initFn) { initFn() } @@ -201,7 +194,23 @@ describe('boundary-map', () => { ) }) - it('adds boundary layers when style is already loaded', async () => { + it('adds all layers when style is already loaded', async () => { + createMapElement(validGeojson, 'https://example.com/style.json', { + edpBoundary: validEdpBoundaryGeojson, + edpIntersection: validEdpIntersectionGeojson + }) + const mapInstance = createMockMapInstance(true) + const mockDefra = createMockDefra(mapInstance) + globalThis.defra = mockDefra + + await loadModule() + mockDefra._triggerReady() + + expect(mapInstance.addSource).toHaveBeenCalledTimes(3) + expect(mapInstance.addLayer).toHaveBeenCalledTimes(6) + }) + + it('adds boundary source and layers with correct data when style is loaded', async () => { createMapElement(validGeojson) const mapInstance = createMockMapInstance(true) const mockDefra = createMockDefra(mapInstance) @@ -239,7 +248,6 @@ describe('boundary-map', () => { expect.any(Function) ) - // Trigger the style.load callback const styleLoadCallback = mapInstance.once.mock.calls[0][1] styleLoadCallback() diff --git a/src/server/quote/map/controller.test.js b/src/server/quote/map/controller.test.js index 0d5cb9c..39ea17e 100644 --- a/src/server/quote/map/controller.test.js +++ b/src/server/quote/map/controller.test.js @@ -31,7 +31,27 @@ describe('map controller', () => { const mockEdpGeojson = { geometry: mockGeometry, - intersecting_edps: [{ label: 'EDP 1', n2k_site_name: 'Site 1' }], + intersecting_edps: [ + { + label: 'EDP 1', + n2k_site_name: 'Site 1', + intersection_geometry: { + type: 'Polygon', + coordinates: [ + [ + [-1.5, 52.0], + [-1.4, 52.0], + [-1.4, 52.1], + [-1.5, 52.1], + [-1.5, 52.0] + ] + ] + }, + overlap_area_ha: 0.5, + overlap_area_sqm: 5000.0, + overlap_percentage: 25.0 + } + ], intersects_edp: true } diff --git a/src/server/quote/map/get-view-model.js b/src/server/quote/map/get-view-model.js index a760056..ca3e154 100644 --- a/src/server/quote/map/get-view-model.js +++ b/src/server/quote/map/get-view-model.js @@ -14,13 +14,45 @@ export default function getViewModel(boundaryGeojson) { const intersectsEdp = boundaryGeojson?.intersects_edp ?? false const featureCount = geometry?.features?.length ?? 0 + const edpBoundaryGeojson = JSON.stringify({ + type: 'FeatureCollection', + features: intersectingEdps + .filter((edp) => edp.edp_geometry) + .map((edp) => ({ + type: 'Feature', + geometry: edp.edp_geometry, + properties: { label: edp.label } + })) + }) + + const edpIntersectionGeojson = JSON.stringify({ + type: 'FeatureCollection', + features: intersectingEdps + .filter((edp) => edp.intersection_geometry) + .map((edp) => ({ + type: 'Feature', + geometry: edp.intersection_geometry, + properties: { + label: edp.label, + overlap_area_ha: edp.overlap_area_ha, + overlap_percentage: edp.overlap_percentage + } + })) + }) + + const mappedEdps = intersectingEdps.map((edp) => ({ + ...edp, + natura2000SiteName: edp.n2k_site_name // n2k = Natura 2000 (EU protected sites network) + })) + return { pageTitle: getPageTitle(title), pageHeading: title, boundaryGeojson: JSON.stringify(geometry), - intersectingEdps, + intersectingEdps: mappedEdps, intersectsEdp, - boundaryResponseJson: JSON.stringify(boundaryGeojson, null, 2), + edpBoundaryGeojson, + edpIntersectionGeojson, featureCount, backLinkPath: uploadBoundaryPath, uploadBoundaryPath, diff --git a/src/server/quote/map/get-view-model.test.js b/src/server/quote/map/get-view-model.test.js index 9a94a90..c24e6f4 100644 --- a/src/server/quote/map/get-view-model.test.js +++ b/src/server/quote/map/get-view-model.test.js @@ -50,7 +50,9 @@ describe('getViewModel', () => { const result = getViewModel(response) expect(result.intersectsEdp).toBe(true) - expect(result.intersectingEdps).toEqual(edps) + expect(result.intersectingEdps).toEqual( + edps.map((edp) => ({ ...edp, natura2000SiteName: edp.n2k_site_name })) + ) }) it('should handle missing fields gracefully', () => { @@ -68,4 +70,76 @@ describe('getViewModel', () => { expect(result.cancelPath).toBe('/quote/boundary-type') expect(result.mapStyleUrl).toBeDefined() }) + + it('should build edpBoundaryGeojson and edpIntersectionGeojson from enhanced EDP data', () => { + const edpGeometry = { + type: 'Polygon', + coordinates: [ + [ + [-1.6, 51.9], + [-1.3, 51.9], + [-1.3, 52.2], + [-1.6, 52.2], + [-1.6, 51.9] + ] + ] + } + const intersectionGeometry = { + type: 'Polygon', + coordinates: [ + [ + [-1.5, 52.0], + [-1.4, 52.0], + [-1.4, 52.1], + [-1.5, 52.1], + [-1.5, 52.0] + ] + ] + } + const edps = [ + { + label: 'EDP Area 1', + n2k_site_name: 'Site A', + edp_geometry: edpGeometry, + intersection_geometry: intersectionGeometry, + overlap_area_ha: 0.5, + overlap_area_sqm: 5000.0, + overlap_percentage: 25.0 + } + ] + const response = { + geometry: { type: 'FeatureCollection', features: [] }, + intersecting_edps: edps, + intersects_edp: true + } + + const result = getViewModel(response) + + const boundary = JSON.parse(result.edpBoundaryGeojson) + expect(boundary.type).toBe('FeatureCollection') + expect(boundary.features).toHaveLength(1) + expect(boundary.features[0].geometry).toEqual(edpGeometry) + expect(boundary.features[0].properties.label).toBe('EDP Area 1') + + const intersection = JSON.parse(result.edpIntersectionGeojson) + expect(intersection.type).toBe('FeatureCollection') + expect(intersection.features).toHaveLength(1) + expect(intersection.features[0].geometry).toEqual(intersectionGeometry) + expect(intersection.features[0].properties.overlap_area_ha).toBe(0.5) + expect(intersection.features[0].properties.overlap_percentage).toBe(25.0) + }) + + it('should return empty FeatureCollections when no EDPs have geometries', () => { + const edps = [{ label: 'EDP Area 1', n2k_site_name: 'Site A' }] + const response = { + geometry: { type: 'FeatureCollection', features: [] }, + intersecting_edps: edps, + intersects_edp: true + } + + const result = getViewModel(response) + + expect(JSON.parse(result.edpBoundaryGeojson).features).toHaveLength(0) + expect(JSON.parse(result.edpIntersectionGeojson).features).toHaveLength(0) + }) }) diff --git a/src/server/quote/map/index.njk b/src/server/quote/map/index.njk index 3fda926..df16850 100644 --- a/src/server/quote/map/index.njk +++ b/src/server/quote/map/index.njk @@ -34,26 +34,26 @@
Map view is not available. The boundary data has been validated.
{{ boundaryResponseJson }}
- Your red line boundary is within {{ intersectingEdps | length }} EDP{{ "s" if intersectingEdps | length != 1 }}: