Compare commits

...

24 Commits

Author SHA1 Message Date
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
Matt Walsh
63d27d1a26 fix wide-enhanced radar tiles #188 #196 #193 #194 2026-04-06 16:53:11 -05:00
Matt Walsh
97ac0a1656 radar expanded, to do fix number of tiles 2026-04-06 16:41:03 -05:00
Matt Walsh
8158afd039 fix radar crop when wide 2026-04-06 00:48:38 -05:00
Matt Walsh
5fffc495ae regional map enhanced #188 #193 #196 2026-04-06 00:35:24 -05:00
Matt Walsh
b2a424a64f css constants cleanup 2026-04-06 00:06:01 -05:00
Matt Walsh
9f6b90919c hourly enhanced #188 #195 #196 #193 #194 2026-04-05 23:58:50 -05:00
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
Matt Walsh
2a4dc03cf7 don't build screen-enhance images 2026-04-04 11:02:12 -05:00
Matt Walsh
8c13128005 add screen enhancement template 2026-04-04 10:45:31 -05:00
Matt Walsh
942fa8b817 6.5.4 2026-03-26 14:45:41 -05:00
Matt Walsh
15b68eba2f fix extended forecast day names close #190 2026-03-26 14:44:13 -05:00
Matt Walsh
933a289d03 update dependencies 2026-03-26 14:12:38 -05:00
52 changed files with 1349 additions and 940 deletions

View File

@@ -0,0 +1,11 @@
---
name: Screen Enhancement
about: Items and tasks related to the screen enhancement project
title: '[Project]: '
labels: screen-enhance
projects: ['netbymatt/5']
assignees: ''
---
Describe the task, how it affects the overall project and what is considered complete.

View File

@@ -4,6 +4,8 @@ on:
push:
branches:
- '**'
- '!screen-enhance'
- '!screen-enhance/**'
tags:
- 'v*.*.*'
- 'v*.*'

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,7 +15,7 @@ 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';
@@ -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;

1358
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Binary file not shown.

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

Binary file not shown.

View File

@@ -47,10 +47,7 @@ class Almanac extends WeatherDisplay {
}
calcSunMoonData(weatherParameters) {
const sun = [
SunCalc.getTimes(new Date(), weatherParameters.latitude, weatherParameters.longitude),
SunCalc.getTimes(DateTime.local().plus({ days: 1 }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude),
];
const sun = [0, 1, 2, 3, 4, 5, 6].map((days) => SunCalc.getTimes(DateTime.local().plus({ days }).toJSDate(), weatherParameters.latitude, weatherParameters.longitude));
// brute force the moon phases by scanning the next 30 days
const moon = [];
@@ -72,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,
@@ -126,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,11 @@ class CurrentWeather extends WeatherDisplay {
debugContext: 'currentweather',
});
// copy enhanced data and restore the timestamp if it was overwritten by older data from mapclick
const { timestamp } = candidateObservation.features[0].properties;
candidateObservation.features[0].properties = enhancedResult.data;
candidateObservation.features[0].properties.timestamp = timestamp;
const { missingFields } = enhancedResult;
const missingRequired = missingFields.filter((fieldName) => {
const field = requiredFields.find((f) => f.name === fieldName && f.required);
@@ -192,7 +194,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.enhancedScreens?.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,13 @@ class ExtendedForecast extends WeatherDisplay {
super(navId, elemId, 'Extended Forecast', true);
// set timings
this.timing.totalScreens = 2;
if (settings.portrait?.value) {
this.timing.totalScreens = 1;
this.perPage = 4;
} else {
this.timing.totalScreens = 2;
this.perPage = 3;
}
}
async getData(weatherParameters, refresh) {
@@ -54,7 +60,7 @@ class ExtendedForecast extends WeatherDisplay {
// determine bounds
// 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
const days = forecast.map((Day) => {
@@ -97,11 +103,9 @@ const parse = (fullForecast, forecastUrl) => {
// Skip the first period if it's nighttime (like "Tonight") since extended forecast
// should focus on upcoming full days, not the end of the current day
let startIndex = 0;
let dateOffset = 0; // offset for date labels when we skip periods
if (activePeriods.length > 0 && !activePeriods[0].isDaytime) {
startIndex = 1;
dateOffset = 1; // start date labels from tomorrow since we're skipping tonight
if (debugFlag('extendedforecast')) {
console.log(`ExtendedForecast: Skipping first period "${activePeriods[0].name}" because it's nighttime`);
}
@@ -111,25 +115,14 @@ const parse = (fullForecast, forecastUrl) => {
}
}
// create a list of days starting with the appropriate day
const Days = [0, 1, 2, 3, 4, 5, 6];
const dates = Days.map((shift) => {
const date = DateTime.local().startOf('day').plus({ days: shift + dateOffset });
return date.toLocaleString({ weekday: 'short' });
});
if (debugFlag('extendedforecast')) {
console.log(`ExtendedForecast: Generated date labels: [${dates.join(', ')}]`);
}
// track the destination forecast index
let destIndex = 0;
const forecast = [];
// if the first period is nighttime it is skipped above via startIndex
for (let i = startIndex; i < activePeriods.length; i += 1) {
const period = activePeriods[i];
// create the destination object if necessary
if (!forecast[destIndex]) {
forecast.push({
dayName: '', low: undefined, high: undefined, text: undefined, icon: undefined,
@@ -143,7 +136,7 @@ const parse = (fullForecast, forecastUrl) => {
fDay.high = period.temperature;
fDay.icon = getLargeIcon(period.icon);
fDay.text = shortenExtendedForecastText(period.shortForecast);
fDay.dayName = dates[destIndex];
fDay.dayName = DateTime.fromISO(period.startTime).startOf('day').toLocaleString({ weekday: 'short' });
// preload the icon
preloadImg(fDay.icon);
// Wait for the corresponding night period to increment

View File

@@ -5,10 +5,30 @@ import getHourlyData from './hourly.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay, timeZone } from './navigation.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import settings from './settings.mjs';
// get available space
const availableWidth = 532;
const availableHeight = 285;
// set up spacing and scales
const scaling = () => {
const available = {
width: 532,
height: 285,
};
const dataLength = {
hours: 36,
xTicks: 4,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
available.width = available.width + 107 + 107;
available.height = 285;
dataLength.hours = 48;
dataLength.xTicks = 6;
}
return {
available,
dataLength,
};
};
class HourlyGraph extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
@@ -46,28 +66,43 @@ class HourlyGraph extends WeatherDisplay {
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
};
// get the data length for current settings
const { dataLength } = scaling();
// clamp down the data to the allowed size
Object.entries(this.data).forEach(([key, value]) => {
if (Array.isArray(value)) {
this.data[key] = value.slice(0, dataLength.hours);
}
});
this.setStatus(STATUS.loaded);
}
drawCanvas() {
// get scaling parameters
const { dataLength, available } = scaling();
// get the image
if (!this.image) this.image = this.elem.querySelector('.chart img');
this.image.width = availableWidth;
this.image.height = availableHeight;
// set up image
this.image.width = available.width;
this.image.height = available.height;
// get context
const canvas = document.createElement('canvas');
canvas.width = availableWidth;
canvas.height = availableHeight;
canvas.width = available.width;
canvas.height = available.height;
const ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
// calculate time scale
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
const timeStep = this.data.temperature.length / 4;
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, available.width);
const timeStep = this.data.temperature.length / (dataLength.xTicks);
const startTime = DateTime.now().startOf('hour');
let prevTime = startTime;
Array(5).fill().forEach((val, idx) => {
Array(dataLength.xTicks + 1).fill().forEach((val, idx) => {
// track the previous label so a day of week can be added when it changes
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
prevTime = label.ts;
@@ -77,7 +112,7 @@ class HourlyGraph extends WeatherDisplay {
// order is important last line drawn is on top
// clouds
const percentScale = calcScale(0, availableHeight - 10, 100, 10);
const percentScale = calcScale(0, available.height - 10, 100, 10);
const cloud = createPath(this.data.skyCover, timeScale, percentScale);
drawPath(cloud, ctx, {
strokeStyle: 'lightgrey',
@@ -97,7 +132,7 @@ class HourlyGraph extends WeatherDisplay {
const thirdScale = (maxScale - minScale) / 3;
const midScale1 = Math.round(minScale + thirdScale);
const midScale2 = Math.round(minScale + (thirdScale * 2));
const tempScale = calcScale(minScale, availableHeight - 10, maxScale, 10);
const tempScale = calcScale(minScale, available.height - 10, maxScale, 10);
// dewpoint
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);

View File

@@ -238,7 +238,7 @@ const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPr
};
// expand a set of values with durations to an hour-by-hour array
const expand = (data, maxHours = 36) => {
const expand = (data, maxHours = 48) => {
const startOfHour = DateTime.utc().startOf('hour').toMillis();
const result = []; // resulting expanded values
data.forEach((item) => {

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.enhancedScreens?.value) ? 20 : 14;
const weatherLimit = (settings.wide?.value && settings.enhancedScreens?.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
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

@@ -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

@@ -1,5 +1,56 @@
import settings from './settings.mjs';
const radarFinalSize = () => {
const size = {
width: 640, height: 367,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
size.width = 854;
}
return size;
};
const radarSourceSize = () => {
const size = {
width: 240,
height: 163,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
size.width = 240 / 640 * 854; // original size of 640 scaled up to wide at 854
}
return size;
};
const radarOffset = () => {
const offset = {
x: 240,
y: 138,
};
if (settings.wide?.value && settings.enhancedScreens?.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;
}
return offset;
};
// shift the base coordinates to align with enhanced radar window sizes
const radarShift = () => {
const shift = {
x: 0,
y: 0,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
shift.x = 107;
}
return shift;
};
export const TILE_SIZE = { x: 680, y: 387 };
export const TILE_COUNT = { x: 10, y: 11 };
export const TILE_FULL_SIZE = { x: 6800, y: 4255 };
export const RADAR_FULL_SIZE = { width: 2550, height: 1600 };
export const RADAR_FINAL_SIZE = { width: 640, height: 367 };
export const RADAR_FINAL_SIZE = radarFinalSize;
export const RADAR_SOURCE_SIZE = radarSourceSize;
export const RADAR_OFFSET = radarOffset;
export const RADAR_SHIFT = radarShift;

View File

@@ -1,5 +1,5 @@
import { removeDopplerRadarImageNoise } from './radar-utils.mjs';
import { RADAR_FULL_SIZE, RADAR_FINAL_SIZE } from './radar-constants.mjs';
import { RADAR_FULL_SIZE, RADAR_FINAL_SIZE, RADAR_SOURCE_SIZE } from './radar-constants.mjs';
// process a single radar image and place it on the provided canvas
const processRadar = async (data) => {
@@ -13,8 +13,8 @@ const processRadar = async (data) => {
// calculate offsets and sizes
const radarSource = {
width: 240,
height: 163,
width: RADAR_SOURCE_SIZE().width,
height: RADAR_SOURCE_SIZE().height,
x: Math.round(radarSourceXY.x / 2),
y: Math.round(radarSourceXY.y / 2),
};
@@ -52,11 +52,11 @@ const processRadar = async (data) => {
// stretch the radar image
const stretchCanvas = document.createElement('canvas');
stretchCanvas.width = RADAR_FINAL_SIZE.width;
stretchCanvas.height = RADAR_FINAL_SIZE.height;
stretchCanvas.width = RADAR_FINAL_SIZE().width;
stretchCanvas.height = RADAR_FINAL_SIZE().height;
const stretchContext = stretchCanvas.getContext('2d', { willReadFrequently: true });
stretchContext.imageSmoothingEnabled = false;
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, RADAR_FINAL_SIZE.width, RADAR_FINAL_SIZE.height);
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, RADAR_FINAL_SIZE().width, RADAR_FINAL_SIZE().height);
return stretchCanvas.toDataURL();
};

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 };
};
@@ -29,28 +31,33 @@ const setTiles = (data) => {
// determine the basemap images needed
const baseMapTiles = [
pixelToFile(sourceXY.x, sourceXY.y),
pixelToFile(sourceXY.x + TILE_SIZE.x, sourceXY.y),
pixelToFile(sourceXY.x, sourceXY.y + TILE_SIZE.y),
pixelToFile(sourceXY.x + TILE_SIZE.x, sourceXY.y + TILE_SIZE.y),
pixelToFile(sourceXY.x + TILE_SIZE.x * 0, sourceXY.y),
pixelToFile(sourceXY.x + TILE_SIZE.x * 1, sourceXY.y),
pixelToFile(sourceXY.x + TILE_SIZE.x * 2, sourceXY.y),
pixelToFile(sourceXY.x + TILE_SIZE.x * 0, sourceXY.y + TILE_SIZE.y),
pixelToFile(sourceXY.x + TILE_SIZE.x * 1, sourceXY.y + TILE_SIZE.y),
pixelToFile(sourceXY.x + TILE_SIZE.x * 2, sourceXY.y + TILE_SIZE.y),
];
// do some calculations
// the tiles are arranged as follows, with the horizontal axis as x, and correlating with the second set of digits in the image file number
// T[0] T[1]
// T[2] T[3]
// T[0] T[1] T[2]
// T[3] T[4] T[5]
// calculate the shift of tile 0 (upper left)
const tileShift = modTile(sourceXY.x, sourceXY.y);
// determine which tiles are used
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.y - tileShift.y < RADAR_FINAL_SIZE.width,
tileShift.x + TILE_SIZE.x > RADAR_FINAL_SIZE.width,
tileShift.x + (TILE_SIZE.x * 2) > RADAR_FINAL_SIZE.width,
secondRow,
];
// if we need t[1] and t[2] then we also need t[3]
usedTiles.push(usedTiles[1] && usedTiles[2]);
// second row is a copy of the first row when in use
// calculate T[4] and T[5]
usedTiles.push(secondRow && usedTiles[1], secondRow && usedTiles[2]);
// helper function for populating tiles
const populateTile = (tileName) => (elem, index) => {

View File

@@ -1,4 +1,6 @@
import { TILE_SIZE, TILE_FULL_SIZE } from './radar-constants.mjs';
import {
TILE_SIZE, TILE_FULL_SIZE, RADAR_OFFSET, RADAR_SHIFT,
} from './radar-constants.mjs';
// limit a value to within a range
const coerce = (low, value, high) => Math.max(Math.min(value, high), low);
@@ -9,16 +11,16 @@ const getXYFromLatitudeLongitudeMap = (pos) => {
// 589 466 -122.3615246 47.63177832
// 5288 3638 -80.18297384 25.77018996
// map position is calculated as a regresion from the above values (=/- a manual adjustment factor)
// map position is calculated as a regresion from the above values (+/- a manual adjustment factor) and shifting for enhanced views
// then shifted by half of the tile size (to center the map)
// then they are limited to values between 0 and the width or height of the map
const y = coerce(0, (-145.095 * pos.latitude + 7377.117) - 27 - (TILE_SIZE.y / 2), TILE_FULL_SIZE.y - (TILE_SIZE.y));
const x = coerce(0, (111.407 * pos.longitude + 14220.972) + 4 - (TILE_SIZE.x / 2), TILE_FULL_SIZE.x - (TILE_SIZE.x));
const y = coerce(0, (-145.095 * pos.latitude + 7377.117) - 27 - (TILE_SIZE.y / 2) - RADAR_SHIFT().y, TILE_FULL_SIZE.y - (TILE_SIZE.y));
const x = coerce(0, (111.407 * pos.longitude + 14220.972) + 4 - (TILE_SIZE.x / 2) - RADAR_SHIFT().x, TILE_FULL_SIZE.x - (TILE_SIZE.x));
return { x, y };
};
const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
const getXYFromLatitudeLongitudeDoppler = (pos) => {
const imgHeight = 6000;
const imgWidth = 2800;
@@ -26,8 +28,8 @@ const getXYFromLatitudeLongitudeDoppler = (pos, offsetX, offsetY) => {
// then shifted by half of the tile size (to center the map)
// then they are limited to values between 0 and the width or height of the map
const y = coerce(0, (51 - pos.latitude) * 61.4481 - offsetY, imgHeight);
const x = coerce(0, ((-129.138 - pos.longitude) * 42.1768) * -1 - offsetX, imgWidth);
const y = coerce(0, (51 - pos.latitude) * 61.4481 - RADAR_OFFSET().y, imgHeight);
const x = coerce(0, ((-129.138 - pos.longitude) * 42.1768) * -1 - RADAR_OFFSET().x, imgWidth);
return { x: x * 2, y: y * 2 };
};

View File

@@ -128,10 +128,8 @@ class Radar extends WeatherDisplay {
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
// calculate offsets and sizes
const offsetX = 120 * 2;
const offsetY = 69 * 2;
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters);
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters);
// set up the base map and overlay tiles
setTiles({

View File

@@ -209,7 +209,7 @@ const getMinMaxLatitudeLongitudeHI = (X, Y, OffsetX, OffsetY) => {
};
};
const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
const getXYForCity = (City, MaxLatitude, MinLongitude, state, maxX = 580) => {
if (state === 'AK') getXYForCityAK(City, MaxLatitude, MinLongitude);
if (state === 'HI') getXYForCityHI(City, MaxLatitude, MinLongitude);
let x = (City.lon - MinLongitude) * 57;
@@ -219,7 +219,7 @@ const getXYForCity = (City, MaxLatitude, MinLongitude, state) => {
if (y > 282) y = 282;
if (x < 40) x = 40;
if (x > 580) x = 580;
if (x > maxX) x = maxX;
return { x, y };
};

View File

@@ -14,11 +14,29 @@ import * as utils from './regionalforecast-utils.mjs';
import { getPoint } from './utils/weather.mjs';
import { debugFlag } from './utils/debug.mjs';
import filterExpiredPeriods from './utils/forecast-utils.mjs';
import settings from './settings.mjs';
// map offset
const mapOffsetXY = {
x: 240,
y: 117,
// set up spacing and scales
const scaling = () => {
// available space
const available = {
x: 640,
};
// map offset
const mapOffsetXY = {
x: 240,
y: 117,
};
if (settings.wide?.value && settings.enhancedScreens?.value) {
mapOffsetXY.x = 320;
available.x = 854;
}
return {
mapOffsetXY,
available,
};
};
class RegionalForecast extends WeatherDisplay {
@@ -45,6 +63,7 @@ class RegionalForecast extends WeatherDisplay {
this.elem.querySelector('.map img').src = baseMap;
// get user's location in x/y
const { available, mapOffsetXY } = scaling();
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
// get latitude and longitude limits
@@ -102,7 +121,7 @@ class RegionalForecast extends WeatherDisplay {
}
// get XY on map for city
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state);
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state, available - 60);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
@@ -188,7 +207,8 @@ class RegionalForecast extends WeatherDisplay {
}
// draw the map
const scale = 640 / (mapOffsetXY.x * 2);
const { available, mapOffsetXY } = scaling();
const scale = available.x / (mapOffsetXY.x * 2);
const map = this.elem.querySelector('.map');
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;

View File

@@ -32,6 +32,25 @@ const wideScreenChange = (value) => {
window.dispatchEvent(new Event('resize'));
};
const enhancedScreenChange = (value) => {
const container = document.querySelector('#divTwc');
if (!container) {
// DOM not ready; defer enabling if set
if (value) {
deferredDomSettings.add('enhancedScreens');
}
return;
}
if (value) {
container.classList.add('enhanced');
} else {
container.classList.remove('enhanced');
}
// Trigger resize to recalculate scaling for new width
window.dispatchEvent(new Event('redraw'));
};
const kioskChange = (value) => {
const body = document.querySelector('body');
if (!body) {
@@ -130,6 +149,17 @@ const init = () => {
changeAction: wideScreenChange,
sticky: true,
});
settings.portrait = new Setting('portrait', {
name: 'Allow Portrait',
defaultValue: false,
sticky: true,
});
settings.enhancedScreens = new Setting('enhancedScreens', {
name: 'Enhanced Screens',
defaultValue: false,
changeAction: enhancedScreenChange,
sticky: true,
});
settings.kiosk = new Setting('kiosk', {
name: 'Kiosk',
defaultValue: false,

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

@@ -346,7 +346,7 @@ var TimeIndicator;
TimeIndicator["TL"] = "TL";
})(TimeIndicator || (TimeIndicator = {}));
/**
* https://www.aviationweather.gov/taf/decoder
* https://web.archive.org/web/20230318235549/https://aviationweather.gov/taf/decoder
*/
var WeatherChangeType;
(function (WeatherChangeType) {
@@ -2535,7 +2535,8 @@ class MetarParser extends AbstractParser {
while (i < trendParts.length &&
trendParts[i] !== this.TEMPO &&
trendParts[i] !== this.INTER &&
trendParts[i] !== this.BECMG) {
trendParts[i] !== this.BECMG &&
trendParts[i] !== this.RMK) {
if (trendParts[i].startsWith(this.FM) ||
trendParts[i].startsWith(this.TL) ||
trendParts[i].startsWith(this.AT)) {

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-repeat: repeat-x;
}
}
.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

@@ -1,8 +1,10 @@
@use 'shared/_colors' as c;
@use 'shared/_utils' as u;
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
.weather-display .main.current-weather {
&.main {
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
.col {
height: 50px;
@@ -12,12 +14,17 @@
padding-top: 10px;
position: absolute;
.wide.enhanced & {
width: 300px;
margin-left: 25px;
margin-right: 25px;
}
@include u.text-shadow();
&.left {
font-family: 'Star4000 Extended';
font-size: 24pt;
}
&.right {
@@ -92,4 +99,4 @@
text-wrap: nowrap;
}
}
}
}

View File

@@ -1,5 +1,6 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
#hazards-html.weather-display {
background-image: url('../images/backgrounds/7.png');
@@ -8,7 +9,7 @@
.weather-display .main.hazards {
&.main {
overflow-y: hidden;
height: 480px;
height: p.$standard-height;
background-color: rgb(112, 35, 35);

View File

@@ -4,6 +4,12 @@
#hourly-graph-html {
background-image: url(../images/backgrounds/1-chart.png);
// change background for wide-enhanced
.wide.enhanced & {
background-image: url(../images/backgrounds/1-chart-wide.png);
background-position-x: 0px;
}
.header {
.right {
position: absolute;
@@ -84,10 +90,51 @@
&.l-5 {
left: calc(532px / 4 * 4);
}
// adjust when enhanced
.wide.enhanced & {
&.l-1 {
left: 0px;
}
&.l-2 {
left: calc(726px / 6 * 1);
}
&.l-3 {
left: calc(726px / 6 * 2);
}
&.l-4 {
left: calc(726px / 6 * 3);
}
&.l-5 {
left: calc(726px / 6 * 4);
}
&.l-6 {
left: calc(726px / 6 * 5);
}
&.l-7 {
left: calc(726px / 6 * 6);
}
}
// only in wide + enhanced
&.l-6,
&.l-7 {
display: none;
.wide.enhanced & {
display: block;
}
}
}
}
.chart {
@@ -97,6 +144,11 @@
img {
width: 532px;
height: 285px;
// wide and enhanced
.wide.enhanced & {
width: 746px;
}
}
}
@@ -128,32 +180,5 @@
}
}
.column-headers {
background-color: c.$column-header;
height: 20px;
position: absolute;
width: 100%;
}
.column-headers {
position: sticky;
top: 0px;
z-index: 5;
.temp {
left: 355px;
}
.like {
left: 435px;
}
.wind {
left: 535px;
}
}
}
}

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

@@ -46,6 +46,39 @@
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;
}
&.wind-chill {
color: c.$wind-chill;
}
}
.weather {
left: 470px;
}
.wind {
left: 630px;
}
}
.observation-lines {
min-height: 338px;
padding-top: 10px;

View File

@@ -1,7 +1,14 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
.weather-display .local-forecast {
// clamp width to standard
&.main {
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
}
.container {
position: relative;
top: 15px;

View File

@@ -1,5 +1,6 @@
@use 'shared/_utils'as u;
@use 'shared/_colors'as c;
@use 'shared/positions'as p;
@font-face {
font-family: "Star4000";
@@ -33,7 +34,7 @@ body {
}
#divQuery {
max-width: 640px;
max-width: p.$standard-width;
padding: 8px;
.buttons {
@@ -146,11 +147,11 @@ body {
background-color: #000000;
color: #ffffff;
width: 100%;
max-width: 640px;
max-width: p.$standard-width;
margin: 0; // Ensure edge-to-edge display
&.wide {
max-width: 854px;
max-width: p.$wide-width;
}
}
@@ -159,12 +160,12 @@ body {
}
#divTwcMain {
width: 640px;
height: 480px;
width: p.$standard-width;
height: p.$standard-height;
position: relative;
.wide & {
width: 854px;
width: p.$wide-width;
}
}
@@ -209,10 +210,10 @@ body {
background-color: #000000;
color: #ffffff;
width: 640px;
width: p.$standard-width;
.wide & {
width: 854px;
width: p.$wide-width;
}
@media (prefers-color-scheme: dark) {
@@ -274,7 +275,7 @@ body {
flex-direction: row;
background-color: #000000;
color: #ffffff;
max-width: 640px;
max-width: p.$standard-width;
}
#divTwcNav>div {
@@ -336,8 +337,8 @@ body {
#container {
position: relative;
width: 640px;
height: 480px;
width: p.$standard-width;
height: p.$standard-height;
// overflow: hidden;
background-image: url(../images/backgrounds/1.png);
transform-origin: 0 0;
@@ -345,8 +346,7 @@ body {
}
.wide #container {
padding-left: 107px;
padding-right: 107px;
width: p.$wide-width;
background: url(../images/backgrounds/1-wide.png);
background-repeat: no-repeat;
}
@@ -359,8 +359,8 @@ body {
}
#loading {
width: 640px;
height: 480px;
width: p.$standard-width;
height: p.$standard-height;
max-width: 100%;
text-shadow: 4px 4px black;
display: flex;
@@ -368,6 +368,10 @@ body {
text-align: center;
justify-content: center;
.wide & {
margin-left: p.$wide-margin;
}
.title {
font-family: Star4000 Large;
font-size: 36px;

View File

@@ -1,17 +1,23 @@
@use 'shared/_colors' as c;
@use 'shared/_utils' as u;
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
.weather-display .progress {
@include u.text-shadow();
font-family: 'Star4000 Extended';
font-size: 19pt;
// clamp width to standard
&.main {
width: calc(p.$standard-width - (2 * p.$blue-box-margin));
}
.container {
position: relative;
top: 15px;
margin: 0px 10px;
box-sizing: border-box;
height: 310px;
height: p.$standard-scroll-height;
overflow: hidden;
line-height: 28px;
@@ -118,4 +124,4 @@
transition: width 1s steps(6);
}
}
}
}

View File

@@ -1,5 +1,6 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
#radar-html.weather-display {
background-image: url('../images/backgrounds/4.png');
@@ -104,12 +105,13 @@
.weather-display .main.radar {
overflow: hidden;
height: 367px;
width: p.$standard-width;
.container {
.tiles {
position: absolute;
width: 1400px;
width: 2040px;
img {
vertical-align: middle;

View File

@@ -1,5 +1,6 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
#spc-outlook-html.weather-display {
background-image: url('../images/backgrounds/6.png');

View File

@@ -1,29 +1,50 @@
@use 'shared/_colors'as c;
@use 'shared/_utils'as u;
@use 'shared/positions'as p;
.weather-display {
width: 640px;
height: 480px;
width: p.$standard-width;
height: p.$standard-height;
overflow: hidden;
position: relative;
background-image: url(../images/backgrounds/1.png);
// adjust for wide
.wide & {
width: p.$wide-width;
background-position-x: p.$wide-margin;
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;
&.show {
height: 480px;
height: p.$standard-height;
}
.template {
display: none;
}
.header {
width: 640px;
>.header {
width: p.$standard-width;
height: 60px;
position: relative;
padding-top: 30px;
// adjust for wide
.wide & {
left: p.$wide-margin;
}
.title {
color: c.$title-color;
@include u.text-shadow(3px, 1.5px);
@@ -92,10 +113,23 @@
.main {
position: relative;
// adjust for wide
.wide & {
left: p.$wide-margin;
}
// adjust for enhanced when possible
.wide.enhanced & {
&.can-enhance {
left: 0px;
width: p.$wide-width;
}
}
&.has-scroll {
width: 640px;
width: p.$standard-width;
margin-top: 0;
height: 310px;
height: p.$standard-scroll-height;
overflow: hidden;
&.no-header {
@@ -105,9 +139,15 @@
}
&.has-box {
margin-left: 64px;
margin-right: 64px;
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)
}
}
}
}
@@ -117,7 +157,7 @@
#container>.scroll {
display: none;
@include u.text-shadow(3px, 1.5px);
width: 640px;
width: p.$standard-width;
height: 77px;
overflow: hidden;
margin-top: 3px;
@@ -125,12 +165,17 @@
bottom: 0px;
z-index: 1;
// adjust for wide
.wide & {
left: p.$wide-margin;
}
&.hazard {
background-color: rgb(112, 35, 35);
}
.scroll-container {
width: 640px;
width: p.$standard-width;
.fixed,
.scroll-header {
@@ -156,7 +201,7 @@
position: relative;
// the following added by js code as it is dependent on the content of the element
// transition: left (x)s;
// left: calc((elem width) - 640px);
// left: calc((elem width) - p.$standard-width);
}
}
}
@@ -166,10 +211,10 @@
}
.wide #container>.scroll {
width: 854px;
margin-left: -107px;
width: p.$wide-width;
margin-left: -1*p.$wide-margin;
.scroll-container {
margin-left: 107px;
margin-left: p.$wide-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;

View File

@@ -0,0 +1,14 @@
// standard positioning
$standard-width: 640px;
$standard-height: 480px;
// height with scroll
$standard-scroll-height: 310px;
// blue box size
$blue-box-margin: 64px;
// wide screen positioning
$wide-padding: 107px;
$wide-margin: 107px;
$wide-width: 854px;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -64,9 +64,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "25.4.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz",
"integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==",
"version": "25.5.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -163,9 +163,9 @@
}
},
"node_modules/bare-fs": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz",
"integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==",
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.6.tgz",
"integrity": "sha512-1QovqDrR80Pmt5HPAsMsXTCFcDYr+NSUKW6nd6WO5v0JBmnItc/irNRzm2KOQ5oZ69P37y+AMujNyNtG+1Rggw==",
"license": "Apache-2.0",
"dependencies": {
"bare-events": "^2.5.4",
@@ -187,9 +187,9 @@
}
},
"node_modules/bare-os": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.7.1.tgz",
"integrity": "sha512-ebvMaS5BgZKmJlvuWh14dg9rbUI84QeV3WlWn6Ph6lFI8jJoh7ADtVTyD2c93euwbe+zgi0DVrl4YmqXeM9aIA==",
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz",
"integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==",
"license": "Apache-2.0",
"engines": {
"bare": ">=1.14.0"
@@ -205,19 +205,23 @@
}
},
"node_modules/bare-stream": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz",
"integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==",
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.11.0.tgz",
"integrity": "sha512-Y/+iQ49fL3rIn6w/AVxI/2+BRrpmzJvdWt5Jv8Za6Ngqc6V227c+pYjYYgLdpR3MwQ9ObVXD0ZrqoBztakM0rw==",
"license": "Apache-2.0",
"dependencies": {
"streamx": "^2.21.0",
"streamx": "^2.25.0",
"teex": "^1.0.1"
},
"peerDependencies": {
"bare-abort-controller": "*",
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-abort-controller": {
"optional": true
},
"bare-buffer": {
"optional": true
},
@@ -227,9 +231,9 @@
}
},
"node_modules/bare-url": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz",
"integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==",
"license": "Apache-2.0",
"dependencies": {
"bare-path": "^3.0.0"
@@ -821,9 +825,9 @@
}
},
"node_modules/puppeteer": {
"version": "24.39.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.39.0.tgz",
"integrity": "sha512-uMpGyuPqz94YInmdHSbD9ssgwsddrwe8qXr08UaEwjzrEvOa8gGl8za0h+MWoEG+/6sIBsJwzRfwuGCYRbbcpg==",
"version": "24.40.0",
"resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-24.40.0.tgz",
"integrity": "sha512-IxQbDq93XHVVLWHrAkFP7F7iHvb9o0mgfsSIMlhHb+JM+JjM1V4v4MNSQfcRWJopx9dsNOr9adYv0U5fm9BJBQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
@@ -831,7 +835,7 @@
"chromium-bidi": "14.0.0",
"cosmiconfig": "^9.0.0",
"devtools-protocol": "0.0.1581282",
"puppeteer-core": "24.39.0",
"puppeteer-core": "24.40.0",
"typed-query-selector": "^2.12.1"
},
"bin": {
@@ -842,9 +846,9 @@
}
},
"node_modules/puppeteer-core": {
"version": "24.39.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.0.tgz",
"integrity": "sha512-SzIxz76Kgu17HUIi57HOejPiN0JKa9VCd2GcPY1sAh6RA4BzGZarFQdOYIYrBdUVbtyH7CrDb9uhGEwVXK/YNA==",
"version": "24.40.0",
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz",
"integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==",
"license": "Apache-2.0",
"dependencies": {
"@puppeteer/browsers": "2.13.0",
@@ -938,9 +942,9 @@
}
},
"node_modules/streamx": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz",
"integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==",
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
"integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
"license": "MIT",
"dependencies": {
"events-universal": "^1.0.0",
@@ -1067,9 +1071,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"

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,8 +1,8 @@
<%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %>
<div class="main has-scroll hourly-graph">
<div class="main has-scroll hourly-graph can-enhance">
<div class="top-right template ">
<div class="temperature">Temperature</div>
<div class="dewpoint">Dewpoint</div>
<div class="dewpoint">Dewpoint</div>
<div class="cloud">Cloud %</div>
<div class="rain">Precip %</div>
</div>
@@ -10,7 +10,7 @@
<div class="label l-1">75</div>
<div class="label l-2">65</div>
<div class="label l-3">55</div>
<div class="label l-4">45</div>
<div class="label l-4">45</div>
</div>
<div class="chart">
<img id="chart-area"></img>
@@ -21,5 +21,7 @@
<div class="label l-3">12p</div>
<div class="label l-4">6p</div>
<div class="label l-5">12a</div>
<div class="label l-6">6a</div>
<div class="label l-7">12p</div>
</div>
</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') %>

View File

@@ -1,45 +1,45 @@
<div class="header">
<div class="logo"><img src="images/logos/logo-corner.png" /></div>
<div class="title dual">
<div class="top">
Local
</div>
<div class="bottom">
Radar
</div>
</div>
<div class="right">
<div class="precip">
<div class="precip-header">PRECIP</div>
<div class="scale">
<div class="text">Light</div>
<div class="scale-table">
<div class="box box-1"></div>
<div class="box box-2"></div>
<div class="box box-3"></div>
<div class="box box-4"></div>
<div class="box box-5"></div>
<div class="box box-6"></div>
<div class="box box-7"></div>
<div class="box box-7"></div>
</div>
<div class="text">Heavy</div>
</div>
<div class="time"></div>
</div>
</div>
<div class="logo"><img src="images/logos/logo-corner.png" /></div>
<div class="title dual">
<div class="top">
Local
</div>
<div class="bottom">
Radar
</div>
</div>
<div class="right">
<div class="precip">
<div class="precip-header">PRECIP</div>
<div class="scale">
<div class="text">Light</div>
<div class="scale-table">
<div class="box box-1"></div>
<div class="box box-2"></div>
<div class="box box-3"></div>
<div class="box box-4"></div>
<div class="box box-5"></div>
<div class="box box-6"></div>
<div class="box box-7"></div>
<div class="box box-7"></div>
</div>
<div class="text">Heavy</div>
</div>
<div class="time"></div>
</div>
</div>
</div>
<div class="main radar">
<div class="container">
<div class="map-tiles tiles"><img/><img/><img/><img/></div>
<div class="scroll-area">
<div class="frame template">
<div class="map">
<img/>
</div>
</div>
</div>
<div class="overlay-tiles tiles"><img/><img/><img/><img/></div>
</div>
<div class="main radar can-enhance">
<div class="container">
<div class="map-tiles tiles"><img /><img /><img /><img /><img /><img /></div>
<div class="scroll-area">
<div class="frame template">
<div class="map">
<img />
</div>
</div>
</div>
<div class="overlay-tiles tiles"><img /><img /><img /><img /><img /></div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %>
<div class="main has-scroll regional-forecast">
<div class="main has-scroll regional-forecast can-enhance">
<div class="map"><img src="images/maps/basemap.webp" /></div>
<div class="location-container">
<div class="location template">