Skip to content

Commit

Permalink
Merge branch 'integration/latest-levels-app' into feature/FSR-1293
Browse files Browse the repository at this point in the history
  • Loading branch information
Keyurx11 authored Oct 9, 2024
2 parents 33a01cc + 3f97c9a commit 1f3a2bb
Show file tree
Hide file tree
Showing 28 changed files with 286 additions and 88 deletions.
23 changes: 23 additions & 0 deletions server/models/views/lib/latest-levels.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,39 @@ const { formatElapsedTime } = require('../../../util')

const WARNING_THRESHOLD_TYPES = ['FW RES FW', 'FW ACT FW', 'FW ACTCON FW']

function adjustThresholdValue (value, stageDatum, subtract, postProcess) {
if (postProcess) {
if (stageDatum && stageDatum > 0) {
value -= stageDatum
} else if (stageDatum <= 0 && subtract && subtract > 0) {
value -= subtract
} else {
return parseFloat(value).toFixed(2)
}
}
return parseFloat(value).toFixed(2)
}

function getThresholdsForTargetArea (thresholds) {
const filteredThresholds = thresholds.filter(threshold =>
threshold.status !== 'Closed' &&
!(threshold.iswales && threshold.latest_level === null)
)

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 adjustThresholdValue for threshold_value adjustment
threshold.threshold_value = adjustThresholdValue(
threshold.threshold_value,
threshold.stage_datum,
threshold.subtract,
threshold.post_process
)

return threshold
})
}
Expand Down
62 changes: 55 additions & 7 deletions server/models/views/station.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ 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
Expand All @@ -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) {
Expand Down Expand Up @@ -230,19 +231,20 @@ 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,
description: 'Latest level',
shortname: ''
})
}
// Add the highest level threshold if available
if (this.station.porMaxValue) {
thresholds.push({
id: 'highest',
Expand All @@ -253,7 +255,6 @@ class ViewModel {
shortname: 'Highest level on record'
})
}

this.imtdThresholds = imtdThresholds?.length > 0
? filterImtdThresholds(imtdThresholds)
: []
Expand All @@ -265,9 +266,33 @@ class ViewModel {
this.station.post_process,
this.station.percentile5
)

thresholds.push(...processedImtdThresholds)

if (this.station.percentile5) {
// Only push typical range if it has a percentil5
thresholds.push({
id: 'pc5',
value: this.station.percentile5,
description: 'This is the top of the 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)].filter(Boolean)

// Set chartThreshold property
this.chartThreshold = chartThreshold

// Add impacts
if (impacts.length > 0) {
this.station.hasImpacts = true
Expand Down Expand Up @@ -354,7 +379,6 @@ class ViewModel {
this.zoom = 14

// Forecast Data Calculations

let forecastData
if (isForecast) {
this.isFfoi = isForecast
Expand Down Expand Up @@ -424,4 +448,28 @@ function telemetryForecastBuilder (telemetryRawData, forecastRawData, stationTyp
}
}

// Function to retrieve a threshold by tid or fall back to 'pc5' or 'alertThreshold'
const getThresholdByThresholdId = (tid, imtdThresholds, thresholds) => {
// Check if a threshold exists based on tid
const tidThreshold = tid && imtdThresholds?.find(thresh => thresh.station_threshold_id === tid)
if (tidThreshold) {
return {
id: tidThreshold.station_threshold_id,
value: Number(tidThreshold.value).toFixed(2),
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
7 changes: 5 additions & 2 deletions server/routes/station.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,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 })
Expand All @@ -68,11 +71,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 })
}
},
Expand Down
44 changes: 27 additions & 17 deletions server/src/js/components/chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -776,24 +776,25 @@ function LineChart (containerId, stationId, data, options = {}) {
.attr('class', 'threshold__line')
.attr('aria-hidden', true)
.attr('x2', xScale(xExtent[1])).attr('y2', 0)

// Label
const copy = `${threshold.level}m ${threshold.name}`.match(/[\s\S]{1,35}(?!\S)/g, '$&\n')
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: ')
copy.map((l, i) => text.append('tspan').attr('x', 10).attr('y', (i + 1) * 22).text(l.trim()))
// 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)
const textHeight = Math.round(text.node().getBBox().height)
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`)
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})`)

// Remove button

const remove = thresholdContainer.append('a')
.attr('role', 'button')
.attr('class', 'threshold__remove')
Expand Down Expand Up @@ -1430,16 +1431,25 @@ if (document.getElementById('bar-chart')) {
window.flood.charts.createBarChart('bar-chart', window.flood.model.stationId, window.flood.model.telemetry)
}

// Line chart
if (document.getElementById('line-chart')) {
const lineChart = window.flood.charts.createLineChart('line-chart', window.flood.model.id, window.flood.model.telemetry)
const thresholdId = 'threshold-pc5'
const threshold = document.querySelector(`[data-id="${thresholdId}"]`)
// Line chart setup to display thresholds based on chartThreshold configuration -----
function addThresholdToChart (lineChart, threshold) {
if (threshold) {
lineChart.addThreshold({
id: thresholdId,
name: threshold.getAttribute('data-name'),
level: Number(threshold.getAttribute('data-level'))
id: threshold.id,
name: threshold.shortname,
level: Number(threshold.value)
})
} else {
console.error('No valid threshold found to add to the chart.')
}
}

// Initialize the line chart and set thresholds using chartThreshold from ViewModel
if (document.getElementById('line-chart')) {
const lineChart = window.flood.charts.createLineChart('line-chart', window.flood.model.id, window.flood.model.telemetry)

// Iterate through each threshold in chartThreshold and add it to the chart
window.flood.model.chartThreshold.forEach(threshold => {
addThresholdToChart(lineChart, threshold)
})
}
8 changes: 8 additions & 0 deletions server/src/sass/components/_latest-levels-box.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion server/src/sass/objects/_buttons.scss
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,4 @@ button.defra-button-secondary {
margin-bottom: -4px;
top: 0px;
}
}
}
7 changes: 4 additions & 3 deletions server/views/partials/latest-levels.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,22 @@ <h2 class="defra-live__title">Latest level{% if model.latestLevels.length > 1 %}
{% for warnings in model.latestLevels %}
<div class="defra-live__item">
{% if warnings.isSuspendedOrOffline %}
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-bottom-0"><strong>Latest Level</strong></p>
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-bottom-0"><strong>{{ warnings.formatted_time }}</strong></p>
<p>The {{ warnings.river_name }} level at {{ warnings.agency_name }} is currently unavailable.</p>
{% else %}
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-bottom-0"><strong>{{ warnings.formatted_time }}</strong></p>
<p>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 %}
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}?tid={{warnings.station_threshold_id}}">Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}</a>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}{% if not warnings.iswales %}?tid={{ warnings.station_threshold_id }}{% endif %}">Monitor the {{ warnings.river_name }} level at {{ warnings.agency_name }}</a>
{% endif %}
</p>
{% if model.latestLevels.length == 1 %}
<p>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}">Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}</a>
<a href="/station/{{ warnings.rloi_id }}{% if warnings.direction == 'd' %}-downstage{% endif %}{% if not warnings.iswales %}?tid={{ warnings.station_threshold_id }}{% endif %}">Monitor the latest{% if model.latestLevels.length > 1 %} {{ warnings.river_name }}{% endif %} level at {{ warnings.agency_name }}</a>
</p>
{% endif %}
{% endif %}
</div>
{% endfor %}
<p class="defra-live__supplementary">{% if model.latestLevels.length > 1 %}These levels{% else %}This level{% endif %} will update automatically</p>
</div>
36 changes: 18 additions & 18 deletions server/views/target-area.html
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,35 @@

<!-- Page title -->
<h1 class="govuk-heading-xl govuk-!-margin-bottom-1">{{ model.pageTitle }}</h1>

<!-- Sign-up -->
<p>{% include "partials/sign-up-for-flood-warnings.html" %}</p>

<!-- Last situation change time -->
<p class="defra-flood-meta defra-flood-meta--no-border govuk-!-margin-top-1 govuk-!-margin-bottom-0">
<time datetime="{{model.situationChanged}}">{{model.situationChanged}}</time>
</p>

<!-- Map placeholder -->
<span id="map-live"></span>

<!-- Situation description -->
<div class="govuk-!-padding-top-6">
{{ model.situation | safe }}
</div>

<!-- Map placeholder -->
<span id="map-live" ></span>

<!-- Area description -->
<p>{{ model.areaDescription | safe }}</p>

<!-- Feedback link -->
{% if model.severity %}
<p>
<a href="https://defragroup.eu.qualtrics.com/jfe/form/SV_26xlu1fVwKTHkwu?Source={{fullUrl}}">
Give feedback on this flood warning information
</a>
</p>
{% endif %}

<!-- Latest water level information -->
{% if model.latestLevels and model.latestLevels.length > 0 and model.latestLevels.length <= 4 %}
{% include "partials/latest-levels.html" %}
Expand All @@ -75,22 +88,9 @@ <h1 class="govuk-heading-xl govuk-!-margin-bottom-1">{{ model.pageTitle }}</h1>
<!-- Link to river, sea, groundwater, or rainfall levels in the area -->
<p>
<a data-journey-click="Target Area:Station list:TA - View station list" href="/river-and-sea-levels/target-area/{{ model.targetArea }}">
Find other river and sea levels
</a>
</p>

<!-- Sign-up -->
{% include "partials/sign-up-for-flood-warnings.html" %}

<!-- Feedback link -->
{% if model.severity %}
<p>
Could this information be better?
<a href="https://defragroup.eu.qualtrics.com/jfe/form/SV_26xlu1fVwKTHkwu?Source={{fullUrl}}">
Tell us how to improve it.
Find a river, sea, groundwater or rainfall level in this area
</a>
</p>
{% endif %}

<!-- Context footer -->
{% include "partials/context-footer.html" %}
Expand All @@ -111,7 +111,7 @@ <h1 class="govuk-heading-xl govuk-!-margin-bottom-1">{{ model.pageTitle }}</h1>
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'
Expand Down
3 changes: 2 additions & 1 deletion test/data/nullTelemetry.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,6 @@
"lon": -0.306835309255374,
"lat": 50.8828385361225
},
"warningsAlerts": []
"warningsAlerts": [],
"requestUrl": "http://localhost:3000/station/1001"
}
3 changes: 2 additions & 1 deletion test/data/stationAWSW.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,5 +124,6 @@
"severity":"Flood warning",
"geometry":"{\"type\":\"Point\",\"coordinates\":[-3.03674273776571,53.8811798090409]}"
}
]
],
"requestUrl": "http://localhost:3000/station/1001"
}
Loading

0 comments on commit 1f3a2bb

Please sign in to comment.