Skip to content

Commit af53cca

Browse files
committed
add si units close #52
1 parent 94470db commit af53cca

15 files changed

+204
-57
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ A hook is provided as `/server/scripts/custom.js` to allow customizations to you
104104

105105
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.
106106

107+
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.
108+
107109
## Disclaimer
108110

109111
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.

server/scripts/modules/currentweather.mjs

+23-28
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
88
import WeatherDisplay from './weatherdisplay.mjs';
99
import { registerDisplay } from './navigation.mjs';
1010
import {
11-
celsiusToFahrenheit, kphToMph, pascalToInHg, metersToFeet, kilometersToMiles,
11+
temperature, windSpeed, pressure, distanceMeters, distanceKilometers,
1212
} from './utils/units.mjs';
1313

1414
// some stations prefixed do not provide all the necessary data
@@ -159,23 +159,32 @@ const shortConditions = (_condition) => {
159159

160160
// format the received data
161161
const parseData = (data) => {
162+
// get the unit converter
163+
const windConverter = windSpeed();
164+
const temperatureConverter = temperature();
165+
const metersConverter = distanceMeters();
166+
const kilometersConverter = distanceKilometers();
167+
const pressureConverter = pressure();
168+
162169
const observations = data.features[0].properties;
163170
// values from api are provided in metric
164171
data.observations = observations;
165-
data.Temperature = Math.round(observations.temperature.value);
166-
data.TemperatureUnit = 'C';
167-
data.DewPoint = Math.round(observations.dewpoint.value);
168-
data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0);
169-
data.CeilingUnit = 'm.';
170-
data.Visibility = Math.round(observations.visibility.value / 1000);
171-
data.VisibilityUnit = ' km.';
172-
data.WindSpeed = Math.round(observations.windSpeed.value);
172+
data.Temperature = temperatureConverter(observations.temperature.value);
173+
data.TemperatureUnit = temperatureConverter.units;
174+
data.DewPoint = temperatureConverter(observations.dewpoint.value);
175+
data.Ceiling = metersConverter(observations.cloudLayers[0]?.base?.value ?? 0);
176+
data.CeilingUnit = metersConverter.units;
177+
data.Visibility = kilometersConverter(observations.visibility.value);
178+
data.VisibilityUnit = kilometersConverter.units;
179+
data.Pressure = pressureConverter(observations.barometricPressure.value);
180+
data.PressureUnit = pressureConverter.units;
181+
data.HeatIndex = temperatureConverter(observations.heatIndex.value);
182+
data.WindChill = temperatureConverter(observations.windChill.value);
183+
data.WindSpeed = windConverter(observations.windSpeed.value);
173184
data.WindDirection = directionToNSEW(observations.windDirection.value);
174-
data.Pressure = Math.round(observations.barometricPressure.value);
175-
data.HeatIndex = Math.round(observations.heatIndex.value);
176-
data.WindChill = Math.round(observations.windChill.value);
177-
data.WindGust = Math.round(observations.windGust.value);
178-
data.WindUnit = 'KPH';
185+
data.WindGust = windConverter(observations.windGust.value);
186+
data.WindSpeed = windConverter(data.WindSpeed);
187+
data.WindUnit = windConverter.units;
179188
data.Humidity = Math.round(observations.relativeHumidity.value);
180189
data.Icon = getWeatherIconFromIconLink(observations.icon);
181190
data.PressureDirection = '';
@@ -186,20 +195,6 @@ const parseData = (data) => {
186195
if (pressureDiff > 150) data.PressureDirection = 'R';
187196
if (pressureDiff < -150) data.PressureDirection = 'F';
188197

189-
// convert to us units
190-
data.Temperature = celsiusToFahrenheit(data.Temperature);
191-
data.TemperatureUnit = 'F';
192-
data.DewPoint = celsiusToFahrenheit(data.DewPoint);
193-
data.Ceiling = Math.round(metersToFeet(data.Ceiling) / 100) * 100;
194-
data.CeilingUnit = 'ft.';
195-
data.Visibility = kilometersToMiles(observations.visibility.value / 1000);
196-
data.VisibilityUnit = ' mi.';
197-
data.WindSpeed = kphToMph(data.WindSpeed);
198-
data.WindUnit = 'MPH';
199-
data.Pressure = pascalToInHg(data.Pressure).toFixed(2);
200-
data.HeatIndex = celsiusToFahrenheit(data.HeatIndex);
201-
data.WindChill = celsiusToFahrenheit(data.WindChill);
202-
data.WindGust = kphToMph(data.WindGust);
203198
return data;
204199
};
205200

server/scripts/modules/currentweatherscroll.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ const screens = [
7171
(data) => `Humidity: ${data.Humidity}% Dewpoint: ${data.DewPoint}${degree}${data.TemperatureUnit}`,
7272

7373
// barometric pressure
74-
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureDirection}`,
74+
(data) => `Barometric Pressure: ${data.Pressure} ${data.PressureUnit} ${data.PressureDirection}`,
7575

7676
// wind
7777
(data) => {

server/scripts/modules/extendedforecast.mjs

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getWeatherIconFromIconLink } from './icons.mjs';
88
import { preloadImg } from './utils/image.mjs';
99
import WeatherDisplay from './weatherdisplay.mjs';
1010
import { registerDisplay } from './navigation.mjs';
11+
import settings from './settings.mjs';
1112

1213
class ExtendedForecast extends WeatherDisplay {
1314
constructor(navId, elemId) {
@@ -26,7 +27,7 @@ class ExtendedForecast extends WeatherDisplay {
2627
try {
2728
forecast = await json(weatherParameters.forecast, {
2829
data: {
29-
units: 'us',
30+
units: settings.units.value,
3031
},
3132
retryCount: 3,
3233
stillWaiting: () => this.stillWaiting(),
@@ -131,7 +132,7 @@ const shortenExtendedForecastText = (long) => {
131132
[/dense /gi, ''],
132133
[/Thunderstorm/g, 'T\'Storm'],
133134
];
134-
// run all regexes
135+
// run all regexes
135136
const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long);
136137

137138
let conditions = short.split(' ');

server/scripts/modules/hourly-graph.mjs

+4-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class HourlyGraph extends WeatherDisplay {
3838
const skyCover = data.map((d) => d.skyCover);
3939

4040
this.data = {
41-
skyCover, temperature, probabilityOfPrecipitation,
41+
skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit,
4242
};
4343

4444
this.setStatus(STATUS.loaded);
@@ -107,6 +107,9 @@ class HourlyGraph extends WeatherDisplay {
107107
// set the image source
108108
this.image.src = canvas.toDataURL();
109109

110+
// change the units in the header
111+
this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`;
112+
110113
super.drawCanvas();
111114
this.finishDraw();
112115
}

server/scripts/modules/hourly.mjs

+15-5
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import STATUS from './status.mjs';
44
import { DateTime, Interval, Duration } from '../vendor/auto/luxon.mjs';
55
import { json } from './utils/fetch.mjs';
6-
import { celsiusToFahrenheit, kilometersToMiles } from './utils/units.mjs';
6+
import { temperature as temperatureUnit, distanceKilometers } from './utils/units.mjs';
77
import { getHourlyIcon } from './icons.mjs';
88
import { directionToNSEW } from './utils/calc.mjs';
99
import WeatherDisplay from './weatherdisplay.mjs';
@@ -56,6 +56,9 @@ class Hourly extends WeatherDisplay {
5656
const list = this.elem.querySelector('.hourly-lines');
5757
list.innerHTML = '';
5858

59+
// get a unit converter
60+
const temperatureConverter = temperatureUnit();
61+
5962
const startingHour = DateTime.local();
6063

6164
const lines = this.data.map((data, index) => {
@@ -66,7 +69,7 @@ class Hourly extends WeatherDisplay {
6669
fillValues.hour = formattedHour;
6770

6871
// temperatures, convert to strings with no decimal
69-
const temperature = Math.round(data.temperature).toString().padStart(3);
72+
const temperature = temperatureConverter(data.temperature).toString().padStart(3);
7073
const feelsLike = Math.round(data.apparentTemperature).toString().padStart(3);
7174
fillValues.temp = temperature;
7275
// only plot apparent temperature if there is a difference
@@ -132,6 +135,11 @@ class Hourly extends WeatherDisplay {
132135

133136
// extract specific values from forecast and format as an array
134137
const parseForecast = async (data) => {
138+
// get unit converters
139+
const temperatureConverter = temperatureUnit();
140+
const distanceConverter = distanceKilometers();
141+
142+
// parse data
135143
const temperature = expand(data.temperature.values);
136144
const apparentTemperature = expand(data.apparentTemperature.values);
137145
const windSpeed = expand(data.windSpeed.values);
@@ -145,9 +153,11 @@ const parseForecast = async (data) => {
145153
const icons = await determineIcon(skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed);
146154

147155
return temperature.map((val, idx) => ({
148-
temperature: celsiusToFahrenheit(temperature[idx]),
149-
apparentTemperature: celsiusToFahrenheit(apparentTemperature[idx]),
150-
windSpeed: kilometersToMiles(windSpeed[idx]),
156+
temperature: temperatureConverter(temperature[idx]),
157+
temperatureUnit: temperatureConverter.units,
158+
apparentTemperature: temperatureConverter(apparentTemperature[idx]),
159+
windSpeed: distanceConverter(windSpeed[idx]),
160+
windUnit: distanceConverter.units,
151161
windDirection: directionToNSEW(windDirection[idx]),
152162
probabilityOfPrecipitation: probabilityOfPrecipitation[idx],
153163
skyCover: skyCover[idx],

server/scripts/modules/latestobservations.mjs

+18-7
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { distance as calcDistance, directionToNSEW } from './utils/calc.mjs';
33
import { json } from './utils/fetch.mjs';
44
import STATUS from './status.mjs';
55
import { locationCleanup } from './utils/string.mjs';
6-
import { celsiusToFahrenheit, kphToMph } from './utils/units.mjs';
6+
import { temperature, windSpeed } from './utils/units.mjs';
77
import WeatherDisplay from './weatherdisplay.mjs';
88
import { registerDisplay } from './navigation.mjs';
9+
import settings from './settings.mjs';
910

1011
class LatestObservations extends WeatherDisplay {
1112
constructor(navId, elemId) {
@@ -64,14 +65,22 @@ class LatestObservations extends WeatherDisplay {
6465
// sort array by station name
6566
const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1));
6667

67-
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
68-
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
68+
if (settings.units.value === 'us') {
69+
this.elem.querySelector('.column-headers .temp.english').classList.add('show');
70+
this.elem.querySelector('.column-headers .temp.metric').classList.remove('show');
71+
} else {
72+
this.elem.querySelector('.column-headers .temp.english').classList.remove('show');
73+
this.elem.querySelector('.column-headers .temp.metric').classList.add('show');
74+
}
75+
// get unit converters
76+
const windConverter = windSpeed();
77+
const temperatureConverter = temperature();
6978

7079
const lines = sortedConditions.map((condition) => {
7180
const windDirection = directionToNSEW(condition.windDirection.value);
7281

73-
const Temperature = Math.round(celsiusToFahrenheit(condition.temperature.value));
74-
const WindSpeed = Math.round(kphToMph(condition.windSpeed.value));
82+
const Temperature = temperatureConverter(condition.temperature.value);
83+
const WindSpeed = windConverter(condition.windSpeed.value);
7584

7685
const fill = {
7786
location: locationCleanup(condition.city).substr(0, 14),
@@ -94,6 +103,8 @@ class LatestObservations extends WeatherDisplay {
94103
linesContainer.innerHTML = '';
95104
linesContainer.append(...lines);
96105

106+
// update temperature unit header
107+
97108
this.finishDraw();
98109
}
99110
}
@@ -122,8 +133,8 @@ const getStations = async (stations) => {
122133
const data = await json(`https://api.weather.gov/stations/${station.id}/observations/latest`, { retryCount: 1, stillWaiting: () => this.stillWaiting() });
123134
// test for temperature, weather and wind values present
124135
if (data.properties.temperature.value === null
125-
|| data.properties.textDescription === ''
126-
|| data.properties.windSpeed.value === null) return false;
136+
|| data.properties.textDescription === ''
137+
|| data.properties.windSpeed.value === null) return false;
127138
// format the return values
128139
return {
129140
...data.properties,

server/scripts/modules/localforecast.mjs

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import STATUS from './status.mjs';
44
import { json } from './utils/fetch.mjs';
55
import WeatherDisplay from './weatherdisplay.mjs';
66
import { registerDisplay } from './navigation.mjs';
7+
import settings from './settings.mjs';
78

89
class LocalForecast extends WeatherDisplay {
910
constructor(navId, elemId) {
@@ -61,7 +62,7 @@ class LocalForecast extends WeatherDisplay {
6162
try {
6263
return await json(weatherParameters.forecast, {
6364
data: {
64-
units: 'us',
65+
units: settings.units.value,
6566
},
6667
retryCount: 3,
6768
stillWaiting: () => this.stillWaiting(),

server/scripts/modules/regionalforecast.mjs

+6-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import STATUS from './status.mjs';
55
import { distance as calcDistance } from './utils/calc.mjs';
66
import { json } from './utils/fetch.mjs';
7-
import { celsiusToFahrenheit } from './utils/units.mjs';
7+
import { temperature as temperatureUnit } from './utils/units.mjs';
88
import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
99
import { preloadImg } from './utils/image.mjs';
1010
import { DateTime } from '../vendor/auto/luxon.mjs';
@@ -59,7 +59,7 @@ class RegionalForecast extends WeatherDisplay {
5959
const regionalCities = [];
6060
combinedCities.forEach((city) => {
6161
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
62-
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
62+
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
6363
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
6464
const targetDist = city.targetDistance || 1;
6565
// 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 {
7171
}
7272
});
7373

74+
// get a unit converter
75+
const temperatureConverter = temperatureUnit();
76+
7477
// get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov)
7578
const regionalDataAll = await Promise.all(regionalCities.map(async (city) => {
7679
try {
@@ -93,7 +96,7 @@ class RegionalForecast extends WeatherDisplay {
9396
// format the observation the same as the forecast
9497
const regionalObservation = {
9598
daytime: !!/\/day\//.test(observation.icon),
96-
temperature: celsiusToFahrenheit(observation.temperature.value),
99+
temperature: temperatureConverter(observation.temperature.value),
97100
name: utils.formatCity(city.city),
98101
icon: observation.icon,
99102
x: cityXY.x,

server/scripts/modules/settings.mjs

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ const init = () => {
1818
[1.25, 'Slow'],
1919
[1.5, 'Very Slow'],
2020
]);
21+
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
22+
['us', 'US'],
23+
['si', 'Metric'],
24+
]);
2125

2226
// generate html objects
2327
const settingHtml = Object.values(settings).map((d) => d.generate());
@@ -47,4 +51,13 @@ const kioskChange = (value) => {
4751
}
4852
};
4953

54+
const unitChange = () => {
55+
// reload the data at the top level to refresh units
56+
// after the initial load
57+
if (unitChange.firstRunDone) {
58+
window.location.reload();
59+
}
60+
unitChange.firstRunDone = true;
61+
};
62+
5063
export default settings;

server/scripts/modules/travelforecast.mjs

+6-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { getWeatherRegionalIconFromIconLink } from './icons.mjs';
55
import { DateTime } from '../vendor/auto/luxon.mjs';
66
import WeatherDisplay from './weatherdisplay.mjs';
77
import { registerDisplay } from './navigation.mjs';
8+
import settings from './settings.mjs';
89

910
class TravelForecast extends WeatherDisplay {
1011
constructor(navId, elemId, defaultActive) {
@@ -34,7 +35,11 @@ class TravelForecast extends WeatherDisplay {
3435
try {
3536
// get point then forecast
3637
if (!city.point) throw new Error('No pre-loaded point');
37-
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`);
38+
const forecast = await json(`https://api.weather.gov/gridpoints/${city.point.wfo}/${city.point.x},${city.point.y}/forecast`, {
39+
data: {
40+
units: settings.units.value,
41+
},
42+
});
3843
// determine today or tomorrow (shift periods by 1 if tomorrow)
3944
const todayShift = forecast.properties.periods[0].isDaytime ? 0 : 1;
4045
// return a pared-down forecast

server/scripts/modules/utils/setting.mjs

+14-2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ class Setting {
2424
if (type === 'select' && urlValue !== undefined) {
2525
urlState = parseFloat(urlValue);
2626
}
27+
if (type === 'select' && urlValue !== undefined && Number.isNaN(urlState)) {
28+
// couldn't parse as a float, store as a string
29+
urlState = urlValue;
30+
}
2731

2832
// get existing value if present
2933
const storedValue = urlState ?? this.getFromLocalStorage();
@@ -59,7 +63,11 @@ class Setting {
5963

6064
this.values.forEach(([value, text]) => {
6165
const option = document.createElement('option');
62-
option.value = value.toFixed(2);
66+
if (typeof value === 'number') {
67+
option.value = value.toFixed(2);
68+
} else {
69+
option.value = value;
70+
}
6371

6472
option.innerHTML = text;
6573
select.append(option);
@@ -108,6 +116,10 @@ class Setting {
108116
selectChange(e) {
109117
// update the value
110118
this.myValue = parseFloat(e.target.value);
119+
if (Number.isNaN(this.myValue)) {
120+
// was a string, store as such
121+
this.myValue = e.target.value;
122+
}
111123
this.storeToLocalStorage(this.myValue);
112124

113125
// call the change action
@@ -168,7 +180,7 @@ class Setting {
168180
selectHighlight(newValue) {
169181
// set the dropdown to the provided value
170182
this.element.querySelectorAll('option').forEach((elem) => {
171-
elem.selected = newValue.toFixed(2) === elem.value;
183+
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
172184
});
173185
}
174186

0 commit comments

Comments
 (0)