Compare commits
No commits in common. "a88e55a8a1a8d88ca455d12fc65cfd7e55f0be29" and "2e304d41c660195eb867b02349ed672ca109edba" have entirely different histories.
a88e55a8a1
...
2e304d41c6
5 changed files with 24 additions and 323 deletions
|
|
@ -134,7 +134,6 @@ const compressHtml = async () => src(htmlSources)
|
||||||
const otherFiles = [
|
const otherFiles = [
|
||||||
'server/robots.txt',
|
'server/robots.txt',
|
||||||
'server/manifest.json',
|
'server/manifest.json',
|
||||||
'server/alert/**/*.mp3',
|
|
||||||
'server/music/**/*.mp3',
|
'server/music/**/*.mp3',
|
||||||
];
|
];
|
||||||
const copyOtherFiles = () => src(otherFiles, { base: 'server/', encoding: false })
|
const copyOtherFiles = () => src(otherFiles, { base: 'server/', encoding: false })
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,9 +1,7 @@
|
||||||
// hourly forecast list
|
// hourly forecast list
|
||||||
|
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { setAlertToneActive } from './media.mjs';
|
|
||||||
import { safeJson } from './utils/fetch.mjs';
|
import { safeJson } from './utils/fetch.mjs';
|
||||||
import deriveHazards from './utils/derived-hazards.mjs';
|
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay } from './navigation.mjs';
|
import { registerDisplay } from './navigation.mjs';
|
||||||
import calculateScrollTiming from './utils/scroll-timing.mjs';
|
import calculateScrollTiming from './utils/scroll-timing.mjs';
|
||||||
|
|
@ -57,6 +55,13 @@ class Hazards extends WeatherDisplay {
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
// super checks for enabled
|
// super checks for enabled
|
||||||
const superResult = super.getData(weatherParameters, refresh);
|
const superResult = super.getData(weatherParameters, refresh);
|
||||||
|
if (!this.weatherParameters?.supportsNoaaAlerts) {
|
||||||
|
this.data = [];
|
||||||
|
this.timing.totalScreens = 0;
|
||||||
|
this.getDataCallback();
|
||||||
|
this.setStatus(STATUS.loaded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
// hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available
|
// hazards performs a silent refresh, but does not fall back to a previous fetch if no data is available
|
||||||
// this is intentional to ensure the latest alerts only are displayed.
|
// this is intentional to ensure the latest alerts only are displayed.
|
||||||
|
|
||||||
|
|
@ -74,31 +79,26 @@ class Hazards extends WeatherDisplay {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const previousSignature = this.alertSignature;
|
const previousSignature = this.alertSignature;
|
||||||
if (!this.weatherParameters?.supportsNoaaAlerts) {
|
// get the forecast using centralized safe handling
|
||||||
this.data = deriveHazards(this.weatherParameters);
|
const url = new URL('https://api.weather.gov/alerts/active');
|
||||||
} else {
|
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
||||||
// get the forecast using centralized safe handling
|
url.searchParams.append('status', 'actual');
|
||||||
const url = new URL('https://api.weather.gov/alerts/active');
|
const alerts = await safeJson(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||||
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
|
||||||
url.searchParams.append('status', 'actual');
|
|
||||||
const alerts = await safeJson(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
|
||||||
|
|
||||||
if (!alerts) {
|
if (!alerts) {
|
||||||
if (debugFlag('verbose-failures')) {
|
if (debugFlag('verbose-failures')) {
|
||||||
console.warn('Active Alerts request failed; assuming no active alerts');
|
console.warn('Active Alerts request failed; assuming no active alerts');
|
||||||
}
|
|
||||||
this.data = [];
|
|
||||||
} else {
|
|
||||||
const allUnsortedAlerts = alerts.features ?? [];
|
|
||||||
const unsortedAlerts = allUnsortedAlerts.slice(0, 5);
|
|
||||||
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
|
|
||||||
const sortedAlerts = unsortedAlerts.sort((a, b) => (calcSeverity(b.properties.severity, b.properties.event)) - (calcSeverity(a.properties.severity, a.properties.event)));
|
|
||||||
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
|
|
||||||
this.data = filteredAlerts;
|
|
||||||
}
|
}
|
||||||
|
this.data = [];
|
||||||
|
} else {
|
||||||
|
const allUnsortedAlerts = alerts.features ?? [];
|
||||||
|
const unsortedAlerts = allUnsortedAlerts.slice(0, 5);
|
||||||
|
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
|
||||||
|
const sortedAlerts = unsortedAlerts.sort((a, b) => (calcSeverity(b.properties.severity, b.properties.event)) - (calcSeverity(a.properties.severity, a.properties.event)));
|
||||||
|
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
|
||||||
|
this.data = filteredAlerts;
|
||||||
}
|
}
|
||||||
this.alertSignature = getAlertSignature(this.data);
|
this.alertSignature = getAlertSignature(this.data);
|
||||||
setAlertToneActive(this.data.length > 0);
|
|
||||||
const alertsChanged = previousSignature !== this.alertSignature;
|
const alertsChanged = previousSignature !== this.alertSignature;
|
||||||
if (alertsChanged) {
|
if (alertsChanged) {
|
||||||
this.viewedAlerts.clear();
|
this.viewedAlerts.clear();
|
||||||
|
|
@ -129,7 +129,6 @@ class Hazards extends WeatherDisplay {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
console.error(`Unexpected Active Alerts error: ${error.message}`);
|
||||||
setAlertToneActive(false);
|
|
||||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
// return undefined to other subscribers
|
// return undefined to other subscribers
|
||||||
this.getDataCallback(undefined);
|
this.getDataCallback(undefined);
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,13 @@
|
||||||
import { text } from './utils/fetch.mjs';
|
import { text } from './utils/fetch.mjs';
|
||||||
import Setting from './utils/setting.mjs';
|
import Setting from './utils/setting.mjs';
|
||||||
import { registerHiddenSetting } from './share.mjs';
|
import { registerHiddenSetting } from './share.mjs';
|
||||||
import { withBasePath } from './utils/base-path.mjs';
|
|
||||||
|
|
||||||
let playlist;
|
let playlist;
|
||||||
let currentTrack = 0;
|
let currentTrack = 0;
|
||||||
let player;
|
let player;
|
||||||
let alertTonePlayer;
|
|
||||||
let sliderTimeout = null;
|
let sliderTimeout = null;
|
||||||
let volumeSlider = null;
|
let volumeSlider = null;
|
||||||
let volumeSliderInput = null;
|
let volumeSliderInput = null;
|
||||||
let alertToneActive = false;
|
|
||||||
let alertTonePending = false;
|
|
||||||
let resumeMediaAfterAlertTone = false;
|
|
||||||
let audioUnlocked = false;
|
|
||||||
let alertToneTimeout = null;
|
|
||||||
|
|
||||||
const ALERT_TONE_DURATION_MS = 30_000;
|
|
||||||
|
|
||||||
const mediaPlaying = new Setting('mediaPlaying', {
|
const mediaPlaying = new Setting('mediaPlaying', {
|
||||||
name: 'Media Playing',
|
name: 'Media Playing',
|
||||||
|
|
@ -42,26 +33,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
|
||||||
// get the playlist
|
// get the playlist
|
||||||
getMedia();
|
getMedia();
|
||||||
registerAudioUnlockHandlers();
|
|
||||||
|
|
||||||
// register the volume setting
|
// register the volume setting
|
||||||
registerHiddenSetting(mediaVolume.elemId, mediaVolume);
|
registerHiddenSetting(mediaVolume.elemId, mediaVolume);
|
||||||
});
|
});
|
||||||
|
|
||||||
const unlockAudio = () => {
|
|
||||||
if (audioUnlocked) return;
|
|
||||||
audioUnlocked = true;
|
|
||||||
if (alertToneActive && alertTonePending) {
|
|
||||||
startAlertTone();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const registerAudioUnlockHandlers = () => {
|
|
||||||
['pointerdown', 'keydown', 'touchstart'].forEach((eventName) => {
|
|
||||||
document.addEventListener(eventName, unlockAudio, { passive: true, once: true });
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const scanMusicDirectory = async () => {
|
const scanMusicDirectory = async () => {
|
||||||
const parseDirectory = async (path, prefix = '') => {
|
const parseDirectory = async (path, prefix = '') => {
|
||||||
const listing = await text(path);
|
const listing = await text(path);
|
||||||
|
|
@ -194,7 +170,6 @@ const startMedia = async () => {
|
||||||
if (!player) {
|
if (!player) {
|
||||||
initializePlayer();
|
initializePlayer();
|
||||||
} else {
|
} else {
|
||||||
if (alertToneActive) return;
|
|
||||||
try {
|
try {
|
||||||
await player.play();
|
await player.play();
|
||||||
setTrackName(playlist.availableFiles[currentTrack]);
|
setTrackName(playlist.availableFiles[currentTrack]);
|
||||||
|
|
@ -300,73 +275,6 @@ const initializePlayer = () => {
|
||||||
volumeSliderInput.value = Math.round(mediaVolume.value * 100);
|
volumeSliderInput.value = Math.round(mediaVolume.value * 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initializeAlertTonePlayer = () => {
|
|
||||||
if (alertTonePlayer) return;
|
|
||||||
alertTonePlayer = new Audio(withBasePath('alert/tone.mp3'));
|
|
||||||
alertTonePlayer.type = 'audio/mpeg';
|
|
||||||
alertTonePlayer.preload = 'auto';
|
|
||||||
alertTonePlayer.addEventListener('ended', () => {
|
|
||||||
if (alertToneActive) {
|
|
||||||
alertTonePlayer.currentTime = 0;
|
|
||||||
alertTonePlayer.play().catch((e) => {
|
|
||||||
console.error('Couldn\'t continue alert tone');
|
|
||||||
console.error(e);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const startAlertTone = async () => {
|
|
||||||
if (!audioUnlocked) {
|
|
||||||
alertTonePending = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initializeAlertTonePlayer();
|
|
||||||
try {
|
|
||||||
await alertTonePlayer.play();
|
|
||||||
alertTonePending = false;
|
|
||||||
resumeMediaAfterAlertTone = mediaPlaying.value === true;
|
|
||||||
if (alertToneTimeout) clearTimeout(alertToneTimeout);
|
|
||||||
alertToneTimeout = setTimeout(() => {
|
|
||||||
if (alertToneActive) {
|
|
||||||
setAlertToneActive(false);
|
|
||||||
}
|
|
||||||
}, ALERT_TONE_DURATION_MS);
|
|
||||||
if (player && !player.paused) {
|
|
||||||
player.pause();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Couldn\'t play alert tone');
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopAlertTone = () => {
|
|
||||||
alertTonePending = false;
|
|
||||||
if (alertToneTimeout) {
|
|
||||||
clearTimeout(alertToneTimeout);
|
|
||||||
alertToneTimeout = null;
|
|
||||||
}
|
|
||||||
if (alertTonePlayer) {
|
|
||||||
alertTonePlayer.pause();
|
|
||||||
alertTonePlayer.currentTime = 0;
|
|
||||||
}
|
|
||||||
if (resumeMediaAfterAlertTone && mediaPlaying.value === true) {
|
|
||||||
startMedia();
|
|
||||||
}
|
|
||||||
resumeMediaAfterAlertTone = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const setAlertToneActive = (active) => {
|
|
||||||
if (active === alertToneActive) return;
|
|
||||||
alertToneActive = active;
|
|
||||||
if (alertToneActive) {
|
|
||||||
startAlertTone();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
stopAlertTone();
|
|
||||||
};
|
|
||||||
|
|
||||||
const playerCanPlay = async () => {
|
const playerCanPlay = async () => {
|
||||||
// check to make sure they user still wants music (protect against slow loading music)
|
// check to make sure they user still wants music (protect against slow loading music)
|
||||||
if (!mediaPlaying.value) return;
|
if (!mediaPlaying.value) return;
|
||||||
|
|
@ -396,6 +304,6 @@ const setTrackName = (fileName) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
handleClick,
|
handleClick,
|
||||||
setAlertToneActive,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
import { DateTime } from '../../vendor/auto/luxon.mjs';
|
|
||||||
|
|
||||||
const LOOKAHEAD_HOURS = 6;
|
|
||||||
const METERS_PER_MILE = 1609.344;
|
|
||||||
const KPH_PER_MPH = 1.609344;
|
|
||||||
|
|
||||||
const SEVERITY_RANK = {
|
|
||||||
Extreme: 2,
|
|
||||||
Severe: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const RULE_PRIORITY = {
|
|
||||||
thunderstorm: 5,
|
|
||||||
freezing: 4,
|
|
||||||
snow: 3,
|
|
||||||
rain: 2,
|
|
||||||
wind: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const WEATHER_CODES = {
|
|
||||||
freezing: new Set([56, 57, 66, 67]),
|
|
||||||
snow: new Set([71, 73, 75, 77, 85, 86]),
|
|
||||||
thunderstorm: new Set([95, 96, 99]),
|
|
||||||
rain: new Set([51, 53, 55, 61, 63, 65, 80, 81, 82]),
|
|
||||||
};
|
|
||||||
|
|
||||||
const thresholds = {
|
|
||||||
lowVisibilitySevere: 5 * METERS_PER_MILE,
|
|
||||||
lowVisibilityExtreme: 2 * METERS_PER_MILE,
|
|
||||||
gustSevere: 20 * KPH_PER_MPH,
|
|
||||||
gustExtreme: 35 * KPH_PER_MPH,
|
|
||||||
highWindSevere: 40 * KPH_PER_MPH,
|
|
||||||
highWindExtreme: 55 * KPH_PER_MPH,
|
|
||||||
freezingTempC: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const buildDerivedHazard = ({ id, severity, description, priority }) => ({
|
|
||||||
id,
|
|
||||||
priority,
|
|
||||||
properties: {
|
|
||||||
event: 'Severe Weather Alert',
|
|
||||||
severity,
|
|
||||||
urgency: 'Expected',
|
|
||||||
description: `This is a derived local alert based on forecast conditions. ${description}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const getUpcomingHours = (weatherParameters) => {
|
|
||||||
const zone = weatherParameters?.timeZone || 'UTC';
|
|
||||||
const now = DateTime.now().setZone(zone);
|
|
||||||
const end = now.plus({ hours: LOOKAHEAD_HOURS });
|
|
||||||
const allHours = Object.values(weatherParameters?.forecast ?? {})
|
|
||||||
.flatMap((day) => day?.hours ?? [])
|
|
||||||
.map((hour) => ({
|
|
||||||
...hour,
|
|
||||||
forecastTime: DateTime.fromISO(hour.time, { zone }),
|
|
||||||
}))
|
|
||||||
.filter((hour) => hour.forecastTime.isValid)
|
|
||||||
.sort((a, b) => a.forecastTime.toMillis() - b.forecastTime.toMillis());
|
|
||||||
|
|
||||||
return allHours.filter((hour) => hour.forecastTime >= now && hour.forecastTime <= end);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getWorstHour = (hours, evaluator) => hours.reduce((worst, hour) => {
|
|
||||||
const candidate = evaluator(hour);
|
|
||||||
if (!candidate) return worst;
|
|
||||||
if (!worst) return candidate;
|
|
||||||
if (SEVERITY_RANK[candidate.severity] > SEVERITY_RANK[worst.severity]) return candidate;
|
|
||||||
return worst;
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
const evaluateThunderstorm = (hours) => getWorstHour(hours, (hour) => {
|
|
||||||
const code = Number(hour.weather_code ?? 0);
|
|
||||||
if (!WEATHER_CODES.thunderstorm.has(code)) return null;
|
|
||||||
if (code === 96 || code === 99) {
|
|
||||||
return {
|
|
||||||
severity: 'Extreme',
|
|
||||||
description: 'Thunderstorms with hail are possible in the next several hours and may create dangerous outdoor conditions.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
severity: 'Severe',
|
|
||||||
description: 'Thunderstorms are possible in the next several hours and may create hazardous outdoor conditions.',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const evaluateFreezing = (hours) => getWorstHour(hours, (hour) => {
|
|
||||||
const code = Number(hour.weather_code ?? 0);
|
|
||||||
if (!WEATHER_CODES.freezing.has(code)) return null;
|
|
||||||
const temperature = hour.temperature_2m ?? Number.POSITIVE_INFINITY;
|
|
||||||
if (temperature > thresholds.freezingTempC) return null;
|
|
||||||
const visibility = hour.visibility ?? Number.POSITIVE_INFINITY;
|
|
||||||
const gusts = hour.wind_gusts_10m ?? 0;
|
|
||||||
const isExtreme = visibility <= thresholds.lowVisibilityExtreme || gusts >= thresholds.gustExtreme;
|
|
||||||
return {
|
|
||||||
severity: isExtreme ? 'Extreme' : 'Severe',
|
|
||||||
description: isExtreme
|
|
||||||
? 'Freezing precipitation with poor visibility or strong gusts is expected in the next several hours and may create dangerous travel conditions.'
|
|
||||||
: 'Freezing precipitation is expected in the next several hours and may create slippery travel conditions.',
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const evaluateSnow = (hours) => getWorstHour(hours, (hour) => {
|
|
||||||
const code = Number(hour.weather_code ?? 0);
|
|
||||||
if (!WEATHER_CODES.snow.has(code)) return null;
|
|
||||||
const visibility = hour.visibility ?? Number.POSITIVE_INFINITY;
|
|
||||||
const gusts = hour.wind_gusts_10m ?? 0;
|
|
||||||
if (visibility <= thresholds.lowVisibilitySevere && gusts >= thresholds.gustSevere) {
|
|
||||||
return {
|
|
||||||
severity: visibility <= thresholds.lowVisibilityExtreme ? 'Extreme' : 'Severe',
|
|
||||||
description: visibility <= thresholds.lowVisibilityExtreme
|
|
||||||
? 'Snow, poor visibility, and gusty winds are expected in the next several hours and may create dangerous travel conditions.'
|
|
||||||
: 'Snow, reduced visibility, and gusty winds are expected in the next several hours and may create hazardous travel conditions.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const evaluateRain = (hours) => getWorstHour(hours, (hour) => {
|
|
||||||
const code = Number(hour.weather_code ?? 0);
|
|
||||||
const visibility = hour.visibility ?? Number.POSITIVE_INFINITY;
|
|
||||||
const gusts = hour.wind_gusts_10m ?? 0;
|
|
||||||
const hasRain = WEATHER_CODES.rain.has(code) || (hour.rain ?? 0) > 0 || (hour.showers ?? 0) > 0;
|
|
||||||
if (!hasRain) return null;
|
|
||||||
if (visibility <= thresholds.lowVisibilityExtreme && gusts >= thresholds.gustExtreme) {
|
|
||||||
return {
|
|
||||||
severity: 'Extreme',
|
|
||||||
description: 'Heavy rain, very low visibility, and strong gusts are expected in the next several hours and may create dangerous travel conditions.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (visibility <= thresholds.lowVisibilitySevere && gusts >= thresholds.gustSevere) {
|
|
||||||
return {
|
|
||||||
severity: 'Severe',
|
|
||||||
description: 'Heavy rain, reduced visibility, and gusty winds are expected in the next several hours and may create hazardous travel conditions.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const evaluateWind = (hours) => getWorstHour(hours, (hour) => {
|
|
||||||
const gusts = hour.wind_gusts_10m ?? 0;
|
|
||||||
if (gusts >= thresholds.highWindExtreme) {
|
|
||||||
return {
|
|
||||||
severity: 'Extreme',
|
|
||||||
description: 'Very strong wind gusts are expected in the next several hours and may create dangerous conditions for travel and outdoor activity.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (gusts >= thresholds.highWindSevere) {
|
|
||||||
return {
|
|
||||||
severity: 'Severe',
|
|
||||||
description: 'Strong wind gusts are expected in the next several hours and may create hazardous conditions for travel and outdoor activity.',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
|
|
||||||
const deriveHazards = (weatherParameters) => {
|
|
||||||
const upcomingHours = getUpcomingHours(weatherParameters);
|
|
||||||
if (upcomingHours.length === 0) return [];
|
|
||||||
const thunderstormCandidate = evaluateThunderstorm(upcomingHours);
|
|
||||||
const freezingCandidate = evaluateFreezing(upcomingHours);
|
|
||||||
const snowCandidate = evaluateSnow(upcomingHours);
|
|
||||||
const rainCandidate = evaluateRain(upcomingHours);
|
|
||||||
const windCandidate = evaluateWind(upcomingHours);
|
|
||||||
|
|
||||||
const candidates = [
|
|
||||||
thunderstormCandidate && buildDerivedHazard({
|
|
||||||
id: 'derived-severe-weather-alert-thunderstorm',
|
|
||||||
priority: RULE_PRIORITY.thunderstorm,
|
|
||||||
...thunderstormCandidate,
|
|
||||||
}),
|
|
||||||
freezingCandidate && buildDerivedHazard({
|
|
||||||
id: 'derived-severe-weather-alert-freezing',
|
|
||||||
priority: RULE_PRIORITY.freezing,
|
|
||||||
...freezingCandidate,
|
|
||||||
}),
|
|
||||||
snowCandidate && buildDerivedHazard({
|
|
||||||
id: 'derived-severe-weather-alert-snow',
|
|
||||||
priority: RULE_PRIORITY.snow,
|
|
||||||
...snowCandidate,
|
|
||||||
}),
|
|
||||||
rainCandidate && buildDerivedHazard({
|
|
||||||
id: 'derived-severe-weather-alert-rain',
|
|
||||||
priority: RULE_PRIORITY.rain,
|
|
||||||
...rainCandidate,
|
|
||||||
}),
|
|
||||||
windCandidate && buildDerivedHazard({
|
|
||||||
id: 'derived-severe-weather-alert-wind',
|
|
||||||
priority: RULE_PRIORITY.wind,
|
|
||||||
...windCandidate,
|
|
||||||
}),
|
|
||||||
].filter(Boolean);
|
|
||||||
|
|
||||||
if (candidates.length === 0) return [];
|
|
||||||
|
|
||||||
candidates.sort((a, b) => {
|
|
||||||
const severityDiff = SEVERITY_RANK[b.properties.severity] - SEVERITY_RANK[a.properties.severity];
|
|
||||||
if (severityDiff !== 0) return severityDiff;
|
|
||||||
return b.priority - a.priority;
|
|
||||||
});
|
|
||||||
|
|
||||||
return [candidates[0]];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default deriveHazards;
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue