mirror of
https://github.com/netbymatt/ws4kp.git
synced 2026-04-14 07:39:29 -07:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b812c7b25c | ||
|
|
707144cabd | ||
|
|
372cb0cfab | ||
|
|
cab2da5e62 | ||
|
|
471d322cde | ||
|
|
8f9be046ac | ||
|
|
c34dc1ff25 | ||
|
|
9b4eed7332 | ||
|
|
ef1477f9eb | ||
|
|
e2876df177 | ||
|
|
d6335b2878 | ||
|
|
781128100e | ||
|
|
56261ded4b | ||
|
|
58540ad67b | ||
|
|
af53cca45e | ||
|
|
94470db9a7 | ||
|
|
3c7a77e200 | ||
|
|
d472df2e26 | ||
|
|
fdbf11dcd4 | ||
|
|
b7e9091320 | ||
|
|
d24284d340 | ||
|
|
4e8dc35739 |
@@ -16,6 +16,13 @@ This project is based on the work of [Mike Battaglia](https://github.com/vbguyny
|
||||
* [Icon](https://twcclassics.com/downloads.html) sets
|
||||
* Countless photos and videos of WeatherStar 4000 forecasts used as references.
|
||||
|
||||
## Does WeatherStar 4000+ work outside of the USA?
|
||||
|
||||
This project is tightly coupled to [NOAA's Weather API](https://www.weather.gov/documentation/services-web-api), which is exclsuive to the United States. Using NOAA's Weather API is a crucial requirement to provide an authentic WeatherStar 4000+ experience.
|
||||
|
||||
If you would like to display weather information for international locations (outside of the USA), please checkout a fork of this project created by [@mwood77](https://github.com/mwood77):
|
||||
- [`ws4kp-international`](https://github.com/mwood77/ws4kp-international)
|
||||
|
||||
## Run Your WeatherStar
|
||||
There are a lot of CORS considerations and issues with api.weather.gov that are easiest to deal with by running a local server to see this in action (or use the live link above). You'll need Node.js >12.0 to run the local server.
|
||||
|
||||
@@ -104,6 +111,8 @@ A hook is provided as `/server/scripts/custom.js` to allow customizations to you
|
||||
|
||||
Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser.
|
||||
|
||||
Note: not all units are converted to metric, if selected. Some text-based products such as warnings are simple text strings provided from the national weather service and thus have baked-in units such as "gusts up to 60 mph." These values will not be converted.
|
||||
|
||||
## Disclaimer
|
||||
|
||||
This web site should NOT be used in life threatening weather situations, or be relied on to inform the public of such situations. The Internet is an unreliable network subject to server and network outages and by nature is not suitable for such mission critical use. If you require such access to NWS data, please consider one of their subscription services. The authors of this web site shall not be held liable in the event of injury, death or property damage that occur as a result of disregarding this warning.
|
||||
|
||||
2
dist/index.html
vendored
2
dist/index.html
vendored
File diff suppressed because one or more lines are too long
2
dist/resources/ws.min.js
vendored
2
dist/resources/ws.min.js
vendored
File diff suppressed because one or more lines are too long
3574
package-lock.json
generated
3574
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ws4kp",
|
||||
"version": "5.13.4",
|
||||
"version": "5.14.4",
|
||||
"description": "Welcome to the WeatherStar 4000+ project page!",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"homepage": "https://github.com/netbymatt/ws4kp#readme",
|
||||
"devDependencies": {
|
||||
"del": "^7.1.0",
|
||||
"del": "^8.0.0",
|
||||
"jquery": "^3.6.0",
|
||||
"jquery-touchswipe": "^1.6.19",
|
||||
"luxon": "^3.0.0",
|
||||
@@ -38,7 +38,7 @@
|
||||
"gulp-ejs": "^5.1.0",
|
||||
"gulp-htmlmin": "^5.0.1",
|
||||
"gulp-rename": "^2.0.0",
|
||||
"gulp-sass": "^5.1.0",
|
||||
"gulp-sass": "^6.0.0",
|
||||
"gulp-terser": "^2.0.0",
|
||||
"terser-webpack-plugin": "^5.3.6",
|
||||
"webpack-stream": "^7.0.0",
|
||||
|
||||
BIN
server/images/social/1200x600.png
Normal file
BIN
server/images/social/1200x600.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 243 KiB |
@@ -2,8 +2,7 @@
|
||||
// it is intended to allow for customizations that do not get published back to the git repo
|
||||
// for example, changing the logo
|
||||
|
||||
// start running after all content is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const customTask = () => {
|
||||
// get all of the logo images
|
||||
const logos = document.querySelectorAll('.logo img');
|
||||
// loop through each logo
|
||||
@@ -11,4 +10,16 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
// change the source
|
||||
elem.src = 'my-custom-logo.gif';
|
||||
});
|
||||
};
|
||||
|
||||
// start running after all content is loaded, or immediately if page content is already loaded
|
||||
if (document.readyState === 'loading') {
|
||||
// Loading hasn't finished yet
|
||||
document.addEventListener('DOMContentLoaded', customTask);
|
||||
} else {
|
||||
// `DOMContentLoaded` has already fired
|
||||
customTask();
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import settings from './modules/settings.mjs';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
init();
|
||||
getCustomCode();
|
||||
});
|
||||
|
||||
const categories = [
|
||||
@@ -413,3 +414,15 @@ const fullScreenResizeCheck = () => {
|
||||
// store state of fullscreen element for next change detection
|
||||
fullScreenResizeCheck.wasFull = !!document.fullscreenElement;
|
||||
};
|
||||
|
||||
const getCustomCode = async () => {
|
||||
// fetch the custom file and see if it returns a 200 status
|
||||
const response = await fetch('scripts/custom.js', { method: 'HEAD' });
|
||||
if (response.ok) {
|
||||
// add the script element to the page
|
||||
const customElem = document.createElement('script');
|
||||
customElem.src = 'scripts/custom.js';
|
||||
customElem.type = 'text/javascript';
|
||||
document.body.append(customElem);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { loadImg, preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
|
||||
class Almanac extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -123,10 +123,10 @@ class Almanac extends WeatherDisplay {
|
||||
// sun and moon data
|
||||
this.elem.querySelector('.day-1').innerHTML = Today.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).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.rise-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.rise-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunrise).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-1').innerHTML = DateTime.fromJSDate(info.sun[0].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
this.elem.querySelector('.set-2').innerHTML = DateTime.fromJSDate(info.sun[1].sunset).setZone(timeZone()).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase();
|
||||
|
||||
const days = info.moon.map((MoonPhase) => {
|
||||
const fill = {};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import {
|
||||
celsiusToFahrenheit, kphToMph, pascalToInHg, metersToFeet, kilometersToMiles,
|
||||
temperature, windSpeed, pressure, distanceMeters, distanceKilometers,
|
||||
} from './utils/units.mjs';
|
||||
|
||||
// some stations prefixed do not provide all the necessary data
|
||||
@@ -159,23 +159,32 @@ const shortConditions = (_condition) => {
|
||||
|
||||
// format the received data
|
||||
const parseData = (data) => {
|
||||
// get the unit converter
|
||||
const windConverter = windSpeed();
|
||||
const temperatureConverter = temperature();
|
||||
const metersConverter = distanceMeters();
|
||||
const kilometersConverter = distanceKilometers();
|
||||
const pressureConverter = pressure();
|
||||
|
||||
const observations = data.features[0].properties;
|
||||
// values from api are provided in metric
|
||||
data.observations = observations;
|
||||
data.Temperature = Math.round(observations.temperature.value);
|
||||
data.TemperatureUnit = 'C';
|
||||
data.DewPoint = Math.round(observations.dewpoint.value);
|
||||
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||
data.CeilingUnit = 'm.';
|
||||
data.Visibility = Math.round(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' km.';
|
||||
data.WindSpeed = Math.round(observations.windSpeed.value);
|
||||
data.Temperature = temperatureConverter(observations.temperature.value);
|
||||
data.TemperatureUnit = temperatureConverter.units;
|
||||
data.DewPoint = temperatureConverter(observations.dewpoint.value);
|
||||
data.Ceiling = metersConverter(observations.cloudLayers[0]?.base?.value ?? 0);
|
||||
data.CeilingUnit = metersConverter.units;
|
||||
data.Visibility = kilometersConverter(observations.visibility.value);
|
||||
data.VisibilityUnit = kilometersConverter.units;
|
||||
data.Pressure = pressureConverter(observations.barometricPressure.value);
|
||||
data.PressureUnit = pressureConverter.units;
|
||||
data.HeatIndex = temperatureConverter(observations.heatIndex.value);
|
||||
data.WindChill = temperatureConverter(observations.windChill.value);
|
||||
data.WindSpeed = windConverter(observations.windSpeed.value);
|
||||
data.WindDirection = directionToNSEW(observations.windDirection.value);
|
||||
data.Pressure = Math.round(observations.barometricPressure.value);
|
||||
data.HeatIndex = Math.round(observations.heatIndex.value);
|
||||
data.WindChill = Math.round(observations.windChill.value);
|
||||
data.WindGust = Math.round(observations.windGust.value);
|
||||
data.WindUnit = 'KPH';
|
||||
data.WindGust = windConverter(observations.windGust.value);
|
||||
data.WindSpeed = windConverter(data.WindSpeed);
|
||||
data.WindUnit = windConverter.units;
|
||||
data.Humidity = Math.round(observations.relativeHumidity.value);
|
||||
data.Icon = getWeatherIconFromIconLink(observations.icon);
|
||||
data.PressureDirection = '';
|
||||
@@ -186,20 +195,6 @@ const parseData = (data) => {
|
||||
if (pressureDiff > 150) data.PressureDirection = 'R';
|
||||
if (pressureDiff < -150) data.PressureDirection = 'F';
|
||||
|
||||
// convert to us units
|
||||
data.Temperature = celsiusToFahrenheit(data.Temperature);
|
||||
data.TemperatureUnit = 'F';
|
||||
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
|
||||
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
|
||||
data.CeilingUnit = 'ft.';
|
||||
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
|
||||
data.VisibilityUnit = ' mi.';
|
||||
data.WindSpeed = kphToMph(data.WindSpeed);
|
||||
data.WindUnit = 'MPH';
|
||||
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
|
||||
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
|
||||
data.WindChill = celsiusToFahrenheit(data.WindChill);
|
||||
data.WindGust = kphToMph(data.WindGust);
|
||||
return data;
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ const screens = [
|
||||
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
|
||||
|
||||
// barometric pressure
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
|
||||
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
|
||||
|
||||
// wind
|
||||
(data) => {
|
||||
|
||||
@@ -8,6 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class ExtendedForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -26,7 +27,7 @@ class ExtendedForecast extends WeatherDisplay {
|
||||
try {
|
||||
forecast = await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
units: 'us',
|
||||
units: settings.units.value,
|
||||
},
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
@@ -131,7 +132,7 @@ const shortenExtendedForecastText = (long) => {
|
||||
[/dense /gi, ''],
|
||||
[/Thunderstorm/g, 'T\'Storm'],
|
||||
];
|
||||
// run all regexes
|
||||
// run all regexes
|
||||
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
|
||||
|
||||
let conditions = short.split(' ');
|
||||
|
||||
@@ -122,7 +122,7 @@ class Hazards extends WeatherDisplay {
|
||||
// base count change callback
|
||||
baseCountChange(count) {
|
||||
// calculate scroll offset and don't go past end
|
||||
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').getBoundingClientRect().height - 390, (count - 150));
|
||||
let offsetY = Math.min(this.elem.querySelector('.hazard-lines').offsetHeight - 390, (count - 150));
|
||||
|
||||
// don't let offset go negative
|
||||
if (offsetY < 0) offsetY = 0;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import STATUS from './status.mjs';
|
||||
import getHourlyData from './hourly.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
|
||||
class HourlyGraph extends WeatherDisplay {
|
||||
@@ -38,7 +38,7 @@ class HourlyGraph extends WeatherDisplay {
|
||||
const skyCover = data.map((d) => d.skyCover);
|
||||
|
||||
this.data = {
|
||||
skyCover, temperature, probabilityOfPrecipitation,
|
||||
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
|
||||
};
|
||||
|
||||
this.setStatus(STATUS.loaded);
|
||||
@@ -107,6 +107,9 @@ class HourlyGraph extends WeatherDisplay {
|
||||
// set the image source
|
||||
this.image.src = canvas.toDataURL();
|
||||
|
||||
// change the units in the header
|
||||
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
|
||||
|
||||
super.drawCanvas();
|
||||
this.finishDraw();
|
||||
}
|
||||
@@ -142,7 +145,7 @@ const drawPath = (path, ctx, options) => {
|
||||
};
|
||||
|
||||
// format as 1p, 12a, etc.
|
||||
const formatTime = (time) => time.toFormat('ha').slice(0, -1);
|
||||
const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1);
|
||||
|
||||
// register display
|
||||
registerDisplay(new HourlyGraph(4, 'hourly-graph'));
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
import STATUS from './status.mjs';
|
||||
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { celsiusToFahrenheit, kilometersToMiles } from './utils/units.mjs';
|
||||
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
|
||||
import { getHourlyIcon } from './icons.mjs';
|
||||
import { directionToNSEW } from './utils/calc.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import { registerDisplay, timeZone } from './navigation.mjs';
|
||||
import getSun from './almanac.mjs';
|
||||
|
||||
class Hourly extends WeatherDisplay {
|
||||
@@ -56,7 +56,7 @@ class Hourly extends WeatherDisplay {
|
||||
const list = this.elem.querySelector('.hourly-lines');
|
||||
list.innerHTML = '';
|
||||
|
||||
const startingHour = DateTime.local();
|
||||
const startingHour = DateTime.local().setZone(timeZone());
|
||||
|
||||
const lines = this.data.map((data, index) => {
|
||||
const fillValues = {};
|
||||
@@ -66,8 +66,8 @@ class Hourly extends WeatherDisplay {
|
||||
fillValues.hour = formattedHour;
|
||||
|
||||
// temperatures, convert to strings with no decimal
|
||||
const temperature = Math.round(data.temperature).toString().padStart(3);
|
||||
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
|
||||
const temperature = data.temperature.toString().padStart(3);
|
||||
const feelsLike = data.apparentTemperature.toString().padStart(3);
|
||||
fillValues.temp = temperature;
|
||||
// only plot apparent temperature if there is a difference
|
||||
// if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike;
|
||||
@@ -132,6 +132,11 @@ class Hourly extends WeatherDisplay {
|
||||
|
||||
// extract specific values from forecast and format as an array
|
||||
const parseForecast = async (data) => {
|
||||
// get unit converters
|
||||
const temperatureConverter = temperatureUnit();
|
||||
const distanceConverter = distanceKilometers();
|
||||
|
||||
// parse data
|
||||
const temperature = expand(data.temperature.values);
|
||||
const apparentTemperature = expand(data.apparentTemperature.values);
|
||||
const windSpeed = expand(data.windSpeed.values);
|
||||
@@ -145,9 +150,11 @@ const parseForecast = async (data) => {
|
||||
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
|
||||
|
||||
return temperature.map((val, idx) => ({
|
||||
temperature: celsiusToFahrenheit(temperature[idx]),
|
||||
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
|
||||
windSpeed: kilometersToMiles(windSpeed[idx]),
|
||||
temperature: temperatureConverter(temperature[idx]),
|
||||
temperatureUnit: temperatureConverter.units,
|
||||
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
|
||||
windSpeed: distanceConverter(windSpeed[idx]),
|
||||
windUnit: distanceConverter.units,
|
||||
windDirection: directionToNSEW(windDirection[idx]),
|
||||
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
|
||||
skyCover: skyCover[idx],
|
||||
|
||||
@@ -3,9 +3,10 @@ import { distance as calcDistance, directionToNSEW } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import STATUS from './status.mjs';
|
||||
import { locationCleanup } from './utils/string.mjs';
|
||||
import { celsiusToFahrenheit, kphToMph } from './utils/units.mjs';
|
||||
import { temperature, windSpeed } from './utils/units.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class LatestObservations extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -64,14 +65,22 @@ class LatestObservations extends WeatherDisplay {
|
||||
// sort array by station name
|
||||
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
|
||||
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
||||
if (settings.units.value === 'us') {
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
|
||||
} else {
|
||||
this.elem.querySelector('.column-headers .temp.english').classList.remove('show');
|
||||
this.elem.querySelector('.column-headers .temp.metric').classList.add('show');
|
||||
}
|
||||
// get unit converters
|
||||
const windConverter = windSpeed();
|
||||
const temperatureConverter = temperature();
|
||||
|
||||
const lines = sortedConditions.map((condition) => {
|
||||
const windDirection = directionToNSEW(condition.windDirection.value);
|
||||
|
||||
const Temperature = Math.round(celsiusToFahrenheit(condition.temperature.value));
|
||||
const WindSpeed = Math.round(kphToMph(condition.windSpeed.value));
|
||||
const Temperature = temperatureConverter(condition.temperature.value);
|
||||
const WindSpeed = windConverter(condition.windSpeed.value);
|
||||
|
||||
const fill = {
|
||||
location: locationCleanup(condition.city).substr(0, 14),
|
||||
@@ -94,6 +103,8 @@ class LatestObservations extends WeatherDisplay {
|
||||
linesContainer.innerHTML = '';
|
||||
linesContainer.append(...lines);
|
||||
|
||||
// update temperature unit header
|
||||
|
||||
this.finishDraw();
|
||||
}
|
||||
}
|
||||
@@ -122,8 +133,8 @@ const getStations = async (stations) => {
|
||||
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 1, stillWaiting: () => this.stillWaiting() });
|
||||
// test for temperature, weather and wind values present
|
||||
if (data.properties.temperature.value === null
|
||||
|| data.properties.textDescription === ''
|
||||
|| data.properties.windSpeed.value === null) return false;
|
||||
|| data.properties.textDescription === ''
|
||||
|| data.properties.windSpeed.value === null) return false;
|
||||
// format the return values
|
||||
return {
|
||||
...data.properties,
|
||||
|
||||
@@ -4,6 +4,7 @@ import STATUS from './status.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class LocalForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId) {
|
||||
@@ -61,7 +62,7 @@ class LocalForecast extends WeatherDisplay {
|
||||
try {
|
||||
return await json(weatherParameters.forecast, {
|
||||
data: {
|
||||
units: 'us',
|
||||
units: settings.units.value,
|
||||
},
|
||||
retryCount: 3,
|
||||
stillWaiting: () => this.stillWaiting(),
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||
|
||||
const buildForecast = (forecast, city, cityXY) => ({
|
||||
daytime: forecast.isDaytime,
|
||||
temperature: forecast.temperature || 0,
|
||||
name: formatCity(city.city),
|
||||
icon: forecast.icon,
|
||||
x: cityXY.x,
|
||||
y: cityXY.y,
|
||||
time: forecast.startTime,
|
||||
});
|
||||
const buildForecast = (forecast, city, cityXY) => {
|
||||
// get a unit converter
|
||||
const temperatureConverter = temperatureUnit('us');
|
||||
return {
|
||||
daytime: forecast.isDaytime,
|
||||
temperature: temperatureConverter(forecast.temperature || 0),
|
||||
name: formatCity(city.city),
|
||||
icon: forecast.icon,
|
||||
x: cityXY.x,
|
||||
y: cityXY.y,
|
||||
time: forecast.startTime,
|
||||
};
|
||||
};
|
||||
|
||||
const getRegionalObservation = async (point, city) => {
|
||||
try {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import STATUS from './status.mjs';
|
||||
import { distance as calcDistance } from './utils/calc.mjs';
|
||||
import { json } from './utils/fetch.mjs';
|
||||
import { celsiusToFahrenheit } from './utils/units.mjs';
|
||||
import { temperature as temperatureUnit } from './utils/units.mjs';
|
||||
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { preloadImg } from './utils/image.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
@@ -59,7 +59,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
const regionalCities = [];
|
||||
combinedCities.forEach((city) => {
|
||||
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
|
||||
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
||||
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
|
||||
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
|
||||
const targetDist = city.targetDistance || 1;
|
||||
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
|
||||
@@ -71,6 +71,9 @@ class RegionalForecast extends WeatherDisplay {
|
||||
}
|
||||
});
|
||||
|
||||
// get a unit converter
|
||||
const temperatureConverter = temperatureUnit();
|
||||
|
||||
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
|
||||
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
|
||||
try {
|
||||
@@ -93,7 +96,7 @@ class RegionalForecast extends WeatherDisplay {
|
||||
// format the observation the same as the forecast
|
||||
const regionalObservation = {
|
||||
daytime: !!/\/day\//.test(observation.icon),
|
||||
temperature: celsiusToFahrenheit(observation.temperature.value),
|
||||
temperature: temperatureConverter(observation.temperature.value),
|
||||
name: utils.formatCity(city.city),
|
||||
icon: observation.icon,
|
||||
x: cityXY.x,
|
||||
|
||||
@@ -9,7 +9,7 @@ const settings = { speed: { value: 1.0 } };
|
||||
|
||||
const init = () => {
|
||||
// create settings
|
||||
settings.wide = new Setting('wide', 'Widescreen', 'boolean', false, wideScreenChange, true);
|
||||
settings.wide = new Setting('wide', 'Widescreen', 'checkbox', false, wideScreenChange, true);
|
||||
settings.kiosk = new Setting('kiosk', 'Kiosk', 'boolean', false, kioskChange, false);
|
||||
settings.speed = new Setting('speed', 'Speed', 'select', 1.0, null, true, [
|
||||
[0.5, 'Very Fast'],
|
||||
@@ -18,6 +18,10 @@ const init = () => {
|
||||
[1.25, 'Slow'],
|
||||
[1.5, 'Very Slow'],
|
||||
]);
|
||||
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
|
||||
['us', 'US'],
|
||||
['si', 'Metric'],
|
||||
]);
|
||||
|
||||
// generate html objects
|
||||
const settingHtml = Object.values(settings).map((d) => d.generate());
|
||||
@@ -47,4 +51,13 @@ const kioskChange = (value) => {
|
||||
}
|
||||
};
|
||||
|
||||
const unitChange = () => {
|
||||
// reload the data at the top level to refresh units
|
||||
// after the initial load
|
||||
if (unitChange.firstRunDone) {
|
||||
window.location.reload();
|
||||
}
|
||||
unitChange.firstRunDone = true;
|
||||
};
|
||||
|
||||
export default settings;
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
|
||||
import { DateTime } from '../vendor/auto/luxon.mjs';
|
||||
import WeatherDisplay from './weatherdisplay.mjs';
|
||||
import { registerDisplay } from './navigation.mjs';
|
||||
import settings from './settings.mjs';
|
||||
|
||||
class TravelForecast extends WeatherDisplay {
|
||||
constructor(navId, elemId, defaultActive) {
|
||||
@@ -34,7 +35,11 @@ class TravelForecast extends WeatherDisplay {
|
||||
try {
|
||||
// get point then forecast
|
||||
if (!city.point) throw new Error('No pre-loaded point');
|
||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
|
||||
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`, {
|
||||
data: {
|
||||
units: settings.units.value,
|
||||
},
|
||||
});
|
||||
// determine today or tomorrow (shift periods by 1 if tomorrow)
|
||||
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
|
||||
// return a pared-down forecast
|
||||
|
||||
@@ -24,6 +24,10 @@ class Setting {
|
||||
if (type === 'select' && urlValue !== undefined) {
|
||||
urlState = parseFloat(urlValue);
|
||||
}
|
||||
if (type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
|
||||
// couldn't parse as a float, store as a string
|
||||
urlState = urlValue;
|
||||
}
|
||||
|
||||
// get existing value if present
|
||||
const storedValue = urlState ?? this.getFromLocalStorage();
|
||||
@@ -59,7 +63,11 @@ class Setting {
|
||||
|
||||
this.values.forEach(([value, text]) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = value.toFixed(2);
|
||||
if (typeof value === 'number') {
|
||||
option.value = value.toFixed(2);
|
||||
} else {
|
||||
option.value = value;
|
||||
}
|
||||
|
||||
option.innerHTML = text;
|
||||
select.append(option);
|
||||
@@ -108,6 +116,10 @@ class Setting {
|
||||
selectChange(e) {
|
||||
// update the value
|
||||
this.myValue = parseFloat(e.target.value);
|
||||
if (Number.isNaN(this.myValue)) {
|
||||
// was a string, store as such
|
||||
this.myValue = e.target.value;
|
||||
}
|
||||
this.storeToLocalStorage(this.myValue);
|
||||
|
||||
// call the change action
|
||||
@@ -168,7 +180,7 @@ class Setting {
|
||||
selectHighlight(newValue) {
|
||||
// set the dropdown to the provided value
|
||||
this.element.querySelectorAll('option').forEach((elem) => {
|
||||
elem.selected = newValue.toFixed(2) === elem.value;
|
||||
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,113 @@
|
||||
// get the settings for units
|
||||
import settings from '../settings.mjs';
|
||||
// *********************************** unit conversions ***********************
|
||||
|
||||
// round 2 provided for lat/lon formatting
|
||||
const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals;
|
||||
|
||||
const kphToMph = (Kph) => Math.round(Kph / 1.609_34);
|
||||
const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32);
|
||||
const fahrenheitToCelsius = (Fahrenheit) => Math.round((Fahrenheit - 32) * 5 / 9);
|
||||
const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34);
|
||||
const metersToFeet = (Meters) => Math.round(Meters / 0.3048);
|
||||
const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2);
|
||||
|
||||
// each module/page/slide creates it's own unit converter as needed by providing the base units available
|
||||
// the factory function then returns an appropriate converter or pass-thru function for use on the page
|
||||
|
||||
const windSpeed = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = kphToMph;
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = 'kph';
|
||||
} else {
|
||||
converter.units = 'MPH';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const temperature = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
if (defaultUnit === 'us') {
|
||||
converter = fahrenheitToCelsius;
|
||||
} else {
|
||||
converter = celsiusToFahrenheit;
|
||||
}
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = 'C';
|
||||
} else {
|
||||
converter.units = 'F';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const distanceMeters = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
// rounded to the nearest 100 (ceiling)
|
||||
converter = (value) => Math.round(metersToFeet(value) / 100) * 100;
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = 'm.';
|
||||
} else {
|
||||
converter.units = 'ft.';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const distanceKilometers = (defaultUnit = 'si') => {
|
||||
// default to passthru
|
||||
let converter = (passthru) => Math.round(passthru / 1000);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = (value) => Math.round(kilometersToMiles(value) / 1000);
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = ' km.';
|
||||
} else {
|
||||
converter.units = ' mi.';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
const pressure = (defaultUnit = 'si') => {
|
||||
// default to passthru (millibar)
|
||||
let converter = (passthru) => Math.round(passthru / 100);
|
||||
// change the converter if there is a mismatch
|
||||
if (defaultUnit !== settings.units.value) {
|
||||
converter = (value) => pascalToInHg(value).toFixed(2);
|
||||
}
|
||||
// append units
|
||||
if (settings.units.value === 'si') {
|
||||
converter.units = ' mbar';
|
||||
} else {
|
||||
converter.units = ' in.hg';
|
||||
}
|
||||
return converter;
|
||||
};
|
||||
|
||||
export {
|
||||
kphToMph,
|
||||
celsiusToFahrenheit,
|
||||
kilometersToMiles,
|
||||
metersToFeet,
|
||||
pascalToInHg,
|
||||
// unit conversions
|
||||
windSpeed,
|
||||
temperature,
|
||||
distanceMeters,
|
||||
distanceKilometers,
|
||||
pressure,
|
||||
|
||||
// formatter
|
||||
round2,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
|
||||
|
||||
@@ -10,17 +10,19 @@
|
||||
<meta name="author" content="Matt Walsh" />
|
||||
<meta name="application-name" content="WeatherStar 4000+" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,minimum-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="icon" href="images/Logo192.png" />
|
||||
<meta property="og:image" content="images/social/1200x600.png">
|
||||
<meta property="og:image:width" content="1200">
|
||||
<meta property="og:image:height" content="627">
|
||||
|
||||
<% if (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/vendor.min.js?_=<%=production%>"></script>
|
||||
<script type="text/javascript" src="resources/ws.min.js?_=<%=production%>"></script>
|
||||
<script type="text/javascript" src="scripts/custom.js?_=<%=production%>"></script>
|
||||
<% } else { %>
|
||||
<link rel="stylesheet" type="text/css" href="styles/main.css" />
|
||||
<script type="text/javascript" src="scripts/vendor/auto/jquery.js"></script>
|
||||
@@ -45,14 +47,10 @@
|
||||
<script type="module" src="scripts/modules/radar.mjs"></script>
|
||||
<script type="module" src="scripts/modules/settings.mjs"></script>
|
||||
<script type="module" src="scripts/index.mjs"></script>
|
||||
<script type="text/javascript" src="scripts/custom.js"></script>
|
||||
<!-- data -->
|
||||
<script type="text/javascript" src="scripts/data/travelcities.js"></script>
|
||||
<script type="text/javascript" src="scripts/data/regionalcities.js"></script>
|
||||
<script type="text/javascript" src="scripts/data/stations.js"></script>
|
||||
|
||||
<script type="text/javascript" src="scripts/custom.js"></script>
|
||||
|
||||
<% } %>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
"devbridge",
|
||||
"gifs",
|
||||
"ltrim",
|
||||
"mbar",
|
||||
"Noaa",
|
||||
"nosleep",
|
||||
"Pngs",
|
||||
@@ -51,12 +52,15 @@
|
||||
"[html]": {
|
||||
"editor.defaultFormatter": "j69.ejs-beautify"
|
||||
},
|
||||
"files.exclude": {},
|
||||
"files.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/debug.log": true,
|
||||
"server/scripts/custom.js": true
|
||||
},
|
||||
"files.eol": "\n",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit"
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user