mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 17:19:30 -07:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
205fa77f51 | ||
|
|
28bb8f2e2a | ||
|
|
cf9a99a6ca | ||
|
|
a83afa71cd | ||
|
|
74f1abd6f8 | ||
|
|
1bd45bdeeb | ||
|
|
232061b4d8 | ||
|
|
10d10ffbfb | ||
|
|
25ac2059a6 | ||
|
|
25626a98c9 | ||
|
|
002e037bbd | ||
|
|
8d20f7672c | ||
|
|
5567fe37a6 | ||
|
|
2dcc33f210 | ||
|
|
8f86f80eb5 | ||
|
|
1609ab3d38 | ||
|
|
0be23ee988 | ||
|
|
a3ea2c3708 | ||
|
|
09fb698350 | ||
|
|
6f6efe801c | ||
|
|
bc77a1891c |
@@ -12,7 +12,8 @@
|
|||||||
"RegionalCities": "readonly",
|
"RegionalCities": "readonly",
|
||||||
"StationInfo": "readonly",
|
"StationInfo": "readonly",
|
||||||
"SunCalc": "readonly",
|
"SunCalc": "readonly",
|
||||||
"NoSleep": "readonly"
|
"NoSleep": "readonly",
|
||||||
|
"OVERRIDES": "readonly"
|
||||||
},
|
},
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ import webpack from 'webpack-stream';
|
|||||||
import TerserPlugin from 'terser-webpack-plugin';
|
import TerserPlugin from 'terser-webpack-plugin';
|
||||||
import { readFile } from 'fs/promises';
|
import { readFile } from 'fs/promises';
|
||||||
import file from 'gulp-file';
|
import file from 'gulp-file';
|
||||||
|
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
||||||
|
import OVERRIDES from '../src/overrides.mjs';
|
||||||
|
|
||||||
// get cloudfront
|
// get cloudfront
|
||||||
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
|
|
||||||
import reader from '../src/playlist-reader.mjs';
|
import reader from '../src/playlist-reader.mjs';
|
||||||
|
|
||||||
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
const clean = () => deleteAsync(['./dist/**/*', '!./dist/readme.txt']);
|
||||||
@@ -113,6 +114,7 @@ const compressHtml = async () => {
|
|||||||
.pipe(ejs({
|
.pipe(ejs({
|
||||||
production: version,
|
production: version,
|
||||||
version,
|
version,
|
||||||
|
OVERRIDES,
|
||||||
}))
|
}))
|
||||||
.pipe(rename({ extname: '.html' }))
|
.pipe(rename({ extname: '.html' }))
|
||||||
.pipe(htmlmin({ collapseWhitespace: true }))
|
.pipe(htmlmin({ collapseWhitespace: true }))
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import corsPassThru from './cors/index.mjs';
|
|||||||
import radarPassThru from './cors/radar.mjs';
|
import radarPassThru from './cors/radar.mjs';
|
||||||
import outlookPassThru from './cors/outlook.mjs';
|
import outlookPassThru from './cors/outlook.mjs';
|
||||||
import playlist from './src/playlist.mjs';
|
import playlist from './src/playlist.mjs';
|
||||||
|
import OVERRIDES from './src/overrides.mjs';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.WS4KP_PORT ?? 8080;
|
const port = process.env.WS4KP_PORT ?? 8080;
|
||||||
@@ -57,6 +58,7 @@ const index = (req, res) => {
|
|||||||
res.render('index', {
|
res.render('index', {
|
||||||
production: false,
|
production: false,
|
||||||
version,
|
version,
|
||||||
|
OVERRIDES,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
745
package-lock.json
generated
745
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "5.20.2",
|
"version": "5.21.2",
|
||||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||||
"main": "index.mjs",
|
"main": "index.mjs",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
BIN
server/fonts/Star4000 Large.ttf
Normal file
BIN
server/fonts/Star4000 Large.ttf
Normal file
Binary file not shown.
@@ -61,7 +61,7 @@ const init = () => {
|
|||||||
paramName: 'text',
|
paramName: 'text',
|
||||||
params: {
|
params: {
|
||||||
f: 'json',
|
f: 'json',
|
||||||
countryCode: 'USA', // 'USA,PRI,VIR,GUM,ASM',
|
countryCode: 'USA',
|
||||||
category,
|
category,
|
||||||
maxSuggestions: 10,
|
maxSuggestions: 10,
|
||||||
},
|
},
|
||||||
@@ -82,7 +82,6 @@ const init = () => {
|
|||||||
|
|
||||||
// attempt to parse the url parameters
|
// attempt to parse the url parameters
|
||||||
const parsedParameters = parseQueryString();
|
const parsedParameters = parseQueryString();
|
||||||
|
|
||||||
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
|
const loadFromParsed = parsedParameters.latLonQuery && parsedParameters.latLon;
|
||||||
|
|
||||||
// Auto load the parsed parameters and fall back to the previous query
|
// Auto load the parsed parameters and fall back to the previous query
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// display sun and moon data
|
// display sun and moon data
|
||||||
import { loadImg, preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
@@ -9,9 +9,6 @@ class Almanac extends WeatherDisplay {
|
|||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Almanac', true);
|
super(navId, elemId, 'Almanac', true);
|
||||||
|
|
||||||
// pre-load background images (returns promises)
|
|
||||||
this.backgroundImage0 = loadImg('images/backgrounds/1.png');
|
|
||||||
|
|
||||||
// preload the moon images
|
// preload the moon images
|
||||||
preloadImg(imageName('Full'));
|
preloadImg(imageName('Full'));
|
||||||
preloadImg(imageName('Last'));
|
preloadImg(imageName('Last'));
|
||||||
@@ -122,10 +119,10 @@ class Almanac extends WeatherDisplay {
|
|||||||
// sun and moon data
|
// sun and moon data
|
||||||
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector('.day-1').innerHTML = Today.toLocaleString({ weekday: 'long' });
|
||||||
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
this.elem.querySelector('.day-2').innerHTML = Tomorrow.toLocaleString({ weekday: 'long' });
|
||||||
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.rise-1').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[0].sunrise));
|
||||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.rise-2').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[1].sunrise));
|
||||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.set-1').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[0].sunset));
|
||||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
this.elem.querySelector('.set-2').innerHTML = timeFormat(DateTime.fromJSDate(info.sun[1].sunset));
|
||||||
|
|
||||||
const days = info.moon.map((MoonPhase) => {
|
const days = info.moon.map((MoonPhase) => {
|
||||||
const fill = {};
|
const fill = {};
|
||||||
@@ -171,6 +168,8 @@ const imageName = (type) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const timeFormat = (dt) => dt.setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
const display = new Almanac(9, 'almanac');
|
const display = new Almanac(9, 'almanac');
|
||||||
registerDisplay(display);
|
registerDisplay(display);
|
||||||
|
|||||||
@@ -3,43 +3,24 @@ import { json } from './utils/fetch.mjs';
|
|||||||
|
|
||||||
const KEYS = {
|
const KEYS = {
|
||||||
ESC: 27,
|
ESC: 27,
|
||||||
TAB: 9,
|
|
||||||
RETURN: 13,
|
|
||||||
LEFT: 37,
|
|
||||||
UP: 38,
|
UP: 38,
|
||||||
RIGHT: 39,
|
|
||||||
DOWN: 40,
|
DOWN: 40,
|
||||||
ENTER: 13,
|
ENTER: 13,
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULT_OPTIONS = {
|
const DEFAULT_OPTIONS = {
|
||||||
autoSelectFirst: false,
|
|
||||||
serviceUrl: null,
|
serviceUrl: null,
|
||||||
lookup: null,
|
|
||||||
onSelect: () => { },
|
|
||||||
onHint: null,
|
|
||||||
width: 'auto',
|
|
||||||
minChars: 3,
|
minChars: 3,
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
deferRequestBy: 0,
|
deferRequestBy: 0,
|
||||||
params: {},
|
params: {},
|
||||||
delimiter: null,
|
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
noCache: false,
|
|
||||||
preserveInput: false,
|
|
||||||
containerClass: 'autocomplete-suggestions',
|
containerClass: 'autocomplete-suggestions',
|
||||||
tabDisabled: false,
|
|
||||||
dataType: 'text',
|
|
||||||
currentRequest: null,
|
|
||||||
triggerSelectOnValidInput: true,
|
|
||||||
preventBadQueries: true,
|
|
||||||
paramName: 'query',
|
paramName: 'query',
|
||||||
transformResult: (a) => a,
|
transformResult: (a) => a,
|
||||||
showNoSuggestionNotice: false,
|
showNoSuggestionNotice: false,
|
||||||
noSuggestionNotice: 'No results',
|
noSuggestionNotice: 'No results',
|
||||||
orientation: 'bottom',
|
|
||||||
forceFixPosition: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const escapeRegExChars = (string) => string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
const escapeRegExChars = (string) => string.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// current weather conditions display
|
// current weather conditions display
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { loadImg, preloadImg } from './utils/image.mjs';
|
import { preloadImg } from './utils/image.mjs';
|
||||||
import { json } from './utils/fetch.mjs';
|
import { json } from './utils/fetch.mjs';
|
||||||
import { directionToNSEW } from './utils/calc.mjs';
|
import { directionToNSEW } from './utils/calc.mjs';
|
||||||
import { locationCleanup } from './utils/string.mjs';
|
import { locationCleanup } from './utils/string.mjs';
|
||||||
@@ -17,8 +17,6 @@ const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F'
|
|||||||
class CurrentWeather extends WeatherDisplay {
|
class CurrentWeather extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Current Conditions', true);
|
super(navId, elemId, 'Current Conditions', true);
|
||||||
// pre-load background image (returns promise)
|
|
||||||
this.backgroundImage = loadImg('images/backgrounds/1.png');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
|
|||||||
@@ -59,11 +59,10 @@ class ExtendedForecast extends WeatherDisplay {
|
|||||||
date: Day.dayName,
|
date: Day.dayName,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { low } = Day;
|
const { low, high } = Day;
|
||||||
if (low !== undefined) {
|
if (low !== undefined) {
|
||||||
fill['value-lo'] = Math.round(low);
|
fill['value-lo'] = Math.round(low);
|
||||||
}
|
}
|
||||||
const { high } = Day;
|
|
||||||
fill['value-hi'] = Math.round(high);
|
fill['value-hi'] = Math.round(high);
|
||||||
|
|
||||||
// return the filled template
|
// return the filled template
|
||||||
@@ -121,17 +120,17 @@ const parse = (fullForecast) => {
|
|||||||
return forecast;
|
return forecast;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const regexList = [
|
||||||
|
[/ and /gi, ' '],
|
||||||
|
[/slight /gi, ''],
|
||||||
|
[/chance /gi, ''],
|
||||||
|
[/very /gi, ''],
|
||||||
|
[/patchy /gi, ''],
|
||||||
|
[/areas /gi, ''],
|
||||||
|
[/dense /gi, ''],
|
||||||
|
[/Thunderstorm/g, 'T\'Storm'],
|
||||||
|
];
|
||||||
const shortenExtendedForecastText = (long) => {
|
const shortenExtendedForecastText = (long) => {
|
||||||
const regexList = [
|
|
||||||
[/ and /gi, ' '],
|
|
||||||
[/slight /gi, ''],
|
|
||||||
[/chance /gi, ''],
|
|
||||||
[/very /gi, ''],
|
|
||||||
[/patchy /gi, ''],
|
|
||||||
[/areas /gi, ''],
|
|
||||||
[/dense /gi, ''],
|
|
||||||
[/Thunderstorm/g, 'T\'Storm'],
|
|
||||||
];
|
|
||||||
// run all regexes
|
// run all regexes
|
||||||
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ class Hazards extends WeatherDisplay {
|
|||||||
// get the forecast
|
// get the forecast
|
||||||
const url = new URL('https://api.weather.gov/alerts/active');
|
const url = new URL('https://api.weather.gov/alerts/active');
|
||||||
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
url.searchParams.append('point', `${this.weatherParameters.latitude},${this.weatherParameters.longitude}`);
|
||||||
url.searchParams.append('limit', 5);
|
|
||||||
const alerts = await json(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
const alerts = await json(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
|
||||||
const unsortedAlerts = alerts.features ?? [];
|
const allUnsortedAlerts = alerts.features ?? [];
|
||||||
|
const unsortedAlerts = allUnsortedAlerts.slice(0, 5);
|
||||||
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
|
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
|
||||||
const sortedAlerts = unsortedAlerts.sort((a, b) => (calcSeverity(b.properties.severity, b.properties.event)) - (calcSeverity(a.properties.severity, a.properties.event)));
|
const sortedAlerts = unsortedAlerts.sort((a, b) => (calcSeverity(b.properties.severity, b.properties.event)) - (calcSeverity(a.properties.severity, a.properties.event)));
|
||||||
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
|
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
|
||||||
@@ -50,7 +50,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
// show alert indicator
|
// show alert indicator
|
||||||
if (this.data.length > 0) alert.classList.add('show');
|
if (this.data.length > 0) alert.classList.add('show');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Get hourly forecast failed');
|
console.error('Get hazards failed');
|
||||||
console.error(error.status, error.responseJSON);
|
console.error(error.status, error.responseJSON);
|
||||||
if (this.isEnabled) this.setStatus(STATUS.failed);
|
if (this.isEnabled) this.setStatus(STATUS.failed);
|
||||||
// return undefined to other subscribers
|
// return undefined to other subscribers
|
||||||
@@ -129,7 +129,7 @@ class Hazards extends WeatherDisplay {
|
|||||||
// don't let offset go negative
|
// don't let offset go negative
|
||||||
if (offsetY < 0) offsetY = 0;
|
if (offsetY < 0) offsetY = 0;
|
||||||
|
|
||||||
// copy the scrolled portion of the canvas
|
// move the element
|
||||||
this.elem.querySelector('.main').scrollTo(0, offsetY);
|
this.elem.querySelector('.main').scrollTo(0, offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import WeatherDisplay from './weatherdisplay.mjs';
|
|||||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
|
|
||||||
|
// get available space
|
||||||
|
const availableWidth = 532;
|
||||||
|
const availableHeight = 285;
|
||||||
|
|
||||||
class HourlyGraph extends WeatherDisplay {
|
class HourlyGraph extends WeatherDisplay {
|
||||||
constructor(navId, elemId, defaultActive) {
|
constructor(navId, elemId, defaultActive) {
|
||||||
super(navId, elemId, 'Hourly Graph', defaultActive);
|
super(navId, elemId, 'Hourly Graph', defaultActive);
|
||||||
@@ -47,10 +51,6 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
drawCanvas() {
|
drawCanvas() {
|
||||||
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
if (!this.image) this.image = this.elem.querySelector('.chart img');
|
||||||
|
|
||||||
// get available space
|
|
||||||
const availableWidth = 532;
|
|
||||||
const availableHeight = 285;
|
|
||||||
|
|
||||||
this.image.width = availableWidth;
|
this.image.width = availableWidth;
|
||||||
this.image.height = availableHeight;
|
this.image.height = availableHeight;
|
||||||
|
|
||||||
|
|||||||
@@ -69,8 +69,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
const fillValues = {};
|
const fillValues = {};
|
||||||
// hour
|
// hour
|
||||||
const hour = startingHour.plus({ hours: index });
|
const hour = startingHour.plus({ hours: index });
|
||||||
const formattedHour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' });
|
fillValues.hour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' });
|
||||||
fillValues.hour = formattedHour;
|
|
||||||
|
|
||||||
// temperatures, convert to strings with no decimal
|
// temperatures, convert to strings with no decimal
|
||||||
const temperature = data.temperature.toString().padStart(3);
|
const temperature = data.temperature.toString().padStart(3);
|
||||||
@@ -81,12 +80,11 @@ class Hourly extends WeatherDisplay {
|
|||||||
fillValues.like = feelsLike;
|
fillValues.like = feelsLike;
|
||||||
|
|
||||||
// wind
|
// wind
|
||||||
let wind = 'Calm';
|
fillValues.wind = 'Calm';
|
||||||
if (data.windSpeed > 0) {
|
if (data.windSpeed > 0) {
|
||||||
const windSpeed = Math.round(data.windSpeed).toString();
|
const windSpeed = Math.round(data.windSpeed).toString();
|
||||||
wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed;
|
fillValues.wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed;
|
||||||
}
|
}
|
||||||
fillValues.wind = wind;
|
|
||||||
|
|
||||||
// image
|
// image
|
||||||
fillValues.icon = { type: 'img', src: data.icon };
|
fillValues.icon = { type: 'img', src: data.icon };
|
||||||
@@ -96,8 +94,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
// alter the color of the feels like column to reflect wind chill or heat index
|
// alter the color of the feels like column to reflect wind chill or heat index
|
||||||
if (feelsLike < temperature) {
|
if (feelsLike < temperature) {
|
||||||
filledRow.querySelector('.like').classList.add('wind-chill');
|
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||||
}
|
} else if (feelsLike > temperature) {
|
||||||
if (feelsLike > temperature) {
|
|
||||||
filledRow.querySelector('.like').classList.add('heat-index');
|
filledRow.querySelector('.like').classList.add('heat-index');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
const hourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => {
|
// internal function to add path to returned icon
|
||||||
// internal function to add path to returned icon
|
const addPath = (icon) => `images/icons/regional-maps/${icon}`;
|
||||||
const addPath = (icon) => `images/icons/regional-maps/${icon}`;
|
|
||||||
|
|
||||||
|
const hourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => {
|
||||||
// possible phenomenon
|
// possible phenomenon
|
||||||
let thunder = false;
|
let thunder = false;
|
||||||
let snow = false;
|
let snow = false;
|
||||||
|
|||||||
@@ -22,8 +22,7 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
// this is intentional because up to 30 stations are available to pull data from
|
// this is intentional because up to 30 stations are available to pull data from
|
||||||
|
|
||||||
// calculate distance to each station
|
// calculate distance to each station
|
||||||
const stationsByDistance = Object.keys(StationInfo).map((key) => {
|
const stationsByDistance = Object.values(StationInfo).map((station) => {
|
||||||
const station = StationInfo[key];
|
|
||||||
const distance = calcDistance(station.lat, station.lon, this.weatherParameters.latitude, this.weatherParameters.longitude);
|
const distance = calcDistance(station.lat, station.lon, this.weatherParameters.latitude, this.weatherParameters.longitude);
|
||||||
return { ...station, distance };
|
return { ...station, distance };
|
||||||
});
|
});
|
||||||
@@ -104,8 +103,6 @@ class LatestObservations extends WeatherDisplay {
|
|||||||
linesContainer.innerHTML = '';
|
linesContainer.innerHTML = '';
|
||||||
linesContainer.append(...lines);
|
linesContainer.append(...lines);
|
||||||
|
|
||||||
// update temperature unit header
|
|
||||||
|
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,9 +253,9 @@ const resize = () => {
|
|||||||
|
|
||||||
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
const scale = Math.min(widthZoomPercent, heightZoomPercent);
|
||||||
if (scale < 1.0 || document.fullscreenElement || settings.kiosk) {
|
if (scale < 1.0 || document.fullscreenElement || settings.kiosk) {
|
||||||
document.querySelector('#container').style.transform = `scale(${scale})`;
|
document.querySelector('#container').style.zoom = scale;
|
||||||
} else {
|
} else {
|
||||||
document.querySelector('#container').style.transform = 'unset';
|
document.querySelector('#container').style.zoom = 'unset';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,6 +266,7 @@ const resetStatuses = () => {
|
|||||||
|
|
||||||
// allow displays to register themselves
|
// allow displays to register themselves
|
||||||
const registerDisplay = (display) => {
|
const registerDisplay = (display) => {
|
||||||
|
if (displays[display.navId]) console.warn(`Display nav ID ${display.navId} already in use`);
|
||||||
displays[display.navId] = display;
|
displays[display.navId] = display;
|
||||||
|
|
||||||
// generate checkboxes
|
// generate checkboxes
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// regional forecast and observations
|
// regional forecast and observations
|
||||||
import { loadImg } from './utils/image.mjs';
|
|
||||||
import STATUS, { calcStatusClass, statusClasses } from './status.mjs';
|
import STATUS, { calcStatusClass, statusClasses } from './status.mjs';
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -10,9 +9,6 @@ class Progress extends WeatherDisplay {
|
|||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, '', false);
|
super(navId, elemId, '', false);
|
||||||
|
|
||||||
// pre-load background image (returns promise)
|
|
||||||
this.backgroundImage = loadImg('images/backgrounds/1.png');
|
|
||||||
|
|
||||||
// disable any navigation timing
|
// disable any navigation timing
|
||||||
this.timing = false;
|
this.timing = false;
|
||||||
|
|
||||||
|
|||||||
106
server/scripts/modules/radar-worker.mjs
Normal file
106
server/scripts/modules/radar-worker.mjs
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import * as utils from './radar-utils.mjs';
|
||||||
|
|
||||||
|
const radarFullSize = { width: 2550, height: 1600 };
|
||||||
|
const radarFinalSize = { width: 640, height: 367 };
|
||||||
|
|
||||||
|
const fetchAsBlob = async (url) => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
return response.blob();
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseMapImages = new Promise((resolve) => {
|
||||||
|
fetchAsBlob('/images/maps/radar.webp').then((blob) => {
|
||||||
|
createImageBitmap(blob).then((imageBitmap) => {
|
||||||
|
// extract the black pixels to overlay on to the final image (boundaries)
|
||||||
|
const canvas = new OffscreenCanvas(imageBitmap.width, imageBitmap.height);
|
||||||
|
const context = canvas.getContext('2d');
|
||||||
|
context.drawImage(imageBitmap, 0, 0);
|
||||||
|
const imageData = context.getImageData(0, 0, imageBitmap.width, imageBitmap.height);
|
||||||
|
|
||||||
|
// go through the image data and preserve the black pixels, making the rest transparent
|
||||||
|
for (let i = 0; i < imageData.data.length; i += 4) {
|
||||||
|
if (imageData.data[i + 0] >= 116 || imageData.data[i + 1] >= 116 || imageData.data[i + 2] >= 116) {
|
||||||
|
// make it transparent
|
||||||
|
imageData.data[i + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// write the image data back
|
||||||
|
context.putImageData(imageData, 0, 0);
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
fullMap: imageBitmap,
|
||||||
|
overlay: canvas,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
onmessage = async (e) => {
|
||||||
|
const {
|
||||||
|
url, RADAR_HOST, OVERRIDES, radarSourceXY, sourceXY, offsetX, offsetY,
|
||||||
|
} = e.data;
|
||||||
|
|
||||||
|
// get the image
|
||||||
|
const modifiedRadarUrl = OVERRIDES.RADAR_HOST ? url.replace(RADAR_HOST, OVERRIDES.RADAR_HOST) : url;
|
||||||
|
const radarResponsePromise = fetch(modifiedRadarUrl);
|
||||||
|
|
||||||
|
// calculate offsets and sizes
|
||||||
|
|
||||||
|
const radarSource = {
|
||||||
|
width: 240,
|
||||||
|
height: 163,
|
||||||
|
x: Math.round(radarSourceXY.x / 2),
|
||||||
|
y: Math.round(radarSourceXY.y / 2),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create destination context
|
||||||
|
const baseCanvas = new OffscreenCanvas(radarFinalSize.width, radarFinalSize.height);
|
||||||
|
const baseContext = baseCanvas.getContext('2d');
|
||||||
|
baseContext.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
// create working context for manipulation
|
||||||
|
const radarCanvas = new OffscreenCanvas(radarFullSize.width, radarFullSize.height);
|
||||||
|
const radarContext = radarCanvas.getContext('2d');
|
||||||
|
radarContext.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
// get the base map
|
||||||
|
const baseMaps = await baseMapImages;
|
||||||
|
baseContext.drawImage(baseMaps.fullMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, radarFinalSize.width, radarFinalSize.height);
|
||||||
|
|
||||||
|
// test response
|
||||||
|
const radarResponse = await radarResponsePromise;
|
||||||
|
if (!radarResponse.ok) throw new Error(`Unable to fetch radar error ${radarResponse.status} ${radarResponse.statusText} from ${radarResponse.url}`);
|
||||||
|
|
||||||
|
// get the blob
|
||||||
|
const radarImgBlob = await radarResponse.blob();
|
||||||
|
|
||||||
|
// assign to an html image element
|
||||||
|
const radarImgElement = await createImageBitmap(radarImgBlob);
|
||||||
|
// draw the entire image
|
||||||
|
radarContext.clearRect(0, 0, radarFullSize.width, radarFullSize.height);
|
||||||
|
radarContext.drawImage(radarImgElement, 0, 0, radarFullSize.width, radarFullSize.height);
|
||||||
|
|
||||||
|
// crop the radar image without scaling
|
||||||
|
const croppedRadarCanvas = new OffscreenCanvas(radarSource.width, radarSource.height);
|
||||||
|
const croppedRadarContext = croppedRadarCanvas.getContext('2d');
|
||||||
|
croppedRadarContext.imageSmoothingEnabled = false;
|
||||||
|
croppedRadarContext.drawImage(radarCanvas, radarSource.x, radarSource.y, croppedRadarCanvas.width, croppedRadarCanvas.height, 0, 0, croppedRadarCanvas.width, croppedRadarCanvas.height);
|
||||||
|
|
||||||
|
// clean the image
|
||||||
|
utils.removeDopplerRadarImageNoise(croppedRadarContext);
|
||||||
|
|
||||||
|
// stretch the radar image
|
||||||
|
const stretchCanvas = new OffscreenCanvas(radarFinalSize.width, radarFinalSize.height);
|
||||||
|
const stretchContext = stretchCanvas.getContext('2d', { willReadFrequently: true });
|
||||||
|
stretchContext.imageSmoothingEnabled = false;
|
||||||
|
stretchContext.drawImage(croppedRadarCanvas, 0, 0, radarSource.width, radarSource.height, 0, 0, radarFinalSize.width, radarFinalSize.height);
|
||||||
|
|
||||||
|
// put the radar on the base map
|
||||||
|
baseContext.drawImage(stretchCanvas, 0, 0);
|
||||||
|
// put the road/boundaries overlay on the map
|
||||||
|
baseContext.drawImage(baseMaps.overlay, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, radarFinalSize.width, radarFinalSize.height);
|
||||||
|
|
||||||
|
const processedRadar = baseCanvas.transferToImageBitmap();
|
||||||
|
|
||||||
|
postMessage(processedRadar, [processedRadar]);
|
||||||
|
};
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
// current weather conditions display
|
// current weather conditions display
|
||||||
import STATUS from './status.mjs';
|
import STATUS from './status.mjs';
|
||||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||||
import { loadImg } from './utils/image.mjs';
|
|
||||||
import { text } from './utils/fetch.mjs';
|
import { text } from './utils/fetch.mjs';
|
||||||
import { rewriteUrl } from './utils/cors.mjs';
|
|
||||||
import WeatherDisplay from './weatherdisplay.mjs';
|
import WeatherDisplay from './weatherdisplay.mjs';
|
||||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||||
import * as utils from './radar-utils.mjs';
|
import * as utils from './radar-utils.mjs';
|
||||||
|
|
||||||
|
const RADAR_HOST = 'mesonet.agron.iastate.edu';
|
||||||
class Radar extends WeatherDisplay {
|
class Radar extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Local Radar', true);
|
super(navId, elemId, 'Local Radar', true);
|
||||||
@@ -40,6 +39,9 @@ class Radar extends WeatherDisplay {
|
|||||||
{ time: 1, si: 4 },
|
{ time: 1, si: 4 },
|
||||||
{ time: 12, si: 5 },
|
{ time: 12, si: 5 },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// get some web workers started
|
||||||
|
this.workers = (new Array(this.dopplerRadarImageMax)).fill(null).map(() => radarWorker());
|
||||||
}
|
}
|
||||||
|
|
||||||
async getData(weatherParameters, refresh) {
|
async getData(weatherParameters, refresh) {
|
||||||
@@ -51,12 +53,8 @@ class Radar extends WeatherDisplay {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the base map
|
const baseUrl = `https://${RADAR_HOST}/archive/data/`;
|
||||||
const src = 'images/maps/radar.webp';
|
const baseUrlEnd = '/GIS/uscomp/?F=0&P=n0r*.png';
|
||||||
this.baseMap = await loadImg(src);
|
|
||||||
|
|
||||||
const baseUrl = 'https://mesonet.agron.iastate.edu/archive/data/';
|
|
||||||
const baseUrlEnd = '/GIS/uscomp/';
|
|
||||||
const baseUrls = [];
|
const baseUrls = [];
|
||||||
let date = DateTime.utc().minus({ days: 1 }).startOf('day');
|
let date = DateTime.utc().minus({ days: 1 }).startOf('day');
|
||||||
|
|
||||||
@@ -102,91 +100,45 @@ class Radar extends WeatherDisplay {
|
|||||||
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
const urls = sortedPngs.slice(-(this.dopplerRadarImageMax));
|
||||||
|
|
||||||
// calculate offsets and sizes
|
// calculate offsets and sizes
|
||||||
let offsetX = 120;
|
const offsetX = 120 * 2;
|
||||||
let offsetY = 69;
|
const offsetY = 69 * 2;
|
||||||
const width = 2550;
|
|
||||||
const height = 1600;
|
|
||||||
offsetX *= 2;
|
|
||||||
offsetY *= 2;
|
|
||||||
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters, offsetX, offsetY);
|
const sourceXY = utils.getXYFromLatitudeLongitudeMap(this.weatherParameters, offsetX, offsetY);
|
||||||
|
|
||||||
// calculate radar offsets
|
|
||||||
const radarOffsetX = 120;
|
|
||||||
const radarOffsetY = 70;
|
|
||||||
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
|
const radarSourceXY = utils.getXYFromLatitudeLongitudeDoppler(this.weatherParameters, offsetX, offsetY);
|
||||||
const radarSourceX = radarSourceXY.x / 2;
|
|
||||||
const radarSourceY = radarSourceXY.y / 2;
|
|
||||||
|
|
||||||
// Load the most recent doppler radar images.
|
// Load the most recent doppler radar images.
|
||||||
const radarInfo = await Promise.all(urls.map(async (url) => {
|
const radarInfo = await Promise.all(urls.map(async (url, index) => {
|
||||||
// create destination context
|
const processedRadar = await this.workers[index].processRadar({
|
||||||
const canvas = document.createElement('canvas');
|
url,
|
||||||
canvas.width = 640;
|
RADAR_HOST,
|
||||||
canvas.height = 367;
|
OVERRIDES,
|
||||||
const context = canvas.getContext('2d');
|
sourceXY,
|
||||||
context.imageSmoothingEnabled = false;
|
radarSourceXY,
|
||||||
|
offsetX,
|
||||||
// create working context for manipulation
|
offsetY,
|
||||||
const workingCanvas = document.createElement('canvas');
|
});
|
||||||
workingCanvas.width = width;
|
|
||||||
workingCanvas.height = height;
|
|
||||||
const workingContext = workingCanvas.getContext('2d');
|
|
||||||
workingContext.imageSmoothingEnabled = false;
|
|
||||||
|
|
||||||
// get the image
|
|
||||||
const response = await fetch(rewriteUrl(url));
|
|
||||||
|
|
||||||
// test response
|
|
||||||
if (!response.ok) throw new Error(`Unable to fetch radar error ${response.status} ${response.statusText} from ${response.url}`);
|
|
||||||
|
|
||||||
// get the blob
|
|
||||||
const blob = await response.blob();
|
|
||||||
|
|
||||||
// store the time
|
// store the time
|
||||||
const timeMatch = url.match(/_(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)\./);
|
const timeMatch = url.match(/_(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)\./);
|
||||||
let time;
|
|
||||||
if (timeMatch) {
|
|
||||||
const [, year, month, day, hour, minute] = timeMatch;
|
|
||||||
time = DateTime.fromObject({
|
|
||||||
year,
|
|
||||||
month,
|
|
||||||
day,
|
|
||||||
hour,
|
|
||||||
minute,
|
|
||||||
}, {
|
|
||||||
zone: 'UTC',
|
|
||||||
}).setZone(timeZone());
|
|
||||||
} else {
|
|
||||||
time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// assign to an html image element
|
const [, year, month, day, hour, minute] = timeMatch;
|
||||||
const imgBlob = await loadImg(blob);
|
const time = DateTime.fromObject({
|
||||||
|
year,
|
||||||
|
month,
|
||||||
|
day,
|
||||||
|
hour,
|
||||||
|
minute,
|
||||||
|
}, {
|
||||||
|
zone: 'UTC',
|
||||||
|
}).setZone(timeZone());
|
||||||
|
|
||||||
// draw the entire image
|
const onscreenCanvas = document.createElement('canvas');
|
||||||
workingContext.clearRect(0, 0, width, 1600);
|
onscreenCanvas.width = processedRadar.width;
|
||||||
workingContext.drawImage(imgBlob, 0, 0, width, 1600);
|
onscreenCanvas.height = processedRadar.height;
|
||||||
|
const onscreenContext = onscreenCanvas.getContext('bitmaprenderer');
|
||||||
// get the base map
|
onscreenContext.transferFromImageBitmap(processedRadar);
|
||||||
context.drawImage(this.baseMap, sourceXY.x, sourceXY.y, offsetX * 2, offsetY * 2, 0, 0, 640, 367);
|
|
||||||
|
|
||||||
// crop the radar image
|
|
||||||
const cropCanvas = document.createElement('canvas');
|
|
||||||
cropCanvas.width = 640;
|
|
||||||
cropCanvas.height = 367;
|
|
||||||
const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true });
|
|
||||||
cropContext.imageSmoothingEnabled = false;
|
|
||||||
cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367);
|
|
||||||
// clean the image
|
|
||||||
utils.removeDopplerRadarImageNoise(cropContext);
|
|
||||||
|
|
||||||
// merge the radar and map
|
|
||||||
utils.mergeDopplerRadarImage(context, cropContext);
|
|
||||||
|
|
||||||
const elem = this.fillTemplate('frame', { map: { type: 'img', src: canvas.toDataURL() } });
|
|
||||||
|
|
||||||
|
const elem = this.fillTemplate('frame', { map: { type: 'canvas', canvas: onscreenCanvas } });
|
||||||
return {
|
return {
|
||||||
canvas,
|
|
||||||
time,
|
time,
|
||||||
elem,
|
elem,
|
||||||
};
|
};
|
||||||
@@ -199,8 +151,6 @@ class Radar extends WeatherDisplay {
|
|||||||
|
|
||||||
// set max length
|
// set max length
|
||||||
this.timing.totalScreens = radarInfo.length;
|
this.timing.totalScreens = radarInfo.length;
|
||||||
// store the images
|
|
||||||
this.data = radarInfo.map((radar) => radar.canvas);
|
|
||||||
|
|
||||||
this.times = radarInfo.map((radar) => radar.time);
|
this.times = radarInfo.map((radar) => radar.time);
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
@@ -223,5 +173,30 @@ class Radar extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a radar worker with helper functions
|
||||||
|
const radarWorker = () => {
|
||||||
|
// create the worker
|
||||||
|
const worker = new Worker(new URL('./radar-worker.mjs', import.meta.url), { type: 'module' });
|
||||||
|
|
||||||
|
const processRadar = (url) => new Promise((resolve, reject) => {
|
||||||
|
// prepare for done message
|
||||||
|
worker.onmessage = (e) => {
|
||||||
|
if (e?.data instanceof Error) {
|
||||||
|
reject(e.data);
|
||||||
|
} else if (e?.data instanceof ImageBitmap) {
|
||||||
|
resolve(e.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// start up the worker
|
||||||
|
worker.postMessage(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
// return the object
|
||||||
|
return {
|
||||||
|
processRadar,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new Radar(11, 'radar'));
|
registerDisplay(new Radar(11, 'radar'));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const buildForecast = (forecast, city, cityXY) => {
|
|||||||
const getRegionalObservation = async (point, city) => {
|
const getRegionalObservation = async (point, city) => {
|
||||||
try {
|
try {
|
||||||
// get stations
|
// get stations
|
||||||
const stations = await json(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations`);
|
const stations = await json(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=1`);
|
||||||
|
|
||||||
// get the first station
|
// get the first station
|
||||||
const station = stations.features[0].id;
|
const station = stations.features[0].id;
|
||||||
|
|||||||
@@ -13,6 +13,12 @@ import { registerDisplay } from './navigation.mjs';
|
|||||||
import * as utils from './regionalforecast-utils.mjs';
|
import * as utils from './regionalforecast-utils.mjs';
|
||||||
import { getPoint } from './utils/weather.mjs';
|
import { getPoint } from './utils/weather.mjs';
|
||||||
|
|
||||||
|
// map offset
|
||||||
|
const mapOffsetXY = {
|
||||||
|
x: 240,
|
||||||
|
y: 117,
|
||||||
|
};
|
||||||
|
|
||||||
class RegionalForecast extends WeatherDisplay {
|
class RegionalForecast extends WeatherDisplay {
|
||||||
constructor(navId, elemId) {
|
constructor(navId, elemId) {
|
||||||
super(navId, elemId, 'Regional Forecast', true);
|
super(navId, elemId, 'Regional Forecast', true);
|
||||||
@@ -36,23 +42,18 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
this.elem.querySelector('.map img').src = baseMap;
|
this.elem.querySelector('.map img').src = baseMap;
|
||||||
|
|
||||||
// map offset
|
|
||||||
const offsetXY = {
|
|
||||||
x: 240,
|
|
||||||
y: 117,
|
|
||||||
};
|
|
||||||
// get user's location in x/y
|
// get user's location in x/y
|
||||||
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state);
|
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
|
||||||
|
|
||||||
// get latitude and longitude limits
|
// get latitude and longitude limits
|
||||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, this.weatherParameters.state);
|
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
|
||||||
|
|
||||||
// get a target distance
|
// get a target distance
|
||||||
let targetDistance = 2.5;
|
let targetDistance = 2.5;
|
||||||
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
if (this.weatherParameters.state === 'HI') targetDistance = 1;
|
||||||
|
|
||||||
// make station info into an array
|
// make station info into an array
|
||||||
const stationInfoArray = Object.values(StationInfo).map((value) => ({ ...value, targetDistance }));
|
const stationInfoArray = Object.values(StationInfo).map((station) => ({ ...station, targetDistance }));
|
||||||
// combine regional cities with station info for additional stations
|
// combine regional cities with station info for additional stations
|
||||||
// stations are intentionally after cities to allow cities priority when drawing the map
|
// stations are intentionally after cities to allow cities priority when drawing the map
|
||||||
const combinedCities = [...RegionalCities, ...stationInfoArray];
|
const combinedCities = [...RegionalCities, ...stationInfoArray];
|
||||||
@@ -137,7 +138,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
// return the weather data and offsets
|
// return the weather data and offsets
|
||||||
this.data = {
|
this.data = {
|
||||||
regionalData,
|
regionalData,
|
||||||
offsetXY,
|
mapOffsetXY,
|
||||||
sourceXY,
|
sourceXY,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
drawCanvas() {
|
drawCanvas() {
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
// break up data into useful values
|
// break up data into useful values
|
||||||
const { regionalData: data, sourceXY, offsetXY } = this.data;
|
const { regionalData: data, sourceXY } = this.data;
|
||||||
|
|
||||||
// draw the header graphics
|
// draw the header graphics
|
||||||
|
|
||||||
@@ -170,7 +171,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// draw the map
|
// draw the map
|
||||||
const scale = 640 / (offsetXY.x * 2);
|
const scale = 640 / (mapOffsetXY.x * 2);
|
||||||
const map = this.elem.querySelector('.map');
|
const map = this.elem.querySelector('.map');
|
||||||
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { elemForEach } from './utils/elem.mjs';
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => init());
|
document.addEventListener('DOMContentLoaded', () => init());
|
||||||
|
|
||||||
// shorthand mappings for frequently used values
|
// shorthand mappings for frequently used values
|
||||||
@@ -19,21 +21,18 @@ const init = () => {
|
|||||||
const createLink = async (e) => {
|
const createLink = async (e) => {
|
||||||
// cancel default event (click on hyperlink)
|
// cancel default event (click on hyperlink)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// get all checkboxes on page
|
|
||||||
const checkboxes = document.querySelectorAll('input[type=checkbox]');
|
|
||||||
|
|
||||||
// list to receive checkbox statuses
|
// list to receive checkbox statuses
|
||||||
const queryStringElements = {};
|
const queryStringElements = {};
|
||||||
|
|
||||||
[...checkboxes].forEach((elem) => {
|
elemForEach('input[type=checkbox]', (elem) => {
|
||||||
if (elem?.id) {
|
if (elem?.id) {
|
||||||
queryStringElements[elem.id] = elem?.checked ?? false;
|
queryStringElements[elem.id] = elem?.checked ?? false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// get all select boxes
|
// get all select boxes
|
||||||
const selects = document.querySelectorAll('select');
|
elemForEach('select', (elem) => {
|
||||||
[...selects].forEach((elem) => {
|
|
||||||
if (elem?.id) {
|
if (elem?.id) {
|
||||||
queryStringElements[elem.id] = elem?.value ?? 0;
|
queryStringElements[elem.id] = elem?.value ?? 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,4 @@
|
|||||||
import { blob } from './fetch.mjs';
|
import { blob } from './fetch.mjs';
|
||||||
import { rewriteUrl } from './cors.mjs';
|
|
||||||
|
|
||||||
// ****************************** load images *********************************
|
|
||||||
// load an image from a blob or url
|
|
||||||
const loadImg = (imgData, cors = false) => new Promise((resolve) => {
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = (e) => {
|
|
||||||
resolve(e.target);
|
|
||||||
};
|
|
||||||
if (imgData instanceof Blob) {
|
|
||||||
img.src = window.URL.createObjectURL(imgData);
|
|
||||||
} else {
|
|
||||||
let url = imgData;
|
|
||||||
if (cors) url = rewriteUrl(imgData);
|
|
||||||
img.src = url;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// preload an image
|
// preload an image
|
||||||
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
|
// the goal is to get it in the browser's cache so it is available more quickly when the browser needs it
|
||||||
@@ -29,6 +12,6 @@ const preloadImg = (src) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loadImg,
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
preloadImg,
|
preloadImg,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { json } from './fetch.mjs';
|
|||||||
|
|
||||||
const getPoint = async (lat, lon) => {
|
const getPoint = async (lat, lon) => {
|
||||||
try {
|
try {
|
||||||
return await json(`https://api.weather.gov/points/${lat},${lon}`);
|
return await json(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`Unable to get point ${lat}, ${lon}`);
|
console.log(`Unable to get point ${lat}, ${lon}`);
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
} from './navigation.mjs';
|
} from './navigation.mjs';
|
||||||
import { parseQueryString } from './share.mjs';
|
import { parseQueryString } from './share.mjs';
|
||||||
import settings from './settings.mjs';
|
import settings from './settings.mjs';
|
||||||
|
import { elemForEach } from './utils/elem.mjs';
|
||||||
|
|
||||||
class WeatherDisplay {
|
class WeatherDisplay {
|
||||||
constructor(navId, elemId, name, defaultEnabled) {
|
constructor(navId, elemId, name, defaultEnabled) {
|
||||||
@@ -391,8 +392,7 @@ class WeatherDisplay {
|
|||||||
this.templates = {};
|
this.templates = {};
|
||||||
this.elem = document.querySelector(`#${this.elemId}-html`);
|
this.elem = document.querySelector(`#${this.elemId}-html`);
|
||||||
if (!this.elem) return;
|
if (!this.elem) return;
|
||||||
const templates = this.elem.querySelectorAll('.template');
|
elemForEach(`#${this.elemId}-html .template`, (template) => {
|
||||||
templates.forEach((template) => {
|
|
||||||
const className = template.classList[0];
|
const className = template.classList[0];
|
||||||
const node = template.cloneNode(true);
|
const node = template.cloneNode(true);
|
||||||
node.classList.remove('template');
|
node.classList.remove('template');
|
||||||
@@ -421,6 +421,8 @@ class WeatherDisplay {
|
|||||||
} else if (value?.type === 'img') {
|
} else if (value?.type === 'img') {
|
||||||
// fill the image source
|
// fill the image source
|
||||||
elem.querySelector('img').src = value.src;
|
elem.querySelector('img').src = value.src;
|
||||||
|
} else if (value?.type === 'canvas') {
|
||||||
|
elem.append(value.canvas);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -189,11 +189,33 @@ body {
|
|||||||
#divTwcBottom>div {
|
#divTwcBottom>div {
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
|
|
||||||
|
// scale down the buttons on narrower screens
|
||||||
|
@media (max-width: 550px) {
|
||||||
|
zoom: 0.90;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 500px) {
|
||||||
|
zoom: 0.80;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 450px) {
|
||||||
|
zoom: 0.70;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
zoom: 0.60;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 350px) {
|
||||||
|
zoom: 0.50;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#divTwcBottomLeft {
|
#divTwcBottomLeft {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#divTwcBottomMiddle {
|
#divTwcBottomMiddle {
|
||||||
@@ -260,7 +282,7 @@ body {
|
|||||||
|
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Star4000 Large';
|
font-family: 'Star4000 Large';
|
||||||
src: url('../fonts/Star4000 Large.woff') format('woff');
|
src: url('../fonts/Star4000 Large.ttf') format('truetype');
|
||||||
font-display: swap;
|
font-display: swap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
10
src/overrides.mjs
Normal file
10
src/overrides.mjs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
// read overrides from environment variables
|
||||||
|
|
||||||
|
const OVERRIDES = {};
|
||||||
|
Object.entries(process.env).forEach(([key, value]) => {
|
||||||
|
if (key.match(/^OVERRIDE_/)) {
|
||||||
|
OVERRIDES[key.replace('OVERRIDE_', '')] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default OVERRIDES;
|
||||||
@@ -18,19 +18,22 @@
|
|||||||
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
|
<meta property="og:image" content="https://weatherstar.netbymatt.com/images/social/1200x600.png">
|
||||||
<meta property="og:image:width" content="1200">
|
<meta property="og:image:width" content="1200">
|
||||||
<meta property="og:image:height" content="627">
|
<meta property="og:image:height" content="627">
|
||||||
<link rel="preload" href="fonts/Star4000.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000.woff" as="font" type="font/woff" crossorigin>
|
||||||
<link rel="preload" href="fonts/Star4000 Extended.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000 Extended.woff" as="font" type="font/woff" crossorigin>
|
||||||
<link rel="preload" href="fonts/Star4000 Large.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000 Large.ttf" as="font" type="font/ttf" crossorigin>
|
||||||
<link rel="preload" href="fonts/Star4000 Small.woff" as="font" type="font/woff" crossorigin>
|
<link rel="prefetch" href="fonts/Star4000 Small.woff" as="font" type="font/woff" crossorigin>
|
||||||
|
|
||||||
<% if (production) { %>
|
<% if (production) { %>
|
||||||
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
<link rel="stylesheet" type="text/css" href="resources/ws.min.css?_=<%=production%>" />
|
||||||
<script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script>
|
<script type="text/javascript" src="resources/data.min.js?_=<%=production%>"></script>
|
||||||
<script type="text/javascript" src="resources/vendor.min.js?_=<%=production%>"></script>
|
<script type="text/javascript" src="resources/vendor.min.js?_=<%=production%>"></script>
|
||||||
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
|
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
|
||||||
|
<script type="text/javascript">const OVERRIDES=<%-JSON.stringify(OVERRIDES)%>;</script>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
||||||
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script>
|
<!--<script type="text/javascript">const OVERRIDES={};</script>-->
|
||||||
|
<script type="text/javascript">OVERRIDES=<%-JSON.stringify(OVERRIDES)%>;</script>
|
||||||
|
<script type="text/javascript" src="scripts/vendor/auto/nosleep.js"></script>
|
||||||
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script>
|
<script type="text/javascript" src="scripts/vendor/auto/swiped-events.js"></script>
|
||||||
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
|
<script type="text/javascript" src="scripts/vendor/auto/suncalc.js"></script>
|
||||||
<script type="module" src="scripts/modules/hazards.mjs"></script>
|
<script type="module" src="scripts/modules/hazards.mjs"></script>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@
|
|||||||
<div class="scroll-area">
|
<div class="scroll-area">
|
||||||
<div class="frame template">
|
<div class="frame template">
|
||||||
<div class="map">
|
<div class="map">
|
||||||
<img src="images/maps/radar.webp" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user