diff --git a/package-lock.json b/package-lock.json index bfcf001c9..29016b8dd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,8 +26,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", diff --git a/package.json b/package.json index 2e79b78b1..ab7143af3 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/server/src/images/map-symbols-2x.png b/server/src/images/map-symbols-2x.png index 0c32545ee..8bc86f937 100644 Binary files a/server/src/images/map-symbols-2x.png and b/server/src/images/map-symbols-2x.png differ diff --git a/server/src/js/components/line-chart.js b/server/src/js/components/line-chart.js index b1e913bf7..352e707b5 100644 --- a/server/src/js/components/line-chart.js +++ b/server/src/js/components/line-chart.js @@ -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 diff --git a/server/src/js/components/map/container.js b/server/src/js/components/map/container.js index 0464d95f2..a59446f7d 100644 --- a/server/src/js/components/map/container.js +++ b/server/src/js/components/map/container.js @@ -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 ? 'Exit map' : 'Exit map' + exitMapButtonElement.className = 'defra-map__exit' const exitMapButton = new Control({ element: exitMapButtonElement, target: controlsElement @@ -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 = 'Open key' const openKeyButton = new Control({ element: openKeyButtonElement, target: controlsElement @@ -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 = 'Reset location' resetButtonElement.setAttribute('disabled', '') resetButtonElement.setAttribute('aria-controls', 'viewport') const resetButton = new Control({ @@ -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 = 'Zoom in' + zoomOutElement.setAttribute('aria-controls', 'viewport') + zoomOutElement.removeAttribute('title') + zoomOutElement.innerHTML = 'Zoom out' map.addControl(zoom) // Create attribution control const attributtionElement = document.createElement('button') attributtionElement.className = 'defra-map-attribution' - attributtionElement.innerHTML = 'Copyright information' + attributtionElement.innerHTML = 'Copyright information' const attributionButton = new Control({ element: attributtionElement, target: controlsBottomElement @@ -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 = 'Close' const infoContainer = document.createElement('div') infoContainer.className = 'defra-map-info__container' const infoContent = document.createElement('div') @@ -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 = 'Close key' keyContainer.appendChild(closeKeyButton) const keyContent = document.createElement('div') keyContent.className = 'defra-map-key__content' diff --git a/server/src/js/components/map/labels.js b/server/src/js/components/map/labels.js new file mode 100644 index 000000000..9a398fcef --- /dev/null +++ b/server/src/js/components/map/labels.js @@ -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) +} diff --git a/server/src/js/components/map/layers.js b/server/src/js/components/map/layers.js index a75dd5c46..83e23f1f2 100644 --- a/server/src/js/components/map/layers.js +++ b/server/src/js/components/map/layers.js @@ -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) => { @@ -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' @@ -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', @@ -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, @@ -160,7 +164,6 @@ window.flood.maps.layers = { url: '/api/outlook.geojson' }), style: window.flood.maps.styles.outlookPolygons, - opacity: 0.6, zIndex: 4 }) }, @@ -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 + }) } } diff --git a/server/src/js/components/map/live.js b/server/src/js/components/map/live.js index 87cd07e7f..3ce194eb5 100644 --- a/server/src/js/components/map/live.js +++ b/server/src/js/components/map/live.js @@ -5,7 +5,7 @@ // It uses the MapContainer -import { View, Overlay, Feature } from 'ol' +import { View, Feature } from 'ol' import { transform, transformExtent } from 'ol/proj' import { unByKey } from 'ol/Observable' import { defaults as defaultInteractions } from 'ol/interaction' @@ -15,6 +15,8 @@ import { Vector as VectorSource } from 'ol/source' import moment from 'moment-timezone' import { createMapButton } from './button' +import { toggleVisibleFeatures, toggleSelectedFeature } from './labels' + const { addOrUpdateParameter, getParameterByName, forEach } = window.flood.utils const maps = window.flood.maps const { setExtentFromLonLat, getLonLatFromExtent } = window.flood.maps @@ -52,12 +54,14 @@ function LiveMap (mapId, options) { const stations = maps.layers.stations() const rainfall = maps.layers.rainfall() const selected = maps.layers.selected() + const labels = maps.layers.labels() // These layers are static const defaultLayers = [ road, satellite, - selected + selected, + labels ] // These layers can be manipulated @@ -154,9 +158,9 @@ function LiveMap (mapId, options) { } else if (props.type === 'C') { // Tide if (props.status === 'Suspended' || props.status === 'Closed' || (!props.value && !props.iswales)) { - state = 'tideError' + state = 'seaError' } else { - state = 'tide' + state = 'sea' } } else if (props.type === 'R') { // Rainfall @@ -179,7 +183,7 @@ function LiveMap (mapId, options) { (props.severity_value && props.severity_value === 4 && lyrCodes.includes('tr')) || // Rivers (ref === 'stations' && ['S', 'M'].includes(props.type) && lyrCodes.includes('ri')) || - // Tide + // Sea (ref === 'stations' && props.type === 'C' && lyrCodes.includes('ti')) || // Ground (ref === 'stations' && props.type === 'G' && lyrCodes.includes('gr')) || @@ -189,7 +193,7 @@ function LiveMap (mapId, options) { (targetArea.pointFeature && targetArea.pointFeature.getId() === feature.getId()) ) // WebGl: Feature properties must be strings or numbers - feature.set('isVisible', Boolean(isVisible).toString()) + feature.set('isVisible', isVisible) }) } @@ -232,9 +236,15 @@ function LiveMap (mapId, options) { // Toggle key symbols based on resolution const toggleKeySymbol = () => { - forEach(containerElement.querySelectorAll('.defra-map-key__symbol'), (symbol) => { + forEach(containerElement.querySelectorAll('.defra-map-key__symbol[data-display="toggle-image"]'), (symbol) => { const isBigZoom = map.getView().getResolution() <= maps.liveMaxBigZoom - isBigZoom ? symbol.classList.add('defra-map-key__symbol--big') : symbol.classList.remove('defra-map-key__symbol--big') + if (isBigZoom) { + symbol.classList.add('defra-map-key__symbol--big') + symbol.classList.remove('defra-map-key__symbol--small') + } else { + symbol.classList.add('defra-map-key__symbol--small') + symbol.classList.remove('defra-map-key__symbol--big') + } }) } @@ -246,99 +256,6 @@ function LiveMap (mapId, options) { window.history.replaceState(data, title, uri) } - // Generate feature name - const featureName = (feature) => { - let name = '' - if (feature.get('type') === 'C') { - name = `Tide 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')}` - } - return name - } - - // Get features visible in the current viewport - const getVisibleFeatures = () => { - const features = [] - const lyrs = getParameterByName('lyr') ? getParameterByName('lyr').split(',') : [] - const resolution = map.getView().getResolution() - const extent = map.getView().calculateExtent(map.getSize()) - const isBigZoom = resolution <= maps.liveMaxBigZoom - const layers = dataLayers.filter(layer => lyrs.some(lyr => layer.get('featureCodes').includes(lyr))) - if (!layers.includes(warnings) && targetArea.pointFeature) { - layers.push(warnings) - } - layers.forEach((layer) => { - layer.getSource().forEachFeatureIntersectingExtent(extent, (feature) => { - if (!feature.get('isVisible')) { return false } - features.push({ - id: feature.getId(), - name: featureName(feature), - state: layer.get('ref'), // Used to style the overlay - isBigZoom, - centre: feature.getGeometry().getCoordinates() - }) - }) - }) - return features - } - - // Show overlays - const showOverlays = () => { - state.visibleFeatures = getVisibleFeatures() - const numFeatures = state.visibleFeatures.length - const numWarnings = state.visibleFeatures.filter((feature) => feature.state === 'warnings').length - const numStations = state.visibleFeatures.filter((feature) => feature.state === 'stations').length - const numRainfall = state.visibleFeatures.filter((feature) => feature.state === 'rainfall').length - const numMeasurements = numStations + numRainfall - const features = state.visibleFeatures.slice(0, 9) - // Show visual overlays - hideOverlays() - if (maps.isKeyboard && numFeatures >= 1 && numFeatures <= 9) { - state.hasOverlays = true - features.forEach((feature, i) => { - const overlayElement = document.createElement('span') - overlayElement.setAttribute('aria-hidden', true) - overlayElement.innerText = i + 1 - const selected = feature.id === state.selectedFeatureId ? 'defra-key-symbol--selected' : '' - map.addOverlay( - new Overlay({ - id: feature.id, - element: overlayElement, - position: feature.centre, - className: `defra-key-symbol defra-key-symbol--${feature.state}${feature.isBigZoom ? '-bigZoom' : ''} ${selected}`, - offset: [0, 0] - }) - ) - }) - } - // Show non-visual feature details - const model = { - numFeatures, - numWarnings, - mumMeasurements: numMeasurements, - features - } - const html = window.nunjucks.render('description-live.html', { model }) - viewportDescription.innerHTML = html - } - - // Hide overlays - const hideOverlays = () => { - state.hasOverlays = false - map.getOverlays().clear() - } - // Set target area polygon opacity const setOpacityTargetAreaPolygons = () => { // Hide or show layer depending on resolution @@ -504,7 +421,7 @@ function LiveMap (mapId, options) { if (!warnings.getSource().getFeatureById(targetArea.pointFeature.getId())) { // Add point feature warnings.getSource().addFeature(targetArea.pointFeature) - // VectorSource: Add polygon not required if VectorTileSource + // VectorSource: Add polygon not required if targetAreaPolygonsource if (targetArea.polygonFeature && targetAreaPolygons.getSource() instanceof VectorSource) { targetAreaPolygons.getSource().addFeature(targetArea.polygonFeature) } @@ -526,7 +443,7 @@ function LiveMap (mapId, options) { // Attempt to set selected feature when layer is ready setSelectedFeature(state.selectedFeatureId) // Show overlays - showOverlays() + // showOverlays() } }) }) @@ -546,7 +463,7 @@ function LiveMap (mapId, options) { timer = setTimeout(() => { if (!container.map) return // Show overlays for visible features - showOverlays() + toggleVisibleFeatures({ labels, container, dataLayers, maps, targetAreaPolygons, warnings, bigZoom: maps.liveMaxBigZoom, targetArea, viewportDescription }) // Update url (history state) to reflect new extent const ext = getLonLatFromExtent(map.getView().calculateExtent(map.getSize())) replaceHistory('ext', ext.join(',')) @@ -571,10 +488,6 @@ function LiveMap (mapId, options) { // Set selected feature if map is clicked // Clear overlays if non-keyboard interaction map.addEventListener('click', (e) => { - // Hide overlays if non-keyboard interaction - if (!maps.isKeyboard) { - hideOverlays() - } // Get mouse coordinates and check for feature const featureId = map.forEachFeatureAtPixel(e.pixel, (feature, layer) => { if (!defaultLayers.includes(layer)) { @@ -592,7 +505,7 @@ function LiveMap (mapId, options) { // Show overlays on first tab in from browser controls viewport.addEventListener('focus', (e) => { if (maps.isKeyboard) { - showOverlays() + toggleVisibleFeatures({ labels, container, dataLayers, maps, targetAreaPolygons, warnings, bigZoom: maps.liveMaxBigZoom, targetArea, viewportDescription }) } }) @@ -616,7 +529,7 @@ function LiveMap (mapId, options) { targetAreaPolygons.setStyle(maps.styles.targetAreaPolygons) lyrs = lyrs.join(',') replaceHistory('lyr', lyrs) - showOverlays() + toggleVisibleFeatures({ labels, container, dataLayers, maps, targetAreaPolygons, warnings, bigZoom: maps.liveMaxBigZoom, targetArea, viewportDescription }) } }) @@ -639,17 +552,19 @@ function LiveMap (mapId, options) { // Handle all liveMap specific key presses containerElement.addEventListener('keyup', (e) => { - // Show overlays when any key is pressed other than Escape - if (e.key !== 'Escape') { - showOverlays() - } // Clear selected feature when pressing escape if (e.key === 'Escape' && state.selectedFeatureId !== '') { - setSelectedFeature() + toggleSelectedFeature({ replaceHistory, dataLayers, selected, container, setFeatureHtml, state, targetAreaPolygons, maps }) } // Set selected feature on [1-9] key presss - if (!isNaN(e.key) && e.key >= 1 && e.key <= state.visibleFeatures.length && state.visibleFeatures.length <= 9) { - setSelectedFeature(state.visibleFeatures[e.key - 1].id) + const visibleFeatures = labels.getSource().getFeatures() + if (!isNaN(e.key) && e.key >= 1 && e.key <= visibleFeatures.length && visibleFeatures.length <= 9) { + const featureId = labels.getSource().getFeatureById(e.key).get('featureId') + toggleSelectedFeature({ newFeatureId: featureId, replaceHistory, dataLayers, selected, container, setFeatureHtml, state, targetAreaPolygons, maps }) + } + // Show overlays when any key is pressed other than Escape + if (e.key !== 'Escape' && visibleFeatures.length > 9) { + toggleVisibleFeatures({ labels, container, dataLayers, maps, targetAreaPolygons, warnings, bigZoom: maps.liveMaxBigZoom, targetArea, viewportDescription }) } }) diff --git a/server/src/js/components/map/styles.js b/server/src/js/components/map/styles.js index f35c12996..c43fd234c 100644 --- a/server/src/js/components/map/styles.js +++ b/server/src/js/components/map/styles.js @@ -3,6 +3,7 @@ Sets up the window.flood.maps styles objects */ import { Style, Icon, Fill, Stroke, Text, Circle } from 'ol/style' +import { asString as colorAsString } from 'ol/color' window.flood.maps.styles = { @@ -10,7 +11,7 @@ window.flood.maps.styles = { // Vector styles live // - targetAreaPolygons: (feature) => { + targetAreaPolygons: (feature, resolution) => { // Use corresposnding warning feature propeties for styling const warningsSource = window.flood.maps.warningsSource let warningId = feature.getId() @@ -22,49 +23,51 @@ window.flood.maps.styles = { warningId = 'flood.' + feature.getId() } const warning = warningsSource.getFeatureById(warningId) - if (!warning || warning.get('isVisible') !== 'true') { return new Style() } + if (!warning || !warning.get('isVisible')) { return new Style() } + const alpha = resolution <= 14 ? resolution >= 4 ? (Math.floor(resolution) / 20) : 0.4 : 0.7 const severity = warning.get('severity_value') const isSelected = warning.get('isSelected') const isGroundwater = warning.getId().substring(6, 9) === 'FAG' // Defaults - let strokeColour = 'transparent' + const strokeColour = isSelected ? colorAsString([11, 12, 12, 0.65]) : 'transparent' let fillColour = 'transparent' let zIndex = 1 switch (severity) { case 3: // Severe warning - strokeColour = '#D4351C' - fillColour = targetAreaPolygonPattern('severe') + fillColour = colorAsString([140, 20, 25, alpha]) zIndex = 11 break case 2: // Warning - strokeColour = '#D4351C' - fillColour = targetAreaPolygonPattern('warning') + fillColour = colorAsString([227, 0, 15, alpha]) zIndex = 10 break case 1: // Alert - strokeColour = '#F47738' - fillColour = targetAreaPolygonPattern('alert') + fillColour = colorAsString([241, 135, 0, alpha]) zIndex = isGroundwater ? 4 : 7 break default: // Removed or inactive - strokeColour = '#626A6E' - fillColour = targetAreaPolygonPattern('removed') + fillColour = colorAsString([130, 151, 167, alpha]) zIndex = 1 } zIndex = isSelected ? zIndex + 2 : zIndex - const selectedStroke = new Style({ stroke: new Stroke({ color: '#FFDD00', width: 16 }), zIndex }) - const stroke = new Style({ stroke: new Stroke({ color: strokeColour, width: 2 }), zIndex }) - const fill = new Style({ fill: new Fill({ color: fillColour }), zIndex }) - - return isSelected ? [selectedStroke, stroke, fill] : [stroke, fill] + return new Style({ + stroke: new Stroke({ + color: strokeColour, + width: 2 + }), + fill: new Fill({ + color: fillColour + }), + zIndex + }) }, warnings: (feature, resolution) => { // Hide warning symbols or hide when polygon is shown - if (feature.get('isVisible') !== 'true' || resolution < window.flood.maps.liveMaxBigZoom) { + if (!feature.get('isVisible') || resolution < window.flood.maps.liveMaxBigZoom) { return } const severity = feature.get('severity_value') @@ -82,7 +85,7 @@ window.flood.maps.styles = { }, stations: (feature, resolution) => { - if (feature.get('isVisible') !== 'true') { + if (!feature.get('isVisible')) { return } const state = feature.get('state') @@ -97,10 +100,10 @@ window.flood.maps.styles = { case 'riverError': return isSelected ? (isSymbol ? styleCache.riverErrorSelected : styleCache.measurementErrorSelected) : (isSymbol ? styleCache.riverError : styleCache.measurementError) // Tide - case 'tide': - return isSelected ? (isSymbol ? styleCache.tideSelected : styleCache.measurementSelected) : (isSymbol ? styleCache.tide : styleCache.measurement) - case 'tideError': - return isSelected ? (isSymbol ? styleCache.tideErrorSelected : styleCache.measurementErrorSelected) : (isSymbol ? styleCache.tideError : styleCache.measurementError) + case 'sea': + return isSelected ? (isSymbol ? styleCache.seaSelected : styleCache.measurementSelected) : (isSymbol ? styleCache.sea : styleCache.measurement) + case 'seaError': + return isSelected ? (isSymbol ? styleCache.seaErrorSelected : styleCache.measurementErrorSelected) : (isSymbol ? styleCache.seaError : styleCache.measurementError) // Ground case 'groundHigh': return isSelected ? (isSymbol ? styleCache.groundHighSelected : styleCache.measurementAlertSelected) : (isSymbol ? styleCache.groundHigh : styleCache.measurementAlert) @@ -112,7 +115,7 @@ window.flood.maps.styles = { }, rainfall: (feature, resolution) => { - if (feature.get('isVisible') !== 'true') { + if (!feature.get('isVisible')) { return } const state = feature.get('state') @@ -135,19 +138,20 @@ window.flood.maps.styles = { const zIndex = feature.get('z-index') const lineDash = [2, 3] let strokeColour = '#85994b' - let fillColour = outlookPolygonPattern('veryLow') + let fillColour = '#85994b' if (feature.get('risk-level') === 2) { strokeColour = '#ffdd00' - fillColour = outlookPolygonPattern('low') + fillColour = '#ffdd00' } else if (feature.get('risk-level') === 3) { strokeColour = '#F47738' - fillColour = outlookPolygonPattern('medium') + fillColour = '#F47738' } else if (feature.get('risk-level') === 4) { strokeColour = '#D4351C' - fillColour = outlookPolygonPattern('high') + fillColour = '#D4351C' } const isSelected = feature.get('isSelected') - const selectedStroke = new Style({ stroke: new Stroke({ color: '#FFDD00', width: 16 }), zIndex }) + const selectedStroke = new Style({ stroke: new Stroke({ color: '#0b0c0c', width: 4 }), zIndex }) + const style = new Style({ stroke: new Stroke({ color: strokeColour, width: 1 }), fill: new Fill({ color: fillColour }), @@ -157,6 +161,29 @@ window.flood.maps.styles = { return isSelected ? [selectedStroke, style] : style }, + labels: (feature, resolution) => { + let offsetY = resolution >= window.flood.maps.liveMaxBigZoom ? 30 : 35 + if (feature.get('type') === 'TA') { + offsetY = resolution >= window.flood.maps.liveMaxBigZoom ? 37 : 0 + } + return new Style({ + text: new Text({ + font: 'Bold 16px GDS Transport, Arial, sans-serif', + text: feature.getId().toString(), + offsetY: -Math.abs(offsetY) + }), + zIndex: feature.get('type') === 'warning' ? 0 : 1, + image: new Icon({ + src: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 30 30"%3E%3Cpath d="M1,4c0,-1.656 1.344,-3 3,-3c-0,0 22,0 22,0c1.656,0 3,1.344 3,3l-0,22c-0,1.649 -1.334,2.99 -2.981,3l-22.019,0c-1.656,0 -3,-1.344 -3,-3l0,-22Z" style="fill:%23fff;stroke:%23000;stroke-width:2px;"/%3E%3Cpath d="M29,25c0,1.656 -1.344,3 -3,3l-22,0c-1.656,0 -3,-1.344 -3,-3" style="fill:none;stroke:%23000;stroke-width:2px;"/%3E%3C/svg%3E%0A', + size: [30, 30], + anchorYUnits: 'pixels', + anchor: [0.5, offsetY + 15], + offset: [0, 0], + scale: 1 + }) + }) + }, + places: (feature, resolution) => { // Hide places that are not appropriate for resolution const d = parseInt(feature.get('d')) @@ -195,176 +222,6 @@ window.flood.maps.styles = { } -// -// SVG fill paterns -// - -const targetAreaPolygonPattern = (style) => { - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') - const dpr = window.devicePixelRatio || 1 - canvas.width = 8 * dpr - canvas.height = 8 * dpr - ctx.scale(dpr, dpr) - switch (style) { - case 'severe': - ctx.fillStyle = '#D4351C' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#ffffff' - ctx.moveTo(0, 3.3) - ctx.lineTo(4.7, 8) - ctx.lineTo(3.3, 8) - ctx.lineTo(0, 4.7) - ctx.closePath() - ctx.moveTo(3.3, 0) - ctx.lineTo(4.7, 0) - ctx.lineTo(8, 3.3) - ctx.lineTo(8, 4.7) - ctx.closePath() - ctx.fill() - break - case 'warning': - ctx.fillStyle = '#D4351C' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#ffffff' - ctx.moveTo(3.3, 0) - ctx.lineTo(4.7, 0) - ctx.lineTo(0, 4.7) - ctx.lineTo(0, 3.3) - ctx.closePath() - ctx.moveTo(3.3, 8) - ctx.lineTo(4.7, 8) - ctx.lineTo(8, 4.7) - ctx.lineTo(8, 3.3) - ctx.closePath() - ctx.moveTo(4.7, 0) - ctx.lineTo(8, 3.3) - ctx.lineTo(7.3, 4) - ctx.lineTo(4, 0.7) - ctx.closePath() - ctx.moveTo(0, 4.7) - ctx.lineTo(3.3, 8) - ctx.lineTo(4, 7.3) - ctx.lineTo(0.7, 4) - ctx.closePath() - ctx.fill() - break - case 'alert': - ctx.fillStyle = '#ffffff' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#F47738' - ctx.moveTo(0, 3.3) - ctx.lineTo(0, 4.7) - ctx.lineTo(4.7, 0) - ctx.lineTo(3.3, 0) - ctx.closePath() - ctx.moveTo(3.3, 8) - ctx.lineTo(4.7, 8) - ctx.lineTo(8, 4.7) - ctx.lineTo(8, 3.3) - ctx.closePath() - ctx.fill() - break - case 'removed': - ctx.fillStyle = '#ffffff' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#626A6E' - ctx.arc(4, 4, 1, 0, 2 * Math.PI) - ctx.closePath() - ctx.fill() - break - } - ctx.restore() - return ctx.createPattern(canvas, 'repeat') -} - -const outlookPolygonPattern = (style) => { - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') - const dpr = window.devicePixelRatio || 1 - canvas.width = 8 * dpr - canvas.height = 8 * dpr - ctx.scale(dpr, dpr) - switch (style) { - case 'high': - ctx.fillStyle = '#D4351C' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#ffffff' - ctx.moveTo(0, 3.3) - ctx.lineTo(4.7, 8) - ctx.lineTo(3.3, 8) - ctx.lineTo(0, 4.7) - ctx.closePath() - ctx.moveTo(3.3, 0) - ctx.lineTo(4.7, 0) - ctx.lineTo(8, 3.3) - ctx.lineTo(8, 4.7) - ctx.closePath() - ctx.fill() - break - case 'medium': - ctx.fillStyle = '#F47738' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#ffffff' - ctx.moveTo(3.3, 0) - ctx.lineTo(4.7, 0) - ctx.lineTo(0, 4.7) - ctx.lineTo(0, 3.3) - ctx.closePath() - ctx.moveTo(3.3, 8) - ctx.lineTo(4.7, 8) - ctx.lineTo(8, 4.7) - ctx.lineTo(8, 3.3) - ctx.closePath() - ctx.moveTo(4.7, 0) - ctx.lineTo(8, 3.3) - ctx.lineTo(7.3, 4) - ctx.lineTo(4, 0.7) - ctx.closePath() - ctx.moveTo(0, 4.7) - ctx.lineTo(3.3, 8) - ctx.lineTo(4, 7.3) - ctx.lineTo(0.7, 4) - ctx.closePath() - ctx.fill() - break - case 'low': - ctx.fillStyle = '#ffdd00' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#ffffff' - ctx.moveTo(0, 3.3) - ctx.lineTo(0, 4.7) - ctx.lineTo(4.7, 0) - ctx.lineTo(3.3, 0) - ctx.closePath() - ctx.moveTo(3.3, 8) - ctx.lineTo(4.7, 8) - ctx.lineTo(8, 4.7) - ctx.lineTo(8, 3.3) - ctx.closePath() - ctx.fill() - break - case 'veryLow': - ctx.fillStyle = '#85994b' - ctx.fillRect(0, 0, 8, 8) - ctx.beginPath() - ctx.fillStyle = '#ffffff' - ctx.arc(4, 4, 1, 0, 2 * Math.PI) - ctx.closePath() - ctx.fill() - break - } - ctx.restore() - return ctx.createPattern(canvas, 'repeat') -} - // // Style caching, improves render performance // @@ -443,10 +300,10 @@ const styleCache = { riverError: createIconStyle({ offset: [0, 700], zIndex: 1 }), riverErrorSelected: createIconStyle({ offset: [100, 700], zIndex: 10 }), // Tide - tide: createIconStyle({ offset: [0, 800], zIndex: 2 }), - tideSelected: createIconStyle({ offset: [100, 800], zIndex: 10 }), - tideError: createIconStyle({ offset: [0, 900], zIndex: 1 }), - tideErrorSelected: createIconStyle({ offset: [100, 900], zIndex: 10 }), + sea: createIconStyle({ offset: [0, 800], zIndex: 2 }), + seaSelected: createIconStyle({ offset: [100, 800], zIndex: 10 }), + seaError: createIconStyle({ offset: [0, 900], zIndex: 1 }), + seaErrorSelected: createIconStyle({ offset: [100, 900], zIndex: 10 }), // Groundwater ground: createIconStyle({ offset: [0, 1100], zIndex: 2 }), groundSelected: createIconStyle({ offset: [100, 1100], zIndex: 10 }), diff --git a/server/src/sass/components/map/_map-info.scss b/server/src/sass/components/map/_map-info.scss index 908f2b870..b9cbebfcf 100644 --- a/server/src/sass/components/map/_map-info.scss +++ b/server/src/sass/components/map/_map-info.scss @@ -37,12 +37,9 @@ &__buttons { margin: 15px -51px 0px -15px; padding: 0px 15px 0px; - button { - padding-left: 26px; - } - button svg { - left:6px; - margin-top: -10px; + svg { + margin-right: 4px !important; + top: -1px; } } diff --git a/server/src/sass/components/map/_map-key.scss b/server/src/sass/components/map/_map-key.scss index 1dc59a820..4e52c503a 100644 --- a/server/src/sass/components/map/_map-key.scss +++ b/server/src/sass/components/map/_map-key.scss @@ -59,7 +59,10 @@ display:block; margin-left:34px; &--multi { - margin-top:19px; + margin-top:24px; + @include mq ($from: tablet) { + margin-top:19px; + } } } .defra-map-key__symbol { @@ -77,57 +80,24 @@ background-size: 32px; background-repeat: no-repeat; border:0; - // Map background - &--map { - background-image: svg-url(''); - } - &--satellite { - background-image: svg-url('') - } - // Alerts and warnings - &--severe { - background-image: svg-url(''); - &.defra-map-key__symbol--big { - background-image: svg-url('') - } - } - &--warning { - background-image: svg-url(''); - &.defra-map-key__symbol--big { - background-image: svg-url('') - } + color: govuk-color('black'); + svg { + display: block; } - &--alert { - background-image: svg-url(''); - &.defra-map-key__symbol--big { - background-image: svg-url('') - } - } - // Measurements - &--river { - background-image: svg-url(''); - } - &--tide { - background-image: svg-url(''); + &[data-display="toggle-image"] svg { + display: none; } - &--ground { - background-image: svg-url(''); + &--big svg:first-child { + display: none; } - &--rainfall { - background-image: svg-url(''); + &--big svg:last-child { + display: block; } - // Outlook - &--high { - background-image: svg-url('') + &--small svg:first-child { + display: block; } - &--medium { - background-image: svg-url(''); - } - &--very-low { - background-image: svg-url(''); - } - &--low { - background-image: svg-url(''); + &--small svg:last-child { + display: none; } } @@ -141,4 +111,4 @@ .govuk-radios__label:after { top:14px; } -} \ No newline at end of file +} diff --git a/server/src/sass/components/map/_map.scss b/server/src/sass/components/map/_map.scss index 1c42031d3..f536d1719 100755 --- a/server/src/sass/components/map/_map.scss +++ b/server/src/sass/components/map/_map.scss @@ -1,50 +1,60 @@ .defra-map { - position:fixed !important; + position: fixed !important; z-index: 999; - top:0; - bottom:0; - left:0; - right:0; - background-color:#b1c7ee; - canvas { // Open layers bug fix + top: 0; + bottom: 0; + left: 0; + right: 0; + background-color: #b1c7ee; + + canvas { + // Open layers bug fix display: block !important; - -webkit-tap-highlight-color: rgba(0,0,0,0); + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } + @include mq ($from: desktop) { - padding-right:290px; + padding-right: 290px; } + &:focus { outline: none; } + &:focus[keyboard-focus]:after { - position:absolute; - content:''; - left:3px; - right:3px; - top:3px; - bottom:3px; + position: absolute; + content: ''; + left: 3px; + right: 3px; + top: 3px; + bottom: 3px; pointer-events: none; outline: 3px solid $govuk-focus-colour; z-index: 99; } + &__title { @include defra-visually-hidden; } } + .defra-map-html { height: 100vh; } + .defra-map-body { position: fixed; overflow: hidden; - top:0px; - right:0px; - bottom:0px; - left:0px; + top: 0px; + right: 0px; + bottom: 0px; + left: 0px; } + .defra-map-visibility-hidden { visibility: hidden; } + .defra-map-viewport { -webkit-touch-callout: none; -webkit-user-select: none; @@ -53,91 +63,99 @@ -ms-user-select: none; user-select: none; &:focus[keyboard-focus]:after { - position:absolute; - content:''; - left:3px; - right:3px; - top:3px; - bottom:3px; - pointer-events: none; - outline: 3px solid $govuk-focus-colour; - z-index: 99; + @include focus($glow: 5px, $strong: 2px, $background: 0px, $inset: 1px); } } + .defra-map-key { - display:none; - background-color:white; - position:absolute; - z-index:2; + display: none; + background-color: white; + position: absolute; + z-index: 2; touch-action: none; + max-width: 100%; @include mq ($from: desktop) { display: block; - right:0px; - top:0px; - bottom:0px; - width:290px; + right: 0px; + top: 0px; + bottom: 0px; + width: 290px; } + &__container { display: flex; flex-direction: column; - height:100%; - width:100%; + height: 100%; + width: 100%; } + &:focus { outline: none; } + &:focus[open="true"][keyboard-focus]:after { - position:absolute; - pointer-events: none; - content:''; - left:3px; - right:3px; - top:3px; - bottom:3px; - outline: 3px solid $govuk-focus-colour; - z-index: 99; + @include focus($glow: 5px, $strong: 2px, $background: 0px, $inset: 1px); } } // Map controls - exit -.defra-map__exit, -.defra-map__back { - float:left; - margin:10px 0px 0px 10px; - border:0; - padding:0; - height:40px; - width:40px; - cursor:pointer; - background-color:white; +.defra-map__exit { + float: left; + position: relative; + margin: 10px 0px 0px 10px; + border: 0; + padding: 0; + height: 40px; + width: 40px; + cursor: pointer; + background-color: white; + color: govuk-colour('black'); + font-size: 16px; + line-height: 40px; color: govuk-colour('black'); - font-size:16px; - line-height:40px; - background-image: svg-url(''); - background-size: 20px; - background-position: 10px 10px; - background-repeat: no-repeat; - text-indent:-5000px; + + svg { + display: inline-block; + margin: 10px; + vertical-align: top; + } + + span { + vertical-align: top; + + @include mq ($until: desktop) { + // Visually hidden + clip: rect(0 0 0 0); + clip-path: inset(50%); + height: 1px; + overflow: hidden; + position: absolute; + white-space: nowrap; + width: 1px; + } + } + @include mq ($from: desktop) { - text-indent:0px; - width:auto; - padding-left:38px; - padding-right:11px; + width: auto; + padding-right: 11px; } + &:hover { - background-color:$govuk-border-colour; + background-color: govuk-colour('light-grey'); } + &:focus { outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; - color: $govuk-text-colour; - outline:none; + + &:focus[keyboard-focus]:after { + @include focus; } + &::-moz-focus-inner { border: 0; } + -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -145,51 +163,50 @@ -ms-user-select: none; pointer-events: auto; } -.defra-map__exit { - background-image: svg-url(''); - background-size: 14px; - background-position: 13px; - &:focus[keyboard-focus] { - outline:none; - } -} // Map controls - open key .defra-map__open-key { - float:left; - margin-left:5px; - margin-top:10px; - height:40px; - width:40px; - padding:0; - border:0; - cursor:pointer; - background-color:white; + float: left; + position: relative; + margin-left: 5px; + margin-top: 10px; + height: 40px; + width: 40px; + padding: 0; + border: 0; + cursor: pointer; + background-color: white; color: govuk-colour('black'); - font-size:16px; - line-height:40px; + font-size: 16px; + line-height: 40px; text-indent: -5000px; - background-image: svg-url(''); - background-size: 18px 17px; - background-position: 11px 11.5px; - background-repeat: no-repeat; + color: govuk-colour('black'); + @include mq ($from: desktop) { display: none; } + + svg { + display: block; + margin: 10px; + } + &:hover { - background-color:$govuk-border-colour; + background-color: govuk-colour('light-grey'); } + &:focus { outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; - background-image: svg-url(''); - color: $govuk-text-colour; + + &:focus[keyboard-focus]:after { + @include focus; } + &::-moz-focus-inner { border: 0; } + -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -201,6 +218,7 @@ // Map controls - open key .defra-map__keyboard { @extend .govuk-skip-link; + &--visible { position: static !important; overflow: visible !important; @@ -208,33 +226,37 @@ -webkit-clip-path: none !important; clip-path: none !important; white-space: inherit !important; - float:left; + float: left; margin: 10px 0px 0px 5px !important; z-index: 3; - border:0; - height:39px !important; - width:auto !important; - padding:0 10px; - border:0; - cursor:pointer; - background-color:white; + border: 0; + height: 39px !important; + width: auto !important; + padding: 0 10px; + border: 0; + cursor: pointer; + background-color: white; color: govuk-colour('black'); - font-size:16px; - line-height:39px; + font-size: 16px; + line-height: 39px; } + &:hover { - background-color:$govuk-border-colour; + background-color: $govuk-border-colour; } + &:focus { outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; - color: $govuk-text-colour; + + &:focus[keyboard-focus]:after { + @include focus; } + &::-moz-focus-inner { border: 0; } + -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; @@ -248,15 +270,17 @@ .defra-map-key { @include mq ($until: desktop) { // display: block; // Safari v14 bug needs to be applied with JS - left:0; - top:0px; - bottom:0px; - width:290px; + left: 0; + top: 0px; + bottom: 0px; + width: 290px; } + @include mq ($until: tablet) { - width:259px; + width: 259px; } } + .defra-map__exit, .defra-map__back, .defra-map__open-key { @@ -265,15 +289,17 @@ } } } + .defra-map-key__title { display: block; @include govuk-font($size: 24, $weight: bold, $line-height: 51px); - padding:0 15px; - margin:0; + padding: 0 15px; + margin: 0; } + .defra-map-key__content { flex: 1; - overflow:auto; + overflow: auto; -webkit-overflow-scrolling: touch; } @@ -282,6 +308,7 @@ @include mq ($from: desktop) { display: none; } + position:absolute; right:5px; top:5px; @@ -293,21 +320,25 @@ border:0; cursor:pointer; background-color: #ffffff; - background-image: svg-url(''); - background-size: 14px; - background-position: 13px; - background-repeat: no-repeat; - color: white; - text-indent:-5000px; + color: govuk-colour('black'); + + svg { + display: block; + margin: 10px; + } + &:hover { - background-color: govuk-colour('mid-grey'); + background-color: govuk-colour('light-grey'); } + &:focus { - outline:none; + outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; + + &:focus[keyboard-focus]:after { + @include focus; } + &::-moz-focus-inner { border: 0; } @@ -322,17 +353,21 @@ right: 0px; bottom: 0px; left: 0px; + @include mq ($from: desktop) { - right:290px; + right: 290px; } + pointer-events: none; } + .defra-map-controls__bottom { - position:absolute; - left:10px; - bottom:10px; - right:10px; + position: absolute; + left: 10px; + bottom: 10px; + right: 10px; pointer-events: none; + * { pointer-events: auto; } @@ -340,30 +375,37 @@ // Map controls - reset location .defra-map-reset { - float:right; - border:0; - height:40px; - width:40px; - padding:0; - cursor:pointer; - background-image: svg-url(''); - background-color:white; - background-size:30px; - background-position:5px 5px; - overflow: hidden; - text-indent: -5000px; + float: right; + position: relative; + border: 0; + height: 40px; + width: 40px; + padding: 0; + cursor: pointer; + background-color: white; + color: govuk-colour('black'); + + svg { + display: block; + margin: 10px; + } + &::-moz-focus-inner { border: 0; } + &:hover { - background-color: $govuk-border-colour; + background-color: govuk-colour('light-grey'); } + &:focus { - outline:none; + outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; + + &:focus[keyboard-focus]:after { + @include focus; } + &[disabled] { display: none; } @@ -372,146 +414,162 @@ // Map controls - zoom .defra-map-zoom { display: none; + @include mq ($from: tablet) { display: block; - float:right; - clear:right; - width:40px; - height:auto; - margin-top:5px; + float: right; + clear: right; + width: 40px; + height: auto; + margin-top: 5px; } + &-in, &-out { - display:block; - border:0; - height:40px; - width:40px; - padding:0; - cursor:pointer; - background-color:white; - background-size:30px; - background-position:5px 5px; - overflow: hidden; - text-indent: -5000px; + display: block; + position: relative; + border: 0; + height: 40px; + width: 40px; + padding: 0; + cursor: pointer; + background-color: white; + color: govuk-colour('black'); + } + + &-in svg, + &-out svg { + display: block; + margin: 10px; } + &-in::-moz-focus-inner, &-out::-moz-focus-inner { border: 0; } - &-in { - background-image: svg-url(''); - } - &-out { - background-image: svg-url(''); - } + &-in:hover, &-out:hover { - background-color: $govuk-border-colour; + background-color: govuk-colour('light-grey'); } + &-in:focus, &-out:focus { - outline:none; + outline: none; } - &-in:focus[keyboard-focus], - &-out:focus[keyboard-focus] { - background-color: $govuk-focus-colour; + + &-in:focus[keyboard-focus]:after, + &-out:focus[keyboard-focus]:after { + @include focus; } } // Map controls - Show attributions .defra-map-attribution { - float:right; + float: right; clear: both; + position: relative; @include govuk-font($size: 19); - border:0; - height:40px; - width:40px; - padding:0; + border: 0; + height: 40px; + width: 40px; + padding: 0; margin-top: 5px; - cursor:pointer; - background-color:white; - background-image: svg-url(''); - background-size: 15px; - background-repeat: no-repeat; - background-position: center center; + cursor: pointer; + background-color: white; + color: govuk-colour('black'); + + svg { + display: block; + margin: 10px; + } + &::-moz-focus-inner { border: 0; } - &:hover, &:active { - background-color: $govuk-border-colour; - color: $govuk-text-colour; + + &:hover { + background-color: govuk-colour('light-grey'); } + &:focus { - outline:none; + outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; + + &:focus[keyboard-focus]:after { + @include focus; } } // Map information dialog .defra-map-info { - position:absolute; - background-color:white; - bottom:0; - left:0; - right:0px; + position: absolute; + background-color: white; + bottom: 0; + left: 0; + right: 0px; + @include mq ($from: tablet) { - width:auto; - right:51px; - max-width:378px; - bottom:10px; - left:10px; + width: auto; + right: 51px; + max-width: 378px; + bottom: 10px; + left: 10px; } + &:focus { outline: none; } &:focus[keyboard-focus]:after { - position:absolute; - pointer-events: none; - content:''; - left:3px; - right:3px; - top:3px; - bottom:3px; - outline: 3px solid $govuk-focus-colour; - z-index: 99; + @include mq ($until: tablet) { + @include focus($glow: 5px, $strong: 2px, $background: 0px, $inset: 1px); + } + @include mq ($from: tablet) { + @include focus(); + } } &__container { margin-right:36px; } } + .defra-map-info__close { - position:absolute; - top:0px; - right:0px; - width:40px; - height:40px; - padding:0; - border:0; - cursor:pointer; + position: absolute; + top: 3px; + right: 3px; + width: 40px; + height: 40px; + padding: 0; + border: 0; + cursor: pointer; background-color: #ffffff; - background-image: svg-url(''); - background-size: 14px; - background-position: 13px; - background-repeat: no-repeat; - overflow: hidden; - text-indent: -5000px; + color: govuk-colour('black'); + + svg { + display: block; + margin: 10px; + } + &:hover { - background-color: govuk-colour('mid-grey'); + background-color: govuk-colour('light-grey'); } + &:focus { - outline:none; + outline: none; } - &:focus[keyboard-focus] { - background-color: $govuk-focus-colour; + + &:focus[keyboard-focus]:after { + @include focus; } + &::-moz-focus-inner { border: 0; } } + .defra-map--info-open .defra-map-controls__bottom { visibility: hidden; + @include mq ($from: tablet) { visibility: visible; } @@ -519,15 +577,26 @@ // Viewport focus keyboard access tooltip .defra-map-tooltip { - position: absolute; + position: absolute; @include govuk-font($size: 16); background-color: $govuk-focus-colour; - bottom:10px; - left:10px; - right:10px; - padding:10px 12px; + bottom: 10px; + left: 10px; + right: 10px; + padding: 10px 12px; + @include mq ($from: tablet) { - right:auto; - width:360px; + right: auto; + width: 360px; } -} \ No newline at end of file +} + +// Layer blur +#map-outlook .ol-layer canvas { + opacity: 0.6; +} + +// Layer blend modes +.defra-map-vl-layer canvas { + mix-blend-mode: darken; +} diff --git a/server/src/templates/description-live.html b/server/src/templates/description-live.html index e43305bb5..a282f5158 100644 --- a/server/src/templates/description-live.html +++ b/server/src/templates/description-live.html @@ -1,12 +1,13 @@ {% if model.numFeatures > 9 %}
{{ model.numWarnings }} flood alerts or warnings and {{ model.mumMeasurements }} water level measurements in the current map area. To list these zoom in to nine or fewer.
{% elif model.numFeatures >= 1 %} -{{ model.numWarnings }} flood alerts or warnings and {{ model.mumMeasurements }} water level measurements in the current map area. Use number keys to select.
-+{{ model.numWarnings }} flood alerts or warnings and {{ model.mumMeasurements }} water level measurements in the current map area. Use number keys to select. + +{% for feature in model.features %} + {{loop.index}}: {{ feature.name }}; +{% endfor %} +
{% else %}No flood alerts, warnings or water level measurements in the current map area.
-{% endif %} \ No newline at end of file +{% endif %} diff --git a/server/src/templates/info-live.html b/server/src/templates/info-live.html index 264a0c2d1..73beb6abd 100644 --- a/server/src/templates/info-live.html +++ b/server/src/templates/info-live.html @@ -99,14 +99,18 @@ {% if model.up or model.down %}