Compare commits
No commits in common. "e4f66d5bb0f54d6003c17abf2a885498fb47970e" and "69cfdd2f1e08d6194d59aaa42b314f33bcf01025" have entirely different histories.
e4f66d5bb0
...
69cfdd2f1e
13 changed files with 14 additions and 398 deletions
|
|
@ -68,7 +68,6 @@ const mjsSources = [
|
||||||
'server/scripts/modules/currentweatherscroll.mjs',
|
'server/scripts/modules/currentweatherscroll.mjs',
|
||||||
'server/scripts/modules/hazards.mjs',
|
'server/scripts/modules/hazards.mjs',
|
||||||
'server/scripts/modules/currentweather.mjs',
|
'server/scripts/modules/currentweather.mjs',
|
||||||
'server/scripts/modules/latestobservations.mjs',
|
|
||||||
'server/scripts/modules/almanac.mjs',
|
'server/scripts/modules/almanac.mjs',
|
||||||
'server/scripts/modules/spc-outlook.mjs',
|
'server/scripts/modules/spc-outlook.mjs',
|
||||||
'server/scripts/modules/icons.mjs',
|
'server/scripts/modules/icons.mjs',
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import { round2 } from './modules/utils/units.mjs';
|
||||||
import { registerHiddenSetting } from './modules/share.mjs';
|
import { registerHiddenSetting } from './modules/share.mjs';
|
||||||
import settings from './modules/settings.mjs';
|
import settings from './modules/settings.mjs';
|
||||||
import './modules/utils/theme.mjs';
|
import './modules/utils/theme.mjs';
|
||||||
import './modules/latestobservations.mjs';
|
|
||||||
import AutoComplete from './modules/autocomplete.mjs';
|
import AutoComplete from './modules/autocomplete.mjs';
|
||||||
import { loadAllData } from './modules/utils/data-loader.mjs';
|
import { loadAllData } from './modules/utils/data-loader.mjs';
|
||||||
import { debugFlag } from './modules/utils/debug.mjs';
|
import { debugFlag } from './modules/utils/debug.mjs';
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import { registerDisplay } from './navigation.mjs';
|
||||||
import {
|
import {
|
||||||
temperature, windSpeed, pressure, distanceKilometers, distanceMeters,
|
temperature, windSpeed, pressure, distanceKilometers, distanceMeters,
|
||||||
} from './utils/units.mjs';
|
} from './utils/units.mjs';
|
||||||
import { getConditionTextWithWind } from './utils/weather.mjs';
|
import { getConditionText } from './utils/weather.mjs';
|
||||||
import { getLargeIconFromWmoCodeWithWind } from './icons.mjs';
|
import { getLargeIconFromWmoCode } from './icons.mjs';
|
||||||
|
|
||||||
class CurrentWeather extends WeatherDisplay {
|
class CurrentWeather extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
|
|
@ -37,12 +37,8 @@ class CurrentWeather extends WeatherDisplay {
|
||||||
async drawCanvas() {
|
async drawCanvas() {
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
|
|
||||||
let condition = getConditionTextWithWind(
|
let condition = getConditionText(this.data.TextConditions);
|
||||||
this.data.TextConditions,
|
if (condition.length > 15) {
|
||||||
this.data.WindSpeedRaw,
|
|
||||||
this.data.WindGustRaw
|
|
||||||
);
|
|
||||||
if (condition.length > 23) {
|
|
||||||
condition = shortConditions(condition);
|
condition = shortConditions(condition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -152,21 +148,14 @@ const parseData = (weatherParameters) => {
|
||||||
Visibility: visibilityConverter(currentForecast.visibility),
|
Visibility: visibilityConverter(currentForecast.visibility),
|
||||||
VisibilityUnit: visibilityConverter.units,
|
VisibilityUnit: visibilityConverter.units,
|
||||||
WindSpeed: windConverter(currentForecast.wind_speed_10m),
|
WindSpeed: windConverter(currentForecast.wind_speed_10m),
|
||||||
WindSpeedRaw: currentForecast.wind_speed_10m,
|
|
||||||
WindDirection: directionToNSEW(currentForecast.wind_direction_10m ?? 0),
|
WindDirection: directionToNSEW(currentForecast.wind_direction_10m ?? 0),
|
||||||
Pressure: pressureConverter((currentForecast.pressure_msl ?? 0) * 100),
|
Pressure: pressureConverter((currentForecast.pressure_msl ?? 0) * 100),
|
||||||
PressureDirection: currentForecast.pressureTrend,
|
PressureDirection: currentForecast.pressureTrend,
|
||||||
Humidity: Math.round(currentForecast.relative_humidity_2m ?? 0),
|
Humidity: Math.round(currentForecast.relative_humidity_2m ?? 0),
|
||||||
WindGust: windConverter(currentForecast.wind_gusts_10m),
|
WindGust: windConverter(currentForecast.wind_gusts_10m),
|
||||||
WindGustRaw: currentForecast.wind_gusts_10m,
|
|
||||||
WindUnit: windConverter.units,
|
WindUnit: windConverter.units,
|
||||||
TextConditions: Number(currentForecast.weather_code ?? 0),
|
TextConditions: Number(currentForecast.weather_code ?? 0),
|
||||||
Icon: getLargeIconFromWmoCodeWithWind(
|
Icon: getLargeIconFromWmoCode(currentForecast.weather_code, Boolean(currentForecast.is_day)),
|
||||||
currentForecast.weather_code,
|
|
||||||
Boolean(currentForecast.is_day),
|
|
||||||
currentForecast.wind_speed_10m,
|
|
||||||
currentForecast.wind_gusts_10m
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import largeIcon from './icons/icons-large.mjs';
|
||||||
import smallIcon from './icons/icons-small.mjs';
|
import smallIcon from './icons/icons-small.mjs';
|
||||||
import hourlyIcon from './icons/icons-hourly.mjs';
|
import hourlyIcon from './icons/icons-hourly.mjs';
|
||||||
import { withBasePath } from './utils/base-path.mjs';
|
import { withBasePath } from './utils/base-path.mjs';
|
||||||
import { getWindDescriptor } from './utils/weather.mjs';
|
|
||||||
|
|
||||||
const getWeatherGovTokenFromWmoCode = (code) => {
|
const getWeatherGovTokenFromWmoCode = (code) => {
|
||||||
switch (Number(code)) {
|
switch (Number(code)) {
|
||||||
|
|
@ -51,42 +50,10 @@ const buildSyntheticIconUrl = (code, isDaytime = true) => withBasePath(`icons/la
|
||||||
const getLargeIconFromWmoCode = (code, isDaytime = true) => largeIcon(buildSyntheticIconUrl(code, isDaytime), !isDaytime);
|
const getLargeIconFromWmoCode = (code, isDaytime = true) => largeIcon(buildSyntheticIconUrl(code, isDaytime), !isDaytime);
|
||||||
const getSmallIconFromWmoCode = (code, isDaytime = true) => smallIcon(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 {
|
export {
|
||||||
largeIcon as getLargeIcon,
|
largeIcon as getLargeIcon,
|
||||||
smallIcon as getSmallIcon,
|
smallIcon as getSmallIcon,
|
||||||
hourlyIcon as getHourlyIcon,
|
hourlyIcon as getHourlyIcon,
|
||||||
getLargeIconFromWmoCode,
|
getLargeIconFromWmoCode,
|
||||||
getSmallIconFromWmoCode,
|
getSmallIconFromWmoCode,
|
||||||
getLargeIconFromWmoCodeWithWind,
|
|
||||||
getSmallIconFromWmoCodeWithWind,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,183 +0,0 @@
|
||||||
// Latest Observations display - shows current conditions for 7 nearby cities
|
|
||||||
import STATUS from './status.mjs';
|
|
||||||
import { directionToNSEW } from './utils/calc.mjs';
|
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
|
||||||
import { registerDisplay } from './navigation.mjs';
|
|
||||||
import {
|
|
||||||
temperature, windSpeed,
|
|
||||||
} from './utils/units.mjs';
|
|
||||||
import { getConditionText, getOpenMeteoObservationSnapshot } from './utils/weather.mjs';
|
|
||||||
import { loadRadarCities } from './utils/leaflet-weather-map.mjs';
|
|
||||||
|
|
||||||
class LatestObservations extends WeatherDisplay {
|
|
||||||
constructor(navId, elemId) {
|
|
||||||
super(navId, elemId, 'Latest Observations', true);
|
|
||||||
this.nearbyCities = [];
|
|
||||||
this.observations = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
|
||||||
const superResult = super.getData(weatherParameters, refresh);
|
|
||||||
this.data = await parseData(this.weatherParameters);
|
|
||||||
if (!this.data) {
|
|
||||||
this.setStatus(STATUS.failed);
|
|
||||||
return superResult;
|
|
||||||
}
|
|
||||||
this.setStatus(STATUS.loaded);
|
|
||||||
return superResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
async drawCanvas() {
|
|
||||||
super.drawCanvas();
|
|
||||||
if (!this.data || this.data.length === 0) {
|
|
||||||
this.finishDraw();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Templates are extracted by WeatherDisplay.loadTemplates(), so rebuild rows from stored templates only.
|
|
||||||
const container = this.elem.querySelector('.observation-lines');
|
|
||||||
container.innerHTML = '';
|
|
||||||
|
|
||||||
// Add observation rows
|
|
||||||
this.data.forEach((obs) => {
|
|
||||||
const row = this.fillTemplate('observation-row', {
|
|
||||||
city: obs.city,
|
|
||||||
temp: obs.temp,
|
|
||||||
conditions: obs.conditions,
|
|
||||||
wind: obs.wind,
|
|
||||||
});
|
|
||||||
if (row) container.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.finishDraw();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate city name to 15 characters
|
|
||||||
const truncateCityName = (name) => {
|
|
||||||
if (!name) return '';
|
|
||||||
if (name.length <= 15) return name;
|
|
||||||
return name.substring(0, 15);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Shorten weather conditions (similar to currentweather.mjs)
|
|
||||||
const shortConditions = (condition) => {
|
|
||||||
if (!condition) return '';
|
|
||||||
|
|
||||||
// Apply abbreviations
|
|
||||||
let result = condition;
|
|
||||||
result = result.replace(/Light/g, 'Lt');
|
|
||||||
result = result.replace(/Heavy/g, 'Hvy');
|
|
||||||
result = result.replace(/Moderate/g, 'Mod');
|
|
||||||
result = result.replace(/Partly/g, 'Pt');
|
|
||||||
result = result.replace(/Mostly/g, 'Mt');
|
|
||||||
result = result.replace(/Thunderstorm/g, 'T-storm');
|
|
||||||
result = result.replace(/Freezing Rain/g, 'Frz Rn');
|
|
||||||
result = result.replace(/Freezing/g, 'Frz');
|
|
||||||
result = result.replace(/Drizzle/g, 'Drzl');
|
|
||||||
result = result.replace(/Showers/g, 'Shwrs');
|
|
||||||
result = result.replace(/Slight/g, 'Slt');
|
|
||||||
|
|
||||||
// Truncate to 8 characters if still too long
|
|
||||||
if (result.length > 8) {
|
|
||||||
result = result.substring(0, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Calculate distance between two lat/lng points in meters (Haversine formula)
|
|
||||||
const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
||||||
const R = 6371e3; // Earth's radius in meters
|
|
||||||
const φ1 = (lat1 * Math.PI) / 180;
|
|
||||||
const φ2 = (lat2 * Math.PI) / 180;
|
|
||||||
const Δφ = ((lat2 - lat1) * Math.PI) / 180;
|
|
||||||
const Δλ = ((lon2 - lon1) * Math.PI) / 180;
|
|
||||||
|
|
||||||
const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2)
|
|
||||||
+ Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
|
|
||||||
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
||||||
|
|
||||||
return R * c;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Select nearby cities by distance (simpler version without map dependency)
|
|
||||||
const selectNearbyCitiesSimple = (sourceLocation, cities, maxCities = 7, minDistanceMeters = 15000) => {
|
|
||||||
const citiesWithDistance = cities
|
|
||||||
.map((city) => ({
|
|
||||||
...city,
|
|
||||||
distance: calculateDistance(
|
|
||||||
sourceLocation.latitude,
|
|
||||||
sourceLocation.longitude,
|
|
||||||
city.lat,
|
|
||||||
city.lon
|
|
||||||
),
|
|
||||||
}))
|
|
||||||
.filter((city) => city.distance > minDistanceMeters)
|
|
||||||
.sort((a, b) => a.distance - b.distance);
|
|
||||||
|
|
||||||
return citiesWithDistance.slice(0, maxCities);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseData = async (weatherParameters) => {
|
|
||||||
if (!weatherParameters?.latitude || !weatherParameters?.longitude) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load radar cities and select 7 nearby
|
|
||||||
const radarCities = await loadRadarCities();
|
|
||||||
if (!radarCities || radarCities.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nearbyCities = selectNearbyCitiesSimple({
|
|
||||||
latitude: weatherParameters.latitude,
|
|
||||||
longitude: weatherParameters.longitude,
|
|
||||||
}, radarCities, 7, 15000);
|
|
||||||
|
|
||||||
if (!nearbyCities || nearbyCities.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch observations for each city
|
|
||||||
const temperatureConverter = temperature();
|
|
||||||
const windConverter = windSpeed();
|
|
||||||
|
|
||||||
const observations = await Promise.all(
|
|
||||||
nearbyCities.map(async (city) => {
|
|
||||||
try {
|
|
||||||
const observation = await getOpenMeteoObservationSnapshot(city.lat, city.lon);
|
|
||||||
if (!observation || observation.temperature === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format condition text
|
|
||||||
const conditionText = getConditionText(observation.weatherCode);
|
|
||||||
const shortCondition = shortConditions(conditionText);
|
|
||||||
|
|
||||||
// Format wind
|
|
||||||
const windDir = directionToNSEW(observation.windDirection || 0);
|
|
||||||
const windSpd = Math.round(windConverter(observation.windSpeed));
|
|
||||||
const windText = windSpd > 0 ? `${windDir} ${windSpd}` : 'Calm';
|
|
||||||
|
|
||||||
return {
|
|
||||||
city: truncateCityName(city.name),
|
|
||||||
temp: temperatureConverter(observation.temperature),
|
|
||||||
conditions: shortCondition,
|
|
||||||
wind: windText,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to get observation for ${city.name}:`, e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter out failed observations and limit to 7
|
|
||||||
return observations.filter((obs) => obs !== null).slice(0, 7);
|
|
||||||
};
|
|
||||||
|
|
||||||
const display = new LatestObservations(2, 'latest-observations');
|
|
||||||
registerDisplay(display);
|
|
||||||
|
|
||||||
export default display;
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { safePromiseAll } from './fetch.mjs';
|
import { safePromiseAll } from './fetch.mjs';
|
||||||
import { loadData } from './data-loader.mjs';
|
import { loadData } from './data-loader.mjs';
|
||||||
import { getSmallIconFromWmoCodeWithWind } from '../icons.mjs';
|
import { getSmallIconFromWmoCode } from '../icons.mjs';
|
||||||
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
|
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
|
||||||
import { temperature } from './units.mjs';
|
import { temperature } from './units.mjs';
|
||||||
import { withBasePath } from './base-path.mjs';
|
import { withBasePath } from './base-path.mjs';
|
||||||
|
|
@ -107,12 +107,7 @@ const selectNearbyCities = (map, sourceLocation, cities, options = {}) => {
|
||||||
|
|
||||||
const buildNearbyWeatherMarker = (city, observation) => {
|
const buildNearbyWeatherMarker = (city, observation) => {
|
||||||
const temperatureConverter = temperature();
|
const temperatureConverter = temperature();
|
||||||
const icon = getSmallIconFromWmoCodeWithWind(
|
const icon = getSmallIconFromWmoCode(observation.weatherCode, observation.isDay);
|
||||||
observation.weatherCode,
|
|
||||||
observation.isDay,
|
|
||||||
observation.windSpeed,
|
|
||||||
observation.windGusts
|
|
||||||
);
|
|
||||||
const markerHtml = `
|
const markerHtml = `
|
||||||
<div class="nearby-weather-marker-inner">
|
<div class="nearby-weather-marker-inner">
|
||||||
<div class="city">${city.name}</div>
|
<div class="city">${city.name}</div>
|
||||||
|
|
@ -164,5 +159,4 @@ export {
|
||||||
setPrimaryLocationMarker,
|
setPrimaryLocationMarker,
|
||||||
loadNearbyObservationMarkers,
|
loadNearbyObservationMarkers,
|
||||||
clearMarkers,
|
clearMarkers,
|
||||||
loadRadarCities,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ const OPEN_METEO_FORECAST_PARAMETERS = [
|
||||||
].join('&');
|
].join('&');
|
||||||
|
|
||||||
const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
|
const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
|
||||||
'hourly=temperature_2m,weather_code,is_day,wind_speed_10m,wind_gusts_10m,wind_direction_10m',
|
'hourly=temperature_2m,weather_code,is_day',
|
||||||
'forecast_days=1',
|
'forecast_days=1',
|
||||||
'timezone=auto',
|
'timezone=auto',
|
||||||
'models=best_match',
|
'models=best_match',
|
||||||
|
|
@ -119,9 +119,6 @@ const getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
||||||
temperature: forecast.hourly.temperature_2m?.[nearestIndex] ?? null,
|
temperature: forecast.hourly.temperature_2m?.[nearestIndex] ?? null,
|
||||||
weatherCode: forecast.hourly.weather_code?.[nearestIndex] ?? 0,
|
weatherCode: forecast.hourly.weather_code?.[nearestIndex] ?? 0,
|
||||||
isDay: Boolean(forecast.hourly.is_day?.[nearestIndex] ?? 1),
|
isDay: Boolean(forecast.hourly.is_day?.[nearestIndex] ?? 1),
|
||||||
windSpeed: forecast.hourly.wind_speed_10m?.[nearestIndex] ?? 0,
|
|
||||||
windGusts: forecast.hourly.wind_gusts_10m?.[nearestIndex] ?? 0,
|
|
||||||
windDirection: forecast.hourly.wind_direction_10m?.[nearestIndex] ?? 0,
|
|
||||||
timezone: forecast.timezone,
|
timezone: forecast.timezone,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -134,8 +131,8 @@ const getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const weatherConditions = [
|
const weatherConditions = [
|
||||||
{ codes: [0], text: ['Clear'] },
|
{ codes: [0], text: ['Clear sky'] },
|
||||||
{ codes: [1, 2, 3], text: ['Mostly Clear', 'Some Clouds', 'Overcast'] },
|
{ codes: [1, 2, 3], text: ['Mainly clear', 'Partly cloudy', 'Overcast'] },
|
||||||
{ codes: [45, 48], text: ['Fog', 'Depositing rime fog'] },
|
{ codes: [45, 48], text: ['Fog', 'Depositing rime fog'] },
|
||||||
{ codes: [51, 53, 55], text: ['Light Drizzle', 'Moderate Drizzle', 'Dense Drizzle'] },
|
{ codes: [51, 53, 55], text: ['Light Drizzle', 'Moderate Drizzle', 'Dense Drizzle'] },
|
||||||
{ codes: [56, 57], text: ['Light Freezing Drizzle', 'Dense Freezing Drizzle'] },
|
{ codes: [56, 57], text: ['Light Freezing Drizzle', 'Dense Freezing Drizzle'] },
|
||||||
|
|
@ -149,32 +146,6 @@ const weatherConditions = [
|
||||||
{ codes: [96, 99], text: ['Thunderstorm with Slight Hail', 'Thunderstorm with Heavy Hail'] },
|
{ 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 getConditionText = (code) => {
|
||||||
const condition = weatherConditions.find((item) => item.codes.includes(Number(code)));
|
const condition = weatherConditions.find((item) => item.codes.includes(Number(code)));
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
|
|
@ -258,6 +229,4 @@ export {
|
||||||
getOpenMeteoObservationSnapshot,
|
getOpenMeteoObservationSnapshot,
|
||||||
aggregateWeatherForecastData,
|
aggregateWeatherForecastData,
|
||||||
getConditionText,
|
getConditionText,
|
||||||
getWindDescriptor,
|
|
||||||
getConditionTextWithWind,
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
&.left {
|
&.left {
|
||||||
font-family: 'Star4000 Extended';
|
font-family: 'Star4000 Extended';
|
||||||
font-size: 18pt;
|
font-size: 24pt;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +62,6 @@
|
||||||
img {
|
img {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: block;
|
display: block;
|
||||||
width: 108px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -85,13 +84,12 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.location {
|
.location {
|
||||||
color: #ff0;
|
color: c.$title-color;
|
||||||
max-height: 32px;
|
max-height: 32px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
padding-left: 15px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -102,6 +100,7 @@
|
||||||
.col {
|
.col {
|
||||||
&.left {
|
&.left {
|
||||||
margin-top: 35px;
|
margin-top: 35px;
|
||||||
|
font-size: 20pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.right {
|
&.right {
|
||||||
|
|
|
||||||
|
|
@ -1,86 +0,0 @@
|
||||||
@use 'shared/_colors' as c;
|
|
||||||
@use 'shared/_utils' as u;
|
|
||||||
|
|
||||||
.weather-display .main.latest-observations {
|
|
||||||
&.main {
|
|
||||||
padding-top: 18px;
|
|
||||||
|
|
||||||
.column-headers {
|
|
||||||
display: flex;
|
|
||||||
font-family: 'Star4000';
|
|
||||||
font-size: 14pt;
|
|
||||||
font-weight: bold;
|
|
||||||
color: #ff0;
|
|
||||||
width: 70%;
|
|
||||||
margin: 8px auto 10px;
|
|
||||||
padding-top: 20px;
|
|
||||||
text-shadow: 3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;
|
|
||||||
|
|
||||||
.city {
|
|
||||||
width: 30%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temp {
|
|
||||||
width: 15%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conditions {
|
|
||||||
width: 30%;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wind {
|
|
||||||
width: 25%;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.observation-lines {
|
|
||||||
width: 70%;
|
|
||||||
margin: 0 auto;
|
|
||||||
|
|
||||||
.observation-row {
|
|
||||||
display: flex;
|
|
||||||
font-family: 'Star4000';
|
|
||||||
font-size: 14pt;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 4px;
|
|
||||||
|
|
||||||
@include u.text-shadow();
|
|
||||||
|
|
||||||
&.template {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.city {
|
|
||||||
width: 30%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.temp {
|
|
||||||
width: 15%;
|
|
||||||
text-align: center;
|
|
||||||
color: c.$title-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.conditions {
|
|
||||||
width: 30%;
|
|
||||||
text-align: center;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wind {
|
|
||||||
width: 25%;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 4px;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
@use 'page';
|
@use 'page';
|
||||||
@use 'weather-display';
|
@use 'weather-display';
|
||||||
@use 'current-weather';
|
@use 'current-weather';
|
||||||
@use 'latest-observations';
|
|
||||||
@use 'extended-forecast';
|
@use 'extended-forecast';
|
||||||
@use 'hourly';
|
@use 'hourly';
|
||||||
@use 'hourly-graph';
|
@use 'hourly-graph';
|
||||||
|
|
|
||||||
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
|
|
@ -118,9 +118,6 @@
|
||||||
<div id="current-weather-html" class="weather-display">
|
<div id="current-weather-html" class="weather-display">
|
||||||
<%- include('partials/current-weather.ejs') %>
|
<%- include('partials/current-weather.ejs') %>
|
||||||
</div>
|
</div>
|
||||||
<div id="latest-observations-html" class="weather-display">
|
|
||||||
<%- include('partials/latest-observations.ejs') %>
|
|
||||||
</div>
|
|
||||||
<div id="local-forecast-html" class="weather-display">
|
<div id="local-forecast-html" class="weather-display">
|
||||||
<%- include('partials/local-forecast.ejs') %>
|
<%- include('partials/local-forecast.ejs') %>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<div class="header">
|
|
||||||
<div class="logo">
|
|
||||||
<img class="theme-logo" src="images/logos/logo-corner.png" />
|
|
||||||
</div>
|
|
||||||
<div class="title dual">
|
|
||||||
<div class="top">Latest</div>
|
|
||||||
<div class="bottom">Observations</div>
|
|
||||||
</div>
|
|
||||||
<div class="date-time date"></div>
|
|
||||||
<div class="date-time time"></div>
|
|
||||||
</div>
|
|
||||||
<div class="main has-scroll latest-observations">
|
|
||||||
<div class="column-headers">
|
|
||||||
<div class="city">CITY</div>
|
|
||||||
<div class="temp">TEMP</div>
|
|
||||||
<div class="conditions">CONDITIONS</div>
|
|
||||||
<div class="wind">WIND</div>
|
|
||||||
</div>
|
|
||||||
<div class="observation-lines">
|
|
||||||
<div class="observation-row template">
|
|
||||||
<div class="city"></div>
|
|
||||||
<div class="temp"></div>
|
|
||||||
<div class="conditions"></div>
|
|
||||||
<div class="wind"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue