Skip to content

Commit

Permalink
Merge branch 'development' into feature/FSR-1294-Station-Chart-Label-…
Browse files Browse the repository at this point in the history
…Wrapping
  • Loading branch information
Keyurx11 authored Oct 22, 2024
2 parents f6a0006 + 4da173b commit f00914c
Show file tree
Hide file tree
Showing 52 changed files with 1,006 additions and 423 deletions.
2 changes: 1 addition & 1 deletion server/models/views/lib/find-min-threshold.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function filterImtdThresholds (imtdThresholds) {

return {
alert: minObjectA ? minObjectA.value : null,
warning: minObjectW ? minObjectW.value : null
warning: minObjectW || null
}
}

Expand Down
11 changes: 11 additions & 0 deletions server/models/views/lib/latest-levels.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { formatElapsedTime } = require('../../../util')
const processThreshold = require('./process-threshold') // Import processThreshold from the module

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

Expand All @@ -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
})
}
Expand Down
93 changes: 66 additions & 27 deletions server/models/views/lib/process-imtd-thresholds.js
Original file line number Diff line number Diff line change
@@ -1,43 +1,82 @@
function processThreshold (threshold, stationStageDatum, stationSubtract, postProcess) {
if (threshold) {
if (postProcess) {
if (stationStageDatum > 0) {
threshold = threshold - stationStageDatum
} else if (stationStageDatum <= 0 && stationSubtract > 0) {
threshold = threshold - stationSubtract
} else {
return parseFloat(threshold).toFixed(2)
}
}
return parseFloat(threshold).toFixed(2)
}
return null
}
const processThreshold = require('./process-threshold')

function processImtdThresholds (imtdThresholds, stationStageDatum, stationSubtract, postProcess) {
const TOP_OF_NORMAL_RANGE = 'Top of normal range'

function processImtdThresholds (imtdThresholds, stationStageDatum, stationSubtract, postProcess, pc5) {
const thresholds = []

const imtdThresholdWarning = processThreshold(imtdThresholds?.warning, stationStageDatum, stationSubtract, postProcess)
const imtdThresholdWarning = calculateWarningThreshold(imtdThresholds, stationStageDatum, stationSubtract, postProcess)
const imtdThresholdAlert = calculateAlertThreshold(imtdThresholds, stationStageDatum, stationSubtract, postProcess, pc5)

if (imtdThresholdWarning) {
// Correct threshold value if value > zero (Above Ordnance Datum) [FSR-595]
thresholds.push(imtdThresholdWarning)
}

if (imtdThresholdAlert.length > 0) {
for (const alert of imtdThresholdAlert) {
thresholds.push(alert)
}
} else if (pc5) {
thresholds.push({
id: 'pc5',
description: 'Top of normal range. Low lying land flooding possible above this level',
shortname: TOP_OF_NORMAL_RANGE,
value: pc5
})
} else { return thresholds }

return thresholds
}

function calculateWarningThreshold (imtdThresholds, stationStageDatum, stationSubtract, postProcess) {
const imtdThresholdWarning = processThreshold(imtdThresholds?.warning?.value, stationStageDatum, stationSubtract, postProcess)

if (imtdThresholdWarning) {
return {
id: 'warningThreshold',
description: 'Property flooding is possible above this level. One or more flood warnings may be issued',
description: 'Property flooding is possible above this level',
shortname: 'Possible flood warnings',
value: imtdThresholdWarning
})
}
}

return null
}

function calculateAlertThreshold (imtdThresholds, stationStageDatum, stationSubtract, postProcess, pc5) {
const imtdThresholdAlert = processThreshold(imtdThresholds?.alert, stationStageDatum, stationSubtract, postProcess)
const imtdThresholdAlerts = []
if (imtdThresholdAlert) {
thresholds.push({
id: 'alertThreshold',
description: 'Low lying land flooding is possible above this level. One or more flood alerts may be issued',
shortname: 'Possible flood alerts',
value: imtdThresholdAlert
})
// First condition: if imtdThresholdAlert is not equal to Top of Normal Range (pc5)
if (Number(imtdThresholdAlert) !== Number(pc5)) {
imtdThresholdAlerts.push({
id: 'alertThreshold',
description: 'Low lying land flooding possible above this level. One or more flood alerts may be issued',
shortname: 'Possible flood alerts',
value: imtdThresholdAlert
})
}
// Second condition: if imtdThresholdAlert is equal to Top of Normal Range (pc5)
if (Number(imtdThresholdAlert) === Number(pc5)) {
imtdThresholdAlerts.push({
id: 'alertThreshold',
description: 'Top of normal range. Low lying land flooding possible above this level. One or more flood alerts may be issued',
shortname: TOP_OF_NORMAL_RANGE,
value: imtdThresholdAlert
})
} else if (Number(pc5)) {
imtdThresholdAlerts.push({
id: 'pc5',
description: TOP_OF_NORMAL_RANGE,
shortname: TOP_OF_NORMAL_RANGE,
value: parseFloat(Number(pc5)).toFixed(2)
})
} else {
return imtdThresholdAlerts
}
}
return thresholds

return imtdThresholdAlerts
}

module.exports = processImtdThresholds
17 changes: 17 additions & 0 deletions server/models/views/lib/process-threshold.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const processThreshold = (threshold, stationStageDatum, stationSubtract, postProcess) => {
if (threshold) {
if (postProcess) {
if (stationStageDatum > 0) {
threshold = threshold - stationStageDatum
} else if (stationStageDatum <= 0 && stationSubtract > 0) {
threshold = threshold - stationSubtract
} else {
return parseFloat(threshold).toFixed(2)
}
}
return parseFloat(threshold).toFixed(2)
}
return null
}

module.exports = processThreshold
65 changes: 65 additions & 0 deletions server/models/views/lib/process-warning-thresholds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const processThreshold = require('./process-threshold')

const FLOOD_WARNING_THRESHOLD = 2
const SEVERE_FLOOD_WARNING_THRESHOLD = 3
function filterThresholdsBySeverity (thresholds) {
return thresholds.filter(item =>
item.fwis_type === 'W' &&
item.severity_value !== null &&
(item.severity_value === FLOOD_WARNING_THRESHOLD || item.severity_value === SEVERE_FLOOD_WARNING_THRESHOLD)
)
}

function getMaxForEachFwisCode (thresholds) {
const maxValuesByFwisCode = {}

thresholds.forEach(threshold => {
const fwisCode = threshold.fwis_code
const severityValue = threshold.severity_value
const thresholdValue = threshold.value

// Check if there's already a threshold for this fwis_code
if (!maxValuesByFwisCode[fwisCode]) {
// If it's the first threshold for this fwis_code, store it
maxValuesByFwisCode[fwisCode] = threshold
} else {
const currentMax = maxValuesByFwisCode[fwisCode]

// Compare based on severity_value first, or if severity_value is the same, compare based on the threshold value
if (severityValue > currentMax.severity_value ||
(severityValue === currentMax.severity_value && parseFloat(thresholdValue) > parseFloat(currentMax.value))) {
maxValuesByFwisCode[fwisCode] = threshold
}
}
})

return Object.values(maxValuesByFwisCode)
}

function createWarningObject (threshold, stationStageDatum, stationSubtract, postProcess) {
const warningType =
threshold.severity_value === SEVERE_FLOOD_WARNING_THRESHOLD
? 'Severe flood warning'
: 'Flood warning'

const imtdThresholdWarning = processThreshold(parseFloat(threshold.threshold_value).toFixed(2), stationStageDatum, stationSubtract, postProcess)

return {
id: 'warningThreshold',
description: `${warningType} issued: <a href="/target-area/${threshold.fwis_code}">${threshold.ta_name}</a>`,
shortname: `${threshold.ta_name}`,
value: imtdThresholdWarning
}
}
function processWarningThresholds (imtdThresholds, stationStageDatum, stationSubtract, postProcess) {
const filteredThresholds = filterThresholdsBySeverity(imtdThresholds)
const maxThresholdsByFwisCode = getMaxForEachFwisCode(filteredThresholds)

const warningObjects = maxThresholdsByFwisCode.map(threshold =>
createWarningObject(threshold, stationStageDatum, stationSubtract, postProcess)
)

return warningObjects
}

module.exports = processWarningThresholds
77 changes: 60 additions & 17 deletions server/models/views/station.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ const Forecast = require('./station-forecast')
const util = require('../../util')
const tz = 'Europe/London'
const processImtdThresholds = require('./lib/process-imtd-thresholds')
const processThreshold = require('./lib/process-threshold')
const processWarningThresholds = require('./lib/process-warning-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
Expand All @@ -39,7 +43,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 @@ -170,7 +173,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')
Expand Down Expand Up @@ -230,30 +233,40 @@ 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',
value: this.station.porMaxValue,
description: this.station.thresholdPorMaxDate
? 'Water reaches the highest level recorded at this measuring station (recorded on ' + this.station.thresholdPorMaxDate + ')'
? `Water reaches the highest level recorded at this measuring station (${this.station.thresholdPorMaxDate})`
: 'Water reaches the highest level recorded at this measuring station',
shortname: 'Highest level on record'
})
}

if (imtdThresholds?.length > 0) {
const processedWarningThresholds = processWarningThresholds(
imtdThresholds,
this.station.stageDatum,
this.station.subtract,
this.station.post_process)
thresholds.push(...processedWarningThresholds)
}

this.imtdThresholds = imtdThresholds?.length > 0
? filterImtdThresholds(imtdThresholds)
: []
Expand All @@ -262,21 +275,26 @@ class ViewModel {
this.imtdThresholds,
this.station.stageDatum,
this.station.subtract,
this.station.post_process
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, 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
Expand Down Expand Up @@ -363,7 +381,6 @@ class ViewModel {
this.zoom = 14

// Forecast Data Calculations

let forecastData
if (isForecast) {
this.isFfoi = isForecast
Expand Down Expand Up @@ -403,6 +420,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
Expand Down Expand Up @@ -432,4 +450,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
Loading

0 comments on commit f00914c

Please sign in to comment.