Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FSR-897 and related | Accessibility Map Changes #531

Merged
merged 14 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,12 @@
"@hapi/vision": "^7.0.3",
"@hapi/wreck": "^18.0.1",
"@hapi/yar": "^11.0.1",
"@turf/helpers": "^6.5.0",
"@turf/intersect": "^6.5.0",
"@turf/polygon-smooth": "^6.5.0",
"@turf/simplify": "^6.5.0",
"@turf/turf": "^6.5.0",
"@turf/union": "^6.5.0",
"accessible-autocomplete": "^2.0.4",
"assert": "^2.0.0",
"babel-loader": "^9.1.3",
Expand Down
Binary file modified server/src/images/map-symbols-2x.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion server/src/js/components/line-chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ function LineChart (containerId, stationId, data, options = {}) {
}

// To follow
// Determin which resolution and range to display
// Determine which resolution and range to display
// Using raw data for now

// Setup array to combine observed and forecast points and identify startPoint for locator
Expand Down
34 changes: 15 additions & 19 deletions server/src/js/components/map/container.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ window.flood.maps.MapContainer = function MapContainer (mapId, options) {

// Create exit map button
const exitMapButtonElement = document.createElement('button')
exitMapButtonElement.className = 'defra-map__' + (state.isBack ? 'back' : 'exit')
exitMapButtonElement.appendChild(document.createTextNode('Exit map'))
exitMapButtonElement.innerHTML = state.isBack ? '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M4.828,11L12.314,18.485L10.899,19.899L1,10L10.899,0.101L12.314,1.515L4.828,9L19,9L19,11L4.828,11Z" style="fill:currentColor;stroke:currentColor;stroke-width:0.1px"/></svg><span>Exit map</span>' : '<svg focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M10,8.6L15.6,3L17,4.4L11.4,10L17,15.6L15.6,17L10,11.4L4.4,17L3,15.6L8.6,10L3,4.4L4.4,3L10,8.6Z" style="fill:currentColor;stroke:currentColor;stroke-width:0.1px;"/></svg><span>Exit map</span>'
exitMapButtonElement.className = 'defra-map__exit'
const exitMapButton = new Control({
element: exitMapButtonElement,
target: controlsElement
Expand All @@ -107,7 +107,7 @@ window.flood.maps.MapContainer = function MapContainer (mapId, options) {
// Create the open key button
const openKeyButtonElement = document.createElement('button')
openKeyButtonElement.className = 'defra-map__open-key'
openKeyButtonElement.innerHTML = 'Open key'
openKeyButtonElement.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M17.215,11.31L19,12.5L10,18.5L1,12.5L2.785,11.31L9.945,16.083C9.978,16.106 10.022,16.106 10.055,16.083L17.215,11.31Z" style="fill:currentColor;"/><path d="M10,1.5L1,7.5L10,13.5L19,7.5L10,1.5ZM10,3.88L15.43,7.5L10,11.12L4.57,7.5L10,3.88Z" style="fill:currentColor;"/></svg><span class="govuk-visually-hidden">Open key</span>'
const openKeyButton = new Control({
element: openKeyButtonElement,
target: controlsElement
Expand All @@ -122,7 +122,7 @@ window.flood.maps.MapContainer = function MapContainer (mapId, options) {
// Create reset control
const resetButtonElement = document.createElement('button')
resetButtonElement.className = 'defra-map-reset'
resetButtonElement.innerHTML = 'Reset location'
resetButtonElement.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M2.054,7.871L5.25,1.407L9.25,8.335L2.054,7.871Z" style="fill:currentColor;"/><path d="M7.25,4.871C8.206,4.317 9.316,4 10.5,4C14.087,4 17,6.913 17,10.5C17,14.087 14.087,17 10.5,17C6.913,17 4,14.087 4,10.5" style="fill:none;fill-rule:nonzero;stroke:currentColor;stroke-width:2px;"/></svg><span class="govuk-visually-hidden">Reset location</span>'
resetButtonElement.setAttribute('disabled', '')
resetButtonElement.setAttribute('aria-controls', 'viewport')
const resetButton = new Control({
Expand All @@ -132,28 +132,24 @@ window.flood.maps.MapContainer = function MapContainer (mapId, options) {
map.addControl(resetButton)

// Create zoom controls
const zoomInElement = document.createElement('span')
zoomInElement.classList.add('govuk-visually-hidden')
zoomInElement.innerText = 'Zoom in'
zoomInElement.setAttribute('aria-controls', 'viewport')
const zoomOutElement = document.createElement('span')
zoomOutElement.classList.add('govuk-visually-hidden')
zoomOutElement.innerText = 'Zoom out'
zoomOutElement.setAttribute('aria-controls', 'viewport')
const zoom = new Zoom({
className: 'defra-map-zoom',
zoomInLabel: zoomInElement,
zoomOutLabel: zoomOutElement,
zoomInTipLabel: '',
zoomOutTipLabel: '',
target: controlsBottomElement
})
const zoomInElement = zoom.element.firstElementChild
const zoomOutElement = zoom.element.lastElementChild
zoomInElement.setAttribute('aria-controls', 'viewport')
zoomInElement.removeAttribute('title')
zoomInElement.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20" style="fill:currentColor;fill-rule:evenodd;clip-rule:evenodd;"><rect x="3" y="9" width="14" height="2"/><rect x="9" y="3" width="2" height="14"/></svg><span class="govuk-visually-hidden">Zoom in</span>'
zoomOutElement.setAttribute('aria-controls', 'viewport')
zoomOutElement.removeAttribute('title')
zoomOutElement.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20" style="fill:currentColor;fill-rule:evenodd;clip-rule:evenodd;"><rect x="3" y="9" width="14" height="2"/></svg><span class="govuk-visually-hidden">Zoom out</span>'
map.addControl(zoom)

// Create attribution control
const attributtionElement = document.createElement('button')
attributtionElement.className = 'defra-map-attribution'
attributtionElement.innerHTML = '<span class="govuk-visually-hidden">Copyright information</span>'
attributtionElement.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M10,2.5C11.286,2.5 12.52,2.823 13.701,3.469C14.883,4.116 15.811,5.038 16.487,6.235C17.162,7.433 17.5,8.688 17.5,10C17.5,11.305 17.169,12.551 16.506,13.735C15.844,14.92 14.92,15.844 13.735,16.506C12.551,17.169 11.305,17.5 10,17.5C8.695,17.5 7.449,17.169 6.265,16.506C5.08,15.844 4.156,14.92 3.494,13.735C2.831,12.551 2.5,11.305 2.5,10C2.5,8.688 2.838,7.433 3.513,6.235C4.189,5.038 5.117,4.116 6.299,3.469C7.48,2.823 8.714,2.5 10,2.5ZM10,3.978C8.969,3.978 7.978,4.236 7.028,4.752C6.079,5.268 5.333,6.007 4.791,6.97C4.249,7.932 3.978,8.943 3.978,10C3.978,11.044 4.244,12.043 4.776,12.996C5.308,13.949 6.051,14.692 7.004,15.224C7.957,15.756 8.956,16.022 10,16.022C11.044,16.022 12.043,15.756 12.996,15.224C13.949,14.692 14.692,13.949 15.224,12.996C15.756,12.043 16.022,11.044 16.022,10C16.022,8.949 15.751,7.941 15.209,6.975C14.667,6.008 13.921,5.268 12.972,4.752C12.022,4.236 11.031,3.978 10,3.978ZM11.821,11.116L13.398,11.645C13.149,12.467 12.738,13.097 12.164,13.535C11.589,13.972 10.904,14.191 10.108,14.191C8.992,14.191 8.081,13.825 7.376,13.094C6.671,12.363 6.319,11.354 6.319,10.069C6.319,9.227 6.475,8.476 6.789,7.817C7.102,7.157 7.554,6.664 8.145,6.338C8.735,6.012 9.396,5.849 10.127,5.849C10.93,5.849 11.601,6.039 12.139,6.421C12.678,6.803 13.097,7.405 13.398,8.228L11.811,8.6C11.629,8.123 11.392,7.777 11.102,7.562C10.811,7.347 10.467,7.239 10.069,7.239C9.475,7.239 8.988,7.462 8.61,7.91C8.231,8.357 8.042,9.057 8.042,10.01C8.042,10.963 8.228,11.664 8.6,12.115C8.972,12.565 9.419,12.79 9.941,12.79C10.379,12.79 10.757,12.662 11.077,12.404C11.397,12.146 11.645,11.717 11.821,11.116Z" style="fill:currentColor;"/></svg><span class="govuk-visually-hidden">Copyright information</span>'
const attributionButton = new Control({
element: attributtionElement,
target: controlsBottomElement
Expand All @@ -175,7 +171,7 @@ window.flood.maps.MapContainer = function MapContainer (mapId, options) {
infoLabel.classList.add('govuk-visually-hidden')
const closeInfoButton = document.createElement('button')
closeInfoButton.className = 'defra-map-info__close'
closeInfoButton.innerHTML = 'Close'
closeInfoButton.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M10,8.6L15.6,3L17,4.4L11.4,10L17,15.6L15.6,17L10,11.4L4.4,17L3,15.6L8.6,10L3,4.4L4.4,3L10,8.6Z" style="fill:currentColor;stroke:currentColor;stroke-width:0.1px;"/></svg><span class="govuk-visually-hidden">Close</span>'
const infoContainer = document.createElement('div')
infoContainer.className = 'defra-map-info__container'
const infoContent = document.createElement('div')
Expand All @@ -201,7 +197,7 @@ window.flood.maps.MapContainer = function MapContainer (mapId, options) {
keyContainer.appendChild(keyTitle)
const closeKeyButton = document.createElement('button')
closeKeyButton.className = 'defra-map-key__close'
closeKeyButton.innerHTML = 'Close key'
closeKeyButton.innerHTML = '<svg aria-hidden="true" focusable="false" width="20" height="20" viewBox="0 0 20 20"><path d="M10,8.6L15.6,3L17,4.4L11.4,10L17,15.6L15.6,17L10,11.4L4.4,17L3,15.6L8.6,10L3,4.4L4.4,3L10,8.6Z" style="fill:currentColor;stroke:currentColor;stroke-width:0.1px;"/></svg><span class="govuk-visually-hidden">Close key</span>'
keyContainer.appendChild(closeKeyButton)
const keyContent = document.createElement('div')
keyContent.className = 'defra-map-key__content'
Expand Down
184 changes: 184 additions & 0 deletions server/src/js/components/map/labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { Feature } from 'ol'
import { fromExtent } from 'ol/geom/Polygon'
import GeoJSON from 'ol/format/GeoJSON'

import { polygon, multiPolygon } from '@turf/helpers'
import simplify from '@turf/simplify'
import intersect from '@turf/intersect'

const { getParameterByName, getSummaryList } = window.flood.utils

// Generate feature name
const featureName = (feature) => {
let name = ''
if (feature.get('type') === 'C') {
name = `Sea level: ${feature.get('name')}`
} else if (feature.get('type') === 'S' || feature.get('type') === 'M') {
name = `River level: ${feature.get('name')}, ${feature.get('river')}`
} else if (feature.get('type') === 'G') {
name = `Groundwater level: ${feature.get('name')}`
} else if (feature.get('type') === 'R') {
name = `Rainfall: ${feature.get('name')}`
} else if (feature.get('severity_value') === 3) {
name = `Severe flood warning: ${feature.get('ta_name')}`
} else if (feature.get('severity_value') === 2) {
name = `Flood warning: ${feature.get('ta_name')}`
} else if (feature.get('severity_value') === 1) {
name = `Flood alert: ${feature.get('ta_name')}`
} else if (feature.get('severity_value') === 4) {
name = `Warning no longer in force: ${feature.get('ta_name')}`
}
return name
}

// Get features visible in the current viewport
export const toggleVisibleFeatures = ({ labels, container, dataLayers, maps, targetAreaPolygons, warnings, bigZoom, targetArea, viewportDescription }) => {
labels.getSource().clear()
const lyrs = getParameterByName('lyr') ? getParameterByName('lyr').split(',') : []
const resolution = container.map.getView().getResolution()
const isBigZoom = resolution <= bigZoom
const extent = container.map.getView().calculateExtent(container.map.getSize())
const layers = dataLayers.filter(layer => layer !== targetAreaPolygons && lyrs.some(lyr => layer.get('featureCodes').includes(lyr)))
// Add target area if it isn't an active alert or warning
if (!layers.includes(warnings) && targetArea.pointFeature) layers.push(warnings)
// Add vectortile polygons to labels
if (layers.includes(warnings) && isBigZoom) {
let warningPolygonFeatures = getWarningPolygonsIntersectingExtent({ extent, targetAreaPolygons, warnings })
warningPolygonFeatures = mergePolygons(warningPolygonFeatures, extent)
addWarningPolygonsToLabels({ features: warningPolygonFeatures, labels })
}
// Add point features to labels
addPointFeaturesToLabels({ layers, extent, container, isBigZoom, labels })
const features = labels.getSource().getFeatures()
// Show labels if count is between 1 and 9
const hasAccessibleFeatures = maps.isKeyboard && features.length <= 9
labels.setVisible(hasAccessibleFeatures)
// Build model
const numWarnings = features.filter(feature => [1, 2].includes(feature.get('severity'))).length
const numAlerts = features.filter(feature => feature.get('severity') === 3).length
const mumLevels = features.length - numWarnings - numAlerts
const model = {
numFeatures: features.length,
summary: getSummaryList([
{ count: numWarnings, text: 'flood warning' },
{ count: numAlerts, text: 'flood alert' },
{ count: mumLevels, text: 'water level measurement' }
]),
features: features.map((feature, i) => ({
type: feature.get('type'),
severity: feature.get('severity'),
name: featureName(feature),
river: feature.get('river')
}))
}
// Update viewport description
const html = window.nunjucks.render('description-live.html', { model })
viewportDescription.innerHTML = html
// Set numeric id and move featureId to properties
if (!hasAccessibleFeatures) return
features.forEach((feature, i) => {
feature.set('featureId', feature.getId())
feature.setId((i + 1))
})
}

// Simplify, clip and merge vector tile polygons
const mergePolygons = (features, extent) => {
const mergedPolygons = []
const turfExtentPolygon = polygon(fromExtent(extent).getCoordinates())
features.forEach(feature => {
const coordinates = feature.getGeometry().getCoordinates()
// Simplify polygons
const options = { tolerance: 100, highQuality: false }
const turfPolygon = feature.getGeometry().getType() === 'MultiPolygon'
? simplify(multiPolygon(coordinates), options)
: simplify(polygon(coordinates), options)
// Clip polygons to extent
const clippedPolygon = intersect(turfPolygon, turfExtentPolygon)
if (!clippedPolygon) return
feature.setGeometry(new GeoJSON().readFeature(clippedPolygon).getGeometry())

mergedPolygons.push(feature)
})
return mergedPolygons
}

// Get Warning Polygons Features Intersecting Extent
const getWarningPolygonsIntersectingExtent = ({ extent, targetAreaPolygons, warnings }) => {
const warningsPolygons = []
targetAreaPolygons.getSource().getFeaturesInExtent(extent).forEach(feature => {
const warning = warnings.getSource().getFeatureById(feature.getId().replace(/^flood_warning_alert./, 'flood.'))
if (warning && warning.get('isVisible')) {
const warningsPolygon = new Feature({
geometry: feature.getGeometry(),
name: warning.get('ta_name'),
type: warning.get('severity'),
severity: warning.get('severity_value')
})
warningsPolygon.setId(feature.getId().replace(/^flood_warning_alert./, 'flood.'))
warningsPolygons.push(warningsPolygon)
}
})
return warningsPolygons
}

// Add point features intersecting extent to labels source
const addPointFeaturesToLabels = ({ layers, extent, container, isBigZoom, labels }) => {
for (const layer of layers) {
if (labels.getSource().getFeatures().length > 9) break
const pointFeatures = layer.getSource().getFeaturesInExtent(extent)
for (const feature of pointFeatures) {
if ((feature.get('isVisible') && layer.get('ref') !== 'warnings') || (layer.get('ref') === 'warnings' && !isBigZoom)) {
const pointFeature = new Feature({
geometry: feature.getGeometry(),
name: feature.get('name'),
type: feature.get('type'),
severity: feature.get('severity'),
river: feature.get('riverName')
})
pointFeature.setId(feature.getId())
if (labels.getSource().getFeatures().length > 9) break
labels.getSource().addFeature(pointFeature)
}
}
}
}

// Add warning polygons to labels source
const addWarningPolygonsToLabels = ({ features, labels }) => {
features.forEach(feature => {
const geometry = feature.getGeometry()
feature.setGeometry(geometry.getType() === 'MultiPolygon'
? geometry.getInteriorPoints()
: geometry.getInteriorPoint()
)
labels.getSource().addFeature(feature)
})
}

// Set selected feature
export const toggleSelectedFeature = ({ newFeatureId = '', replaceHistory, dataLayers, selected, container, setFeatureHtml, state, targetAreaPolygons, maps }) => {
selected.getSource().clear()
dataLayers.forEach(layer => {
if (layer === targetAreaPolygons) return
const originalFeature = layer.getSource().getFeatureById(state.selectedFeatureId)
const newFeature = layer.getSource().getFeatureById(newFeatureId)
if (originalFeature) {
originalFeature.set('isSelected', false)
}
if (newFeature) {
newFeature.set('isSelected', true)
setFeatureHtml(newFeature)
selected.getSource().addFeature(newFeature)
selected.setStyle(maps.styles[layer.get('ref') === 'warnings' ? 'warnings' : 'stations']) // WebGL: layers don't use a style function
container.showInfo('Selected feature information', newFeature.get('html'))
}
if (layer.get('ref') === 'warnings') {
// Refresh vector tiles
targetAreaPolygons.setStyle(maps.styles.targetAreaPolygons)
}
})
state.selectedFeatureId = newFeatureId
// Update url
replaceHistory('fid', newFeatureId)
}
18 changes: 17 additions & 1 deletion server/src/js/components/map/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const { xhr } = window.flood.utils

const targetAreaPolygonsSource = new VectorSource({
format: new GeoJSON(),
className: 'defra-map-vl-canvas',
projection: 'EPSG:3857',
// Custom loader to only send get request if below resolution cutoff
loader: (extent, resolution) => {
Expand Down Expand Up @@ -42,6 +43,7 @@ window.flood.maps.layers = {
topography: () => {
return new TileLayer({
ref: 'road',
className: 'defra-map-bg-canvas',
source: new BingMaps({
key: window.flood.model.bingMaps + '&c4w=1&cstl=rd&src=h&st=me|lv:0_trs|v:0_pt|v:0',
imagerySet: 'RoadOnDemand'
Expand Down Expand Up @@ -82,6 +84,7 @@ window.flood.maps.layers = {
places: () => {
return new VectorLayer({
ref: 'places',
className: 'defra-map-vl-layer',
source: new VectorSource({
format: new GeoJSON(),
projection: 'EPSG:3857',
Expand All @@ -99,6 +102,7 @@ window.flood.maps.layers = {
targetAreaPolygons: () => {
return new VectorLayer({
ref: 'targetAreaPolygons',
className: 'defra-map-vl-layer',
source: targetAreaPolygonsSource,
style: window.flood.maps.styles.targetAreaPolygons,
visible: false,
Expand Down Expand Up @@ -160,7 +164,6 @@ window.flood.maps.layers = {
url: '/api/outlook.geojson'
}),
style: window.flood.maps.styles.outlookPolygons,
opacity: 0.6,
zIndex: 4
})
},
Expand All @@ -174,5 +177,18 @@ window.flood.maps.layers = {
}),
zIndex: 10
})
},
labels: () => {
return new VectorLayer({
ref: 'labels',
source: new VectorSource({
format: new GeoJSON(),
projection: 'EPSG:3857'
}),
style: window.flood.maps.styles.labels,
visible: false,
zIndex: 11,
declutter: true
})
}
}
Loading
Loading