mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-17 09:09:30 -07:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37193112a7 | ||
|
|
0d9c445919 | ||
|
|
6c9fb4cf68 | ||
|
|
59b10ae222 | ||
|
|
d18b13821a | ||
|
|
320d3139c3 | ||
|
|
34dedb44c1 | ||
|
|
18633708f9 | ||
|
|
9b12255e0a |
@@ -1,3 +1,5 @@
|
|||||||
|

|
||||||
|
|
||||||
# WeatherStar 4000+
|
# WeatherStar 4000+
|
||||||
|
|
||||||
A live version of this project is available at https://weatherstar.netbymatt.com
|
A live version of this project is available at https://weatherstar.netbymatt.com
|
||||||
|
|||||||
@@ -729,6 +729,16 @@
|
|||||||
"wfo": "LMK"
|
"wfo": "LMK"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"city": "Lubbock",
|
||||||
|
"lat": 33.5836,
|
||||||
|
"lon": -101.8549,
|
||||||
|
"point": {
|
||||||
|
"x": 49,
|
||||||
|
"y": 34,
|
||||||
|
"wfo": "LUB"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"city": "Manchester",
|
"city": "Manchester",
|
||||||
"lat": 42.9956,
|
"lat": 42.9956,
|
||||||
|
|||||||
@@ -364,6 +364,11 @@
|
|||||||
"lat": 38.2542,
|
"lat": 38.2542,
|
||||||
"lon": -85.7594
|
"lon": -85.7594
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"city": "Lubbock",
|
||||||
|
"lat": 33.5836,
|
||||||
|
"lon": -101.8549
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"city": "Manchester",
|
"city": "Manchester",
|
||||||
"lat": 42.9956,
|
"lat": 42.9956,
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.3.2",
|
"version": "6.4.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "ws4kp",
|
"name": "ws4kp",
|
||||||
"version": "6.3.2",
|
"version": "6.4.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.2",
|
"version": "6.4.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",
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class RegionalForecast extends WeatherDisplay {
|
|||||||
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.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.4;
|
||||||
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
|
||||||
|
|||||||
@@ -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
@@ -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