Add code to infer conditions from wind + gusts when Open-Meteo over simplifies weather
This commit is contained in:
parent
69cfdd2f1e
commit
84ae94e052
6 changed files with 94 additions and 14 deletions
|
|
@ -7,8 +7,8 @@ import { registerDisplay } from './navigation.mjs';
|
|||
import {
|
||||
temperature, windSpeed, pressure, distanceKilometers, distanceMeters,
|
||||
} from './utils/units.mjs';
|
||||
import { getConditionText } from './utils/weather.mjs';
|
||||
import { getLargeIconFromWmoCode } from './icons.mjs';
|
||||
import { getConditionTextWithWind } from './utils/weather.mjs';
|
||||
import { getLargeIconFromWmoCodeWithWind } from './icons.mjs';
|
||||
|
||||
class CurrentWeather extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
|
|
@ -37,8 +37,12 @@ class CurrentWeather extends WeatherDisplay {
|
|||
async drawCanvas() {
|
||||
super.drawCanvas();
|
||||
|
||||
let condition = getConditionText(this.data.TextConditions);
|
||||
if (condition.length > 15) {
|
||||
let condition = getConditionTextWithWind(
|
||||
this.data.TextConditions,
|
||||
this.data.WindSpeedRaw,
|
||||
this.data.WindGustRaw
|
||||
);
|
||||
if (condition.length > 23) {
|
||||
condition = shortConditions(condition);
|
||||
}
|
||||
|
||||
|
|
@ -148,14 +152,21 @@ const parseData = (weatherParameters) => {
|
|||
Visibility: visibilityConverter(currentForecast.visibility),
|
||||
VisibilityUnit: visibilityConverter.units,
|
||||
WindSpeed: windConverter(currentForecast.wind_speed_10m),
|
||||
WindSpeedRaw: currentForecast.wind_speed_10m,
|
||||
WindDirection: directionToNSEW(currentForecast.wind_direction_10m ?? 0),
|
||||
Pressure: pressureConverter((currentForecast.pressure_msl ?? 0) * 100),
|
||||
PressureDirection: currentForecast.pressureTrend,
|
||||
Humidity: Math.round(currentForecast.relative_humidity_2m ?? 0),
|
||||
WindGust: windConverter(currentForecast.wind_gusts_10m),
|
||||
WindGustRaw: currentForecast.wind_gusts_10m,
|
||||
WindUnit: windConverter.units,
|
||||
TextConditions: Number(currentForecast.weather_code ?? 0),
|
||||
Icon: getLargeIconFromWmoCode(currentForecast.weather_code, Boolean(currentForecast.is_day)),
|
||||
Icon: getLargeIconFromWmoCodeWithWind(
|
||||
currentForecast.weather_code,
|
||||
Boolean(currentForecast.is_day),
|
||||
currentForecast.wind_speed_10m,
|
||||
currentForecast.wind_gusts_10m
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import largeIcon from './icons/icons-large.mjs';
|
|||
import smallIcon from './icons/icons-small.mjs';
|
||||
import hourlyIcon from './icons/icons-hourly.mjs';
|
||||
import { withBasePath } from './utils/base-path.mjs';
|
||||
import { getWindDescriptor } from './utils/weather.mjs';
|
||||
|
||||
const getWeatherGovTokenFromWmoCode = (code) => {
|
||||
switch (Number(code)) {
|
||||
|
|
@ -50,10 +51,42 @@ const buildSyntheticIconUrl = (code, isDaytime = true) => withBasePath(`icons/la
|
|||
const getLargeIconFromWmoCode = (code, isDaytime = true) => largeIcon(buildSyntheticIconUrl(code, isDaytime), !isDaytime);
|
||||
const getSmallIconFromWmoCode = (code, isDaytime = true) => smallIcon(buildSyntheticIconUrl(code, isDaytime), !isDaytime);
|
||||
|
||||
// Wind-aware icon selection
|
||||
const getWeatherGovTokenFromWmoCodeWithWind = (code, windSpeedKmh, windGustsKmh) => {
|
||||
const baseToken = getWeatherGovTokenFromWmoCode(code);
|
||||
const windDesc = getWindDescriptor(windSpeedKmh, windGustsKmh);
|
||||
|
||||
// Only use wind icon for non-precipitation conditions
|
||||
// Precipitation codes: drizzle, rain, freezing rain, snow, sleet, thunderstorms
|
||||
const precipitationCodes = [51, 53, 55, 56, 57, 61, 63, 65, 66, 67, 71, 73, 75, 77, 80, 81, 82, 85, 86, 95, 96, 99];
|
||||
|
||||
if (windDesc && !precipitationCodes.includes(Number(code))) {
|
||||
return `wind_${baseToken}`;
|
||||
}
|
||||
return baseToken;
|
||||
};
|
||||
|
||||
const buildSyntheticIconUrlWithWind = (code, isDaytime, windSpeedKmh, windGustsKmh) => {
|
||||
const token = getWeatherGovTokenFromWmoCodeWithWind(code, windSpeedKmh, windGustsKmh);
|
||||
return withBasePath(`icons/land/${isDaytime ? 'day' : 'night'}/${token}`);
|
||||
};
|
||||
|
||||
const getLargeIconFromWmoCodeWithWind = (code, isDaytime, windSpeedKmh, windGustsKmh) => {
|
||||
const iconUrl = buildSyntheticIconUrlWithWind(code, isDaytime, windSpeedKmh, windGustsKmh);
|
||||
return largeIcon(iconUrl, !isDaytime);
|
||||
};
|
||||
|
||||
const getSmallIconFromWmoCodeWithWind = (code, isDaytime, windSpeedKmh, windGustsKmh) => {
|
||||
const iconUrl = buildSyntheticIconUrlWithWind(code, isDaytime, windSpeedKmh, windGustsKmh);
|
||||
return smallIcon(iconUrl, !isDaytime);
|
||||
};
|
||||
|
||||
export {
|
||||
largeIcon as getLargeIcon,
|
||||
smallIcon as getSmallIcon,
|
||||
hourlyIcon as getHourlyIcon,
|
||||
getLargeIconFromWmoCode,
|
||||
getSmallIconFromWmoCode,
|
||||
getLargeIconFromWmoCodeWithWind,
|
||||
getSmallIconFromWmoCodeWithWind,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { safePromiseAll } from './fetch.mjs';
|
||||
import { loadData } from './data-loader.mjs';
|
||||
import { getSmallIconFromWmoCode } from '../icons.mjs';
|
||||
import { getSmallIconFromWmoCodeWithWind } from '../icons.mjs';
|
||||
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
|
||||
import { temperature } from './units.mjs';
|
||||
import { withBasePath } from './base-path.mjs';
|
||||
|
|
@ -107,7 +107,12 @@ const selectNearbyCities = (map, sourceLocation, cities, options = {}) => {
|
|||
|
||||
const buildNearbyWeatherMarker = (city, observation) => {
|
||||
const temperatureConverter = temperature();
|
||||
const icon = getSmallIconFromWmoCode(observation.weatherCode, observation.isDay);
|
||||
const icon = getSmallIconFromWmoCodeWithWind(
|
||||
observation.weatherCode,
|
||||
observation.isDay,
|
||||
observation.windSpeed,
|
||||
observation.windGusts
|
||||
);
|
||||
const markerHtml = `
|
||||
<div class="nearby-weather-marker-inner">
|
||||
<div class="city">${city.name}</div>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const OPEN_METEO_FORECAST_PARAMETERS = [
|
|||
].join('&');
|
||||
|
||||
const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
|
||||
'hourly=temperature_2m,weather_code,is_day',
|
||||
'hourly=temperature_2m,weather_code,is_day,wind_speed_10m,wind_gusts_10m',
|
||||
'forecast_days=1',
|
||||
'timezone=auto',
|
||||
'models=best_match',
|
||||
|
|
@ -119,6 +119,8 @@ const getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
|||
temperature: forecast.hourly.temperature_2m?.[nearestIndex] ?? null,
|
||||
weatherCode: forecast.hourly.weather_code?.[nearestIndex] ?? 0,
|
||||
isDay: Boolean(forecast.hourly.is_day?.[nearestIndex] ?? 1),
|
||||
windSpeed: forecast.hourly.wind_speed_10m?.[nearestIndex] ?? 0,
|
||||
windGusts: forecast.hourly.wind_gusts_10m?.[nearestIndex] ?? 0,
|
||||
timezone: forecast.timezone,
|
||||
};
|
||||
|
||||
|
|
@ -131,8 +133,8 @@ const getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
|||
};
|
||||
|
||||
const weatherConditions = [
|
||||
{ codes: [0], text: ['Clear sky'] },
|
||||
{ codes: [1, 2, 3], text: ['Mainly clear', 'Partly cloudy', 'Overcast'] },
|
||||
{ codes: [0], text: ['Clear'] },
|
||||
{ codes: [1, 2, 3], text: ['Mostly Clear', 'Some Clouds', 'Overcast'] },
|
||||
{ codes: [45, 48], text: ['Fog', 'Depositing rime fog'] },
|
||||
{ codes: [51, 53, 55], text: ['Light Drizzle', 'Moderate Drizzle', 'Dense Drizzle'] },
|
||||
{ codes: [56, 57], text: ['Light Freezing Drizzle', 'Dense Freezing Drizzle'] },
|
||||
|
|
@ -146,6 +148,32 @@ const weatherConditions = [
|
|||
{ codes: [96, 99], text: ['Thunderstorm with Slight Hail', 'Thunderstorm with Heavy Hail'] },
|
||||
];
|
||||
|
||||
// Wind descriptor thresholds (km/h)
|
||||
const getWindDescriptor = (windSpeedKmh, windGustsKmh) => {
|
||||
// Use max of sustained wind or weighted gusts
|
||||
const maxWind = Math.max(windSpeedKmh, (windGustsKmh || 0) * 0.8);
|
||||
|
||||
if (maxWind >= 56) return 'Very Windy';
|
||||
if (maxWind >= 36) return 'Windy';
|
||||
if (maxWind >= 21) return 'Breezy';
|
||||
return null;
|
||||
};
|
||||
|
||||
// Get condition text with wind descriptor
|
||||
const getConditionTextWithWind = (weatherCode, windSpeedKmh, windGustsKmh) => {
|
||||
const baseCondition = getConditionText(weatherCode);
|
||||
const windDesc = getWindDescriptor(windSpeedKmh, windGustsKmh);
|
||||
|
||||
if (windDesc) {
|
||||
// For clear sky conditions, just use the wind descriptor
|
||||
if (weatherCode === 0) {
|
||||
return windDesc;
|
||||
}
|
||||
return `${baseCondition} ${windDesc}`;
|
||||
}
|
||||
return baseCondition;
|
||||
};
|
||||
|
||||
const getConditionText = (code) => {
|
||||
const condition = weatherConditions.find((item) => item.codes.includes(Number(code)));
|
||||
if (!condition) {
|
||||
|
|
@ -229,4 +257,6 @@ export {
|
|||
getOpenMeteoObservationSnapshot,
|
||||
aggregateWeatherForecastData,
|
||||
getConditionText,
|
||||
getWindDescriptor,
|
||||
getConditionTextWithWind,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
&.left {
|
||||
font-family: 'Star4000 Extended';
|
||||
font-size: 24pt;
|
||||
font-size: 18pt;
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +62,7 @@
|
|||
img {
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
width: 108px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,12 +85,13 @@
|
|||
}
|
||||
|
||||
.location {
|
||||
color: c.$title-color;
|
||||
color: #ff0;
|
||||
max-height: 32px;
|
||||
margin-bottom: 10px;
|
||||
padding-top: 4px;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,7 +102,6 @@
|
|||
.col {
|
||||
&.left {
|
||||
margin-top: 35px;
|
||||
font-size: 20pt;
|
||||
}
|
||||
|
||||
&.right {
|
||||
|
|
|
|||
2
server/styles/ws.min.css
vendored
2
server/styles/ws.min.css
vendored
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue