Re-add a new Latest Observations screen
Some checks are pending
build-docker / Build Image (push) Waiting to run
Some checks are pending
build-docker / Build Image (push) Waiting to run
This commit is contained in:
parent
84ae94e052
commit
e4f66d5bb0
10 changed files with 306 additions and 2 deletions
|
|
@ -68,6 +68,7 @@ const mjsSources = [
|
|||
'server/scripts/modules/currentweatherscroll.mjs',
|
||||
'server/scripts/modules/hazards.mjs',
|
||||
'server/scripts/modules/currentweather.mjs',
|
||||
'server/scripts/modules/latestobservations.mjs',
|
||||
'server/scripts/modules/almanac.mjs',
|
||||
'server/scripts/modules/spc-outlook.mjs',
|
||||
'server/scripts/modules/icons.mjs',
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { round2 } from './modules/utils/units.mjs';
|
|||
import { registerHiddenSetting } from './modules/share.mjs';
|
||||
import settings from './modules/settings.mjs';
|
||||
import './modules/utils/theme.mjs';
|
||||
import './modules/latestobservations.mjs';
|
||||
import AutoComplete from './modules/autocomplete.mjs';
|
||||
import { loadAllData } from './modules/utils/data-loader.mjs';
|
||||
import { debugFlag } from './modules/utils/debug.mjs';
|
||||
|
|
|
|||
183
server/scripts/modules/latestobservations.mjs
Normal file
183
server/scripts/modules/latestobservations.mjs
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
// 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;
|
||||
|
|
@ -164,4 +164,5 @@ export {
|
|||
setPrimaryLocationMarker,
|
||||
loadNearbyObservationMarkers,
|
||||
clearMarkers,
|
||||
loadRadarCities,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ const OPEN_METEO_FORECAST_PARAMETERS = [
|
|||
].join('&');
|
||||
|
||||
const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
|
||||
'hourly=temperature_2m,weather_code,is_day,wind_speed_10m,wind_gusts_10m',
|
||||
'hourly=temperature_2m,weather_code,is_day,wind_speed_10m,wind_gusts_10m,wind_direction_10m',
|
||||
'forecast_days=1',
|
||||
'timezone=auto',
|
||||
'models=best_match',
|
||||
|
|
@ -121,6 +121,7 @@ const getOpenMeteoObservationSnapshot = async (lat, lon) => {
|
|||
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,
|
||||
};
|
||||
|
||||
|
|
|
|||
86
server/styles/scss/_latest-observations.scss
Normal file
86
server/styles/scss/_latest-observations.scss
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
@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,6 +1,7 @@
|
|||
@use 'page';
|
||||
@use 'weather-display';
|
||||
@use 'current-weather';
|
||||
@use 'latest-observations';
|
||||
@use 'extended-forecast';
|
||||
@use 'hourly';
|
||||
@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,6 +118,9 @@
|
|||
<div id="current-weather-html" class="weather-display">
|
||||
<%- include('partials/current-weather.ejs') %>
|
||||
</div>
|
||||
<div id="latest-observations-html" class="weather-display">
|
||||
<%- include('partials/latest-observations.ejs') %>
|
||||
</div>
|
||||
<div id="local-forecast-html" class="weather-display">
|
||||
<%- include('partials/local-forecast.ejs') %>
|
||||
</div>
|
||||
|
|
|
|||
27
views/partials/latest-observations.ejs
Normal file
27
views/partials/latest-observations.ejs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<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