Compare commits

...

26 Commits

Author SHA1 Message Date
Matt Walsh
5adf74feee Merge branch 'main' into screen-enhance 2026-04-15 09:40:02 -05:00
Matt Walsh
6298068f2a use flexbox to put displays and settings side by side 2026-04-14 16:58:17 -05:00
Matt Walsh
62fbe1787f switch local forecast to mixed case for better readability and to match the latest captures from the late '00s 2026-04-14 08:52:36 -05:00
Matt Walsh
994c9240b8 shorten permalinks close #206 2026-04-13 16:19:26 -05:00
Matt Walsh
27d75ba62d current observations don't show like when the same close #207 2026-04-13 14:54:18 -05:00
Matt Walsh
63e089451d restructure wide/enhanced settings close #205 2026-04-13 14:36:15 -05:00
Matt Walsh
c07ebe8bdd 6.5.9 2026-04-09 12:19:03 -05:00
Matt Walsh
a41b0da196 more generalized fix for mapclick enhanced timestamps close #203
moves changes made in 0b47cf79c1 to the mapclick processing for benefit of other mapclick calls
2026-04-09 12:18:56 -05:00
Matt Walsh
30887202c8 6.5.8 2026-04-09 11:30:35 -05:00
Matt Walsh
38d1455a4b fix custom text scroll 2026-04-09 11:29:49 -05:00
Matt Walsh
30ec847ed5 Hide cursor in kiosk
via @iapetusz
2026-04-09 11:22:36 -05:00
Matt Walsh
42f1f66117 squeeze preview format into available space 2026-04-08 23:36:33 -05:00
Matt Walsh
d4f648f244 better 'version' numbers for staging site 2026-04-08 23:34:10 -05:00
Matt Walsh
71d52c0b72 fixe wide-enhanced radar and almanac backgrounds 2026-04-08 23:25:05 -05:00
Matt Walsh
d2bf8f3f99 fix radar tiles 2026-04-08 23:01:42 -05:00
Matt Walsh
f4289e6329 Merge branch 'main' into screen-enahnce/magic-number-code 2026-04-08 22:56:20 -05:00
Matt Walsh
11c54391b2 6.5.7 2026-04-08 22:42:07 -05:00
Matt Walsh
0b47cf79c1 don't overwrite timestamps when enhancing with mapclick 2026-04-08 22:41:42 -05:00
Matt Walsh
ba36904477 6.5.6 2026-04-08 11:39:36 -05:00
Matt Walsh
dae5b20bc6 fix radar round/floor mismatch in calculations close #200 2026-04-08 11:39:25 -05:00
Matt Walsh
ccc936d81a 6.5.5 2026-04-08 09:57:24 -05:00
Matt Walsh
5dc214c6a5 filter station list upon receipt 2026-04-08 09:57:18 -05:00
Matt Walsh
ec1169e07b redraw on entering/leaving enhanced close #198 2026-04-08 09:24:54 -05:00
Matt Walsh
eee4519095 almanac #188 #196 #194 #193 2026-04-07 14:17:26 -05:00
Matt Walsh
38cdb46c85 enhance current weather, latest observations #188 #196 #193 #194 2026-04-07 13:50:37 -05:00
Matt Walsh
e70639d7a6 local forecast enhanced-wide #188 #196 #193 #194 2026-04-07 10:38:12 -05:00
47 changed files with 378 additions and 148 deletions

View File

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

View File

@@ -139,8 +139,8 @@ services:
# Following the "Sharing a Permalink" example below, here are a few environment variables defined. Visit that section for a
# more complete list of configuration options.
- WSQS_latLonQuery=Orlando International Airport Orlando FL USA
- WSQS_hazards_checkbox=false
- WSQS_current_weather_checkbox=true
- WSQS_hazards=false
- WSQS_current_weather=true
ports:
- 8080:8080 # change the first 8080 to meet your local network needs
restart: unless-stopped
@@ -192,13 +192,13 @@ Selected displays, the forecast city and widescreen setting are sticky from one
Your permalink will be very long. Here is an example for the Orlando International Airport:
```
https://weatherstar.netbymatt.com/?hazards-checkbox=false&current-weather-checkbox=true&latest-observations-checkbox=true&hourly-checkbox=false&hourly-graph-checkbox=true&travel-checkbox=false&regional-forecast-checkbox=true&local-forecast-checkbox=true&extended-forecast-checkbox=true&almanac-checkbox=false&spc-outlook-checkbox=true&radar-checkbox=true&settings-wide-checkbox=false&settings-kiosk-checkbox=false&settings-scanLines-checkbox=false&settings-speed-select=1.00&settings-units-select=us&latLonQuery=Orlando+International+Airport%2C+Orlando%2C+FL%2C+USA&latLon=%7B%22lat%22%3A28.431%2C%22lon%22%3A-81.3076%7D
https://weatherstar.netbymatt.com/?hazards=false&current-weather=true&latest-observations=true&hourly=false&hourly-graph=true&travel=false&regional-forecast=true&local-forecast=true&extended-forecast=true&almanac=false&spc-outlook=true&radar=true&wide=false&kiosk=false&scanLines=false&speed-select=1.00&units-select=us&latLonQuery=Orlando+International+Airport%2C+Orlando%2C+FL%2C+USA&latLon=%7B%22lat%22%3A28.431%2C%22lon%22%3A-81.3076%7D
```
You can also build your own permalink. Any omitted settings will be filled with defaults. Here are a few examples:
```
https://weatherstar.netbymatt.com/?latLonQuery=Orlando+International+Airport
https://weatherstar.netbymatt.com/?kiosk=true
https://weatherstar.netbymatt.com/?settings-units-select=metric
https://weatherstar.netbymatt.com/?units-select=metric
```
### Kiosk mode
@@ -213,7 +213,7 @@ When serving this via the built-in Express server, it's possible to define envir
Environment variables can be added to the command line as usual, or via a .env file which is parsed with [dotenv](https://github.com/motdotla/dotenv). Both methods have the same effect.
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, its matching environment variable becomes `WSQS_travel_checkbox=true`.
Environment variables that are to be added to the default query string are prefixed with `WSQS_` and then use the same key/value pairs generated by the [Permalink](#sharing-a-permalink-bookmarking) above, with the `- (dash)` character replaced by an `_ (underscore)`. For example, if you wanted to turn the travel forecast on, you would find `travel-checkbox=true` in the permalink, its matching environment variable becomes `WSQS_travel=true`.
When using the Docker container, these environment variables are read on container start-up to generate the static redirect HTML.
@@ -221,7 +221,13 @@ When using the Docker container, these environment variables are read on contain
**Speed:** Controls the playback speed multiplier of the displays, from "Very Fast" (1.5x) to "Very Slow" (0.5x) with "Normal" being 1x
**Widescreen:** Stretches the background to 16:9 to avoid "pillarboxing" on modern displays
**Display Mode:**
- Standard: Classic 4:3 display with the classic (not enhanced, below) screen layouts.
- Widescreen: Stretches the background to 16:9 to avoid "pillarboxing" on modern displays
- Widescreen Enhanced: Stretches as above, and makes use of the additional space to provide wider maps, more weather data and/or additional days in the forecast
- Portrait Enhanced: (in progress) Rotates the screen to a 16:9 portrait orientation and enhances the original displays by adjusting them to fit the new orientation.
**Kiosk:** Immediately activates kiosk mode, which hides all settings. Exit by refreshing the page or using `Ctrl-K`. (Kiosk mode is similar to clicking the "Fullscreen" icon, but scales to the current browser viewport instead of activating the browser's actual "Fullscreen" mode.)

View File

@@ -8,13 +8,11 @@ import states from './stations-states.mjs';
import chunk from './chunk.mjs';
import overrides from './stations-overrides.mjs';
import postProcessor from './stations-postprocessor.mjs';
import { stationFilter } from '../server/scripts/modules/utils/string.mjs';
// check for cached flag
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
const chunkStates = chunk(states, 3);
@@ -41,10 +39,8 @@ if (!USE_CACHE) {
// eslint-disable-next-line no-await-in-loop
const stationsRaw = await https(next);
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
const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
const stationsFiltered = stations.filter(stationFilter);
// add each resulting station to the output
stationsFiltered.forEach((station) => {
const id = station.properties.stationIdentifier;

View File

@@ -15,10 +15,11 @@ import { readFile } from 'fs/promises';
import file from 'gulp-file';
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
import log from 'fancy-log';
import dartSass from 'sass';
import * as dartSass from 'sass';
import gulpSass from 'gulp-sass';
import sourceMaps from 'gulp-sourcemaps';
import OVERRIDES from '../src/overrides.mjs';
import { DateTime } from 'luxon';
// get cloudfront
import reader from '../src/playlist-reader.mjs';
@@ -110,10 +111,9 @@ const htmlSources = [
const packageJson = await readFile('package.json');
let { version } = JSON.parse(packageJson);
const previewVersion = async () => {
// generate a relatively unique timestamp for cache invalidation of the preview site
const now = new Date();
const msNow = now.getTime() % 1_000_000;
version = msNow.toString();
// generate a unique timestamp for cache invalidation of the preview site
const now = DateTime.utc();
version = now.toFormat('yyyyLLddHHmm').substring(3);
};
const compressHtml = async () => src(htmlSources)
@@ -222,7 +222,7 @@ const buildDist = series(clean, parallel(buildJs, compressJsVendor, buildCss, co
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
const publishFrontend = series(buildDist, uploadImages, upload, invalidate, logVersion);
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview);
const stageFrontend = series(previewVersion, buildDist, uploadImagesPreview, uploadPreview, invalidatePreview, logVersion);
export default publishFrontend;

4
package-lock.json generated
View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

BIN
server/images/gimp/1.xcf Normal file

Binary file not shown.

Binary file not shown.

View File

@@ -72,7 +72,7 @@ const init = async () => {
if (!navigator.geolocation) btnGetGps.style.display = 'none';
document.querySelector('#divTwc').addEventListener('mousemove', () => {
if (document.fullscreenElement) updateFullScreenNavigate();
if (document.fullscreenElement || settings.kiosk?.value) updateFullScreenNavigate();
});
document.querySelector('#btnGetLatLng').addEventListener('click', () => autoComplete.directFormSubmit());
@@ -141,7 +141,7 @@ const init = async () => {
}
// Handle kiosk mode initialization
const urlKioskCheckbox = parsedParameters['settings-kiosk-checkbox'];
const urlKioskCheckbox = parsedParameters?.kiosk ?? parsedParameters['settings-kiosk-checkbox'];
// If kiosk=false is specified, disable kiosk mode and clear any stored value
if (urlKioskCheckbox === 'false') {
@@ -384,7 +384,7 @@ const updateFullScreenNavigate = () => {
}
navigateFadeIntervalId = setTimeout(() => {
if (document.fullscreenElement) {
if (document.fullscreenElement || settings.kiosk?.value) {
divTwcBottom.classList.remove('visible');
divTwcBottom.classList.add('hidden');
document.querySelector('#divTwc').classList.add('no-cursor');

View File

@@ -69,7 +69,7 @@ class Almanac extends WeatherDisplay {
// stop after 30 days or 4 moon phases
iterations += 1;
} while (iterations <= 30 && moon.length < 4);
} while (iterations <= 45 && moon.length < 5);
return {
sun,
@@ -123,21 +123,16 @@ class Almanac extends WeatherDisplay {
// Set day names
const Today = DateTime.local();
const Tomorrow = Today.plus({ days: 1 });
this.elem.querySelector('.day-1').textContent = Today.toLocaleString({ weekday: 'long' });
this.elem.querySelector('.day-2').textContent = Tomorrow.toLocaleString({ weekday: 'long' });
// fill all three days, even if some are hidden by non-enhanced
for (let i = 0; i < 3; i += 1) {
this.elem.querySelector(`.day-${i}`).textContent = Today.plus({ days: i }).toLocaleString({ weekday: 'long' });
const todaySunrise = DateTime.fromJSDate(info.sun[0].sunrise);
const todaySunset = DateTime.fromJSDate(info.sun[0].sunset);
const [todaySunriseFormatted, todaySunsetFormatted] = formatTimesForColumn([todaySunrise, todaySunset]);
this.elem.querySelector('.rise-1').textContent = todaySunriseFormatted;
this.elem.querySelector('.set-1').textContent = todaySunsetFormatted;
const tomorrowSunrise = DateTime.fromJSDate(info.sun[1].sunrise);
const tomorrowSunset = DateTime.fromJSDate(info.sun[1].sunset);
const [tomorrowSunriseFormatted, tomorrowSunsetformatted] = formatTimesForColumn([tomorrowSunrise, tomorrowSunset]);
this.elem.querySelector('.rise-2').textContent = tomorrowSunriseFormatted;
this.elem.querySelector('.set-2').textContent = tomorrowSunsetformatted;
const sunrise = DateTime.fromJSDate(info.sun[i].sunrise);
const sunset = DateTime.fromJSDate(info.sun[i].sunset);
const [sunriseFormatted, sunsetFormatted] = formatTimesForColumn([sunrise, sunset]);
this.elem.querySelector(`.rise-${i}`).textContent = sunriseFormatted;
this.elem.querySelector(`.set-${i}`).textContent = sunsetFormatted;
}
// Moon data
const days = info.moon.map((MoonPhase) => {

View File

@@ -14,9 +14,7 @@ import {
import { debugFlag } from './utils/debug.mjs';
import { isDataStale, enhanceObservationWithMapClick } from './utils/mapclick.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'];
import settings from './settings.mjs';
class CurrentWeather extends WeatherDisplay {
constructor(navId, elemId) {
@@ -29,8 +27,8 @@ class CurrentWeather extends WeatherDisplay {
// 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
// filter for 4-letter observation stations, only those contain sky conditions and thus an icon
const filteredStations = this.weatherParameters.stations.filter((station) => station?.properties?.stationIdentifier?.length === 4 && !skipStations.includes(station.properties.stationIdentifier.slice(0, 1)));
// get the available stations
const { stations } = this.weatherParameters;
// Load the observations
let observations;
@@ -38,9 +36,9 @@ class CurrentWeather extends WeatherDisplay {
// station number counter
let stationNum = 0;
while (!observations && stationNum < filteredStations.length) {
while (!observations && stationNum < stations.length) {
// get the station
station = filteredStations[stationNum];
station = stations[stationNum];
const stationId = station.properties.stationIdentifier;
stationNum += 1;
@@ -104,7 +102,10 @@ class CurrentWeather extends WeatherDisplay {
debugContext: 'currentweather',
});
// copy enhanced data and restore the timestamp if it was overwritten by older data from mapclick
candidateObservation.features[0].properties = enhancedResult.data;
const { missingFields } = enhancedResult;
const missingRequired = missingFields.filter((fieldName) => {
const field = requiredFields.find((f) => f.name === fieldName && f.required);
@@ -192,7 +193,9 @@ class CurrentWeather extends WeatherDisplay {
const wind = (typeof this.data.WindSpeed === 'number') ? this.data.WindDirection.padEnd(3, '') + this.data.WindSpeed.toString().padStart(3, ' ') : this.data.WindSpeed;
// get location (city name) from StationInfo if available (allows for overrides)
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, 20);
// longer name allowed if in wide-enhanced
const locationLimit = (settings.wide?.value && settings.enhanced?.value) ? 25 : 20;
const location = (StationInfo[this.data.station.properties.stationIdentifier]?.city ?? locationCleanup(this.data.station.properties.name)).substr(0, locationLimit);
const fill = {
temp: this.data.Temperature + String.fromCharCode(176),

View File

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

View File

@@ -17,7 +17,7 @@ class ExtendedForecast extends WeatherDisplay {
super(navId, elemId, 'Extended Forecast', true);
// set timings
if (settings.enhancedScreens?.value) {
if (settings.portrait?.value) {
this.timing.totalScreens = 1;
this.perPage = 4;
} else {

View File

@@ -18,7 +18,7 @@ const scaling = () => {
xTicks: 4,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
if (settings.wide?.value && settings.enhanced?.value) {
available.width = available.width + 107 + 107;
available.height = 285;
dataLength.hours = 48;

View File

@@ -159,12 +159,17 @@ class LatestObservations extends WeatherDisplay {
const windDirection = directionToNSEW(condition.windDirection.value);
const Temperature = temperatureConverter(condition.temperature.value);
const Like = likeTemperature(condition.heatIndex?.value, condition.windChill?.value, Temperature, temperatureConverter);
const WindSpeed = windConverter(condition.windSpeed.value);
const locationLimit = (settings.wide?.value && settings.enhanced?.value) ? 20 : 14;
const weatherLimit = (settings.wide?.value && settings.enhanced?.value) ? 10 : 9;
const fill = {
location: locationCleanup(condition.city).substr(0, 14),
location: locationCleanup(condition.city).substr(0, locationLimit),
temp: Temperature,
weather: shortenCurrentConditions(condition.textDescription).substr(0, 9),
like: Like.value,
weather: shortenCurrentConditions(condition.textDescription).substr(0, weatherLimit),
};
if (WindSpeed > 0) {
@@ -175,7 +180,12 @@ class LatestObservations extends WeatherDisplay {
fill.wind = 'Calm';
}
return this.fillTemplate('observation-row', fill);
const filledRow = this.fillTemplate('observation-row', fill);
// add the feels like class
if (Like.cssClass) filledRow.querySelector('.like').classList.add(Like.cssClass);
return filledRow;
});
const linesContainer = this.elem.querySelector('.observation-lines');
@@ -186,6 +196,25 @@ class LatestObservations extends WeatherDisplay {
}
}
// generate a "feels like" temperature from heat index and wind chill.
const likeTemperature = (heat, wind, actual, converter) => {
// figure out the feels like value
let value = '';
if (heat) value = converter(heat);
if (wind) value = converter(wind);
// determine if there's a red/blue color class to add
let cssClass;
if (value !== '') {
if (value > actual) cssClass = 'heat-index';
if (value < actual) cssClass = 'wind-chill';
}
return {
value,
cssClass,
};
};
const shortenCurrentConditions = (_condition) => {
let condition = _condition;
condition = condition.replace(/Light/, 'L');

View File

@@ -37,9 +37,9 @@ class LocalForecast extends WeatherDisplay {
// read each text
this.screenTexts = conditions.map((condition) => {
// process the text
let text = `${condition.DayName.toUpperCase()}...`;
let text = `${condition.DayName}...`;
const conditionText = condition.Text;
text += conditionText.toUpperCase().replace('...', ' ');
text += conditionText.replace('...', ' ');
return text;
});
@@ -257,7 +257,7 @@ const parse = (forecast, forecastUrl) => {
return activePeriods.slice(0, 6).map((text) => ({
// format day and text
DayName: text.name.toUpperCase(),
DayName: text.name,
Text: text.detailedForecast,
}));
};

View File

@@ -1,6 +1,5 @@
import { text } from './utils/fetch.mjs';
import Setting from './utils/setting.mjs';
import { registerHiddenSetting } from './share.mjs';
let playlist;
let currentTrack = 0;
@@ -33,9 +32,6 @@ document.addEventListener('DOMContentLoaded', () => {
// get the playlist
getMedia();
// register the volume setting
registerHiddenSetting(mediaVolume.elemId, mediaVolume);
});
const scanMusicDirectory = async () => {
@@ -246,6 +242,7 @@ const mediaVolume = new Setting('mediaVolume', {
[0.25, '25%'],
],
changeAction: setVolume,
visible: false,
});
const initializePlayer = () => {

View File

@@ -6,6 +6,7 @@ import { safeJson } from './utils/fetch.mjs';
import { getPoint } from './utils/weather.mjs';
import { debugFlag } from './utils/debug.mjs';
import settings from './settings.mjs';
import { stationFilter } from './utils/string.mjs';
document.addEventListener('DOMContentLoaded', () => {
init();
@@ -37,6 +38,11 @@ const init = async () => {
resizeTimeout = setTimeout(() => resize(), 100);
});
// redraw current screen (typically from enhanced setting change)
window.addEventListener('redraw', () => {
currentDisplay()?.drawCanvas();
});
// Handle orientation changes (Mobile Safari doesn't always fire resize events on orientation change)
window.addEventListener('orientationchange', () => {
if (debugFlag('resize')) {
@@ -85,7 +91,15 @@ const getWeather = async (latLon, haveDataCallback) => {
return;
}
const StationId = stations.features[0].properties.stationIdentifier;
// filter stations for proper format
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;
const { state } = point.properties.relativeLocation.properties;
@@ -108,7 +122,7 @@ const getWeather = async (latLon, haveDataCallback) => {
weatherParameters.timeZone = point.properties.timeZone;
weatherParameters.forecast = point.properties.forecast;
weatherParameters.forecastGridData = point.properties.forecastGridData;
weatherParameters.stations = stations.features;
weatherParameters.stations = stationsFiltered;
weatherParameters.relativeLocation = point.properties.relativeLocation.properties;
// update the main process for display purposes

View File

@@ -4,7 +4,7 @@ const radarFinalSize = () => {
const size = {
width: 640, height: 367,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
if (settings.wide?.value && settings.enhanced?.value) {
size.width = 854;
}
return size;
@@ -15,7 +15,7 @@ const radarSourceSize = () => {
width: 240,
height: 163,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
if (settings.wide?.value && settings.enhanced?.value) {
size.width = 240 / 640 * 854; // original size of 640 scaled up to wide at 854
}
return size;
@@ -26,7 +26,7 @@ const radarOffset = () => {
x: 240,
y: 138,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
if (settings.wide?.value && settings.enhanced?.value) {
// 107 is the margins shift, 640/854 is the scaling factor normal => wide, /2 is because of the fixed 2:1 scaling between source radar and map tiles
offset.x = 240 + (107 * 640 / 854 / 2); // original size of 640 scaled up to wide at 854;
}
@@ -40,7 +40,7 @@ const radarShift = () => {
x: 0,
y: 0,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
if (settings.wide?.value && settings.enhanced?.value) {
shift.x = 107;
}
return shift;

View File

@@ -9,10 +9,12 @@ const pixelToFile = (xPixel, yPixel) => {
return `${yTile}-${xTile}`;
};
// convert a pixel location in the overall map to a pixel location on the tile
// convert a pixel location in the overall map to a pixel location on the tile set
const modTile = (xPixel, yPixel) => {
const x = Math.round(xPixel) % TILE_SIZE.x;
const y = Math.round(yPixel) % TILE_SIZE.y;
// adjust for additional 1 tile when odd
const x = (Math.floor(xPixel) % (TILE_SIZE.x));
const y = (Math.floor(yPixel) % (TILE_SIZE.y));
return { x, y };
};
@@ -46,11 +48,11 @@ const setTiles = (data) => {
const tileShift = modTile(sourceXY.x, sourceXY.y);
// determine which tiles are used
const secondRow = TILE_SIZE.y - tileShift.y < RADAR_FINAL_SIZE().width;
const secondRow = tileShift.y + TILE_SIZE.y > RADAR_FINAL_SIZE().height;
const usedTiles = [
true,
TILE_SIZE.x - tileShift.x < RADAR_FINAL_SIZE().width,
TILE_SIZE.x - (tileShift.x * 2) < RADAR_FINAL_SIZE().width,
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE().width,
tileShift.x + (TILE_SIZE.x * 2) > RADAR_FINAL_SIZE().width,
secondRow,
];
// second row is a copy of the first row when in use

View File

@@ -29,7 +29,7 @@ const scaling = () => {
y: 117,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
if (settings.wide?.value && settings.enhanced?.value) {
mapOffsetXY.x = 320;
available.x = 854;
}

View File

@@ -10,6 +10,11 @@ const deferredDomSettings = new Set();
// don't show checkboxes for these settings
const hiddenSettings = [
'scanLines',
// wide, portrait and enhanced are handled by a dropdown which sets these individual settings accordingly
'wide',
'portrait',
'enhanced',
];
// Declare change functions first, before they're referenced in init() to avoid the Temporal Dead Zone (TDZ)
@@ -32,7 +37,26 @@ const wideScreenChange = (value) => {
window.dispatchEvent(new Event('resize'));
};
const enhancedScreenChange = (value) => {
const portraitChange = (value) => {
const container = document.querySelector('#divTwc');
if (!container) {
// DOM not ready; defer enabling if set
if (value) {
deferredDomSettings.add('portrait');
}
return;
}
if (value) {
container.classList.add('portrait');
} else {
container.classList.remove('portrait');
}
// Trigger resize to recalculate scaling for new width
window.dispatchEvent(new Event('resize'));
};
const enhancedChange = (value) => {
const container = document.querySelector('#divTwc');
if (!container) {
// DOM not ready; defer enabling if set
@@ -48,7 +72,32 @@ const enhancedScreenChange = (value) => {
container.classList.remove('enhanced');
}
// Trigger resize to recalculate scaling for new width
window.dispatchEvent(new Event('resize'));
window.dispatchEvent(new Event('redraw'));
};
const viewModeChange = (value) => {
// set the appropriate mode bits which triggers change actions above
switch (value) {
case 'wide':
settings.wide.value = true;
settings.enhanced.value = false;
settings.portrait.value = false;
break;
case 'wide-enhanced':
settings.wide.value = true;
settings.enhanced.value = true;
settings.portrait.value = false;
break;
case 'portrait-enhanced':
settings.wide.value = false;
settings.enhanced.value = true;
settings.portrait.value = true;
break;
default:
settings.wide.value = false;
settings.enhanced.value = false;
settings.portrait.value = false;
}
};
const kioskChange = (value) => {
@@ -63,9 +112,11 @@ const kioskChange = (value) => {
if (value) {
body.classList.add('kiosk');
document.querySelector('#divTwc')?.classList.add('no-cursor');
window.dispatchEvent(new Event('resize'));
} else {
body.classList.remove('kiosk');
document.querySelector('#divTwc')?.classList.remove('no-cursor');
window.dispatchEvent(new Event('resize'));
}
@@ -151,15 +202,37 @@ const init = () => {
});
settings.portrait = new Setting('portrait', {
name: 'Allow Portrait',
changeAction: portraitChange,
defaultValue: false,
sticky: true,
});
settings.enhancedScreens = new Setting('enhancedScreens', {
settings.enhanced = new Setting('enhanced', {
name: 'Enhanced Screens',
defaultValue: false,
changeAction: enhancedScreenChange,
changeAction: enhancedChange,
sticky: true,
});
// widescreen, portrait and enhanced are handled by a dropdown
// the dropdown change action sets the above bits accordingly
// first, figure out the default value based on other settings
// this also enforces rules on how these can be combined
let viewModeDefault = 'standard';
if (settings.wide.value && !settings.enhanced.value) viewModeDefault = 'wide';
if (settings.wide.value && settings.enhanced.value) viewModeDefault = 'wide-enhanced';
if (settings.portrait.value) viewModeDefault = 'portrait-enhanced';
settings.viewMode = new Setting('viewMode', {
name: 'Display mode',
type: 'select',
defaultValue: viewModeDefault,
changeAction: viewModeChange,
sticky: false, // not sticky because the above 3 settings are sticky and define this item's starting state
values: [
['standard', 'Standard'],
['wide', 'Widescreen'],
['wide-enhanced', 'Widescreen enhanced'],
['portrait-enhanced', 'Portrait enhanced'],
],
});
settings.kiosk = new Setting('kiosk', {
name: 'Kiosk',
defaultValue: false,
@@ -202,6 +275,7 @@ const init = () => {
['medium', 'Medium (2x)'],
['thick', 'Thick (3x)'],
],
visible: false,
});
settings.units = new Setting('units', {
name: 'Units',
@@ -251,7 +325,7 @@ document.addEventListener('DOMContentLoaded', () => {
const settingHtml = Object.values(settings).map((setting) => {
if (hiddenSettings.includes(setting.shortName)) {
// setting is hidden, register it
registerHiddenSetting(setting.elemId, setting);
registerHiddenSetting(setting.shortName, setting);
return false;
}
// generate HTML for setting
@@ -269,7 +343,6 @@ document.addEventListener('DOMContentLoaded', () => {
} else if (modeSelect) {
modeSelect.style.display = 'none';
}
registerHiddenSetting('settings-scanLineMode-select', settings.scanLineMode);
});
export default settings;

View File

@@ -25,22 +25,28 @@ const createLink = async (e) => {
const queryStringElements = {};
elemForEach('input[type=checkbox]', (elem) => {
if (elem?.id) {
queryStringElements[elem.id] = elem?.checked ?? false;
// use name, and fallback to id (older prefix/suffix permalinks)
const key = elem?.name ?? elem?.id;
if (key) {
queryStringElements[key] = elem?.checked ?? false;
}
});
// get all select boxes
elemForEach('select', (elem) => {
if (elem?.id) {
queryStringElements[elem.id] = encodeURIComponent(elem?.value ?? '');
// use name, and fallback to id (older prefix/suffix permalinks)
const key = elem?.name ?? elem?.id;
if (key) {
queryStringElements[key] = encodeURIComponent(elem?.value ?? '');
}
});
// get all text boxes
elemForEach('input[type=text]', ((elem) => {
if (elem?.id) {
queryStringElements[elem.id] = elem?.value ?? 0;
// use name, and fallback to id (older prefix/suffix permalinks)
const key = elem?.name ?? elem?.id;
if (key && key !== '') {
queryStringElements[key] = elem?.value ?? 0;
}
}));

View File

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

View File

@@ -41,7 +41,9 @@ class Setting {
this.elemId = `settings-${shortName}-${this.type}`;
// get value from url
const urlValue = parseQueryString()?.[this.elemId];
// includes a fallback to the older prefix/suffix version
const queryString = parseQueryString();
const urlValue = queryString?.[shortName] ?? queryString?.[this.elemId];
let urlState;
if (this.type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true';
@@ -92,7 +94,7 @@ class Setting {
const select = document.createElement('select');
select.id = `settings-${this.shortName}-select`;
select.name = `settings-${this.shortName}-select`;
select.name = this.shortName;
select.addEventListener('change', (e) => this.selectChange(e));
this.values.forEach(([value, text]) => {
@@ -125,7 +127,7 @@ class Setting {
checkbox.type = 'checkbox';
checkbox.value = true;
checkbox.id = `settings-${this.shortName}-checkbox`;
checkbox.name = `settings-${this.shortName}-checkbox`;
checkbox.name = this.shortName;
checkbox.checked = this.myValue;
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
const span = document.createElement('span');
@@ -148,14 +150,14 @@ class Setting {
textInput.type = 'text';
textInput.value = this.myValue;
textInput.id = `settings-${this.shortName}-string`;
textInput.name = `settings-${this.shortName}-string`;
textInput.name = this.shortName;
textInput.placeholder = this.placeholder;
// set button
const setButton = document.createElement('input');
setButton.type = 'button';
setButton.value = 'Set';
setButton.id = `settings-${this.shortName}-button`;
setButton.name = `settings-${this.shortName}-button`;
setButton.name = this.shortName;
setButton.addEventListener('click', () => {
this.stringChange({ target: { value: textInput.value } });
});

View File

@@ -13,7 +13,12 @@ const locationCleanup = (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 {
// eslint-disable-next-line import/prefer-default-export
locationCleanup,
stationFilter,
};

View File

@@ -55,8 +55,9 @@ class WeatherDisplay {
// no checkbox if progress
if (this.elemId === 'progress') return false;
// get url provided state
const urlValue = parseQueryString()?.[`${this.elemId}-checkbox`];
// get url provided state, and fall back to the older suffix naming convention
const queryString = parseQueryString();
const urlValue = queryString?.[this.elemId] ?? queryString?.[`${this.elemId}-checkbox`];
let urlState;
if (urlValue !== undefined) {
urlState = urlValue === 'true';
@@ -78,7 +79,7 @@ class WeatherDisplay {
checkbox.type = 'checkbox';
checkbox.value = true;
checkbox.id = `${this.elemId}-checkbox`;
checkbox.name = `${this.elemId}-checkbox`;
checkbox.name = this.elemId;
checkbox.checked = this.isEnabled;
checkbox.addEventListener('change', (e) => this.checkboxChange(e));
const span = document.createElement('span');

View File

@@ -1,8 +1,13 @@
@use 'shared/_colors' as c;
@use 'shared/_utils' as u;
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#almanac-html.weather-display {
background-image: url('../images/backgrounds/3.png');
// repeat the background if wide-enhanced
.wide.enhanced & {
background-image: url('../images/backgrounds/3-wide-enhanced.png');
}
}
.weather-display .main.almanac {
@@ -14,13 +19,17 @@
// Use CSS Grid for cross-browser consistency
// Grid is populated in reading order (left-to-right, top-to-bottom):
display: grid;
grid-template-columns: auto auto auto;
grid-template-rows: auto auto auto;
grid-template-columns: repeat(3, auto);
grid-template-rows: repeat(3, auto);
gap: 0px 90px;
margin: 3px auto 5px auto; // align the bottom of the div with the background
width: fit-content;
line-height: 30px;
.wide.enhanced & {
grid-template-columns: repeat(4, auto);
}
.grid-item {
// Reset inherited styles that interfere with grid layout
width: auto;
@@ -45,6 +54,14 @@
&.time {
text-align: center;
}
&.wide-enhanced {
display: none;
.wide.enhanced & {
display: block;
}
}
}
}
@@ -58,6 +75,10 @@
padding-left: 13px;
}
.days {
text-align: center;
}
.day {
display: inline-block;
text-align: center;
@@ -77,4 +98,4 @@
}
}

View File

@@ -14,6 +14,12 @@
padding-top: 10px;
position: absolute;
.wide.enhanced & {
width: 300px;
margin-left: 25px;
margin-right: 25px;
}
@include u.text-shadow();
&.left {

View File

@@ -84,11 +84,11 @@
left: 425px;
&.heat-index {
color: #e00;
color: c.$heat-index;
}
&.wind-chill {
color: c.$extended-low;
color: c.$wind-chill;
}
}

View File

@@ -32,6 +32,12 @@
display: inline-block;
}
}
.like {
display: none;
}
}
.temp {
@@ -46,6 +52,41 @@
left: 430px;
}
.like {
display: none;
}
// wide and enhanced moves the columns and enables the like column
.wide.enhanced & {
.temp {
left: 320px;
}
.like {
left: 380px;
display: block;
&.heat-index {
color: c.$heat-index;
display: block;
}
&.wind-chill {
display: block;
color: c.$wind-chill;
}
}
.weather {
left: 470px;
}
.wind {
left: 630px;
}
}
.observation-lines {
min-height: 338px;
padding-top: 10px;

View File

@@ -25,7 +25,6 @@
.forecast {
font-family: 'Star4000';
font-size: 24pt;
text-transform: uppercase;
@include u.text-shadow();
min-height: 280px;
line-height: 40px;

View File

@@ -825,4 +825,19 @@ body.kiosk #loading .instructions {
display: grid;
grid-template-columns: 1fr 1fr;
max-width: 250px;
}
.lower-flex-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
align-items: flex-start;
align-content: stretch;
gap: 8px;
column-gap: 64px;
.item {
display: inline-block;
}
}

View File

@@ -5,6 +5,10 @@
#radar-html.weather-display {
background-image: url('../images/backgrounds/4.png');
.wide & {
background: url(../images/backgrounds/4-wide.png);
}
.header {
height: 83px;
@@ -122,8 +126,4 @@
position: relative;
}
}
}
.wide.radar #container {
background: url(../images/backgrounds/4-wide.png);
}

View File

@@ -1,10 +1,6 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
#regional-forecast-html.weather-display {
background-image: url('../images/backgrounds/5.png');
}
.weather-display .main.regional-forecast {

View File

@@ -16,6 +16,13 @@
background-repeat: no-repeat;
}
.wide.enhanced & {
&:has(.can-enhance) {
background-image: url(../images/backgrounds/1-wide-enhanced.png);
background-position-x: 0px;
}
}
/* this method is required to hide blocks so they can be measured while off screen */
height: 0px;
@@ -135,6 +142,12 @@
margin-left: p.$blue-box-margin;
margin-right: p.$blue-box-margin;
width: calc(100% - 128px);
.wide.enhanced & {
&.can-enhance {
width: calc(p.$wide-width - p.$blue-box-margin - p.$blue-box-margin)
}
}
}
}

View File

@@ -13,5 +13,7 @@ $gradient-loading-3: #4f99f9;
$gradient-loading-4: #8ffdfa;
$extended-low: #8080FF;
$wind-chill: #8080FF;
$heat-index: #e00;
$blue-box: #26235a;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -68,10 +68,10 @@
</head>
<body <% if (query && query['settings-kiosk-checkbox'] === 'true' ) { %>class="kiosk" <% }%>>
<body <% if (query && (query['kiosk'] === true || query['settings-kiosk-checkbox'] === 'true' )) { %>class="kiosk" <% }%>>
<div id="divQuery">
<input id="txtLocation" type="text" value="" placeholder="ZIP Code or City, State" data-1p-ignore />
<input id="txtLocation" name="txtLocation" type="text" value="" placeholder="ZIP Code or City, State" data-1p-ignore />
<div class="buttons">
<button id="btnGetGps" type="button" title="Get GPS Location"><img src="images/nav/ic_gps_fixed_black_18dp_1x.png" class="light" />
<img src="images/nav/ic_gps_fixed_white_18dp_1x.png" class="dark" />
@@ -174,21 +174,25 @@
</div>
<div class="media"></div>
<div class='heading'>Selected displays</div>
<div id='enabledDisplays'>
<div class='lower-flex-container'>
<div class='item'>
<div class='heading'>Selected displays</div>
<div id='enabledDisplays'>
</div>
<div class='heading'>Settings</div>
<div id='settings'>
</div>
<div class='heading'>Sharing</div>
<div class='info'>
<a href='' id='share-link'>Copy Permalink</a> <span id="share-link-copied">Link copied to clipboard!</span>
<div id="share-link-instructions">
Copy this long URL:
<input type='text' id="share-link-url">
</div>
</div>
<div class='item'>
<div class='heading'>Settings</div>
<div id='settings'>
<div class='heading'>Sharing</div>
</div>
<div class='item'>
<a href='' id='share-link'>Copy Permalink</a> <span id="share-link-copied">Link copied to clipboard!</span>
<div id="share-link-instructions">
Copy this long URL:
<input type='text' id="share-link-url">
</div>
</div>
</div>
</div>
@@ -211,7 +215,6 @@
<div class="header">Ws4kp Version:</div>
<div class="header"><span><%- version %></span></div>
</div>
</div>
</body>

View File

@@ -1,15 +1,18 @@
<%- include('header.ejs', {title:'Almanac', hasTime: true}) %>
<div class="main has-scroll almanac">
<div class="main has-scroll almanac can-enhance">
<div class="sun">
<div class="grid-item empty"></div>
<div class="grid-item header day-0"></div>
<div class="grid-item header day-1"></div>
<div class="grid-item header day-2"></div>
<div class="grid-item header day-2 wide-enhanced"></div>
<div class="grid-item row-label">Sunrise:</div>
<div class="grid-item time rise-0"></div>
<div class="grid-item time rise-1"></div>
<div class="grid-item time rise-2"></div>
<div class="grid-item time rise-2 wide-enhanced"></div>
<div class="grid-item row-label">Sunset:</div>
<div class="grid-item time set-0"></div>
<div class="grid-item time set-1"></div>
<div class="grid-item time set-2"></div>
<div class="grid-item time set-2 wide-enhanced"></div>
</div>
<div class="moon">
<div class="title">Moon Data:</div>

View File

@@ -1,5 +1,5 @@
<%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %>
<div class="main has-scroll has-box current-weather">
<div class="main has-scroll has-box current-weather can-enhance">
<div class="weather template">
<div class="left col">
<div class="temp center"></div>

View File

@@ -1,9 +1,10 @@
<%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %>
<div class="main has-scroll latest-observations has-box">
<div class="main has-scroll latest-observations has-box can-enhance">
<div class="container">
<div class="column-headers">
<div class="temp english">&deg;F</div>
<div class="temp metric">&deg;C</div>
<div class="like">Like</div>
<div class="weather">Weather</div>
<div class="wind">Wind</div>
</div>
@@ -11,6 +12,7 @@
<div class="observation-row template">
<div class="location"></div>
<div class="temp"></div>
<div class="like"></div>
<div class="weather"></div>
<div class="wind"></div>
</div>

View File

@@ -1,12 +1,12 @@
<%- include('header.ejs', {titleDual:{ top: 'Local' , bottom: 'Forecast' }, hasTime: true, noaaLogo: true}) %>
<div class="main has-scroll has-box local-forecast">
<div class="container">
<div class="forecasts">
<div class="forecast template">
<div class="text">
</div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>
<div class="main has-scroll has-box local-forecast can-enhance">
<div class="container">
<div class="forecasts">
<div class="forecast template">
<div class="text">
</div>
</div>
</div>
</div>
</div>
<%- include('scroll.ejs') %>