2020-09-04 13:02:20 -05:00
// display extended forecast graphically
2025-06-24 23:07:47 -04:00
// (technically this uses the same data as the local forecast, but we'll let the cache deal with that)
2020-09-04 13:02:20 -05:00
2022-11-22 16:19:10 -06:00
import STATUS from './status.mjs' ;
2025-06-24 23:07:47 -04:00
import { safeJson } from './utils/fetch.mjs' ;
2022-11-22 16:19:10 -06:00
import { DateTime } from '../vendor/auto/luxon.mjs' ;
2025-05-14 15:03:35 -05:00
import { getLargeIcon } from './icons.mjs' ;
2022-11-22 16:19:10 -06:00
import { preloadImg } from './utils/image.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-02-23 23:29:39 -06:00
import settings from './settings.mjs' ;
2025-06-24 23:07:47 -04:00
import filterExpiredPeriods from './utils/forecast-utils.mjs' ;
import { debugFlag } from './utils/debug.mjs' ;
2020-09-04 13:02:20 -05:00
class ExtendedForecast extends WeatherDisplay {
2020-10-29 16:44:28 -05:00
constructor ( navId , elemId ) {
2022-11-21 21:50:22 -06:00
super ( navId , elemId , 'Extended Forecast' , true ) ;
2020-09-04 13:02:20 -05:00
// set timings
this . timing . totalScreens = 2 ;
}
2025-04-02 20:52:33 -05:00
async getData ( weatherParameters , refresh ) {
if ( ! super . getData ( weatherParameters , refresh ) ) return ;
2020-09-04 13:02:20 -05:00
try {
2025-06-24 23:07:47 -04:00
// request us or si units using centralized safe handling
this . data = await safeJson ( this . weatherParameters . forecast , {
2020-09-04 13:02:20 -05:00
data : {
2025-02-23 23:29:39 -06:00
units : settings . units . value ,
2020-09-04 13:02:20 -05:00
} ,
2022-12-12 13:53:33 -06:00
retryCount : 3 ,
stillWaiting : ( ) => this . stillWaiting ( ) ,
2020-09-04 13:02:20 -05:00
} ) ;
2025-06-24 23:07:47 -04:00
// if there's no new data and no previous data, fail
2025-04-02 20:58:53 -05:00
if ( ! this . data ) {
2025-06-24 23:07:47 -04:00
// console.warn(`Unable to get extended forecast for ${this.weatherParameters.latitude},${this.weatherParameters.longitude} in ${this.weatherParameters.state}`);
if ( this . isEnabled ) this . setStatus ( STATUS . failed ) ;
2025-04-02 20:58:53 -05:00
return ;
}
2025-06-24 23:07:47 -04:00
// we only get here if there was data (new or existing)
this . screenIndex = 0 ;
this . setStatus ( STATUS . loaded ) ;
} catch ( error ) {
console . error ( ` Unexpected error getting Extended Forecast: ${ error . message } ` ) ;
if ( this . isEnabled ) this . setStatus ( STATUS . failed ) ;
2020-09-04 13:02:20 -05:00
}
}
async drawCanvas ( ) {
super . drawCanvas ( ) ;
// determine bounds
// grab the first three or second set of three array elements
2025-06-24 23:07:47 -04:00
const forecast = parse ( this . data . properties . periods , this . weatherParameters . forecast ) . slice ( 0 + 3 * this . screenIndex , 3 + this . screenIndex * 3 ) ;
2020-09-04 13:02:20 -05:00
2022-08-04 22:03:59 -05:00
// create each day template
const days = forecast . map ( ( Day ) => {
2023-01-06 14:39:39 -06:00
const fill = {
icon : { type : 'img' , src : Day . icon } ,
condition : Day . text ,
2023-01-17 14:13:51 -06:00
date : Day . dayName ,
2023-01-06 14:39:39 -06:00
} ;
2020-09-04 13:02:20 -05:00
2025-05-29 08:30:01 -05:00
const { low , high } = Day ;
2020-09-04 13:02:20 -05:00
if ( low !== undefined ) {
2022-08-04 22:03:59 -05:00
fill [ 'value-lo' ] = Math . round ( low ) ;
2020-09-04 13:02:20 -05:00
}
2022-08-04 22:03:59 -05:00
fill [ 'value-hi' ] = Math . round ( high ) ;
// return the filled template
return this . fillTemplate ( 'day' , fill ) ;
} ) ;
2020-09-04 13:02:20 -05:00
2022-08-04 22:03:59 -05:00
// empty and update the container
const dayContainer = this . elem . querySelector ( '.day-container' ) ;
dayContainer . innerHTML = '' ;
dayContainer . append ( ... days ) ;
2020-09-04 13:02:20 -05:00
this . finishDraw ( ) ;
}
2020-10-29 16:44:28 -05:00
}
2022-11-22 16:19:10 -06:00
2022-12-09 13:51:51 -06:00
// the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures
2025-06-24 23:07:47 -04:00
const parse = ( fullForecast , forecastUrl ) => {
// filter out expired periods first
const activePeriods = filterExpiredPeriods ( fullForecast , forecastUrl ) ;
if ( debugFlag ( 'extendedforecast' ) ) {
console . log ( 'ExtendedForecast: First few active periods:' ) ;
activePeriods . slice ( 0 , 4 ) . forEach ( ( period , index ) => {
console . log ( ` [ ${ index } ] ${ period . name } : ${ period . startTime } to ${ period . endTime } (isDaytime: ${ period . isDaytime } ) ` ) ;
} ) ;
}
// Skip the first period if it's nighttime (like "Tonight") since extended forecast
// should focus on upcoming full days, not the end of the current day
let startIndex = 0 ;
let dateOffset = 0 ; // offset for date labels when we skip periods
if ( activePeriods . length > 0 && ! activePeriods [ 0 ] . isDaytime ) {
startIndex = 1 ;
dateOffset = 1 ; // start date labels from tomorrow since we're skipping tonight
if ( debugFlag ( 'extendedforecast' ) ) {
console . log ( ` ExtendedForecast: Skipping first period " ${ activePeriods [ 0 ] . name } " because it's nighttime ` ) ;
}
} else if ( activePeriods . length > 0 ) {
if ( debugFlag ( 'extendedforecast' ) ) {
console . log ( ` ExtendedForecast: Starting with first period " ${ activePeriods [ 0 ] . name } " because it's daytime ` ) ;
}
}
2022-12-09 13:51:51 -06:00
2025-06-24 23:07:47 -04:00
// create a list of days starting with the appropriate day
const Days = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 ] ;
2022-12-09 13:51:51 -06:00
const dates = Days . map ( ( shift ) => {
2025-06-24 23:07:47 -04:00
const date = DateTime . local ( ) . startOf ( 'day' ) . plus ( { days : shift + dateOffset } ) ;
2022-12-09 13:51:51 -06:00
return date . toLocaleString ( { weekday : 'short' } ) ;
} ) ;
2025-06-24 23:07:47 -04:00
if ( debugFlag ( 'extendedforecast' ) ) {
console . log ( ` ExtendedForecast: Generated date labels: [ ${ dates . join ( ', ' ) } ] ` ) ;
}
2022-12-09 13:51:51 -06:00
// track the destination forecast index
let destIndex = 0 ;
const forecast = [ ] ;
2025-06-24 23:07:47 -04:00
for ( let i = startIndex ; i < activePeriods . length ; i += 1 ) {
const period = activePeriods [ i ] ;
2022-12-09 13:51:51 -06:00
// create the destination object if necessary
if ( ! forecast [ destIndex ] ) {
forecast . push ( {
dayName : '' , low : undefined , high : undefined , text : undefined , icon : undefined ,
} ) ;
}
// get the object to modify/populate
const fDay = forecast [ destIndex ] ;
// preload the icon
preloadImg ( fDay . icon ) ;
if ( period . isDaytime ) {
// day time is the high temperature
fDay . high = period . temperature ;
2025-08-11 22:35:03 -05:00
fDay . icon = getLargeIcon ( period . icon ) ;
fDay . text = shortenExtendedForecastText ( period . shortForecast ) ;
fDay . dayName = dates [ destIndex ] ;
2025-06-24 23:07:47 -04:00
// Wait for the corresponding night period to increment
2022-12-09 13:51:51 -06:00
} else {
// low temperature
fDay . low = period . temperature ;
2025-06-24 23:07:47 -04:00
// Increment after processing night period
destIndex += 1 ;
2022-12-09 13:51:51 -06:00
}
2025-06-24 23:07:47 -04:00
}
if ( debugFlag ( 'extendedforecast' ) ) {
console . log ( 'ExtendedForecast: Final forecast array:' ) ;
forecast . forEach ( ( day , index ) => {
console . log ( ` [ ${ index } ] ${ day . dayName } : High= ${ day . high } °, Low= ${ day . low } °, Text=" ${ day . text } " ` ) ;
} ) ;
}
2022-12-09 13:51:51 -06:00
return forecast ;
} ;
2025-05-29 08:30:01 -05:00
const regexList = [
[ / and /gi , ' ' ] ,
[ /slight /gi , '' ] ,
[ /chance /gi , '' ] ,
[ /very /gi , '' ] ,
[ /patchy /gi , '' ] ,
2025-05-30 09:06:40 -05:00
[ /Areas Of /gi , '' ] ,
2025-05-29 08:30:01 -05:00
[ /areas /gi , '' ] ,
[ /dense /gi , '' ] ,
[ /Thunderstorm/g , 'T\'Storm' ] ,
] ;
2022-12-09 13:51:51 -06:00
const shortenExtendedForecastText = ( long ) => {
2025-02-23 23:29:39 -06:00
// run all regexes
2022-12-09 13:51:51 -06:00
const short = regexList . reduce ( ( working , [ regex , replace ] ) => working . replace ( regex , replace ) , long ) ;
let conditions = short . split ( ' ' ) ;
if ( short . indexOf ( 'then' ) !== - 1 ) {
conditions = short . split ( ' then ' ) ;
conditions = conditions [ 1 ] . split ( ' ' ) ;
}
let short1 = conditions [ 0 ] . substr ( 0 , 10 ) ;
let short2 = '' ;
if ( conditions [ 1 ] ) {
2023-01-06 14:39:39 -06:00
if ( short1 . endsWith ( '.' ) ) {
2022-12-09 13:51:51 -06:00
short1 = short1 . replace ( /\./ , '' ) ;
2023-01-06 14:39:39 -06:00
} else {
short2 = conditions [ 1 ] . substr ( 0 , 10 ) ;
2022-12-09 13:51:51 -06:00
}
if ( short2 === 'Blowing' ) {
short2 = '' ;
}
}
let result = short1 ;
if ( short2 !== '' ) {
result += ` ${ short2 } ` ;
}
return result ;
} ;
2022-12-06 16:14:56 -06:00
// register display
2022-12-14 16:28:33 -06:00
registerDisplay ( new ExtendedForecast ( 8 , 'extended-forecast' ) ) ;