diff --git a/server/scripts/modules/icons/icons-large.mjs b/server/scripts/modules/icons/icons-large.mjs index 41ec0dd..029e267 100644 --- a/server/scripts/modules/icons/icons-large.mjs +++ b/server/scripts/modules/icons/icons-large.mjs @@ -1,27 +1,23 @@ /* spell-checker: disable */ -// internal function to add path to returned icon +import parseIconUrl from './icons-parse.mjs'; + const addPath = (icon) => `images/icons/current-conditions/${icon}`; const largeIcon = (link, _isNightTime) => { - if (!link) return false; + let conditionIcon; + let probability; + let isNightTime; - // extract day or night if not provided - const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0; - - // grab everything after the last slash ending at any of these: ?&, - const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0]; - let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1]; - // using probability as a crude heavy/light indication where possible - const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1]; - - // if a 'DualImage' is captured, adjust to just the j parameter - if (conditionName === 'dualimage') { - const match = link.match(/&j=(.*)&/); - [, conditionName] = match; + try { + ({ conditionIcon, probability, isNightTime } = parseIconUrl(link, _isNightTime)); + } catch (error) { + console.warn(`largeIcon: ${error.message}`); + // Return a fallback icon to prevent downstream errors + return addPath(_isNightTime ? 'Clear.gif' : 'Sunny.gif'); } // find the icon - switch (conditionName + (isNightTime ? '-n' : '')) { + switch (conditionIcon + (isNightTime ? '-n' : '')) { case 'skc': case 'hot': case 'haze': @@ -81,7 +77,7 @@ const largeIcon = (link, _isNightTime) => { case 'snow': case 'snow-n': - if (value > 50) return addPath('Heavy-Snow.gif'); + if (probability > 50) return addPath('Heavy-Snow.gif'); return addPath('Light-Snow.gif'); case 'rain_snow': @@ -132,9 +128,11 @@ const largeIcon = (link, _isNightTime) => { case 'blizzard-n': return addPath('Blowing-Snow.gif'); - default: - console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`); - return false; + default: { + console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`); + // Return a reasonable fallback instead of false to prevent downstream errors + return addPath(isNightTime ? 'Clear-Night.gif' : 'Sunny.gif'); + } } }; diff --git a/server/scripts/modules/icons/icons-parse.mjs b/server/scripts/modules/icons/icons-parse.mjs new file mode 100644 index 0000000..87b4a3e --- /dev/null +++ b/server/scripts/modules/icons/icons-parse.mjs @@ -0,0 +1,93 @@ +/** + * Parses weather.gov icon URLs and extracts weather condition information + * Handles both single and dual condition formats according to the weather.gov API spec + * + * NOTE: The 'icon' properties are marked as deprecated in the API documentation. This + * is because it will eventually be replaced with a more generic value that is not a URL. + */ + +import { debugFlag } from '../utils/debug.mjs'; + +/** + * Parses a weather.gov icon URL and extracts condition and timing information + * @param {string} iconUrl - Icon URL from weather.gov API (e.g., "/icons/land/day/skc?size=medium") + * @param {boolean} _isNightTime - Optional override for night time determination + * @returns {Object} Parsed icon data with conditionIcon, probability, and isNightTime + */ +const parseIconUrl = (iconUrl, _isNightTime) => { + if (!iconUrl) { + throw new Error('No icon URL provided'); + } + + // Parse icon URL according to API spec: /icons/{set}/{timeOfDay}/{condition}?{params} + // where {condition} might be single (skc) or dual (tsra_hi,20/rain,50) + // Each period will have an icon, or two if there is changing weather during that period + // see https://github.com/weather-gov/api/discussions/557#discussioncomment-9949521 + // (On the weather.gov site, changing conditions results in a "dualImage" forecast icon) + const iconUrlPattern = /\/icons\/(?\w+)\/(?day|night)\/(?[^?]+)(?:\?(?.*))?$/i; + const match = iconUrl.match(iconUrlPattern); + + if (!match?.groups) { + throw new Error(`Unable to parse icon URL format: ${iconUrl}`); + } + + const { timeOfDay, condition } = match.groups; + + // Determine if it's night time with preference strategy: + // 1. Primary: use _isNightTime parameter if provided (such as from API's isDaytime property) + // 2. Secondary: use timeOfDay parsed from URL + let isNightTime; + if (_isNightTime !== undefined) { + isNightTime = _isNightTime; + } else if (timeOfDay === 'day') { + isNightTime = false; + } else if (timeOfDay === 'night') { + isNightTime = true; + } else { + console.warn(`parseIconUrl: unexpected timeOfDay value: ${timeOfDay}`); + isNightTime = false; + } + + // Dual conditions can have a probability + // Examples: "tsra_hi,30/sct", "rain_showers,30/tsra_hi,50", "hot/tsra_hi,70" + let conditionIcon; + let probability; + if (condition.includes('/')) { // Two conditions + const conditions = condition.split('/'); + const firstCondition = conditions[0] || ''; + const secondCondition = conditions[1] || ''; + + const [firstIcon, firstProb] = firstCondition.split(','); + const [secondIcon, secondProb] = secondCondition.split(','); + + // Default to 100% probability if not specified (high confidence) + const firstProbability = parseInt(firstProb, 10) || 100; + const secondProbability = parseInt(secondProb, 10) || 100; + + if (secondIcon !== firstIcon) { + // When there's more than one condition, use the second condition + // QUESTION: should the condition with the higher probability determine which one to use? + // if (firstProbability >= secondProbability) { ... } + conditionIcon = secondIcon; + probability = secondProbability; + if (debugFlag('icons')) { + console.debug(`2️⃣ Using second condition: '${secondCondition}' instead of first '${firstCondition}'`); + } + } else { + conditionIcon = firstIcon; + probability = firstProbability; + } + } else { // Single condition + const [name, prob] = condition.split(','); + conditionIcon = name; + probability = parseInt(prob, 10) || 100; + } + + return { + conditionIcon, + probability, + isNightTime, + }; +}; + +export default parseIconUrl; diff --git a/server/scripts/modules/icons/icons-small.mjs b/server/scripts/modules/icons/icons-small.mjs index cc30d6f..d300adf 100644 --- a/server/scripts/modules/icons/icons-small.mjs +++ b/server/scripts/modules/icons/icons-small.mjs @@ -1,24 +1,22 @@ -// internal function to add path to returned icon +import parseIconUrl from './icons-parse.mjs'; + const addPath = (icon) => `images/icons/regional-maps/${icon}`; const smallIcon = (link, _isNightTime) => { - // extract day or night if not provided - const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0; + let conditionIcon; + let probability; + let isNightTime; - // grab everything after the last slash ending at any of these: ?&, - const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0]; - let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1]; - // using probability as a crude heavy/light indication where possible - const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1]; - - // if a 'DualImage' is captured, adjust to just the j parameter - if (conditionName === 'dualimage') { - const match = link.match(/&j=(.*)&/); - [, conditionName] = match; + try { + ({ conditionIcon, probability, isNightTime } = parseIconUrl(link, _isNightTime)); + } catch (error) { + console.warn(`smallIcon: ${error.message}`); + // Return a fallback icon to prevent downstream errors + return addPath(_isNightTime ? 'Clear-1992.gif' : 'Sunny.gif'); } // find the icon - switch (conditionName + (isNightTime ? '-n' : '')) { + switch (conditionIcon + (isNightTime ? '-n' : '')) { case 'skc': return addPath('Sunny.gif'); @@ -72,7 +70,7 @@ const smallIcon = (link, _isNightTime) => { case 'snow': case 'snow-n': - if (value > 50) return addPath('Heavy-Snow-1994.gif'); + if (probability > 50) return addPath('Heavy-Snow-1994.gif'); return addPath('Light-Snow.gif'); case 'rain_snow': @@ -153,8 +151,9 @@ const smallIcon = (link, _isNightTime) => { return addPath('Haze.gif'); default: - console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`); - return false; + console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`); + // Return a reasonable fallback instead of false to prevent downstream errors + return addPath(isNightTime ? 'Clear-1992.gif' : 'Sunny.gif'); } };