mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 15:49:31 -07:00
Centralize icon URL parsing; improve icon error handling
- Move common icon parsing logic into module - Return a "real" icon value during error handling to avoid downstream consumers from trying to use an icon named "false" - Use named regex to parse icon URLs based on API specification
This commit is contained in:
@@ -1,27 +1,23 @@
|
|||||||
/* spell-checker: disable */
|
/* 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 addPath = (icon) => `images/icons/current-conditions/${icon}`;
|
||||||
|
|
||||||
const largeIcon = (link, _isNightTime) => {
|
const largeIcon = (link, _isNightTime) => {
|
||||||
if (!link) return false;
|
let conditionIcon;
|
||||||
|
let probability;
|
||||||
|
let isNightTime;
|
||||||
|
|
||||||
// extract day or night if not provided
|
try {
|
||||||
const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0;
|
({ conditionIcon, probability, isNightTime } = parseIconUrl(link, _isNightTime));
|
||||||
|
} catch (error) {
|
||||||
// grab everything after the last slash ending at any of these: ?&,
|
console.warn(`largeIcon: ${error.message}`);
|
||||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
// Return a fallback icon to prevent downstream errors
|
||||||
let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1];
|
return addPath(_isNightTime ? 'Clear.gif' : 'Sunny.gif');
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the icon
|
// find the icon
|
||||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
switch (conditionIcon + (isNightTime ? '-n' : '')) {
|
||||||
case 'skc':
|
case 'skc':
|
||||||
case 'hot':
|
case 'hot':
|
||||||
case 'haze':
|
case 'haze':
|
||||||
@@ -81,7 +77,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'snow':
|
case 'snow':
|
||||||
case 'snow-n':
|
case 'snow-n':
|
||||||
if (value > 50) return addPath('Heavy-Snow.gif');
|
if (probability > 50) return addPath('Heavy-Snow.gif');
|
||||||
return addPath('Light-Snow.gif');
|
return addPath('Light-Snow.gif');
|
||||||
|
|
||||||
case 'rain_snow':
|
case 'rain_snow':
|
||||||
@@ -132,9 +128,11 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
case 'blizzard-n':
|
case 'blizzard-n':
|
||||||
return addPath('Blowing-Snow.gif');
|
return addPath('Blowing-Snow.gif');
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
return false;
|
// Return a reasonable fallback instead of false to prevent downstream errors
|
||||||
|
return addPath(isNightTime ? 'Clear-Night.gif' : 'Sunny.gif');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
93
server/scripts/modules/icons/icons-parse.mjs
Normal file
93
server/scripts/modules/icons/icons-parse.mjs
Normal file
@@ -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\/(?<set>\w+)\/(?<timeOfDay>day|night)\/(?<condition>[^?]+)(?:\?(?<params>.*))?$/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;
|
||||||
@@ -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 addPath = (icon) => `images/icons/regional-maps/${icon}`;
|
||||||
|
|
||||||
const smallIcon = (link, _isNightTime) => {
|
const smallIcon = (link, _isNightTime) => {
|
||||||
// extract day or night if not provided
|
let conditionIcon;
|
||||||
const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0;
|
let probability;
|
||||||
|
let isNightTime;
|
||||||
|
|
||||||
// grab everything after the last slash ending at any of these: ?&,
|
try {
|
||||||
const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0];
|
({ conditionIcon, probability, isNightTime } = parseIconUrl(link, _isNightTime));
|
||||||
let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1];
|
} catch (error) {
|
||||||
// using probability as a crude heavy/light indication where possible
|
console.warn(`smallIcon: ${error.message}`);
|
||||||
const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1];
|
// Return a fallback icon to prevent downstream errors
|
||||||
|
return addPath(_isNightTime ? 'Clear-1992.gif' : 'Sunny.gif');
|
||||||
// if a 'DualImage' is captured, adjust to just the j parameter
|
|
||||||
if (conditionName === 'dualimage') {
|
|
||||||
const match = link.match(/&j=(.*)&/);
|
|
||||||
[, conditionName] = match;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the icon
|
// find the icon
|
||||||
switch (conditionName + (isNightTime ? '-n' : '')) {
|
switch (conditionIcon + (isNightTime ? '-n' : '')) {
|
||||||
case 'skc':
|
case 'skc':
|
||||||
return addPath('Sunny.gif');
|
return addPath('Sunny.gif');
|
||||||
|
|
||||||
@@ -72,7 +70,7 @@ const smallIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'snow':
|
case 'snow':
|
||||||
case 'snow-n':
|
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');
|
return addPath('Light-Snow.gif');
|
||||||
|
|
||||||
case 'rain_snow':
|
case 'rain_snow':
|
||||||
@@ -153,8 +151,9 @@ const smallIcon = (link, _isNightTime) => {
|
|||||||
return addPath('Haze.gif');
|
return addPath('Haze.gif');
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
return false;
|
// Return a reasonable fallback instead of false to prevent downstream errors
|
||||||
|
return addPath(isNightTime ? 'Clear-1992.gif' : 'Sunny.gif');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user