From 942cd48b1d041e782eccc305bfe73c89421624c6 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:25:30 +0100 Subject: [PATCH 1/5] FSR-1237 | Bug Fix (#820) * FSR-1237: Fix spacing in 'LATEST LEVELS' * FSR-1237: Display '(National Resources Wales)' for Welsh stations in flood warnings and add test * FSR-1237: Update time display for closed/offline/suspended gauges * FSR-1237: Add fullstop for multiple gauges link * FSR-1237: Revert link back to original * FSR-1237: Ensure visibility of LATEST LEVEL in high-contrast mode by adjusting background and border styles * FSR-1237: Reinstate levels auto-update message and refactor unit tests for consistency * FSR-1237: Refactor adjustThresholdValue logic and add tests for threshold adjustment functionality in getThresholdsForTargetArea * FSR-1237: Replace adjustThresholdValue with processThreshold to avoid code repetition --- server/models/views/lib/latest-levels.js | 11 ++++ .../views/lib/process-imtd-thresholds.js | 5 +- server/models/views/station.js | 2 +- .../sass/components/_latest-levels-box.scss | 8 +++ server/views/partials/latest-levels.html | 7 +-- server/views/target-area.html | 2 +- test/models/lib/latest-levels.js | 51 ++++++++++++------- test/models/lib/process-imtd-thresholds.js | 2 +- test/routes/target-area-2.js | 4 +- test/routes/target-area.js | 17 +++++-- 10 files changed, 79 insertions(+), 30 deletions(-) diff --git a/server/models/views/lib/latest-levels.js b/server/models/views/lib/latest-levels.js index 718d4d49b..eb083d682 100644 --- a/server/models/views/lib/latest-levels.js +++ b/server/models/views/lib/latest-levels.js @@ -1,4 +1,5 @@ const { formatElapsedTime } = require('../../../util') +const { processThreshold } = require('./process-imtd-thresholds') // Import processThreshold from the module const WARNING_THRESHOLD_TYPES = ['FW RES FW', 'FW ACT FW', 'FW ACTCON FW'] @@ -9,9 +10,19 @@ function getThresholdsForTargetArea (thresholds) { ) const warningThresholds = findPrioritisedThresholds(filteredThresholds, WARNING_THRESHOLD_TYPES) + return warningThresholds.map(threshold => { threshold.formatted_time = formatElapsedTime(threshold.value_timestamp) threshold.isSuspendedOrOffline = threshold.status === 'Suspended' || (threshold.status === 'Active' && threshold.latest_level === null) + + // Use processThreshold for threshold_value adjustment + threshold.threshold_value = processThreshold( + threshold.threshold_value, + threshold.stage_datum, + threshold.subtract, + threshold.post_process + ) + return threshold }) } diff --git a/server/models/views/lib/process-imtd-thresholds.js b/server/models/views/lib/process-imtd-thresholds.js index 0d84f7e8f..56e17a045 100644 --- a/server/models/views/lib/process-imtd-thresholds.js +++ b/server/models/views/lib/process-imtd-thresholds.js @@ -40,4 +40,7 @@ function processImtdThresholds (imtdThresholds, stationStageDatum, stationSubtra return thresholds } -module.exports = processImtdThresholds +module.exports = { + processImtdThresholds, + processThreshold +} diff --git a/server/models/views/station.js b/server/models/views/station.js index 904cd05da..b7dd4fdd2 100644 --- a/server/models/views/station.js +++ b/server/models/views/station.js @@ -5,7 +5,7 @@ const Station = require('./station-data') const Forecast = require('./station-forecast') const util = require('../../util') const tz = 'Europe/London' -const processImtdThresholds = require('./lib/process-imtd-thresholds') +const { processImtdThresholds } = require('./lib/process-imtd-thresholds') const filterImtdThresholds = require('./lib/find-min-threshold') const bannerIconId3 = 3 diff --git a/server/src/sass/components/_latest-levels-box.scss b/server/src/sass/components/_latest-levels-box.scss index 98886a2b5..599291e5e 100644 --- a/server/src/sass/components/_latest-levels-box.scss +++ b/server/src/sass/components/_latest-levels-box.scss @@ -13,6 +13,14 @@ text-transform: uppercase; background-color: #1C70B8; color: white; + word-spacing: 0.15em; + + // Add contrast-specific styles for high contrast mode + @media (forced-colors: active) { + background-color: Canvas; // Ensures light background in contrast mode + border: 2px solid ButtonText; // Adds a visible border in high contrast + color: ButtonText; // Uses the system's high-contrast text color + } } &__supplementary { @include govuk-font($size: 16); diff --git a/server/views/partials/latest-levels.html b/server/views/partials/latest-levels.html index 099f7e447..99f091b21 100644 --- a/server/views/partials/latest-levels.html +++ b/server/views/partials/latest-levels.html @@ -3,21 +3,22 @@

Latest level{% if model.latestLevels.length > 1 %} {% for warnings in model.latestLevels %}
{% if warnings.isSuspendedOrOffline %} -

Latest Level

+

{{ warnings.formatted_time }}

The {{ warnings.river_name }} level at {{ warnings.agency_name }} is currently unavailable.

{% else %}

{{ warnings.formatted_time }}

The {{ warnings.river_name }} level at {{ warnings.agency_name }} was {{ warnings.latest_level | toFixed(2) }} metres. Property flooding is possible when it goes above {{ warnings.threshold_value | toFixed(2) }} metres. {% if model.latestLevels.length > 1 %} - Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }} + Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}{% if warnings.iswales %} (Natural Resources Wales){% endif %}. {% endif %}

{% if model.latestLevels.length == 1 %}

- Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }} + Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}{% if warnings.iswales %} (Natural Resources Wales){% endif %}

{% endif %} {% endif %}
{% endfor %} +

{% if model.latestLevels.length > 1 %}These levels{% else %}This level{% endif %} will update automatically

\ No newline at end of file diff --git a/server/views/target-area.html b/server/views/target-area.html index d5a2097b2..aa71b1969 100644 --- a/server/views/target-area.html +++ b/server/views/target-area.html @@ -75,7 +75,7 @@

{{ model.pageTitle }}

- Find other river and sea levels + Find a river, sea, groundwater or rainfall level in this area

diff --git a/test/models/lib/latest-levels.js b/test/models/lib/latest-levels.js index 414d7b52e..517e30a11 100644 --- a/test/models/lib/latest-levels.js +++ b/test/models/lib/latest-levels.js @@ -107,43 +107,60 @@ lab.experiment('getThresholdsForTargetArea', () => { expect(result).to.have.length(0) }) - lab.test('should prioritize the first type in the WARNING_THRESHOLD_TYPES array when all thresholds are of the same type', () => { + lab.test('should adjust threshold_value using stageDatum if postProcess is true and stageDatum > 0', () => { const thresholds = [ - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T11:45:00.000Z' }, - { rloi_id: 2, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T12:45:00.000Z' } + { + rloi_id: 1, + threshold_type: 'FW RES FW', + value_timestamp: '2024-08-12T11:45:00.000Z', + threshold_value: '5.00', + stage_datum: 2.5, + subtract: 1.0, + post_process: true + } ] const result = getThresholdsForTargetArea(thresholds) - expect(result).to.have.length(2) - expect(result[0].threshold_type).to.equal('FW RES FW') - expect(result[0].formatted_time).to.equal('More than 1 hour ago') - expect(result[1].formatted_time).to.equal('0 minutes ago') + expect(result).to.have.length(1) + expect(result[0].threshold_value).to.equal('2.50') // 5.00 - 2.5 }) - lab.test('should correctly handle multiple thresholds with the same RLOI ID and type', () => { + lab.test('should adjust threshold_value using subtract if postProcess is true, stageDatum <= 0, and subtract > 0', () => { const thresholds = [ - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T11:45:00.000Z' }, - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T12:45:00.000Z' } + { + rloi_id: 2, + threshold_type: 'FW ACT FW', + value_timestamp: '2024-08-12T10:45:00.000Z', + threshold_value: '5.00', + stage_datum: 0, + subtract: 1.5, + post_process: true + } ] const result = getThresholdsForTargetArea(thresholds) expect(result).to.have.length(1) - expect(result[0].threshold_type).to.equal('FW RES FW') - expect(result[0].formatted_time).to.equal('More than 1 hour ago') + expect(result[0].threshold_value).to.equal('3.50') // 5.00 - 1.5 }) - lab.test('should ignore invalid thresholds and process only valid ones', () => { + lab.test('should not adjust threshold_value if postProcess is false', () => { const thresholds = [ - { rloi_id: 1, threshold_type: 'FW RES FW', value_timestamp: '2024-08-12T11:45:00.000Z' }, - { rloi_id: 2, threshold_type: 'INVALID TYPE', value_timestamp: '2024-08-12T12:45:00.000Z' } + { + rloi_id: 3, + threshold_type: 'FW ACTCON FW', + value_timestamp: '2024-08-12T11:45:00.000Z', + threshold_value: '4.00', + stage_datum: 1.0, + subtract: 1.5, + post_process: false + } ] const result = getThresholdsForTargetArea(thresholds) expect(result).to.have.length(1) - expect(result[0].threshold_type).to.equal('FW RES FW') - expect(result[0].formatted_time).to.equal('More than 1 hour ago') + expect(result[0].threshold_value).to.equal('4.00') // No adjustment }) }) diff --git a/test/models/lib/process-imtd-thresholds.js b/test/models/lib/process-imtd-thresholds.js index aecbb088c..f6b31c698 100644 --- a/test/models/lib/process-imtd-thresholds.js +++ b/test/models/lib/process-imtd-thresholds.js @@ -1,7 +1,7 @@ const Lab = require('@hapi/lab') const Code = require('@hapi/code') const lab = exports.lab = Lab.script() -const processImtdThresholds = require('../../../server/models/views/lib/process-imtd-thresholds') +const { processImtdThresholds } = require('../../../server/models/views/lib/process-imtd-thresholds') const alertExpectedText = { id: 'alertThreshold', description: 'Low lying land flooding is possible above this level. One or more flood alerts may be issued', shortname: 'Possible flood alerts' } const warningExpectedText = { id: 'warningThreshold', description: 'Property flooding is possible above this level. One or more flood warnings may be issued', shortname: 'Possible flood warnings' } diff --git a/test/routes/target-area-2.js b/test/routes/target-area-2.js index 7dee2aa81..b217bfce9 100644 --- a/test/routes/target-area-2.js +++ b/test/routes/target-area-2.js @@ -147,7 +147,7 @@ describe('target-area route', () => { expect(response.statusCode).to.equal(200) const root = parse(response.payload) linkChecker(root.querySelectorAll('a'), - 'Find other river and sea levels', + 'Find a river, sea, groundwater or rainfall level in this area', `/river-and-sea-levels/target-area/${AREA_CODE}` ) }) @@ -175,7 +175,7 @@ describe('target-area route', () => { expect(response.statusCode).to.equal(200) const root = parse(response.payload) linkChecker(root.querySelectorAll('a'), - 'Find other river and sea levels', + 'Find a river, sea, groundwater or rainfall level in this area', `/river-and-sea-levels/target-area/${AREA_CODE}` ) }) diff --git a/test/routes/target-area.js b/test/routes/target-area.js index 19343980c..c16f2e9c5 100644 --- a/test/routes/target-area.js +++ b/test/routes/target-area.js @@ -192,9 +192,10 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest levels

') Code.expect(response.payload).to.contain('

The River Pinn level at Eastcote Road was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.') Code.expect(response.payload).to.contain('

The River Pinn level at Avenue Road was 0.18 metres. Property flooding is possible when it goes above 1.46 metres.') - Code.expect(response.payload).to.contain(' Monitor the River Pinn level at Avenue Road') + Code.expect(response.payload).to.contain('Monitor the River Pinn level at Avenue Road.') Code.expect(response.payload).to.contain('

The River Pinn level at Moss Close was 0.13 metres. Property flooding is possible when it goes above 1.15 metres.') - Code.expect(response.payload).to.contain('Monitor the River Pinn level at Moss Close') + Code.expect(response.payload).to.contain('Monitor the River Pinn level at Moss Close.') + Code.expect(response.payload).to.contain('

These levels will update automatically

') }) lab.test('Check flood severity banner link for Flood warning', async () => { const floodService = require('../../server/services/flood') @@ -286,7 +287,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.statusCode).to.equal(200) const root = parse(response.payload) - Code.expect(response.payload).to.contain('Find other river and sea levels') + Code.expect(response.payload).to.contain('Find a river, sea, groundwater or rainfall level in this area') Code.expect(response.payload).to.contain('') const relatedContentLinks = root.querySelectorAll('.defra-related-items a') @@ -357,7 +358,7 @@ lab.experiment('Target-area tests', () => { // context footer check validateFooterPresent(response) Code.expect(response.payload).to.contain('Severe flood warning for Upper River Derwent, Stonethwaite Beck and Derwent Water') - Code.expect(response.payload).to.contain('Find other river and sea levels') + Code.expect(response.payload).to.contain('Find a river, sea, groundwater or rainfall level in this area') const anchorFound = root.querySelectorAll('a').some(a => a.attributes.href === '/river-and-sea-levels/target-area/011WAFDW' @@ -562,6 +563,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

') Code.expect(response.payload).to.contain('

The River Pinn level at Avenue Road is currently unavailable.

') + Code.expect(response.payload).to.contain('

This level will update automatically

') }) lab.test('Displays latest level for a single suspended station', async () => { @@ -610,6 +612,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

') Code.expect(response.payload).to.contain('

The River Pinn level at Moss Close is currently unavailable.

') + Code.expect(response.payload).to.contain('

This level will update automatically

') }) lab.test('Displays multiple levels with one active but offline, one normal, and one Welsh station with no values', async () => { @@ -708,6 +711,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest levels

') Code.expect(response.payload).to.contain('

The River Pinn level at Eastcote Road was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.') Code.expect(response.payload).to.contain('

The River Pinn level at Avenue Road is currently unavailable.

') + Code.expect(response.payload).to.contain('

These levels will update automatically

') }) lab.test('Displays multiple levels with one Closed and one normal station', async () => { @@ -756,6 +760,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

') Code.expect(response.payload).to.contain('

The River Pinn level at Eastcote Road was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.') + Code.expect(response.payload).to.contain('

This level will update automatically

') }) lab.test('Displays multiple levels with one normal, one active but offline, and one Welsh station with no values', async () => { @@ -806,6 +811,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

The River Pinn level at Eastcote Road was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.') Code.expect(response.payload).to.contain('

The River Pinn level at Avenue Road is currently unavailable.

') Code.expect(response.payload).to.not.contain('

The River Welsh station level is currently unavailable.

') + Code.expect(response.payload).to.contain('

These levels will update automatically

') }) lab.test('Displays multiple levels with one normal, one suspended, and one Closed station', async () => { @@ -856,6 +862,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

The River Pinn level at Eastcote Road was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.') Code.expect(response.payload).to.contain('

The River Pinn level at Avenue Road is currently unavailable.

') Code.expect(response.payload).to.not.contain('

The River Test level at Test Road is currently unavailable.

') + Code.expect(response.payload).to.contain('

These levels will update automatically

') }) lab.test('Does not display levels if all stations are Closed or Welsh with no values', async () => { @@ -950,5 +957,7 @@ lab.experiment('Target-area tests', () => { Code.expect(response.payload).to.contain('

Latest level

') Code.expect(response.payload).to.contain('

The River Welsh level at Welsh Station was 0.35 metres. Property flooding is possible when it goes above 1.40 metres.\n \n

') + Code.expect(response.payload).to.contain('
Monitor the latest level at Welsh Station (Natural Resources Wales)') + Code.expect(response.payload).to.contain('

This level will update automatically

') }) }) From 60d3c0e787287f0506c4ddd1baea5750b198bf10 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:29:11 +0100 Subject: [PATCH 2/5] FSR-1261 | Improve Visibility of Map Link and Sign-up Call to Action on TA Pages (#763) * FSR-1261: Improve visibility of map link and sign-up call to action on TA pages * FSR-1261: Change map button text colour to blue * FSR-1261: Change Icon on map button to be black * FSR-1261: Remove additional CSS to avoid duplication in upcoming merge --- server/src/sass/objects/_buttons.scss | 2 +- server/views/target-area.html | 34 +++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/server/src/sass/objects/_buttons.scss b/server/src/sass/objects/_buttons.scss index 2f350f634..cbbd584f5 100644 --- a/server/src/sass/objects/_buttons.scss +++ b/server/src/sass/objects/_buttons.scss @@ -143,4 +143,4 @@ button.defra-button-secondary { margin-bottom: -4px; top: 0px; } -} +} \ No newline at end of file diff --git a/server/views/target-area.html b/server/views/target-area.html index aa71b1969..039ffdeae 100644 --- a/server/views/target-area.html +++ b/server/views/target-area.html @@ -51,22 +51,35 @@

{{ model.pageTitle }}

+ + +

{% include "partials/sign-up-for-flood-warnings.html" %}

+

- - -
{{ model.situation | safe }}
+ + +

{{ model.areaDescription | safe }}

+ + {% if model.severity %} +

+ + Give feedback on this flood warning information + +

+ {% endif %} + {% if model.latestLevels and model.latestLevels.length > 0 and model.latestLevels.length <= 4 %} {% include "partials/latest-levels.html" %} @@ -79,19 +92,6 @@

{{ model.pageTitle }}

- - {% include "partials/sign-up-for-flood-warnings.html" %} - - - {% if model.severity %} -

- Could this information be better? - - Tell us how to improve it. - -

- {% endif %} - {% include "partials/context-footer.html" %} @@ -111,7 +111,7 @@

{{ model.pageTitle }}

window.flood = {} window.flood.model = {{ model | dump(2) | safe }} window.flood.model.mapButtonText = {{ model.mapButtonText | dump | safe }} - window.flood.model.mapButtonClass = 'defra-button-secondary defra-button-secondary--icon govuk-!-margin-top-4' + window.flood.model.mapButtonClass = 'defra-button-secondary govuk-!-margin-top-4 defra-button-blue-text-black-icon'; window.flood.model.mapLayers = 'mv,ts,tw,ta', window.flood.model.data = { button: 'Target Area:Map view:TA - Map view' From 5857a90ed71511c5b307bd1fcb99f652326fbc01 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:36:52 +0100 Subject: [PATCH 3/5] FSR-1333 | Update Map Buttons Styling (#825) * FSR-1333: Consistent map button CSS * FSR-1333: Change map button text to blue and icon to black * FSR-1333: Refactor CSV download button styling for consistency with map buttons and fix tests * Typo fix --- server/src/js/core.js | 2 +- server/src/sass/objects/_buttons.scss | 6 ++---- server/views/alerts-and-warnings.html | 2 +- server/views/location.html | 2 +- server/views/national.html | 2 +- server/views/rainfall-station.html | 2 +- server/views/river-and-sea-levels.html | 2 +- server/views/station.html | 10 +++++++++- server/views/target-area.html | 2 +- test/routes/station.js | 23 ++++++++++++++++++++--- 10 files changed, 38 insertions(+), 15 deletions(-) diff --git a/server/src/js/core.js b/server/src/js/core.js index 477801d4f..fd8326c7c 100644 --- a/server/src/js/core.js +++ b/server/src/js/core.js @@ -43,7 +43,7 @@ document.addEventListener('readystatechange', () => { if (document.getElementById('map-outlook')) { window.flood.maps.createOutlookMap('map-outlook', { btnText: 'View map showing flood risk areas', - btnClass: 'defra-button-secondary defra-button-secondary--icon', + btnClass: 'defra-button-secondary', days: model.outlookDays, data: model.outlookData || null }) diff --git a/server/src/sass/objects/_buttons.scss b/server/src/sass/objects/_buttons.scss index cbbd584f5..709231fe9 100644 --- a/server/src/sass/objects/_buttons.scss +++ b/server/src/sass/objects/_buttons.scss @@ -14,8 +14,7 @@ } // Secondoary button -a.defra-button-secondary, -button.defra-button-secondary { +.defra-button-secondary { position: relative; display: inline-block; @include govuk-font($size: 16); @@ -27,7 +26,7 @@ button.defra-button-secondary { background-color: white; padding: 10px; cursor: pointer; - color: govuk-colour('black'); + color: govuk-colour('blue'); text-decoration: none; } &:visited { @@ -52,7 +51,6 @@ button.defra-button-secondary { color: govuk-colour('black'); } svg { - color: currentColor; position: relative; display: inline-block; vertical-align: middle; diff --git a/server/views/alerts-and-warnings.html b/server/views/alerts-and-warnings.html index f329878b0..67881fb09 100644 --- a/server/views/alerts-and-warnings.html +++ b/server/views/alerts-and-warnings.html @@ -127,7 +127,7 @@

window.flood = {} window.flood.model = {{ model.expose | dump(2) | safe }} window.flood.model.mapButtonText = 'View map of flood warning and alert areas' - window.flood.model.mapButtonClass = 'defra-button-secondary defra-button-secondary--icon' + window.flood.model.mapButtonClass = 'defra-button-secondary' window.flood.model.mapLayers = 'ts,tw,ta,mv' window.flood.model.extent = window.flood.model.placeBbox diff --git a/server/views/location.html b/server/views/location.html index 603a21429..862659be6 100644 --- a/server/views/location.html +++ b/server/views/location.html @@ -137,7 +137,7 @@

River, sea, groundwater and rainfall levels

window.flood = {} window.flood.model = {{ model.expose | dump(2) | safe }} window.flood.model.mapButtonText = {{ model.expose.mapButtonText | dump | safe }} - window.flood.model.mapButtonClass = 'defra-button-secondary defra-button-secondary--icon govuk-!-margin-bottom-4' + window.flood.model.mapButtonClass = 'defra-button-secondary govuk-!-margin-bottom-4' window.flood.model.data = { button: 'Location:Map view:Location - View national warning map' } diff --git a/server/views/national.html b/server/views/national.html index 7576f25bb..f1bffd412 100644 --- a/server/views/national.html +++ b/server/views/national.html @@ -121,7 +121,7 @@

River, sea, groundwater and rainfall levels

window.flood = {} window.flood.model = {{ model | dump(2) | safe }} window.flood.model.mapButtonText = {{ model.mapButtonText | dump | safe }} - window.flood.model.mapButtonClass = 'defra-button-secondary defra-button-secondary--icon' + window.flood.model.mapButtonClass = 'defra-button-secondary' window.flood.model.mapLayers = 'mv,ts,tw,ta' window.flood.model.outlookDays = {{ model.outlook.days | dump | safe }} window.flood.model.data = { diff --git a/server/views/rainfall-station.html b/server/views/rainfall-station.html index b26d530bd..12bc99879 100644 --- a/server/views/rainfall-station.html +++ b/server/views/rainfall-station.html @@ -111,7 +111,7 @@

Rainfall over the last 5 days in millimetres

- + Download data CSV (12KB)
diff --git a/server/views/river-and-sea-levels.html b/server/views/river-and-sea-levels.html index 12368fd94..da5939009 100644 --- a/server/views/river-and-sea-levels.html +++ b/server/views/river-and-sea-levels.html @@ -159,7 +159,7 @@

window.flood = {} window.flood.model = {{ model.clientModel | dump(2) | safe }} window.flood.model.mapButtonText = 'View map of levels' - window.flood.model.mpaButtonClass = 'defra-button-secondary defra-button-secondary--icon' + window.flood.model.mpaButtonClass = 'defra-button-secondary' window.flood.model.mapLayers = 'mv,ri,ti,gr,rf' window.flood.model.extent = window.flood.model.placeBox window.flood.model.data = { diff --git a/server/views/station.html b/server/views/station.html index d68dc9008..0985a0c08 100644 --- a/server/views/station.html +++ b/server/views/station.html @@ -252,7 +252,15 @@

Height

Levels that are very low or below zero are normal for some stations.

{% endif %} - Download data CSV ({% if model.isFfoi %}16{% else %}12{% endif %}KB) + + + + + + Download data CSV ({% if model.isFfoi %}16{% else %}12{% endif %}KB) + diff --git a/server/views/target-area.html b/server/views/target-area.html index 039ffdeae..6c6723f26 100644 --- a/server/views/target-area.html +++ b/server/views/target-area.html @@ -111,7 +111,7 @@

{{ model.pageTitle }}

window.flood = {} window.flood.model = {{ model | dump(2) | safe }} window.flood.model.mapButtonText = {{ model.mapButtonText | dump | safe }} - window.flood.model.mapButtonClass = 'defra-button-secondary govuk-!-margin-top-4 defra-button-blue-text-black-icon'; + window.flood.model.mapButtonClass = 'defra-button-secondary govuk-!-margin-top-4' window.flood.model.mapLayers = 'mv,ts,tw,ta', window.flood.model.data = { button: 'Target Area:Map view:TA - Map view' diff --git a/test/routes/station.js b/test/routes/station.js index 35b840021..3af2d32d2 100644 --- a/test/routes/station.js +++ b/test/routes/station.js @@ -388,7 +388,8 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.contain('Normal range 0.15m to 3.50m') Code.expect(response.payload).to.contain('Nearby levels') Code.expect(response.payload).to.contain('Upstream') - Code.expect(response.payload).to.contain('Download data CSV (12KB)') + Code.expect(response.payload).to.contain('href="/station-csv/5146"') + Code.expect(response.payload).to.contain('Download data CSV (12KB)') fullRelatedContentChecker(parse(response.payload)) validateFooterPresent(response) }) @@ -1897,7 +1898,8 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.contain('This data feed was interrupted') Code.expect(response.payload).to.contain('Nearby levels') Code.expect(response.payload).to.contain('Upstream') - Code.expect(response.payload).to.contain('Download data CSV (12KB)') + Code.expect(response.payload).to.contain('href="/station-csv/5146"') + Code.expect(response.payload).to.contain('Download data CSV (12KB)') }) lab.test('GET station/5146 with Normal river level does no show IMTD thresholds if not present', async () => { const floodService = require('../../server/services/flood') @@ -2162,7 +2164,22 @@ lab.experiment('Test - /station/{id}', () => { Code.expect(response.payload).to.not.contain('Normal range ') Code.expect(response.payload).to.contain('Nearby levels') Code.expect(response.payload).to.contain('Upstream') - Code.expect(response.payload).to.contain('Download data CSV (12KB)') + // This test has been simplified in other places, but keeping it strict to ensure certainty in checking the entire button structure. + const normalizeHtml = (html) => html.replace(/\s+/g, '').trim() + Code.expect( + normalizeHtml(response.payload) + ).to.contain( + normalizeHtml( + '' + + '' + + '' + + '' + + 'Download data CSV (12KB)' + + '' + ) + ) }) lab.test('GET station/1034 - Coastal River title check ', async () => { const floodService = require('../../server/services/flood') From c17efa052f8c66e440ea3d21c1b0d9c4bf003561 Mon Sep 17 00:00:00 2001 From: Keyurx11 <58322871+Keyurx11@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:17:11 +0100 Subject: [PATCH 4/5] FSR-1295 | Default Station Chart Threshold (#830) * FSR-1295: Display correct chart thresholds based on presence of TID in URL * FSR-1295: Exclude TID in URL for Welsh stations on the latest level links * FSR-1295: Add tests * FSR-1295: Refactor code and fix sonar clould issues * FSR-1295: Add logic to show correct value on chart for stations with stageDatum * Push * FSR-1295: Fix typo in test * Undo typo --- server/models/views/station.js | 60 +++++++++++++--- server/routes/station.js | 7 +- server/src/js/components/chart.js | 90 ++++++++++++++++-------- server/views/partials/latest-levels.html | 4 +- test/data/nullTelemetry.json | 3 +- test/data/stationAWSW.json | 3 +- test/data/stationActiveAlert.json | 3 +- test/data/stationActiveWarning.json | 3 +- test/data/stationCoastal.json | 3 +- test/data/stationForecastData.json | 3 +- test/data/stationGroudwater.json | 3 +- test/data/stationMultipleAW.json | 3 +- test/data/stationRiver.json | 3 +- test/data/stationRiverACTCON.json | 3 +- test/data/stationRiverSpike.json | 3 +- test/data/stationSevereWarning.json | 3 +- test/data/taThresholdsData.json | 6 ++ test/data/toggleTipBelowZeroStation.json | 3 +- test/data/toggleTipRiverBedStation.json | 3 +- test/data/toggleTipSeaLevelStation.json | 3 +- test/models/station.js | 60 +++++++++++++++- test/routes/target-area.js | 7 +- 22 files changed, 217 insertions(+), 62 deletions(-) diff --git a/server/models/views/station.js b/server/models/views/station.js index b7dd4fdd2..8f0ebc613 100644 --- a/server/models/views/station.js +++ b/server/models/views/station.js @@ -5,16 +5,18 @@ const Station = require('./station-data') const Forecast = require('./station-forecast') const util = require('../../util') const tz = 'Europe/London' -const { processImtdThresholds } = require('./lib/process-imtd-thresholds') +const { processImtdThresholds, processThreshold } = require('./lib/process-imtd-thresholds') const filterImtdThresholds = require('./lib/find-min-threshold') const bannerIconId3 = 3 const outOfDateMax = 5 const dataStartDateTimeDaysToSubtract = 5 +const TOP_OF_NORMAL_RANGE = 'Top of normal range' + class ViewModel { constructor (options) { - const { station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts } = options + const { station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts, requestUrl } = options this.station = new Station(station) this.station.riverNavigation = river @@ -39,7 +41,6 @@ class ViewModel { const numSevereWarnings = warningsAlertsGroups['3'] ? warningsAlertsGroups['3'].length : 0 // Determine appropriate warning/alert text for banner - this.banner = numAlerts || numWarnings || numSevereWarnings switch (numAlerts) { @@ -170,7 +171,7 @@ class ViewModel { oneHourAgo.setHours(oneHourAgo.getHours() - 1) - // check if recent value is over one hour old0 + // check if recent value is over one hour old this.dataOverHourOld = new Date(this.recentValue.ts) < oneHourAgo this.recentValue.dateWhen = 'on ' + moment.tz(this.recentValue.ts, tz).format('D/MM/YY') @@ -230,12 +231,12 @@ class ViewModel { } this.metaDescription = `Check the latest recorded ${stationType.toLowerCase()} level and recent 5-day trend at ${stationLocation}` - // Thresholds + // Array to hold thresholds let thresholds = [] + // Check if recent value exists and add it to thresholds if (this.station.recentValue && !this.station.recentValue.err) { const tVal = this.station.type !== 'c' && this.station.recentValue._ <= 0 ? 0 : this.station.recentValue._.toFixed(2) - thresholds.push({ id: 'latest', value: tVal, @@ -243,6 +244,7 @@ class ViewModel { shortname: '' }) } + // Add the highest level threshold if available if (this.station.porMaxValue) { thresholds.push({ id: 'highest', @@ -253,7 +255,6 @@ class ViewModel { shortname: 'Highest level on record' }) } - this.imtdThresholds = imtdThresholds?.length > 0 ? filterImtdThresholds(imtdThresholds) : [] @@ -264,7 +265,6 @@ class ViewModel { this.station.subtract, this.station.post_process ) - thresholds.push(...processedImtdThresholds) if (this.station.percentile5) { @@ -273,10 +273,25 @@ class ViewModel { id: 'pc5', value: this.station.percentile5, description: 'This is the top of the normal range', - shortname: 'Top of normal range' + shortname: TOP_OF_NORMAL_RANGE }) } + // Handle chartThreshold: add tidThreshold if a valid tid is present; if not, fallback to 'pc5'; if 'pc5' is unavailable, use 'alertThreshold' with "Top of normal range" description. + // Extract tid from request URL if valid + let tid = null + try { + tid = requestUrl?.startsWith('http') ? new URL(requestUrl).searchParams.get('tid') : null + } catch (e) { + console.error('Invalid request URL:', e) + } + + // Retrieve the applicable threshold for chartThreshold + const chartThreshold = [getThresholdByThresholdId(tid, imtdThresholds, thresholds, this.station.stageDatum, this.station.subtract, this.station.post_process)].filter(Boolean) + + // Set chartThreshold property + this.chartThreshold = chartThreshold + // Add impacts if (impacts.length > 0) { this.station.hasImpacts = true @@ -363,7 +378,6 @@ class ViewModel { this.zoom = 14 // Forecast Data Calculations - let forecastData if (isForecast) { this.isFfoi = isForecast @@ -403,6 +417,7 @@ function stationTypeCalculator (stationTypeData) { } return stationType } + function telemetryForecastBuilder (telemetryRawData, forecastRawData, stationType) { const observed = telemetryRawData .filter(telemetry => telemetry._ !== null) // Filter out records where telemetry._ is null @@ -432,4 +447,29 @@ function telemetryForecastBuilder (telemetryRawData, forecastRawData, stationTyp } } +// Function to retrieve a threshold by tid or fall back to 'pc5' or 'alertThreshold' +const getThresholdByThresholdId = (tid, imtdThresholds, thresholds, stationStageDatum, stationSubtract, postProcess) => { + // Check if a threshold exists based on tid + const tidThreshold = tid && imtdThresholds?.find(thresh => thresh.station_threshold_id === tid) + if (tidThreshold) { + const thresholdValue = processThreshold(tidThreshold.value, stationStageDatum, stationSubtract, postProcess) + return { + id: tidThreshold.station_threshold_id, + value: thresholdValue, + description: `${tidThreshold.value}m ${tidThreshold.ta_name || ''}`, + shortname: tidThreshold.ta_name || 'Target Area Threshold' + } + } + + // Fallback to 'pc5' if present, else look for 'alertThreshold' + const pc5Threshold = thresholds.find(t => t.id === 'pc5') + if (pc5Threshold) { + return pc5Threshold + } + + // Fallback to 'alertThreshold' if description includes 'Top of normal range' + const alertThreshold = thresholds.find(t => t.id === 'alertThreshold' && t.description.includes(TOP_OF_NORMAL_RANGE)) + return alertThreshold ? { ...alertThreshold, shortname: TOP_OF_NORMAL_RANGE } : null +} + module.exports = ViewModel diff --git a/server/routes/station.js b/server/routes/station.js index 907d37d64..2743e3ed4 100644 --- a/server/routes/station.js +++ b/server/routes/station.js @@ -54,6 +54,9 @@ module.exports = { request.server.methods.flood.getRiverStationByStationId(id, direction) ]) + // const requestUrl = request.url.href + const requestUrl = request.url.toString() + if (station.status === 'Closed') { const river = [] const model = new ViewModel({ station, telemetry, imtdThresholds, impacts, river, warningsAlerts }) @@ -67,11 +70,11 @@ module.exports = { // Forecast station const values = await request.server.methods.flood.getStationForecastData(station.wiski_id) const forecast = { forecastFlag, values } - const model = new ViewModel({ station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts }) + const model = new ViewModel({ station, telemetry, forecast, imtdThresholds, impacts, river, warningsAlerts, requestUrl }) return h.view('station', { model }) } else { // Non-forecast Station - const model = new ViewModel({ station, telemetry, imtdThresholds, impacts, river, warningsAlerts }) + const model = new ViewModel({ station, telemetry, imtdThresholds, impacts, river, warningsAlerts, requestUrl }) return h.view('station', { model }) } }, diff --git a/server/src/js/components/chart.js b/server/src/js/components/chart.js index 227a6ee4e..af3b0e2e2 100644 --- a/server/src/js/components/chart.js +++ b/server/src/js/components/chart.js @@ -2,13 +2,13 @@ // Chart component import '../utils' +import { area as d3Area, line as d3Line, curveMonotoneX } from 'd3-shape' import { axisBottom, axisLeft } from 'd3-axis' import { scaleLinear, scaleBand, scaleTime } from 'd3-scale' import { timeFormat } from 'd3-time-format' +import { timeHour, timeMinute } from 'd3-time' import { select, selectAll, pointer } from 'd3-selection' import { max, bisector, extent } from 'd3-array' -import { timeHour, timeMinute } from 'd3-time' -import { area as d3Area, line as d3Line, curveMonotoneX } from 'd3-shape' const { forEach, simplify } = window.flood.utils @@ -776,18 +776,25 @@ function LineChart (containerId, stationId, data, options = {}) { .attr('class', 'threshold__line') .attr('aria-hidden', true) .attr('x2', xScale(xExtent[1])).attr('y2', 0) - const label = thresholdContainer.append('g') - .attr('class', 'threshold-label') + + // Construct label text and split into lines of up to 35 characters + const thresholdLabel = `${threshold.level}m ${threshold.name}` + const labelSegments = thresholdLabel.match(/.{1,35}(\s|$)/g).map(line => line.trim()) + const label = thresholdContainer.append('g').attr('class', 'threshold-label') const path = label.append('path') .attr('aria-hidden', true) .attr('class', 'threshold-label__bg') const text = label.append('text') .attr('class', 'threshold-label__text') text.append('tspan').attr('font-size', 0).text('Threshold: ') - text.append('tspan').attr('x', 10).attr('y', 22).text(`${threshold.level}m ${threshold.name}`) + // Add each line of the split text (up to 35 characters per line) as separate tspans + labelSegments.forEach((line, i) => { + text.append('tspan').attr('x', 10).attr('y', (i + 1) * 22).text(line.trim()) + }) const textWidth = Math.round(text.node().getBBox().width) - path.attr('d', `m-0.5,-0.5 l${textWidth + 20},0 l0,36 l-${((textWidth + 20) / 2) - 7.5},0 l-7.5,7.5 l-7.5,-7.5 l-${((textWidth + 20) / 2) - 7.5},0 l0,-36 l0,0`) - label.attr('transform', `translate(${Math.round(width / 2 - ((textWidth + 20) / 2))}, -46)`) + const textHeight = Math.round(text.node().getBBox().height) + path.attr('d', `m-0.5,-0.5 l${textWidth + 20},0 l0,${19 + textHeight} l-${((textWidth + 20) / 2) - 7.5},0 l-7.5,7.5 l-7.5,-7.5 l-${((textWidth + 20) / 2) - 7.5},0 l0,-${19 + textHeight} l0,0`) + label.attr('transform', `translate(${Math.round(width / 2 - ((textWidth + 20) / 2))}, -${29 + textHeight})`) const remove = thresholdContainer.append('a') .attr('role', 'button') .attr('class', 'threshold__remove') @@ -800,6 +807,7 @@ function LineChart (containerId, stationId, data, options = {}) { remove.append('circle').attr('class', 'threshold__remove-button').attr('r', 11) remove.append('line').attr('x1', -3).attr('y1', -3).attr('x2', 3).attr('y2', 3) remove.append('line').attr('y1', -3).attr('x2', -3).attr('x1', 3).attr('y2', 3) + // Set individual elements size and position thresholdContainer.attr('transform', 'translate(0,' + Math.round(yScale(threshold.level)) + ')') }) @@ -931,7 +939,7 @@ function LineChart (containerId, stationId, data, options = {}) { const showTooltip = (tooltipY = 10) => { if (!dataPoint) return // Hide threshold label - thresholdsContainer.select('.threshold--selected .threshold-label').style('visibility', 'hidden') + // thresholdsContainer.select('.threshold--selected .threshold-label').style('visibility', 'hidden') // Set tooltip text const value = dataCache.type === 'river' && (Math.round(dataPoint.value * 100) / 100) <= 0 ? '0' : dataPoint.value.toFixed(2) // *DBL below zero addition tooltipValue.text(`${value}m`) // *DBL below zero addition @@ -1109,7 +1117,8 @@ function LineChart (containerId, stationId, data, options = {}) { // const defaults = { - btnAddThresholdClass: 'defra-button-text-s' + btnAddThresholdClass: 'defra-button-text-s', + btnAddThresholdText: 'Show on chart (Visual only)' } options = Object.assign({}, defaults, options) @@ -1170,16 +1179,30 @@ function LineChart (containerId, stationId, data, options = {}) { const tooltipDescription = tooltipText.append('tspan').attr('class', 'tooltip-text').attr('x', 12).attr('dy', '1.4em') // Add optional 'Add threshold' buttons - document.querySelectorAll('[data-threshold-add]').forEach(container => { - const button = document.createElement('button') - button.className = options.btnAddThresholdClass - button.innerHTML = `Show ${container.getAttribute('data-level')}m threshold on chart (Visual only)` - button.setAttribute('aria-controls', `${containerId}-visualisation`) - button.setAttribute('data-id', container.getAttribute('data-id')) - button.setAttribute('data-threshold-add', '') - button.setAttribute('data-level', container.getAttribute('data-level')) - button.setAttribute('data-name', container.getAttribute('data-name')) - container.parentElement.replaceChild(button, container) + document.querySelectorAll('[data-threshold-add]').forEach((container, i) => { + const tooltip = document.createElement('div') + tooltip.className = 'defra-tooltip defra-tooltip--left' + tooltip.setAttribute('data-tooltip', '') + tooltip.innerHTML = ` + +