Compare commits

..

2 Commits

Author SHA1 Message Date
Matt Walsh
778b7f4456 almanac, current weather, extended forecast, hazards #193 2026-04-04 11:36:31 -05:00
Matt Walsh
443114f555 Add new screen enhancement settings close #195 2026-04-04 11:09:21 -05:00
14 changed files with 48 additions and 49 deletions

View File

@@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2020-2026 Matt Walsh Copyright (c) 2020-2025 Matt Walsh
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -8,11 +8,13 @@ import states from './stations-states.mjs';
import chunk from './chunk.mjs'; import chunk from './chunk.mjs';
import overrides from './stations-overrides.mjs'; import overrides from './stations-overrides.mjs';
import postProcessor from './stations-postprocessor.mjs'; import postProcessor from './stations-postprocessor.mjs';
import { stationFilter } from '../server/scripts/modules/utils/string.mjs';
// check for cached flag // check for cached flag
const USE_CACHE = process.argv.includes('--use-cache'); const USE_CACHE = process.argv.includes('--use-cache');
// skip stations starting with these letters
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
// chunk the list of states // chunk the list of states
const chunkStates = chunk(states, 3); const chunkStates = chunk(states, 3);
@@ -39,8 +41,10 @@ if (!USE_CACHE) {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const stationsRaw = await https(next); const stationsRaw = await https(next);
stations = JSON.parse(stationsRaw); stations = JSON.parse(stationsRaw);
// filter stations for 4 letter identifiers
const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/));
// filter against starting letter // filter against starting letter
const stationsFiltered = stations.filter(stationFilter); const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// add each resulting station to the output // add each resulting station to the output
stationsFiltered.forEach((station) => { stationsFiltered.forEach((station) => {
const id = station.properties.stationIdentifier; const id = station.properties.stationIdentifier;

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "6.5.9", "version": "6.5.4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ws4kp", "name": "ws4kp",
"version": "6.5.9", "version": "6.5.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"dotenv": "^17.0.1", "dotenv": "^17.0.1",

View File

@@ -1,6 +1,6 @@
{ {
"name": "ws4kp", "name": "ws4kp",
"version": "6.5.9", "version": "6.5.4",
"description": "Welcome to the WeatherStar 4000+ project page!", "description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.mjs", "main": "index.mjs",
"type": "module", "type": "module",

View File

@@ -72,7 +72,7 @@ const init = async () => {
if (!navigator.geolocation) btnGetGps.style.display = 'none'; if (!navigator.geolocation) btnGetGps.style.display = 'none';
document.querySelector('#divTwc').addEventListener('mousemove', () => { document.querySelector('#divTwc').addEventListener('mousemove', () => {
if (document.fullscreenElement || settings.kiosk?.value) updateFullScreenNavigate(); if (document.fullscreenElement) updateFullScreenNavigate();
}); });
document.querySelector('#btnGetLatLng').addEventListener('click', () => autoComplete.directFormSubmit()); document.querySelector('#btnGetLatLng').addEventListener('click', () => autoComplete.directFormSubmit());
@@ -384,7 +384,7 @@ const updateFullScreenNavigate = () => {
} }
navigateFadeIntervalId = setTimeout(() => { navigateFadeIntervalId = setTimeout(() => {
if (document.fullscreenElement || settings.kiosk?.value) { if (document.fullscreenElement) {
divTwcBottom.classList.remove('visible'); divTwcBottom.classList.remove('visible');
divTwcBottom.classList.add('hidden'); divTwcBottom.classList.add('hidden');
document.querySelector('#divTwc').classList.add('no-cursor'); document.querySelector('#divTwc').classList.add('no-cursor');

View File

@@ -47,10 +47,7 @@ class Almanac extends WeatherDisplay {
} }
calcSunMoonData(weatherParameters) { calcSunMoonData(weatherParameters) {
const sun = [ const sun = [0, 1, 2, 3, 4, 5, 6].map((days) => SunCalc.getTimes(DateTime.local().plus({ days }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude));
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
SunCalc.getTimes(DateTime.local().plus({ days: 1 }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
];
// brute force the moon phases by scanning the next 30 days // brute force the moon phases by scanning the next 30 days
const moon = []; const moon = [];

View File

@@ -15,6 +15,9 @@ import { debugFlag } from './utils/debug.mjs';
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs'; import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs'; import { DateTime } from '../vendor/auto/luxon.mjs';
// some stations prefixed do not provide all the necessary data
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
class CurrentWeather extends WeatherDisplay { class CurrentWeather extends WeatherDisplay {
constructor(navId, elemId) { constructor(navId, elemId) {
super(navId, elemId, 'Current Conditions', true); super(navId, elemId, 'Current Conditions', true);
@@ -26,8 +29,8 @@ class CurrentWeather extends WeatherDisplay {
// note: current weather does not use old data on a silent refresh // note: current weather does not use old data on a silent refresh
// this is deliberate because it can pull data from more than one station in sequence // this is deliberate because it can pull data from more than one station in sequence
// get the available stations // filter for 4-letter observation stations, only those contain sky conditions and thus an icon
const { stations } = this.weatherParameters; const filteredStations = this.weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// Load the observations // Load the observations
let observations; let observations;
@@ -35,9 +38,9 @@ class CurrentWeather extends WeatherDisplay {
// station number counter // station number counter
let stationNum = 0; let stationNum = 0;
while (!observations && stationNum < stations.length) { while (!observations && stationNum < filteredStations.length) {
// get the station // get the station
station = stations[stationNum]; station = filteredStations[stationNum];
const stationId = station.properties.stationIdentifier; const stationId = station.properties.stationIdentifier;
stationNum += 1; stationNum += 1;
@@ -101,10 +104,7 @@ class CurrentWeather extends WeatherDisplay {
debugContext: 'currentweather', debugContext: 'currentweather',
}); });
// copy enhanced data and restore the timestamp if it was overwritten by older data from mapclick
candidateObservation.features[0].properties = enhancedResult.data; candidateObservation.features[0].properties = enhancedResult.data;
const { missingFields } = enhancedResult; const { missingFields } = enhancedResult;
const missingRequired = missingFields.filter((fieldName) => { const missingRequired = missingFields.filter((fieldName) => {
const field = requiredFields.find((f) => f.name === fieldName && f.required); const field = requiredFields.find((f) => f.name === fieldName && f.required);

View File

@@ -17,7 +17,7 @@ const changeEnable = (newValue) => {
// hide the string entry // hide the string entry
newDisplay = 'none'; newDisplay = 'none';
} }
const stringEntry = document.getElementById('settings-customText-string'); const stringEntry = document.getElementById('settings-customText-label');
if (stringEntry) { if (stringEntry) {
stringEntry.style.display = newDisplay; stringEntry.style.display = newDisplay;
} }

View File

@@ -17,7 +17,13 @@ class ExtendedForecast extends WeatherDisplay {
super(navId, elemId, 'Extended Forecast', true); super(navId, elemId, 'Extended Forecast', true);
// set timings // set timings
this.timing.totalScreens = 2; if (settings.enhancedScreens?.value) {
this.timing.totalScreens = 1;
this.perPage = 4;
} else {
this.timing.totalScreens = 2;
this.perPage = 3;
}
} }
async getData(weatherParameters, refresh) { async getData(weatherParameters, refresh) {
@@ -54,7 +60,7 @@ class ExtendedForecast extends WeatherDisplay {
// determine bounds // determine bounds
// grab the first three or second set of three array elements // grab the first three or second set of three array elements
const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3); const forecast = parse(this.data.properties.periods, this.weatherParameters.forecast).slice(0 + this.perPage * this.screenIndex, this.perPage + this.screenIndex * this.perPage);
// create each day template // create each day template
const days = forecast.map((Day) => { const days = forecast.map((Day) => {

View File

@@ -6,7 +6,6 @@ import { safeJson } from './utils/fetch.mjs';
import { getPoint } from './utils/weather.mjs'; import { getPoint } from './utils/weather.mjs';
import { debugFlag } from './utils/debug.mjs'; import { debugFlag } from './utils/debug.mjs';
import settings from './settings.mjs'; import settings from './settings.mjs';
import { stationFilter } from './utils/string.mjs';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
init(); init();
@@ -86,15 +85,7 @@ const getWeather = async (latLon, haveDataCallback) => {
return; return;
} }
// filter stations for proper format const StationId = stations.features[0].properties.stationIdentifier;
const stationsFiltered = stations.features.filter(stationFilter);
// check for stations available after filtering
if (stationsFiltered.length === 0) {
console.warn('No observation stations left for location after filtering');
return;
}
const StationId = stationsFiltered[0].properties.stationIdentifier;
let { city } = point.properties.relativeLocation.properties; let { city } = point.properties.relativeLocation.properties;
const { state } = point.properties.relativeLocation.properties; const { state } = point.properties.relativeLocation.properties;
@@ -117,7 +108,7 @@ const getWeather = async (latLon, haveDataCallback) => {
weatherParameters.timeZone = point.properties.timeZone; weatherParameters.timeZone = point.properties.timeZone;
weatherParameters.forecast = point.properties.forecast; weatherParameters.forecast = point.properties.forecast;
weatherParameters.forecastGridData = point.properties.forecastGridData; weatherParameters.forecastGridData = point.properties.forecastGridData;
weatherParameters.stations = stationsFiltered; weatherParameters.stations = stations.features;
weatherParameters.relativeLocation = point.properties.relativeLocation.properties; weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
// update the main process for display purposes // update the main process for display purposes

View File

@@ -9,12 +9,10 @@ const pixelToFile = (xPixel, yPixel) => {
return `${yTile}-${xTile}`; return `${yTile}-${xTile}`;
}; };
// convert a pixel location in the overall map to a pixel location on the tile set // convert a pixel location in the overall map to a pixel location on the tile
const modTile = (xPixel, yPixel) => { const modTile = (xPixel, yPixel) => {
// adjust for additional 1 tile when odd const x = Math.round(xPixel) % TILE_SIZE.x;
const x = (Math.floor(xPixel) % (TILE_SIZE.x)); const y = Math.round(yPixel) % TILE_SIZE.y;
const y = (Math.floor(yPixel) % (TILE_SIZE.y));
return { x, y }; return { x, y };
}; };
@@ -48,8 +46,8 @@ const setTiles = (data) => {
// determine which tiles are used // determine which tiles are used
const usedTiles = [ const usedTiles = [
true, true,
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE.width, TILE_SIZE.x - tileShift.x < RADAR_FINAL_SIZE.width,
tileShift.y + TILE_SIZE.y > RADAR_FINAL_SIZE.height, TILE_SIZE.y - tileShift.y < RADAR_FINAL_SIZE.width,
]; ];
// if we need t[1] and t[2] then we also need t[3] // if we need t[1] and t[2] then we also need t[3]
usedTiles.push(usedTiles[1] && usedTiles[2]); usedTiles.push(usedTiles[1] && usedTiles[2]);

View File

@@ -44,11 +44,9 @@ const kioskChange = (value) => {
if (value) { if (value) {
body.classList.add('kiosk'); body.classList.add('kiosk');
document.querySelector('#divTwc')?.classList.add('no-cursor');
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} else { } else {
body.classList.remove('kiosk'); body.classList.remove('kiosk');
document.querySelector('#divTwc')?.classList.remove('no-cursor');
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));
} }
@@ -132,6 +130,16 @@ const init = () => {
changeAction: wideScreenChange, changeAction: wideScreenChange,
sticky: true, sticky: true,
}); });
settings.portrait = new Setting('portrait', {
name: 'Allow Portrait',
defaultValue: false,
sticky: true,
});
settings.enhancedScreens = new Setting('enhancedScreens', {
name: 'Enhanced Screens',
defaultValue: false,
sticky: true,
});
settings.kiosk = new Setting('kiosk', { settings.kiosk = new Setting('kiosk', {
name: 'Kiosk', name: 'Kiosk',
defaultValue: false, defaultValue: false,

View File

@@ -650,7 +650,7 @@ export const enhanceObservationWithMapClick = async (observationData, options =
} }
return { return {
data: { ...mapClickProps, timestamp: observationData.timestamp }, data: mapClickProps,
wasImproved: true, wasImproved: true,
improvements, improvements,
missingFields: [...mapClickMissingRequired, ...mapClickMissingOptional], missingFields: [...mapClickMissingRequired, ...mapClickMissingOptional],

View File

@@ -13,12 +13,7 @@ const locationCleanup = (input) => {
return regexes.reduce((value, regex) => value.replace(regex, ''), input); return regexes.reduce((value, regex) => value.replace(regex, ''), input);
}; };
// stations must be 4 alpha characters and not start with the provided list
const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J'];
const stationFilter = (station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/) && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1));
export { export {
// eslint-disable-next-line import/prefer-default-export
locationCleanup, locationCleanup,
stationFilter,
}; };