2020-09-04 13:02:20 -05:00
// current weather conditions display
2022-11-22 16:19:10 -06:00
import STATUS from './status.mjs' ;
2025-05-29 08:30:01 -05:00
import { preloadImg } from './utils/image.mjs' ;
2025-06-24 23:05:51 -04:00
import { safeJson } from './utils/fetch.mjs' ;
2022-11-22 16:19:10 -06:00
import { directionToNSEW } from './utils/calc.mjs' ;
import { locationCleanup } from './utils/string.mjs' ;
2025-05-14 15:03:35 -05:00
import { getLargeIcon } from './icons.mjs' ;
2022-11-22 16:29:10 -06:00
import WeatherDisplay from './weatherdisplay.mjs' ;
2022-12-06 16:14:56 -06:00
import { registerDisplay } from './navigation.mjs' ;
2025-06-24 23:05:51 -04:00
import augmentObservationWithMetar from './utils/metar.mjs' ;
2022-12-06 16:25:28 -06:00
import {
2025-02-23 23:29:39 -06:00
temperature , windSpeed , pressure , distanceMeters , distanceKilometers ,
2022-12-06 16:25:28 -06:00
} from './utils/units.mjs' ;
2025-06-24 23:05:51 -04:00
import { debugFlag } from './utils/debug.mjs' ;
2025-06-24 12:38:14 -04:00
import { isDataStale , enhanceObservationWithMapClick } from './utils/mapclick.mjs' ;
2025-11-10 04:06:24 +00:00
import { DateTime } from '../vendor/auto/luxon.mjs' ;
2020-09-04 13:02:20 -05:00
2022-12-13 16:31:18 -06:00
// some stations prefixed do not provide all the necessary data
const skipStations = [ 'U' , 'C' , 'H' , 'W' , 'Y' , 'T' , 'S' , 'M' , 'O' , 'L' , 'A' , 'F' , 'B' , 'N' , 'V' , 'R' , 'D' , 'E' , 'I' , 'G' , 'J' ] ;
2020-09-04 13:02:20 -05:00
class CurrentWeather extends WeatherDisplay {
2020-10-29 16:44:28 -05:00
constructor ( navId , elemId ) {
2022-11-21 21:50:22 -06:00
super ( navId , elemId , 'Current Conditions' , true ) ;
2020-09-04 13:02:20 -05:00
}
2025-04-02 20:52:33 -05:00
async getData ( weatherParameters , refresh ) {
2022-12-12 13:53:33 -06:00
// always load the data for use in the lower scroll
2025-04-02 20:52:33 -05:00
const superResult = super . getData ( weatherParameters , refresh ) ;
2025-04-02 20:58:53 -05:00
// note: current weather does not use old data on a silent refresh
// this is deliberate because it can pull data from more than one station in sequence
2020-09-25 09:55:29 -05:00
2022-12-13 16:31:18 -06:00
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
2025-04-02 20:52:33 -05:00
const filteredStations = this . weatherParameters . stations . filter ( ( station ) => station ? . properties ? . stationIdentifier ? . length === 4 && ! skipStations . includes ( station . properties . stationIdentifier . slice ( 0 , 1 ) ) ) ;
2022-12-13 16:31:18 -06:00
2020-09-04 13:02:20 -05:00
// Load the observations
2023-01-17 11:26:57 -06:00
let observations ;
let station ;
2020-10-16 15:16:46 -05:00
// station number counter
let stationNum = 0 ;
2022-12-13 16:31:18 -06:00
while ( ! observations && stationNum < filteredStations . length ) {
2020-10-16 15:16:46 -05:00
// get the station
2022-12-13 16:31:18 -06:00
station = filteredStations [ stationNum ] ;
2025-06-24 12:38:14 -04:00
const stationId = station . properties . stationIdentifier ;
2020-10-29 16:44:28 -05:00
stationNum += 1 ;
2025-06-24 23:05:51 -04:00
let candidateObservation ;
2020-10-16 15:16:46 -05:00
try {
2022-03-01 15:54:19 -06:00
// eslint-disable-next-line no-await-in-loop
2025-06-24 23:05:51 -04:00
candidateObservation = await safeJson ( ` ${ station . id } /observations ` , {
2020-10-16 15:16:46 -05:00
data : {
2025-11-10 04:06:24 +00:00
limit : 5 , // we need the two most recent observations to calculate pressure direction, and to back fill any missing data
2020-10-16 15:16:46 -05:00
} ,
2022-12-12 13:53:33 -06:00
retryCount : 3 ,
stillWaiting : ( ) => this . stillWaiting ( ) ,
2020-10-16 15:16:46 -05:00
} ) ;
2025-06-24 23:05:51 -04:00
} catch ( error ) {
2025-06-24 12:38:14 -04:00
console . error ( ` Unexpected error getting Current Conditions for station ${ stationId } : ${ error . message } (trying next station) ` ) ;
2025-06-24 23:05:51 -04:00
candidateObservation = undefined ;
}
2020-09-04 13:02:20 -05:00
2025-06-24 23:05:51 -04:00
// Check if request was successful and has data
if ( candidateObservation && candidateObservation . features ? . length > 0 ) {
// Attempt making observation data usable with METAR data
const originalData = { ... candidateObservation . features [ 0 ] . properties } ;
candidateObservation . features [ 0 ] . properties = augmentObservationWithMetar ( candidateObservation . features [ 0 ] . properties ) ;
const metarFields = [
{ name : 'temperature' , check : ( orig , metar ) => orig . temperature ? . value === null && metar . temperature ? . value !== null } ,
{ name : 'windSpeed' , check : ( orig , metar ) => orig . windSpeed ? . value === null && metar . windSpeed ? . value !== null } ,
{ name : 'windDirection' , check : ( orig , metar ) => orig . windDirection ? . value === null && metar . windDirection ? . value !== null } ,
{ name : 'windGust' , check : ( orig , metar ) => orig . windGust ? . value === null && metar . windGust ? . value !== null } ,
{ name : 'dewpoint' , check : ( orig , metar ) => orig . dewpoint ? . value === null && metar . dewpoint ? . value !== null } ,
{ name : 'barometricPressure' , check : ( orig , metar ) => orig . barometricPressure ? . value === null && metar . barometricPressure ? . value !== null } ,
{ name : 'relativeHumidity' , check : ( orig , metar ) => orig . relativeHumidity ? . value === null && metar . relativeHumidity ? . value !== null } ,
{ name : 'visibility' , check : ( orig , metar ) => orig . visibility ? . value === null && metar . visibility ? . value !== null } ,
{ name : 'ceiling' , check : ( orig , metar ) => orig . cloudLayers ? . [ 0 ] ? . base ? . value === null && metar . cloudLayers ? . [ 0 ] ? . base ? . value !== null } ,
] ;
const augmentedData = candidateObservation . features [ 0 ] . properties ;
const metarReplacements = metarFields . filter ( ( field ) => field . check ( originalData , augmentedData ) ) . map ( ( field ) => field . name ) ;
if ( debugFlag ( 'currentweather' ) && metarReplacements . length > 0 ) {
2025-06-24 12:38:14 -04:00
console . log ( ` Current Conditions for station ${ stationId } were augmented with METAR data for ${ metarReplacements . join ( ', ' ) } ` ) ;
2025-06-24 23:05:51 -04:00
}
// test data quality - check required fields and allow one optional field to be missing
const requiredFields = [
{ name : 'temperature' , check : ( props ) => props . temperature ? . value === null , required : true } ,
{ name : 'textDescription' , check : ( props ) => props . textDescription === null || props . textDescription === '' , required : true } ,
{ name : 'windSpeed' , check : ( props ) => props . windSpeed ? . value === null , required : false } ,
{ name : 'dewpoint' , check : ( props ) => props . dewpoint ? . value === null , required : false } ,
{ name : 'barometricPressure' , check : ( props ) => props . barometricPressure ? . value === null , required : false } ,
{ name : 'visibility' , check : ( props ) => props . visibility ? . value === null , required : false } ,
{ name : 'relativeHumidity' , check : ( props ) => props . relativeHumidity ? . value === null , required : false } ,
{ name : 'ceiling' , check : ( props ) => props . cloudLayers ? . [ 0 ] ? . base ? . value === null , required : false } ,
] ;
2025-06-24 12:38:14 -04:00
// Use enhanced observation with MapClick fallback
// eslint-disable-next-line no-await-in-loop
const enhancedResult = await enhanceObservationWithMapClick ( augmentedData , {
requiredFields ,
maxOptionalMissing : 1 , // Allow one optional field to be missing
stationId ,
stillWaiting : ( ) => this . stillWaiting ( ) ,
debugContext : 'currentweather' ,
} ) ;
candidateObservation . features [ 0 ] . properties = enhancedResult . data ;
const { missingFields } = enhancedResult ;
const missingRequired = missingFields . filter ( ( fieldName ) => {
const field = requiredFields . find ( ( f ) => f . name === fieldName && f . required ) ;
return ! ! field ;
} ) ;
const missingOptional = missingFields . filter ( ( fieldName ) => {
const field = requiredFields . find ( ( f ) => f . name === fieldName && ! f . required ) ;
return ! ! field ;
} ) ;
2025-06-24 23:05:51 -04:00
const missingOptionalCount = missingOptional . length ;
2025-06-24 12:38:14 -04:00
// Check final data quality
2025-06-24 23:05:51 -04:00
// Allow one optional field to be missing
if ( missingRequired . length === 0 && missingOptionalCount <= 1 ) {
// Station data is good, use it
observations = candidateObservation ;
if ( debugFlag ( 'currentweather' ) && missingOptional . length > 0 ) {
2025-06-24 12:38:14 -04:00
console . log ( ` Data for station ${ stationId } is missing optional fields: ${ missingOptional . join ( ', ' ) } (acceptable) ` ) ;
2025-06-24 23:05:51 -04:00
}
} else {
const allMissing = [ ... missingRequired , ... missingOptional ] ;
if ( debugFlag ( 'currentweather' ) ) {
2025-06-24 12:38:14 -04:00
console . log ( ` Data for station ${ stationId } is missing fields: ${ allMissing . join ( ', ' ) } ( ${ missingRequired . length } required, ${ missingOptionalCount } optional) (trying next station) ` ) ;
2025-06-24 23:05:51 -04:00
}
}
} else if ( debugFlag ( 'verbose-failures' ) ) {
if ( ! candidateObservation ) {
2025-06-24 12:38:14 -04:00
console . log ( ` Current Conditions for station ${ stationId } failed, trying next station ` ) ;
2025-06-24 23:05:51 -04:00
} else {
2025-06-24 12:38:14 -04:00
console . log ( ` No features returned for station ${ stationId } , trying next station ` ) ;
2020-10-16 15:16:46 -05:00
}
}
}
// test for data received
if ( ! observations ) {
2025-06-24 23:05:51 -04:00
console . error ( 'Current Conditions failure: all nearby weather stations exhausted!' ) ;
2022-12-14 13:08:49 -06:00
if ( this . isEnabled ) this . setStatus ( STATUS . failed ) ;
2022-12-08 14:41:15 -06:00
// send failed to subscribers
this . getDataCallback ( undefined ) ;
2020-09-04 13:02:20 -05:00
return ;
}
2020-09-23 11:49:15 -05:00
2020-09-04 13:02:20 -05:00
// we only get here if there was no error above
2023-01-17 11:26:57 -06:00
this . data = parseData ( { ... observations , station } ) ;
2020-10-20 20:04:51 -05:00
this . getDataCallback ( ) ;
2022-12-12 13:53:33 -06:00
// stop here if we're disabled
if ( ! superResult ) return ;
2025-06-24 12:38:14 -04:00
// Data is available, ensure we're enabled for display
this . timing . totalScreens = 1 ;
// Check final data age
const { isStale , ageInMinutes } = isDataStale ( observations . features [ 0 ] . properties . timestamp , 80 ) ; // hourly observation + 20 minute propagation delay
this . isStaleData = isStale ;
if ( isStale && debugFlag ( 'currentweather' ) ) {
console . warn ( ` Current Conditions: Data is ${ ageInMinutes . toFixed ( 0 ) } minutes old (from ${ new Date ( observations . features [ 0 ] . properties . timestamp ) . toISOString ( ) } ) ` ) ;
}
// preload the icon if available
if ( observations . features [ 0 ] . properties . icon ) {
const iconResult = getLargeIcon ( observations . features [ 0 ] . properties . icon ) ;
if ( iconResult ) {
preloadImg ( iconResult ) ;
}
}
2022-12-12 13:53:33 -06:00
this . setStatus ( STATUS . loaded ) ;
2020-09-04 13:02:20 -05:00
}
2020-10-29 16:44:28 -05:00
async drawCanvas ( ) {
2020-09-24 22:44:51 -05:00
super . drawCanvas ( ) ;
2020-09-04 13:02:20 -05:00
2025-06-24 12:38:14 -04:00
// Update header text based on data staleness
const headerTop = this . elem . querySelector ( '.header .title .top' ) ;
if ( headerTop ) {
headerTop . textContent = this . isStaleData ? 'Recent' : 'Current' ;
}
2023-01-17 11:26:57 -06:00
let condition = this . data . observations . textDescription ;
if ( condition . length > 15 ) {
condition = shortConditions ( condition ) ;
2020-09-04 13:02:20 -05:00
}
2022-08-02 21:39:27 -05:00
2025-06-27 22:29:56 -05:00
const wind = ( typeof this . data . WindSpeed === 'number' ) ? this . data . WindDirection . padEnd ( 3 , '' ) + this . data . WindSpeed . toString ( ) . padStart ( 3 , ' ' ) : this . data . WindSpeed ;
2025-06-27 15:35:15 -05:00
2025-07-15 22:00:33 -05:00
// get location (city name) from StationInfo if available (allows for overrides)
const location = ( StationInfo [ this . data . station . properties . stationIdentifier ] ? . city ? ? locationCleanup ( this . data . station . properties . name ) ) . substr ( 0 , 20 ) ;
2023-01-17 11:26:57 -06:00
const fill = {
temp : this . data . Temperature + String . fromCharCode ( 176 ) ,
condition ,
2025-06-27 15:35:15 -05:00
wind ,
2025-07-15 22:00:33 -05:00
location ,
2023-01-17 11:26:57 -06:00
humidity : ` ${ this . data . Humidity } % ` ,
dewpoint : this . data . DewPoint + String . fromCharCode ( 176 ) ,
ceiling : ( this . data . Ceiling === 0 ? 'Unlimited' : this . data . Ceiling + this . data . CeilingUnit ) ,
visibility : this . data . Visibility + this . data . VisibilityUnit ,
pressure : ` ${ this . data . Pressure } ${ this . data . PressureDirection } ` ,
icon : { type : 'img' , src : this . data . Icon } ,
} ;
2025-06-19 22:50:09 -05:00
if ( this . data . WindGust !== '-' ) fill [ 'wind-gusts' ] = ` Gusts to ${ this . data . WindGust } ` ;
2023-01-17 11:26:57 -06:00
if ( this . data . observations . heatIndex . value && this . data . HeatIndex !== this . data . Temperature ) {
2022-08-02 21:39:27 -05:00
fill [ 'heat-index-label' ] = 'Heat Index:' ;
2023-01-17 11:26:57 -06:00
fill [ 'heat-index' ] = this . data . HeatIndex + String . fromCharCode ( 176 ) ;
} else if ( this . data . observations . windChill . value && this . data . WindChill !== '' && this . data . WindChill < this . data . Temperature ) {
2022-08-02 21:39:27 -05:00
fill [ 'heat-index-label' ] = 'Wind Chill:' ;
2023-01-17 11:26:57 -06:00
fill [ 'heat-index' ] = this . data . WindChill + String . fromCharCode ( 176 ) ;
2020-09-04 13:02:20 -05:00
}
2020-09-04 13:38:58 -05:00
2022-08-02 21:39:27 -05:00
const area = this . elem . querySelector ( '.main' ) ;
area . innerHTML = '' ;
area . append ( this . fillTemplate ( 'weather' , fill ) ) ;
2020-09-04 13:38:58 -05:00
2020-09-04 13:02:20 -05:00
this . finishDraw ( ) ;
}
2020-10-20 20:04:51 -05:00
// make data available outside this class
// promise allows for data to be requested before it is available
2022-12-12 13:53:33 -06:00
async getCurrentWeather ( stillWaiting ) {
2025-05-02 23:22:00 -05:00
// an external caller has requested data, set up auto reload
this . setAutoReload ( ) ;
2022-12-12 13:53:33 -06:00
if ( stillWaiting ) this . stillWaitingCallbacks . push ( stillWaiting ) ;
2020-10-20 20:04:51 -05:00
return new Promise ( ( resolve ) => {
2025-11-05 05:24:04 +00:00
if ( this . data ) resolve ( { data : this . data , parameters : this . weatherParameters } ) ;
2020-10-20 20:04:51 -05:00
// data not available, put it into the data callback queue
2023-01-17 11:26:57 -06:00
this . getDataCallbacks . push ( ( ) => resolve ( this . data ) ) ;
2020-10-20 20:04:51 -05:00
} ) ;
2020-09-24 22:44:51 -05:00
}
2020-10-29 16:44:28 -05:00
}
2022-12-09 13:51:51 -06:00
const shortConditions = ( _condition ) => {
let condition = _condition ;
condition = condition . replace ( /Light/g , 'L' ) ;
condition = condition . replace ( /Heavy/g , 'H' ) ;
condition = condition . replace ( /Partly/g , 'P' ) ;
condition = condition . replace ( /Mostly/g , 'M' ) ;
condition = condition . replace ( /Few/g , 'F' ) ;
condition = condition . replace ( /Thunderstorm/g , 'T\'storm' ) ;
condition = condition . replace ( / in /g , '' ) ;
condition = condition . replace ( /Vicinity/g , '' ) ;
condition = condition . replace ( / and /g , ' ' ) ;
condition = condition . replace ( /Freezing Rain/g , 'Frz Rn' ) ;
condition = condition . replace ( /Freezing/g , 'Frz' ) ;
condition = condition . replace ( /Unknown Precip/g , '' ) ;
condition = condition . replace ( /L Snow Fog/g , 'L Snw/Fog' ) ;
condition = condition . replace ( / with /g , '/' ) ;
return condition ;
} ;
2023-01-17 11:26:57 -06:00
// format the received data
const parseData = ( data ) => {
2025-02-23 23:29:39 -06:00
// get the unit converter
const windConverter = windSpeed ( ) ;
const temperatureConverter = temperature ( ) ;
const metersConverter = distanceMeters ( ) ;
const kilometersConverter = distanceKilometers ( ) ;
const pressureConverter = pressure ( ) ;
2025-11-10 04:06:24 +00:00
const observations = backfill ( data . features ) ;
2023-01-17 11:26:57 -06:00
// values from api are provided in metric
data . observations = observations ;
2025-02-23 23:29:39 -06:00
data . Temperature = temperatureConverter ( observations . temperature . value ) ;
data . TemperatureUnit = temperatureConverter . units ;
data . DewPoint = temperatureConverter ( observations . dewpoint . value ) ;
data . Ceiling = metersConverter ( observations . cloudLayers [ 0 ] ? . base ? . value ? ? 0 ) ;
data . CeilingUnit = metersConverter . units ;
data . Visibility = kilometersConverter ( observations . visibility . value ) ;
data . VisibilityUnit = kilometersConverter . units ;
data . Pressure = pressureConverter ( observations . barometricPressure . value ) ;
data . PressureUnit = pressureConverter . units ;
data . HeatIndex = temperatureConverter ( observations . heatIndex . value ) ;
data . WindChill = temperatureConverter ( observations . windChill . value ) ;
data . WindSpeed = windConverter ( observations . windSpeed . value ) ;
2023-01-17 11:26:57 -06:00
data . WindDirection = directionToNSEW ( observations . windDirection . value ) ;
2025-02-23 23:29:39 -06:00
data . WindGust = windConverter ( observations . windGust . value ) ;
data . WindUnit = windConverter . units ;
2023-01-17 11:26:57 -06:00
data . Humidity = Math . round ( observations . relativeHumidity . value ) ;
2025-06-24 12:38:14 -04:00
// Get the large icon, but provide a fallback if it returns false
const iconResult = getLargeIcon ( observations . icon ) ;
data . Icon = iconResult || observations . icon ; // Use original icon if getLargeIcon returns false
2023-01-17 11:26:57 -06:00
data . PressureDirection = '' ;
data . TextConditions = observations . textDescription ;
2025-06-27 22:29:56 -05:00
// set wind speed of 0 as calm
if ( data . WindSpeed === 0 ) data . WindSpeed = 'Calm' ;
2025-06-24 12:38:14 -04:00
// if two measurements are available, use the difference (in pascals) to determine pressure trend
if ( data . features . length > 1 && data . features [ 1 ] . properties . barometricPressure ? . value ) {
const pressureDiff = ( observations . barometricPressure . value - data . features [ 1 ] . properties . barometricPressure . value ) ;
if ( pressureDiff > 150 ) data . PressureDirection = 'R' ;
if ( pressureDiff < - 150 ) data . PressureDirection = 'F' ;
}
2023-01-17 11:26:57 -06:00
return data ;
} ;
2025-11-10 04:06:24 +00:00
// default to the latest data in the provided observations, but use older data if something is missing
const backfill = ( data ) => {
// make easy to use timestamps
const sortedData = data . map ( ( observation ) => {
observation . timestamp = DateTime . fromISO ( observation . properties . timestamp ) ;
return observation ;
} ) ;
// sort by timestamp with [0] being the earliest
sortedData . sort ( ( a , b ) => b . timestamp - a . timestamp ) ;
// create the result data
const result = { } ;
// backfill each property
Object . keys ( sortedData [ 0 ] . properties ) . forEach ( ( key ) => {
// qualify the key (must have value)
2026-02-16 20:40:13 -06:00
if ( Object . hasOwn ( sortedData [ 0 ] . properties ? . [ key ] ? ? { } , 'value' ) ) {
2025-11-10 04:06:24 +00:00
// backfill this property
result [ key ] = backfillProperty ( sortedData , key ) ;
} else {
// use the property as is
result [ key ] = sortedData [ 0 ] . properties [ key ] ;
}
} ) ;
return result ;
} ;
// return the property with a value closest to the [0] index
// reduce returns the first non-null value in the array
const backfillProperty = ( data , key ) => data . reduce (
( prev , cur ) => {
const curValue = cur . properties ? . [ key ] ? . value ;
if ( prev . value === null && curValue !== null && curValue !== undefined ) return cur . properties [ key ] ;
return prev ;
} ,
{ value : null } , // null is the default provided by the api
) ;
2022-12-14 16:28:33 -06:00
const display = new CurrentWeather ( 1 , 'current-weather' ) ;
2022-12-06 16:14:56 -06:00
registerDisplay ( display ) ;
export default display . getCurrentWeather . bind ( display ) ;