Skip to content
Merged
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
101 changes: 101 additions & 0 deletions src/client/javascripts/__fixtures__/boundary-map-fixtures.js
Original file line number Diff line number Diff line change
@@ -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: [] }
48 changes: 48 additions & 0 deletions src/client/javascripts/boundary-map-geo.js
Original file line number Diff line number Diff line change
@@ -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 }
)
}
}
82 changes: 82 additions & 0 deletions src/client/javascripts/boundary-map-geo.test.js
Original file line number Diff line number Diff line change
@@ -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()
})
})
116 changes: 116 additions & 0 deletions src/client/javascripts/boundary-map-layers.js
Original file line number Diff line number Diff line change
@@ -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]
}
})
}
Loading
Loading