mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 00:59:29 -07:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7bd21bcf1d | ||
|
|
ec65025ae2 | ||
|
|
194e108037 | ||
|
|
d5b7c6630a | ||
|
|
39bafae394 | ||
|
|
ec8fffbb64 | ||
|
|
0a794eae36 | ||
|
|
8c83736aba | ||
|
|
872162080d | ||
|
|
69d2b0f40b | ||
|
|
37193112a7 | ||
|
|
0d9c445919 | ||
|
|
6c9fb4cf68 | ||
|
|
59b10ae222 | ||
|
|
d18b13821a | ||
|
|
320d3139c3 |
@@ -336,8 +336,10 @@ When using Docker:
|
|||||||
* **Static deployment**: Mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js`
|
* **Static deployment**: Mount your `custom.js` file to `/usr/share/nginx/html/scripts/custom.js`
|
||||||
* **Server deployment**: Mount your `custom.js` file to `/app/server/scripts/custom.js`
|
* **Server deployment**: Mount your `custom.js` file to `/app/server/scripts/custom.js`
|
||||||
|
|
||||||
### RSS feeds and custom scroll
|
### Custom text scroll
|
||||||
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, or show headlines from an rss feed turn on the setting for `Enable RSS Feed/Text` and then enter a URL or text in the resulting text box. Then press set.
|
If you would like your Weatherstar to have custom scrolling text in the bottom blue bar, turn on the setting for `Enable RSS Feed/Text` and then enter text in the resulting text box. Then press set.
|
||||||
|
|
||||||
|
Tip: You can have Weatherstar select randomly between several text strings on each pass through the current conditions. Use a pipe character to separate string. `Welcome to Weatherstar|Thanks for watching`.
|
||||||
|
|
||||||
## Issue reporting and feature requests
|
## Issue reporting and feature requests
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ const mjsSources = [
|
|||||||
'server/scripts/modules/travelforecast.mjs',
|
'server/scripts/modules/travelforecast.mjs',
|
||||||
'server/scripts/modules/progress.mjs',
|
'server/scripts/modules/progress.mjs',
|
||||||
'server/scripts/modules/media.mjs',
|
'server/scripts/modules/media.mjs',
|
||||||
'server/scripts/modules/custom-rss-feed.mjs',
|
'server/scripts/modules/custom-scroll-text.mjs',
|
||||||
'server/scripts/index.mjs',
|
'server/scripts/index.mjs',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,7 @@ if (process.env?.DIST === '1') {
|
|||||||
// 'npm run build' and then 'DIST=1 npm start'
|
// 'npm run build' and then 'DIST=1 npm start'
|
||||||
app.use('/scripts', express.static('./server/scripts', staticOptions));
|
app.use('/scripts', express.static('./server/scripts', staticOptions));
|
||||||
app.use('/geoip', geoip);
|
app.use('/geoip', geoip);
|
||||||
|
app.use('/music', express.static('./server/music', staticOptions));
|
||||||
|
|
||||||
// render the EJS template in production mode (serve compressed files from dist directory)
|
// render the EJS template in production mode (serve compressed files from dist directory)
|
||||||
app.get('/', (req, res) => { renderIndex(req, res, true); });
|
app.get('/', (req, res) => { renderIndex(req, res, true); });
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.3.3",
|
"version": "6.5.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.3.3",
|
"version": "6.5.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotenv": "^17.0.1",
|
"dotenv": "^17.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.3.3",
|
"version": "6.5.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",
|
||||||
|
|||||||
@@ -324,7 +324,7 @@ const backfill = (data) => {
|
|||||||
// backfill each property
|
// backfill each property
|
||||||
Object.keys(sortedData[0].properties).forEach((key) => {
|
Object.keys(sortedData[0].properties).forEach((key) => {
|
||||||
// qualify the key (must have value)
|
// qualify the key (must have value)
|
||||||
if (Object.hasOwn(sortedData[0].properties[key], 'value')) {
|
if (Object.hasOwn(sortedData[0].properties?.[key] ?? {}, 'value')) {
|
||||||
// backfill this property
|
// backfill this property
|
||||||
result[key] = backfillProperty(sortedData, key);
|
result[key] = backfillProperty(sortedData, key);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
import Setting from './utils/setting.mjs';
|
|
||||||
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
|
||||||
import { json } from './utils/fetch.mjs';
|
|
||||||
|
|
||||||
let firstRun = true;
|
|
||||||
|
|
||||||
const parser = new DOMParser();
|
|
||||||
|
|
||||||
// change of enable handler
|
|
||||||
const changeEnable = (newValue) => {
|
|
||||||
let newDisplay;
|
|
||||||
if (newValue) {
|
|
||||||
// add the feed to the scroll
|
|
||||||
parseFeed(customFeed.value);
|
|
||||||
// show the string box
|
|
||||||
newDisplay = 'block';
|
|
||||||
} else {
|
|
||||||
// set scroll back to original
|
|
||||||
resetScroll();
|
|
||||||
// hide the string entry
|
|
||||||
newDisplay = 'none';
|
|
||||||
}
|
|
||||||
const stringEntry = document.getElementById('settings-customFeed-label');
|
|
||||||
if (stringEntry) {
|
|
||||||
stringEntry.style.display = newDisplay;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// parse the feed/text provided
|
|
||||||
const parseFeed = (textInput) => {
|
|
||||||
// skip getting the feed on first run
|
|
||||||
if (firstRun) return;
|
|
||||||
|
|
||||||
// test validity
|
|
||||||
if (textInput === undefined || textInput === '') {
|
|
||||||
resetScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
// test for url
|
|
||||||
if (textInput.match(/https?:\/\//)) {
|
|
||||||
getFeed(textInput);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add single text scroll after hazards if present
|
|
||||||
resetScroll();
|
|
||||||
addScroll(hazards);
|
|
||||||
addScroll(
|
|
||||||
() => (
|
|
||||||
{
|
|
||||||
type: 'scroll',
|
|
||||||
text: textInput,
|
|
||||||
}),
|
|
||||||
// keep the existing scroll
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// get the rss feed and then swap out the current weather scroll
|
|
||||||
const getFeed = async (url) => {
|
|
||||||
// get the text as a string
|
|
||||||
// it needs to be proxied, use a free service
|
|
||||||
const rssResponse = await json(`https://api.allorigins.win/get?url=${url}`);
|
|
||||||
|
|
||||||
// this returns a data url
|
|
||||||
// a few sanity checks
|
|
||||||
if (rssResponse.status.content_type.indexOf('xml') < 0) return;
|
|
||||||
// determine return type
|
|
||||||
const isBase64 = rssResponse.status.content_type.substring(0, 8) !== 'text/xml';
|
|
||||||
|
|
||||||
// base 64 decode everything after the comma
|
|
||||||
const rss = isBase64 ? atob(rssResponse.contents.split('base64,')[1]) : rssResponse.contents;
|
|
||||||
|
|
||||||
// parse the rss
|
|
||||||
const doc = parser.parseFromString(rss, 'text/xml');
|
|
||||||
|
|
||||||
// get the title
|
|
||||||
const rssTitle = doc.querySelector('channel title').textContent;
|
|
||||||
|
|
||||||
// get each item
|
|
||||||
const titles = [...doc.querySelectorAll('item title')].map((t) => t.textContent);
|
|
||||||
|
|
||||||
// reset the scroll, then add the screens
|
|
||||||
resetScroll();
|
|
||||||
// add the hazards scroll first
|
|
||||||
addScroll(hazards);
|
|
||||||
titles.forEach((title) => {
|
|
||||||
// data is provided to the screen handler, so we return a function
|
|
||||||
addScroll(
|
|
||||||
() => ({
|
|
||||||
header: rssTitle,
|
|
||||||
type: 'scroll',
|
|
||||||
text: title,
|
|
||||||
}),
|
|
||||||
// false parameter does not include the default weather scrolls
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// change the feed source and re-load if necessary
|
|
||||||
const changeFeed = (newValue) => {
|
|
||||||
// first pass through won't have custom feed enable ready
|
|
||||||
if (firstRun) return;
|
|
||||||
|
|
||||||
if (customFeedEnable.value) {
|
|
||||||
parseFeed(newValue);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const customFeed = new Setting('customFeed', {
|
|
||||||
name: 'Custom RSS Feed',
|
|
||||||
defaultValue: '',
|
|
||||||
type: 'string',
|
|
||||||
changeAction: changeFeed,
|
|
||||||
placeholder: 'Text or URL',
|
|
||||||
});
|
|
||||||
|
|
||||||
const customFeedEnable = new Setting('customFeedEnable', {
|
|
||||||
name: 'Enable RSS Feed/Text',
|
|
||||||
defaultValue: false,
|
|
||||||
changeAction: changeEnable,
|
|
||||||
});
|
|
||||||
|
|
||||||
// initialize the custom feed inputs on the page
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// add the controls to the page
|
|
||||||
const settingsSection = document.querySelector('#settings');
|
|
||||||
settingsSection.append(customFeedEnable.generate(), customFeed.generate());
|
|
||||||
// clear the first run value
|
|
||||||
firstRun = false;
|
|
||||||
// call change enable with the current value to show/hide the url box
|
|
||||||
// and make the call to get the feed if enabled
|
|
||||||
changeEnable(customFeedEnable.value);
|
|
||||||
});
|
|
||||||
91
server/scripts/modules/custom-scroll-text.mjs
Normal file
91
server/scripts/modules/custom-scroll-text.mjs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import Setting from './utils/setting.mjs';
|
||||||
|
import { reset as resetScroll, addScreen as addScroll, hazards } from './currentweatherscroll.mjs';
|
||||||
|
|
||||||
|
let firstRun = true;
|
||||||
|
|
||||||
|
const parser = new DOMParser();
|
||||||
|
|
||||||
|
// change of enable handler
|
||||||
|
const changeEnable = (newValue) => {
|
||||||
|
let newDisplay;
|
||||||
|
if (newValue) {
|
||||||
|
// add the text to the scroll
|
||||||
|
parseText(customText.value);
|
||||||
|
// show the string box
|
||||||
|
newDisplay = 'block';
|
||||||
|
} else {
|
||||||
|
// set scroll back to original
|
||||||
|
resetScroll();
|
||||||
|
// hide the string entry
|
||||||
|
newDisplay = 'none';
|
||||||
|
}
|
||||||
|
const stringEntry = document.getElementById('settings-customText-label');
|
||||||
|
if (stringEntry) {
|
||||||
|
stringEntry.style.display = newDisplay;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse the text provided
|
||||||
|
const parseText = (textInput) => {
|
||||||
|
// skip updating text on first run
|
||||||
|
if (firstRun) return;
|
||||||
|
|
||||||
|
// test validity
|
||||||
|
if (textInput === undefined || textInput === '') {
|
||||||
|
resetScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the text at pipe characters
|
||||||
|
const texts = textInput.split('|');
|
||||||
|
|
||||||
|
// add single text scroll after hazards if present
|
||||||
|
resetScroll();
|
||||||
|
addScroll(hazards);
|
||||||
|
addScroll(
|
||||||
|
() => {
|
||||||
|
// pick a random string from the available list
|
||||||
|
const randInt = Math.floor(Math.random() * texts.length);
|
||||||
|
return {
|
||||||
|
type: 'scroll',
|
||||||
|
text: texts[randInt],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// keep the existing scroll
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// change the text
|
||||||
|
const changeText = (newValue) => {
|
||||||
|
// first pass through won't have custom text enable ready
|
||||||
|
if (firstRun) return;
|
||||||
|
|
||||||
|
if (customTextEnable.value) {
|
||||||
|
parseText(newValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const customText = new Setting('customText', {
|
||||||
|
name: 'Custom Text',
|
||||||
|
defaultValue: '',
|
||||||
|
type: 'string',
|
||||||
|
changeAction: changeText,
|
||||||
|
placeholder: 'Text to scroll',
|
||||||
|
});
|
||||||
|
|
||||||
|
const customTextEnable = new Setting('customTextEnable', {
|
||||||
|
name: 'Enable Custom Text',
|
||||||
|
defaultValue: false,
|
||||||
|
changeAction: changeEnable,
|
||||||
|
});
|
||||||
|
|
||||||
|
// initialize the custom text inputs on the page
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// add the controls to the page
|
||||||
|
const settingsSection = document.querySelector('#settings');
|
||||||
|
settingsSection.append(customTextEnable.generate(), customText.generate());
|
||||||
|
// clear the first run value
|
||||||
|
firstRun = false;
|
||||||
|
// call change enable with the current value to show/hide the url box
|
||||||
|
changeEnable(customTextEnable.value);
|
||||||
|
});
|
||||||
@@ -40,9 +40,10 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
const temperature = data.map((d) => d.temperature);
|
const temperature = data.map((d) => d.temperature);
|
||||||
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
|
const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation);
|
||||||
const skyCover = data.map((d) => d.skyCover);
|
const skyCover = data.map((d) => d.skyCover);
|
||||||
|
const dewpoint = data.map((d) => d.dewpoint);
|
||||||
|
|
||||||
this.data = {
|
this.data = {
|
||||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, dewpoint,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.setStatus(STATUS.loaded);
|
this.setStatus(STATUS.loaded);
|
||||||
@@ -63,12 +64,16 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
|
|
||||||
// calculate time scale
|
// calculate time scale
|
||||||
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth);
|
||||||
|
const timeStep = this.data.temperature.length / 4;
|
||||||
const startTime = DateTime.now().startOf('hour');
|
const startTime = DateTime.now().startOf('hour');
|
||||||
document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime);
|
let prevTime = startTime;
|
||||||
document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 }));
|
Array(5).fill().forEach((val, idx) => {
|
||||||
document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 }));
|
// track the previous label so a day of week can be added when it changes
|
||||||
document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 }));
|
const label = formatTime(startTime.plus({ hour: idx * timeStep }), prevTime);
|
||||||
document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 }));
|
prevTime = label.ts;
|
||||||
|
// write to page
|
||||||
|
document.querySelector(`.x-axis .l-${idx + 1}`).innerHTML = label.formatted;
|
||||||
|
});
|
||||||
|
|
||||||
// order is important last line drawn is on top
|
// order is important last line drawn is on top
|
||||||
// clouds
|
// clouds
|
||||||
@@ -86,11 +91,22 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
lineWidth: 3,
|
lineWidth: 3,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// calculate temperature scale for min and max of dewpoint and temperature
|
||||||
|
const minScale = Math.min(...this.data.dewpoint, ...this.data.temperature);
|
||||||
|
const maxScale = Math.max(...this.data.dewpoint, ...this.data.temperature);
|
||||||
|
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);
|
||||||
|
|
||||||
|
// dewpoint
|
||||||
|
const dewpointPath = createPath(this.data.dewpoint, timeScale, tempScale);
|
||||||
|
drawPath(dewpointPath, ctx, {
|
||||||
|
strokeStyle: 'green',
|
||||||
|
lineWidth: 3,
|
||||||
|
});
|
||||||
|
|
||||||
// temperature
|
// temperature
|
||||||
const minTemp = Math.min(...this.data.temperature);
|
|
||||||
const maxTemp = Math.max(...this.data.temperature);
|
|
||||||
const midTemp = Math.round((minTemp + maxTemp) / 2);
|
|
||||||
const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10);
|
|
||||||
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
|
const tempPath = createPath(this.data.temperature, timeScale, tempScale);
|
||||||
drawPath(tempPath, ctx, {
|
drawPath(tempPath, ctx, {
|
||||||
strokeStyle: 'red',
|
strokeStyle: 'red',
|
||||||
@@ -100,15 +116,17 @@ class HourlyGraph extends WeatherDisplay {
|
|||||||
// temperature axis labels
|
// temperature axis labels
|
||||||
// limited to 3 characters, sacraficing degree character
|
// limited to 3 characters, sacraficing degree character
|
||||||
const degree = String.fromCharCode(176);
|
const degree = String.fromCharCode(176);
|
||||||
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + degree).substring(0, 3);
|
this.elem.querySelector('.y-axis .l-1').innerHTML = (maxScale + degree).substring(0, 3);
|
||||||
this.elem.querySelector('.y-axis .l-2').innerHTML = (midTemp + degree).substring(0, 3);
|
this.elem.querySelector('.y-axis .l-2').innerHTML = (midScale2 + degree).substring(0, 3);
|
||||||
this.elem.querySelector('.y-axis .l-3').innerHTML = (minTemp + degree).substring(0, 3);
|
this.elem.querySelector('.y-axis .l-3').innerHTML = (midScale1 + degree).substring(0, 3);
|
||||||
|
this.elem.querySelector('.y-axis .l-4').innerHTML = (minScale + degree).substring(0, 3);
|
||||||
|
|
||||||
// set the image source
|
// set the image source
|
||||||
this.image.src = canvas.toDataURL();
|
this.image.src = canvas.toDataURL();
|
||||||
|
|
||||||
// change the units in the header
|
// change the units in the header
|
||||||
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||||
|
this.elem.querySelector('.dewpoint').innerHTML = `Dewpoint ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||||
|
|
||||||
super.drawCanvas();
|
super.drawCanvas();
|
||||||
this.finishDraw();
|
this.finishDraw();
|
||||||
@@ -145,7 +163,18 @@ const drawPath = (path, ctx, options) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// format as 1p, 12a, etc.
|
// format as 1p, 12a, etc.
|
||||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
const formatTime = (time, prev) => {
|
||||||
|
// if the day of the week changes, show the day of the week in the label
|
||||||
|
let format = 'ha';
|
||||||
|
if (prev.weekday !== time.weekday) format = 'ccc ha';
|
||||||
|
|
||||||
|
const ts = time.setZone(timeZone());
|
||||||
|
|
||||||
|
return {
|
||||||
|
ts,
|
||||||
|
formatted: ts.toFormat(format).slice(0, -1),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// register display
|
// register display
|
||||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||||
|
|||||||
@@ -75,7 +75,10 @@ class Hourly extends WeatherDisplay {
|
|||||||
|
|
||||||
const startingHour = DateTime.local().setZone(timeZone());
|
const startingHour = DateTime.local().setZone(timeZone());
|
||||||
|
|
||||||
const lines = this.data.map((data, index) => {
|
// shorten to 24 hours
|
||||||
|
const shortData = this.data.slice(0, 24);
|
||||||
|
|
||||||
|
const lines = shortData.map((data, index) => {
|
||||||
const fillValues = {};
|
const fillValues = {};
|
||||||
// hour
|
// hour
|
||||||
const hour = startingHour.plus({ hours: index });
|
const hour = startingHour.plus({ hours: index });
|
||||||
@@ -102,7 +105,7 @@ class Hourly extends WeatherDisplay {
|
|||||||
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
const filledRow = this.fillTemplate('hourly-row', fillValues);
|
||||||
|
|
||||||
// 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 (data.apparentTemperature < data.temperature) {
|
||||||
filledRow.querySelector('.like').classList.add('wind-chill');
|
filledRow.querySelector('.like').classList.add('wind-chill');
|
||||||
} else if (feelsLike > temperature) {
|
} else if (feelsLike > temperature) {
|
||||||
filledRow.querySelector('.like').classList.add('heat-index');
|
filledRow.querySelector('.like').classList.add('heat-index');
|
||||||
@@ -203,6 +206,7 @@ const parseForecast = async (data) => {
|
|||||||
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
const iceAccumulation = expand(data.iceAccumulation.values); // ice icon
|
||||||
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
const probabilityOfPrecipitation = expand(data.probabilityOfPrecipitation.values); // rain icon
|
||||||
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
|
const snowfallAmount = expand(data.snowfallAmount.values); // snow icon
|
||||||
|
const dewpoint = expand(data.dewpoint.values);
|
||||||
|
|
||||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||||
|
|
||||||
@@ -216,6 +220,7 @@ const parseForecast = async (data) => {
|
|||||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||||
skyCover: skyCover[idx],
|
skyCover: skyCover[idx],
|
||||||
icon: icons[idx],
|
icon: icons[idx],
|
||||||
|
dewpoint: temperatureConverter(dewpoint[idx]),
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -233,7 +238,7 @@ const determineIcon = async (skyCover, weather, iceAccumulation, probabilityOfPr
|
|||||||
};
|
};
|
||||||
|
|
||||||
// expand a set of values with durations to an hour-by-hour array
|
// expand a set of values with durations to an hour-by-hour array
|
||||||
const expand = (data, maxHours = 24) => {
|
const expand = (data, maxHours = 36) => {
|
||||||
const startOfHour = DateTime.utc().startOf('hour').toMillis();
|
const startOfHour = DateTime.utc().startOf('hour').toMillis();
|
||||||
const result = []; // resulting expanded values
|
const result = []; // resulting expanded values
|
||||||
data.forEach((item) => {
|
data.forEach((item) => {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn(`largeIcon: ${error.message}`);
|
console.warn(`largeIcon: ${error.message}`);
|
||||||
// Return a fallback icon to prevent downstream errors
|
// Return a fallback icon to prevent downstream errors
|
||||||
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// find the icon
|
// find the icon
|
||||||
@@ -102,6 +102,8 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'snow_fzra':
|
case 'snow_fzra':
|
||||||
case 'snow_fzra-n':
|
case 'snow_fzra-n':
|
||||||
|
case 'winter_mix':
|
||||||
|
case 'winter_mix-n':
|
||||||
return addPath('Freezing-Rain-Snow.gif');
|
return addPath('Freezing-Rain-Snow.gif');
|
||||||
|
|
||||||
case 'fzra':
|
case 'fzra':
|
||||||
@@ -141,6 +143,8 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
return addPath('Thunderstorm.gif');
|
return addPath('Thunderstorm.gif');
|
||||||
|
|
||||||
case 'wind_skc':
|
case 'wind_skc':
|
||||||
|
case 'wind_':
|
||||||
|
case 'wind_-n':
|
||||||
return addPath('Windy.gif');
|
return addPath('Windy.gif');
|
||||||
|
|
||||||
case 'wind_skc-n':
|
case 'wind_skc-n':
|
||||||
@@ -169,7 +173,7 @@ const largeIcon = (link, _isNightTime) => {
|
|||||||
default: {
|
default: {
|
||||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
// Return a reasonable fallback instead of false to prevent downstream errors
|
// Return a reasonable fallback instead of false to prevent downstream errors
|
||||||
return addPath(`No-Data.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
return addPath(`No-Data-Large.gif?${conditionIcon}${isNightTime ? '-n' : ''}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -133,6 +133,7 @@ const smallIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'wind_few':
|
case 'wind_few':
|
||||||
case 'wind_few-n':
|
case 'wind_few-n':
|
||||||
|
case 'wind_':
|
||||||
return addPath('Wind.gif');
|
return addPath('Wind.gif');
|
||||||
|
|
||||||
case 'wind_sct':
|
case 'wind_sct':
|
||||||
@@ -170,7 +171,7 @@ const smallIcon = (link, _isNightTime) => {
|
|||||||
|
|
||||||
case 'blizzard':
|
case 'blizzard':
|
||||||
case 'blizzard-n':
|
case 'blizzard-n':
|
||||||
return addPath('Blowing Snow.gif');
|
return addPath('Blowing-Snow.gif');
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
console.warn(`Unknown weather condition '${conditionIcon}' from ${link}; using fallback icon`);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const buildForecast = (forecast, city, cityXY) => {
|
|||||||
const getRegionalObservation = async (point, city) => {
|
const getRegionalObservation = async (point, city) => {
|
||||||
try {
|
try {
|
||||||
// get stations using centralized safe handling
|
// get stations using centralized safe handling
|
||||||
const stations = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=1`);
|
const stations = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/stations?limit=10`);
|
||||||
|
|
||||||
if (!stations || !stations.features || stations.features.length === 0) {
|
if (!stations || !stations.features || stations.features.length === 0) {
|
||||||
if (debugFlag('verbose-failures')) {
|
if (debugFlag('verbose-failures')) {
|
||||||
@@ -32,9 +32,13 @@ const getRegionalObservation = async (point, city) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get the first station
|
// get the first station with a 4-letter id (generally has appropriate data)
|
||||||
const station = stations.features[0].id;
|
const station4Letter = stations.features.find((station) => {
|
||||||
const stationId = stations.features[0].properties.stationIdentifier;
|
if (station.properties.stationIdentifier.length === 4) return station.properties;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
const station = station4Letter.id;
|
||||||
|
const stationId = station4Letter.properties.stationIdentifier;
|
||||||
// get the observation data using centralized safe handling
|
// get the observation data using centralized safe handling
|
||||||
const observation = await safeJson(`${station}/observations/latest`);
|
const observation = await safeJson(`${station}/observations/latest`);
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
right: 60px;
|
right: 60px;
|
||||||
width: 360px;
|
width: 360px;
|
||||||
font-family: 'Star4000 Small';
|
font-family: 'Star4000 Small';
|
||||||
font-size: 32px;
|
font-size: 28px;
|
||||||
@include u.text-shadow();
|
@include u.text-shadow();
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
@@ -23,6 +23,10 @@
|
|||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dewpoint {
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
|
||||||
.cloud {
|
.cloud {
|
||||||
color: lightgrey;
|
color: lightgrey;
|
||||||
}
|
}
|
||||||
@@ -52,32 +56,33 @@
|
|||||||
|
|
||||||
.x-axis {
|
.x-axis {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
left: 0px;
|
left: 54px;
|
||||||
width: 640px;
|
width: 532px;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
|
||||||
.label {
|
.label {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 50px;
|
transform: translateX(-50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
&.l-1 {
|
&.l-1 {
|
||||||
left: 25px;
|
left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-2 {
|
&.l-2 {
|
||||||
left: 158px;
|
left: calc(532px / 4 * 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-3 {
|
&.l-3 {
|
||||||
left: 291px;
|
left: calc(532px / 4 * 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-4 {
|
&.l-4 {
|
||||||
left: 424px;
|
left: calc(532px / 4 * 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-5 {
|
&.l-5 {
|
||||||
left: 557px;
|
left: calc(532px / 4 * 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,10 +115,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&.l-2 {
|
&.l-2 {
|
||||||
top: 140px;
|
top: calc(280px / 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.l-3 {
|
&.l-3 {
|
||||||
|
bottom: calc(280px / 3 - 11px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.l-4 {
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2
server/styles/ws.min.css
vendored
2
server/styles/ws.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -62,7 +62,7 @@
|
|||||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/media.mjs"></script>
|
<script type="module" src="scripts/modules/media.mjs"></script>
|
||||||
<script type="module" src="scripts/modules/custom-rss-feed.mjs"></script>
|
<script type="module" src="scripts/modules/custom-scroll-text.mjs"></script>
|
||||||
<script type="module" src="scripts/index.mjs"></script>
|
<script type="module" src="scripts/index.mjs"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
<div class="main has-scroll hourly-graph">
|
<div class="main has-scroll hourly-graph">
|
||||||
<div class="top-right template ">
|
<div class="top-right template ">
|
||||||
<div class="temperature">Temperature</div>
|
<div class="temperature">Temperature</div>
|
||||||
|
<div class="dewpoint">Dewpoint</div>
|
||||||
<div class="cloud">Cloud %</div>
|
<div class="cloud">Cloud %</div>
|
||||||
<div class="rain">Precip %</div>
|
<div class="rain">Precip %</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -9,6 +10,7 @@
|
|||||||
<div class="label l-1">75</div>
|
<div class="label l-1">75</div>
|
||||||
<div class="label l-2">65</div>
|
<div class="label l-2">65</div>
|
||||||
<div class="label l-3">55</div>
|
<div class="label l-3">55</div>
|
||||||
|
<div class="label l-4">45</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<img id="chart-area"></img>
|
<img id="chart-area"></img>
|
||||||
|
|||||||
Reference in New Issue
Block a user