Compare commits

...

56 Commits

Author SHA1 Message Date
Matt Walsh
471d322cde 5.14.3 2025-03-08 13:39:44 -06:00
Matt Walsh
8f9be046ac fix metric conversion on regional forecast map 2025-03-08 13:39:36 -06:00
Matt Walsh
c34dc1ff25 capture dist 2025-02-25 09:51:29 -06:00
Matt Walsh
9b4eed7332 5.14.2 2025-02-25 09:50:46 -06:00
Matt Walsh
ef1477f9eb fix hourly forecast temperature double-converted 2025-02-25 09:49:12 -06:00
Matt Walsh
e2876df177 5.14.1 2025-02-25 09:44:32 -06:00
Matt Walsh
d6335b2878 fix time zones on hourly and almanac close #67 2025-02-25 09:44:24 -06:00
mwood77
781128100e Update README to include ws4kp-international (#66)
* Update README to include `ws4kp-international`

* adjust grammar in readme

* tidy links
2025-02-24 14:16:30 -06:00
Matt Walsh
56261ded4b capture dist 2025-02-23 23:35:05 -06:00
Matt Walsh
58540ad67b 5.14.0 2025-02-23 23:35:05 -06:00
Matt Walsh
af53cca45e add si units close #52 2025-02-23 23:35:00 -06:00
Matt Walsh
94470db9a7 capture dist 2025-02-23 21:08:43 -06:00
Matt Walsh
3c7a77e200 5.13.6 2025-02-23 21:07:55 -06:00
Matt Walsh
d472df2e26 fix hazards don't fully scroll close #62 2025-02-23 21:07:34 -06:00
Matt Walsh
fdbf11dcd4 update dependencies 2025-02-23 20:57:06 -06:00
Matt Walsh
b7e9091320 capture distribution 2025-01-06 22:07:51 -06:00
Matt Walsh
d24284d340 5.13.5 2025-01-06 22:06:58 -06:00
Matt Walsh
4e8dc35739 fix widescreen querystring parameter close #60 2025-01-06 22:06:36 -06:00
Matt Walsh
779d34a0a8 5.13.4 2024-11-25 09:40:50 -06:00
Matt Walsh
efeb45d3d0 fix travel forecast and hourly scrolling close #58 2024-11-25 09:40:36 -06:00
Matt Walsh
e2d7a96971 linting cleanup 2024-10-21 19:21:05 -05:00
Matt Walsh
487c83f664 update license 2024-10-21 19:16:38 -05:00
Matt Walsh
88b8b4a82e merge 2024-10-21 14:56:06 -05:00
Matt Walsh
13b77a0070 better filtering of null station data for current weather close #54
5.13.3
2024-10-19 13:47:22 -05:00
Matt Walsh
c9307768a4 fix radar timestamp
5.13.2
2024-10-19 13:39:34 -05:00
Matt Walsh
844544c364 night time hurricane icons
5.13.1
2024-10-19 13:39:25 -05:00
Matt Walsh
80a68caa27 hazards only on first run through after load/refresh
sorting of hazards based on type tornado, hurricane, etc

5.13.0
2024-10-19 13:39:16 -05:00
Matt Walsh
c7889eaa2c hazards only on first run through after load/refresh
sorting of hazards based on type tornado, hurricane, etc

5.13.0
2024-10-08 10:34:57 -05:00
Matt Walsh
019908684b update dependencies and clarify dev dependencies 2024-08-27 09:00:38 -05:00
Matt Walsh
e1a58b6548 Merge branch 'main' of https://github.com/netbymatt/ws4kp 2024-07-29 23:14:01 -05:00
Matt Walsh
e794976f4d 5.12.0 2024-07-29 23:13:12 -05:00
Matt Walsh
193d742aa3 add speed setting close #49 2024-07-29 23:12:47 -05:00
Matt Walsh
b62339af94 capture dist 2024-07-11 16:20:59 -05:00
Matt Walsh
913dc383f6 5.11.7 2024-07-11 16:06:49 -05:00
Matt Walsh
94249560f2 hide mouse cursor in full screen after timeout 2024-07-11 16:06:43 -05:00
Matt Walsh
75314d92c9 change gulp to mjs 2024-07-07 22:21:53 -05:00
Matt Walsh
168c0c5caf capture dist 2024-07-07 21:51:15 -05:00
Matt Walsh
b6cd75ab42 5.11.6 2024-07-07 21:49:28 -05:00
Matt Walsh
934a489340 update dependencies 2024-07-07 21:49:21 -05:00
Matt Walsh
c5f5c101f9 clean up full screen enter/exit close #48 2024-07-07 21:14:07 -05:00
Matt Walsh
933367974f capture dist 2024-07-07 20:16:33 -05:00
Matt Walsh
0e67eb22dc 5.11.5 2024-07-07 20:15:46 -05:00
Matt Walsh
9df6f6888f Fix copy permalink when on non-secure source close #47 2024-07-07 20:15:29 -05:00
Matt Walsh
04cc5d4252 fix encoding issue with gulp 5 2024-05-19 23:47:58 -05:00
Matt Walsh
543d3f5196 Document custom.js close #43 close #44 2024-05-19 23:03:57 -05:00
Matt Walsh
78ceba9c19 capture dist 2024-05-19 22:40:25 -05:00
Matt Walsh
763d42061e 5.11.4 2024-05-19 22:39:27 -05:00
Matt Walsh
318c55b92d Fix date/time update rate #45 2024-05-19 22:39:15 -05:00
Matt Walsh
84d39101e5 capture dist 2024-05-08 16:42:00 -05:00
Matt Walsh
fd0e42aa67 5.11.3 2024-05-08 16:30:21 -05:00
Matt Walsh
7be20490e8 remove localhost-specific protocol matching 2024-05-08 16:30:11 -05:00
Matt Walsh
20b4d22115 update build dependencies 2024-04-19 21:50:57 -05:00
Matt Walsh
af4ba1b881 update vendor scripts 2024-04-19 21:37:58 -05:00
Matt Walsh
9f9dafa30c add community notes close #37 2024-04-19 21:23:54 -05:00
Matt Walsh
227a959e6a capture dist 2024-04-19 21:15:07 -05:00
Matt Walsh
63703d1fff settings persist when set by querystring 2024-04-19 21:14:28 -05:00
47 changed files with 7078 additions and 5769 deletions

View File

@@ -8,7 +8,6 @@ module.exports = {
},
extends: [
'airbnb-base',
'plugin:sonarjs/recommended',
],
globals: {
Atomics: 'readonly',
@@ -22,16 +21,17 @@ module.exports = {
},
parserOptions: {
ecmaVersion: 2021,
ecmaVersion: 2023,
},
plugins: [
'unicorn',
'sonarjs',
],
rules: {
indent: [
'error',
'tab',
{
SwitchCase: 1
},
],
'no-tabs': 0,
'no-console': 0,
@@ -67,24 +67,6 @@ module.exports = {
json: 'always',
},
],
// unicorn
'unicorn/numeric-separators-style': 'error',
'unicorn/prefer-query-selector': 'error',
'unicorn/catch-error-name': 'error',
'unicorn/no-negated-condition': 'error',
'unicorn/better-regex': 'error',
'unicorn/consistent-function-scoping': 'error',
'unicorn/prefer-array-flat-map': 'error',
'unicorn/prefer-array-find': 'error',
'unicorn/prefer-regexp-test': 'error',
'unicorn/consistent-destructuring': 'error',
'unicorn/prefer-date-now': 'error',
'unicorn/prefer-ternary': 'error',
'unicorn/prefer-dom-node-append': 'error',
'unicorn/explicit-length-check': 'error',
'unicorn/prefer-at': 'error',
// sonarjs
'sonarjs/cognitive-complexity': 0,
},
ignorePatterns: [
'*.min.js',

View File

@@ -7,8 +7,6 @@
"format": "compressed",
"extensionName": ".css",
"savePath": "/server/styles",
"savePathSegmentKeys": null,
"savePathReplaceSegmentsWith": null
}
],
"search.exclude": {
@@ -21,5 +19,8 @@
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"eslint.validate": [
"javascript"
]
}

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2016-2017 Michael Battaglia
Copyright (c) 2020-2024 Matt Walsh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -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.
@@ -78,7 +85,7 @@ I've made several changes to this Weather Star 4000 simulation compared to the o
* "Flavors" are not present in this simulation. Flavors refer to the order of the weather information that was shown on the original units. Instead, the order of the displays has been fixed and a checkboxes can be used to turn on and off individual displays. The travel forecast has been defaulted to off so only local information shows for new users.
## Sharing a permalink (bookmarking)
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it click the "Copy Permalink" near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location. You can then share this link or add it to your bookmarks.
Selected displays, the forecast city and widescreen setting are sticky from one session to the next. However if you would like to share your exact configuration or bookmark it click the "Copy Permalink" (or get "Get Parmalink") near the bottom of the page. A URL will be copied to your clipboard with all of you selected displays and location (or copy it from the page if your browser doesn't support clipboard transfers directly). You can then share this link or add it to your bookmarks.
## Kiosk mode
Kiosk mode can be activated by a checkbox on the page. Note that there is no way out of kiosk mode (except refresh or closing the browser), and the play/pause and other controls will not be available. This is deliberate as a browser's kiosk mode it intended not to be exited or significantly modified.
@@ -91,14 +98,21 @@ As time allows I will be working on the following enhancements.
* Better error reporting when api.weather.gov is down (happens more often than you would think)
And the following technical fixes.
## Community Notes
* Caching of the animated gifs, specifically after they are decompressed
Thanks to the WeatherStar community for providing these discussions to further extend your retro forecasts!
* [Stream as FFMPEG](https://github.com/netbymatt/ws4kp/issues/37#issuecomment-2008491948)
## Customization
A hook is provided as `/server/scripts/custom.js` to allow customizations to your own fork of this project, without accidentally pushing your customizations back upstream to the git repository. An sample file is provided at `/server/scripts/custom.sample.js` and should be renamed to `custom.js` activate it.
## Issue reporting and feature requests
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

File diff suppressed because one or more lines are too long

View File

@@ -19,4 +19,4 @@ function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t
* @author John Doherty <www.johndoherty.info>
* @license MIT
*/
function(e,t){"use strict";"function"!=typeof e.CustomEvent&&(e.CustomEvent=function(e,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var A=t.createEvent("CustomEvent");return A.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),A},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener("touchstart",(function(e){if("true"===e.target.getAttribute("data-swipe-ignore"))return;s=e.target,i=Date.now(),n=e.touches[0].clientX,A=e.touches[0].clientY,r=0,o=0}),!1),t.addEventListener("touchmove",(function(e){if(!n||!A)return;var t=e.touches[0].clientX,i=e.touches[0].clientY;r=n-t,o=A-i}),!1),t.addEventListener("touchend",(function(e){if(s!==e.target)return;var u=parseInt(a(s,"data-swipe-threshold","20"),10),l=a(s,"data-swipe-unit","px"),c=parseInt(a(s,"data-swipe-timeout","500"),10),d=Date.now()-i,f="",p=e.changedTouches||e.touches||[];"vh"===l&&(u=Math.round(u/100*t.documentElement.clientHeight));"vw"===l&&(u=Math.round(u/100*t.documentElement.clientWidth));Math.abs(r)>Math.abs(o)?Math.abs(r)>u&&d<c&&(f=r>0?"swiped-left":"swiped-right"):Math.abs(o)>u&&d<c&&(f=o>0?"swiped-up":"swiped-down");if(""!==f){var h={dir:f.replace(/swiped-/,""),touchType:(p[0]||{}).touchType||"direct",xStart:parseInt(n,10),xEnd:parseInt((p[0]||{}).clientX||-1,10),yStart:parseInt(A,10),yEnd:parseInt((p[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:h})),s.dispatchEvent(new CustomEvent(f,{bubbles:!0,cancelable:!0,detail:h}))}n=null,A=null,i=null}),!1);var n=null,A=null,r=null,o=null,i=null,s=null;function a(e,n,A){for(;e&&e!==t.documentElement;){var r=e.getAttribute(n);if(r)return r;e=e.parentNode}return A}}(window,document),function(){"use strict";var e=Math.PI,t=Math.sin,n=Math.cos,A=Math.tan,r=Math.asin,o=Math.atan2,i=Math.acos,s=e/180,a=864e5,u=2440588,l=2451545;function c(e){return new Date((e+.5-u)*a)}function d(e){return function(e){return e.valueOf()/a-.5+u}(e)-l}var f=23.4397*s;function p(e,r){return o(t(e)*n(f)-A(r)*t(f),n(e))}function h(e,A){return r(t(A)*n(f)+n(A)*t(f)*t(e))}function g(e,r,i){return o(t(e),n(e)*t(r)-A(i)*n(r))}function m(e,A,o){return r(t(A)*t(o)+n(A)*n(o)*n(e))}function v(e,t){return s*(280.16+360.9856235*e)-t}function y(e){return s*(357.5291+.98560028*e)}function b(n){return n+s*(1.9148*t(n)+.02*t(2*n)+3e-4*t(3*n))+102.9372*s+e}function x(e){var t=b(y(e));return{dec:h(t,0),ra:p(t,0)}}var C={getPosition:function(e,t,n){var A=s*-n,r=s*t,o=d(e),i=x(o),a=v(o,A)-i.ra;return{azimuth:g(a,r,i.dec),altitude:m(a,r,i.dec)}}},E=C.times=[[-.833,"sunrise","sunset"],[-.3,"sunriseEnd","sunsetStart"],[-6,"dawn","dusk"],[-12,"nauticalDawn","nauticalDusk"],[-18,"nightEnd","night"],[6,"goldenHourEnd","goldenHour"]];C.addTime=function(e,t,n){E.push([e,t,n])};var w=9e-4;function B(t,n,A){return w+(t+n)/(2*e)+A}function S(e,n,A){return l+e+.0053*t(n)-.0069*t(2*A)}function I(e,A,r,o,s,a,u){var l=function(e,A,r){return i((t(e)-t(A)*t(r))/(n(A)*n(r)))}(e,r,o);return S(B(l,A,s),a,u)}function k(e){var A=s*(134.963+13.064993*e),r=s*(93.272+13.22935*e),o=s*(218.316+13.176396*e)+6.289*s*t(A),i=5.128*s*t(r),a=385001-20905*n(A);return{ra:p(o,i),dec:h(o,i),dist:a}}function T(e,t){return new Date(e.valueOf()+t*a/24)}C.getTimes=function(t,n,A,r){var o,i,a,u,l,f=s*-A,p=s*n,g=function(e){return-2.076*Math.sqrt(e)/60}(r=r||0),m=function(t,n){return Math.round(t-w-n/(2*e))}(d(t),f),v=B(0,f,m),x=y(v),C=b(x),k=h(C,0),T=S(v,x,C),D={solarNoon:c(T),nadir:c(T-.5)};for(o=0,i=E.length;o<i;o+=1)l=T-((u=I(((a=E[o])[0]+g)*s,f,p,k,m,x,C))-T),D[a[1]]=c(l),D[a[2]]=c(u);return D},C.getMoonPosition=function(e,r,i){var a=s*-i,u=s*r,l=d(e),c=k(l),f=v(l,a)-c.ra,p=m(f,u,c.dec),h=o(t(f),A(u)*n(c.dec)-t(c.dec)*n(f));return p+=function(e){return e<0&&(e=0),2967e-7/Math.tan(e+.00312536/(e+.08901179))}(p),{azimuth:g(f,u,c.dec),altitude:p,distance:c.dist,parallacticAngle:h}},C.getMoonIllumination=function(e){var A=d(e||new Date),r=x(A),s=k(A),a=149598e3,u=i(t(r.dec)*t(s.dec)+n(r.dec)*n(s.dec)*n(r.ra-s.ra)),l=o(a*t(u),s.dist-a*n(u)),c=o(n(r.dec)*t(r.ra-s.ra),t(r.dec)*n(s.dec)-n(r.dec)*t(s.dec)*n(r.ra-s.ra));return{fraction:(1+n(l))/2,phase:.5+.5*l*(c<0?-1:1)/Math.PI,angle:c}},C.getMoonTimes=function(e,t,n,A){var r=new Date(e);A?r.setUTCHours(0,0,0,0):r.setHours(0,0,0,0);for(var o,i,a,u,l,c,d,f,p,h,g,m,v,y=.133*s,b=C.getMoonPosition(r,t,n).altitude-y,x=1;x<=24&&(o=C.getMoonPosition(T(r,x),t,n).altitude-y,f=((l=(b+(i=C.getMoonPosition(T(r,x+1),t,n).altitude-y))/2-o)*(d=-(c=(i-b)/2)/(2*l))+c)*d+o,h=0,(p=c*c-4*l*o)>=0&&(g=d-(v=Math.sqrt(p)/(2*Math.abs(l))),m=d+v,Math.abs(g)<=1&&h++,Math.abs(m)<=1&&h++,g<-1&&(g=m)),1===h?b<0?a=x+g:u=x+g:2===h&&(a=x+(f<0?m:g),u=x+(f<0?g:m)),!a||!u);x+=2)b=i;var E={};return a&&(E.rise=T(r,a)),u&&(E.set=T(r,u)),a||u||(E[f>0?"alwaysUp":"alwaysDown"]=!0),E},"object"==typeof exports&&"undefined"!=typeof module?module.exports=C:"function"==typeof define&&define.amd?define(C):window.SunCalc=C}();
function(e,t){"use strict";"function"!=typeof e.CustomEvent&&(e.CustomEvent=function(e,n){n=n||{bubbles:!1,cancelable:!1,detail:void 0};var A=t.createEvent("CustomEvent");return A.initCustomEvent(e,n.bubbles,n.cancelable,n.detail),A},e.CustomEvent.prototype=e.Event.prototype),t.addEventListener("touchstart",(function(e){if("true"===e.target.getAttribute("data-swipe-ignore"))return;s=e.target,i=Date.now(),n=e.touches[0].clientX,A=e.touches[0].clientY,r=0,o=0,a=e.touches.length}),!1),t.addEventListener("touchmove",(function(e){if(!n||!A)return;var t=e.touches[0].clientX,i=e.touches[0].clientY;r=n-t,o=A-i}),!1),t.addEventListener("touchend",(function(e){if(s!==e.target)return;var l=parseInt(u(s,"data-swipe-threshold","20"),10),c=u(s,"data-swipe-unit","px"),d=parseInt(u(s,"data-swipe-timeout","500"),10),f=Date.now()-i,p="",h=e.changedTouches||e.touches||[];"vh"===c&&(l=Math.round(l/100*t.documentElement.clientHeight));"vw"===c&&(l=Math.round(l/100*t.documentElement.clientWidth));Math.abs(r)>Math.abs(o)?Math.abs(r)>l&&f<d&&(p=r>0?"swiped-left":"swiped-right"):Math.abs(o)>l&&f<d&&(p=o>0?"swiped-up":"swiped-down");if(""!==p){var g={dir:p.replace(/swiped-/,""),touchType:(h[0]||{}).touchType||"direct",fingers:a,xStart:parseInt(n,10),xEnd:parseInt((h[0]||{}).clientX||-1,10),yStart:parseInt(A,10),yEnd:parseInt((h[0]||{}).clientY||-1,10)};s.dispatchEvent(new CustomEvent("swiped",{bubbles:!0,cancelable:!0,detail:g})),s.dispatchEvent(new CustomEvent(p,{bubbles:!0,cancelable:!0,detail:g}))}n=null,A=null,i=null}),!1);var n=null,A=null,r=null,o=null,i=null,s=null,a=0;function u(e,n,A){for(;e&&e!==t.documentElement;){var r=e.getAttribute(n);if(r)return r;e=e.parentNode}return A}}(window,document),function(){"use strict";var e=Math.PI,t=Math.sin,n=Math.cos,A=Math.tan,r=Math.asin,o=Math.atan2,i=Math.acos,s=e/180,a=864e5,u=2440588,l=2451545;function c(e){return new Date((e+.5-u)*a)}function d(e){return function(e){return e.valueOf()/a-.5+u}(e)-l}var f=23.4397*s;function p(e,r){return o(t(e)*n(f)-A(r)*t(f),n(e))}function h(e,A){return r(t(A)*n(f)+n(A)*t(f)*t(e))}function g(e,r,i){return o(t(e),n(e)*t(r)-A(i)*n(r))}function m(e,A,o){return r(t(A)*t(o)+n(A)*n(o)*n(e))}function v(e,t){return s*(280.16+360.9856235*e)-t}function y(e){return s*(357.5291+.98560028*e)}function b(n){return n+s*(1.9148*t(n)+.02*t(2*n)+3e-4*t(3*n))+102.9372*s+e}function x(e){var t=b(y(e));return{dec:h(t,0),ra:p(t,0)}}var C={getPosition:function(e,t,n){var A=s*-n,r=s*t,o=d(e),i=x(o),a=v(o,A)-i.ra;return{azimuth:g(a,r,i.dec),altitude:m(a,r,i.dec)}}},E=C.times=[[-.833,"sunrise","sunset"],[-.3,"sunriseEnd","sunsetStart"],[-6,"dawn","dusk"],[-12,"nauticalDawn","nauticalDusk"],[-18,"nightEnd","night"],[6,"goldenHourEnd","goldenHour"]];C.addTime=function(e,t,n){E.push([e,t,n])};var w=9e-4;function B(t,n,A){return w+(t+n)/(2*e)+A}function S(e,n,A){return l+e+.0053*t(n)-.0069*t(2*A)}function I(e,A,r,o,s,a,u){var l=function(e,A,r){return i((t(e)-t(A)*t(r))/(n(A)*n(r)))}(e,r,o);return S(B(l,A,s),a,u)}function k(e){var A=s*(134.963+13.064993*e),r=s*(93.272+13.22935*e),o=s*(218.316+13.176396*e)+6.289*s*t(A),i=5.128*s*t(r),a=385001-20905*n(A);return{ra:p(o,i),dec:h(o,i),dist:a}}function T(e,t){return new Date(e.valueOf()+t*a/24)}C.getTimes=function(t,n,A,r){var o,i,a,u,l,f=s*-A,p=s*n,g=function(e){return-2.076*Math.sqrt(e)/60}(r=r||0),m=function(t,n){return Math.round(t-w-n/(2*e))}(d(t),f),v=B(0,f,m),x=y(v),C=b(x),k=h(C,0),T=S(v,x,C),D={solarNoon:c(T),nadir:c(T-.5)};for(o=0,i=E.length;o<i;o+=1)l=T-((u=I(((a=E[o])[0]+g)*s,f,p,k,m,x,C))-T),D[a[1]]=c(l),D[a[2]]=c(u);return D},C.getMoonPosition=function(e,r,i){var a=s*-i,u=s*r,l=d(e),c=k(l),f=v(l,a)-c.ra,p=m(f,u,c.dec),h=o(t(f),A(u)*n(c.dec)-t(c.dec)*n(f));return p+=function(e){return e<0&&(e=0),2967e-7/Math.tan(e+.00312536/(e+.08901179))}(p),{azimuth:g(f,u,c.dec),altitude:p,distance:c.dist,parallacticAngle:h}},C.getMoonIllumination=function(e){var A=d(e||new Date),r=x(A),s=k(A),a=149598e3,u=i(t(r.dec)*t(s.dec)+n(r.dec)*n(s.dec)*n(r.ra-s.ra)),l=o(a*t(u),s.dist-a*n(u)),c=o(n(r.dec)*t(r.ra-s.ra),t(r.dec)*n(s.dec)-n(r.dec)*t(s.dec)*n(r.ra-s.ra));return{fraction:(1+n(l))/2,phase:.5+.5*l*(c<0?-1:1)/Math.PI,angle:c}},C.getMoonTimes=function(e,t,n,A){var r=new Date(e);A?r.setUTCHours(0,0,0,0):r.setHours(0,0,0,0);for(var o,i,a,u,l,c,d,f,p,h,g,m,v,y=.133*s,b=C.getMoonPosition(r,t,n).altitude-y,x=1;x<=24&&(o=C.getMoonPosition(T(r,x),t,n).altitude-y,f=((l=(b+(i=C.getMoonPosition(T(r,x+1),t,n).altitude-y))/2-o)*(d=-(c=(i-b)/2)/(2*l))+c)*d+o,h=0,(p=c*c-4*l*o)>=0&&(g=d-(v=Math.sqrt(p)/(2*Math.abs(l))),m=d+v,Math.abs(g)<=1&&h++,Math.abs(m)<=1&&h++,g<-1&&(g=m)),1===h?b<0?a=x+g:u=x+g:2===h&&(a=x+(f<0?m:g),u=x+(f<0?g:m)),!a||!u);x+=2)b=i;var E={};return a&&(E.rise=T(r,a)),u&&(E.set=T(r,u)),a||u||(E[f>0?"alwaysUp":"alwaysDown"]=!0),E},"object"==typeof exports&&"undefined"!=typeof module?module.exports=C:"function"==typeof define&&define.amd?define(C):window.SunCalc=C}();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +1,26 @@
/* eslint-disable import/no-extraneous-dependencies */
const gulp = require('gulp');
const concat = require('gulp-concat');
const terser = require('gulp-terser');
const ejs = require('gulp-ejs');
const rename = require('gulp-rename');
const htmlmin = require('gulp-htmlmin');
const del = require('del');
const s3Upload = require('gulp-s3-upload');
const webpack = require('webpack-stream');
const TerserPlugin = require('terser-webpack-plugin');
const path = require('path');
const clean = () => del(['./dist**']);
import {
src, dest, series, parallel,
} from 'gulp';
import concat from 'gulp-concat';
import terser from 'gulp-terser';
import ejs from 'gulp-ejs';
import rename from 'gulp-rename';
import htmlmin from 'gulp-htmlmin';
import { deleteAsync } from 'del';
import s3Upload from 'gulp-s3-upload';
import webpack from 'webpack-stream';
import TerserPlugin from 'terser-webpack-plugin';
import { readFile } from 'fs/promises';
// get cloudfront
const AWS = require('aws-sdk');
import { CloudFrontClient, CreateInvalidationCommand } from '@aws-sdk/client-cloudfront';
AWS.config.update({ region: 'us-east-1' });
const cloudfront = new AWS.CloudFront({ apiVersion: '2020-01-01' });
const clean = () => deleteAsync(['./dist**']);
const cloudfront = new CloudFrontClient({ region: 'us-east-1' });
const RESOURCES_PATH = './dist/resources';
const jsSourcesData = [
'server/scripts/data/travelcities.js',
@@ -33,7 +36,7 @@ const webpackOptions = {
filename: 'ws.min.js',
},
resolve: {
roots: [path.resolve(__dirname, './')],
roots: ['./'],
},
optimization: {
minimize: true,
@@ -51,10 +54,10 @@ const webpackOptions = {
},
};
gulp.task('compress_js_data', () => gulp.src(jsSourcesData)
const compressJsData = () => src(jsSourcesData)
.pipe(concat('data.min.js'))
.pipe(terser())
.pipe(gulp.dest('./dist/resources')));
.pipe(dest(RESOURCES_PATH));
const jsVendorSources = [
'server/scripts/vendor/auto/jquery.js',
@@ -64,10 +67,10 @@ const jsVendorSources = [
'server/scripts/vendor/auto/suncalc.js',
];
gulp.task('compress_js_vendor', () => gulp.src(jsVendorSources)
const compressJsVendor = () => src(jsVendorSources)
.pipe(concat('vendor.min.js'))
.pipe(terser())
.pipe(gulp.dest('./dist/resources')));
.pipe(dest(RESOURCES_PATH));
const mjsSources = [
'server/scripts/modules/currentweatherscroll.mjs',
@@ -87,39 +90,40 @@ const mjsSources = [
'server/scripts/index.mjs',
];
gulp.task('build_js', () => gulp.src(mjsSources)
const buildJs = () => src(mjsSources)
.pipe(webpack(webpackOptions))
.pipe(gulp.dest('dist/resources')));
.pipe(dest(RESOURCES_PATH));
const cssSources = [
'server/styles/main.css',
];
gulp.task('copy_css', () => gulp.src(cssSources)
const copyCss = () => src(cssSources)
.pipe(concat('ws.min.css'))
.pipe(gulp.dest('./dist/resources')));
.pipe(dest(RESOURCES_PATH));
const htmlSources = [
'views/*.ejs',
];
gulp.task('compress_html', () => {
// eslint-disable-next-line global-require
const { version } = require('../package.json');
return gulp.src(htmlSources)
const compressHtml = async () => {
const packageJson = await readFile('package.json');
const { version } = JSON.parse(packageJson);
return src(htmlSources)
.pipe(ejs({
production: version,
version,
}))
.pipe(rename({ extname: '.html' }))
.pipe(htmlmin({ collapseWhitespace: true }))
.pipe(gulp.dest('./dist'));
});
.pipe(dest('./dist'));
};
const otherFiles = [
'server/robots.txt',
'server/manifest.json',
];
gulp.task('copy_other_files', () => gulp.src(otherFiles, { base: 'server/' })
.pipe(gulp.dest('./dist')));
const copyOtherFiles = () => src(otherFiles, { base: 'server/' })
.pipe(dest('./dist'));
const s3 = s3Upload({
useIAM: true,
@@ -130,7 +134,7 @@ const uploadSources = [
'dist/**',
'!dist/**/*.map',
];
gulp.task('upload', () => gulp.src(uploadSources, { base: './dist' })
const upload = () => src(uploadSources, { base: './dist' })
.pipe(s3({
Bucket: 'weatherstar',
StorageClass: 'STANDARD',
@@ -140,21 +144,21 @@ gulp.task('upload', () => gulp.src(uploadSources, { base: './dist' })
return 'max-age=2592000'; // 1 month
},
},
})));
}));
const imageSources = [
'server/fonts/**',
'server/images/**',
];
gulp.task('upload_images', () => gulp.src(imageSources, { base: './server' })
const uploadImages = () => src(imageSources, { base: './server', encoding: false })
.pipe(
s3({
Bucket: 'weatherstar',
StorageClass: 'STANDARD',
}),
));
);
gulp.task('invalidate', async () => cloudfront.createInvalidation({
const invalidate = () => cloudfront.send(new CreateInvalidationCommand({
DistributionId: 'E9171A4KV8KCW',
InvalidationBatch: {
CallerReference: (new Date()).toLocaleString(),
@@ -163,10 +167,12 @@ gulp.task('invalidate', async () => cloudfront.createInvalidation({
Items: ['/*'],
},
},
}).promise());
}));
gulp.task('build-dist', gulp.series(clean, gulp.parallel('build_js', 'compress_js_data', 'compress_js_vendor', 'copy_css', 'compress_html', 'copy_other_files')));
const buildDist = series(clean, parallel(buildJs, compressJsData, compressJsVendor, copyCss, compressHtml, copyOtherFiles));
// upload_images could be in parallel with upload, but _images logs a lot and has little changes
// by running upload last the majority of the changes will be at the bottom of the log for easy viewing
module.exports = gulp.series('build-dist', 'upload_images', 'upload', 'invalidate');
const publishFrontend = series(buildDist, uploadImages, upload, invalidate);
export default publishFrontend;

View File

@@ -1,12 +1,9 @@
/* eslint-disable import/no-extraneous-dependencies */
const gulp = require('gulp');
const del = require('del');
const rename = require('gulp-rename');
import { src, series, dest } from 'gulp';
import { deleteAsync } from 'del';
import rename from 'gulp-rename';
const clean = (cb) => {
del(['./server/scripts/vendor/auto/**']);
cb();
};
const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']);
const vendorFiles = [
'./node_modules/luxon/build/es6/luxon.js',
@@ -17,13 +14,15 @@ const vendorFiles = [
'./node_modules/swiped-events/src/swiped-events.js',
];
const copy = () => gulp.src(vendorFiles)
const copy = () => src(vendorFiles)
.pipe(rename((path) => {
path.dirname = path.dirname.toLowerCase();
path.basename = path.basename.toLowerCase();
path.extname = path.extname.toLowerCase();
if (path.basename === 'luxon') path.extname = '.mjs';
}))
.pipe(gulp.dest('./server/scripts/vendor/auto'));
.pipe(dest('./server/scripts/vendor/auto'));
module.exports = gulp.series(clean, copy);
const updateVendor = series(clean, copy);
export default updateVendor;

View File

@@ -1,4 +0,0 @@
const gulp = require('gulp');
gulp.task('update-vendor', require('./gulp/update-vendor'));
gulp.task('publish-frontend', require('./gulp/publish-frontend'));

7
gulpfile.mjs Normal file
View File

@@ -0,0 +1,7 @@
import updateVendor from './gulp/update-vendor.mjs';
import publishFrontend from './gulp/publish-frontend.mjs';
export {
updateVendor,
publishFrontend,
};

View File

@@ -1,5 +1,4 @@
// express
// eslint-disable-next-line import/no-extraneous-dependencies
const express = require('express');
const app = express();

10110
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "ws4kp",
"version": "5.11.2",
"version": "5.14.3",
"description": "Welcome to the WeatherStar 4000+ project page!",
"main": "index.js",
"scripts": {
@@ -20,30 +20,32 @@
},
"homepage": "https://github.com/netbymatt/ws4kp#readme",
"devDependencies": {
"del": "^6.0.0",
"ejs": "^3.1.5",
"eslint": "^8.21.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-sonarjs": "^0.21.0",
"eslint-plugin-unicorn": "^46.0.0",
"express": "^4.17.1",
"gulp": "^4.0.2",
"gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0",
"gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0",
"gulp-s3-upload": "^1.7.3",
"gulp-sass": "^5.1.0",
"gulp-terser": "^2.0.0",
"del": "^8.0.0",
"jquery": "^3.6.0",
"jquery-touchswipe": "^1.6.19",
"luxon": "^3.0.0",
"nosleep.js": "^0.12.0",
"sass": "^1.54.0",
"suncalc": "^1.8.0",
"swiped-events": "^1.1.4",
"@aws-sdk/client-cloudfront": "^3.609.0",
"gulp-awspublish": "^8.0.0",
"gulp-s3-upload": "^1.7.3",
"eslint": "^8.2.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.10.0",
"gulp": "^5.0.0",
"gulp-concat": "^2.6.1",
"gulp-ejs": "^5.1.0",
"gulp-htmlmin": "^5.0.1",
"gulp-rename": "^2.0.0",
"gulp-sass": "^6.0.0",
"gulp-terser": "^2.0.0",
"terser-webpack-plugin": "^5.3.6",
"webpack-stream": "^7.0.0"
"webpack-stream": "^7.0.0",
"sass": "^1.54.0"
},
"dependencies": {
"express": "^4.17.1",
"ejs": "^3.1.5"
}
}

View File

@@ -0,0 +1,14 @@
// this file is loaded by the main html page (when renamed to custom.js)
// 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', () => {
// get all of the logo images
const logos = document.querySelectorAll('.logo img');
// loop through each logo
logos.forEach((elem) => {
// change the source
elem.src = 'my-custom-logo.gif';
});
});

View File

@@ -43,9 +43,12 @@ const init = () => {
btnGetGps.addEventListener('click', btnGetGpsClick);
if (!navigator.geolocation) btnGetGps.style.display = 'none';
document.querySelector('#divTwc').addEventListener('click', () => {
document.querySelector('#divTwc').addEventListener('mousemove', () => {
if (document.fullscreenElement) updateFullScreenNavigate();
});
// local change detection when exiting full screen via ESC key (or other non button click methods)
window.addEventListener('resize', fullScreenResizeCheck);
fullScreenResizeCheck.wasFull = false;
document.querySelector(TXT_ADDRESS_SELECTOR).addEventListener('keydown', (key) => { if (key.code === 'Enter') formSubmit(); });
document.querySelector('#btnGetLatLng').addEventListener('click', () => formSubmit());
@@ -189,7 +192,7 @@ const enterFullScreen = () => {
if (requestMethod) {
// Native full screen.
requestMethod.call(element, { navigationUI: 'hide' }); // https://bugs.chromium.org/p/chromium/issues/detail?id=933436#c7
requestMethod.call(element, { navigationUI: 'hide' });
} else {
// iOS doesn't support FullScreen API.
window.scrollTo(0, 0);
@@ -217,10 +220,15 @@ const exitFullscreen = () => {
document.msExitFullscreen();
}
resize();
exitFullScreenVisibilityChanges();
};
const exitFullScreenVisibilityChanges = () => {
// change hover text and image
const img = document.querySelector(TOGGLE_FULL_SCREEN_SELECTOR);
img.src = 'images/nav/ic_fullscreen_white_24dp_2x.png';
img.title = 'Enter fullscreen';
document.querySelector('#divTwc').classList.remove('no-cursor');
const divTwcBottom = document.querySelector('#divTwcBottom');
divTwcBottom.classList.remove('hidden');
divTwcBottom.classList.add('visible');
@@ -228,7 +236,6 @@ const exitFullscreen = () => {
const btnNavigateMenuClick = () => {
postMessage('navButton', 'menu');
updateFullScreenNavigate();
return false;
};
@@ -247,35 +254,32 @@ const loadData = (_latLon, haveDataCallback) => {
const swipeCallBack = (direction) => {
switch (direction) {
case 'left':
btnNavigateNextClick();
break;
case 'left':
btnNavigateNextClick();
break;
case 'right':
default:
btnNavigatePreviousClick();
break;
case 'right':
default:
btnNavigatePreviousClick();
break;
}
};
const btnNavigateRefreshClick = () => {
resetStatuses();
loadData();
updateFullScreenNavigate();
return false;
};
const btnNavigateNextClick = () => {
postMessage('navButton', 'next');
updateFullScreenNavigate();
return false;
};
const btnNavigatePreviousClick = () => {
postMessage('navButton', 'previous');
updateFullScreenNavigate();
return false;
};
@@ -287,6 +291,7 @@ const updateFullScreenNavigate = () => {
const divTwcBottom = document.querySelector('#divTwcBottom');
divTwcBottom.classList.remove('hidden');
divTwcBottom.classList.add('visible');
document.querySelector('#divTwc').classList.remove('no-cursor');
if (navigateFadeIntervalId) {
clearTimeout(navigateFadeIntervalId);
@@ -297,6 +302,7 @@ const updateFullScreenNavigate = () => {
if (document.fullscreenElement) {
divTwcBottom.classList.remove('visible');
divTwcBottom.classList.add('hidden');
document.querySelector('#divTwc').classList.add('no-cursor');
}
}, 2000);
};
@@ -306,41 +312,41 @@ const documentKeydown = (e) => {
if (document.fullscreenElement || document.activeElement === document.body) {
switch (key) {
case ' ': // Space
// don't scroll
e.preventDefault();
btnNavigatePlayClick();
return false;
case ' ': // Space
// don't scroll
e.preventDefault();
btnNavigatePlayClick();
return false;
case 'ArrowRight':
case 'PageDown':
// don't scroll
e.preventDefault();
btnNavigateNextClick();
return false;
case 'ArrowRight':
case 'PageDown':
// don't scroll
e.preventDefault();
btnNavigateNextClick();
return false;
case 'ArrowLeft':
case 'PageUp':
// don't scroll
e.preventDefault();
btnNavigatePreviousClick();
return false;
case 'ArrowLeft':
case 'PageUp':
// don't scroll
e.preventDefault();
btnNavigatePreviousClick();
return false;
case 'ArrowUp': // Home
e.preventDefault();
btnNavigateMenuClick();
return false;
case 'ArrowUp': // Home
e.preventDefault();
btnNavigateMenuClick();
return false;
case '0': // "O" Restart
btnNavigateRefreshClick();
return false;
case '0': // "O" Restart
btnNavigateRefreshClick();
return false;
case 'F':
case 'f':
btnFullScreenClick();
return false;
case 'F':
case 'f':
btnFullScreenClick();
return false;
default:
default:
}
}
return false;
@@ -348,7 +354,6 @@ const documentKeydown = (e) => {
const btnNavigatePlayClick = () => {
postMessage('navButton', 'playToggle');
updateFullScreenNavigate();
return false;
};
@@ -393,3 +398,18 @@ const btnGetGpsClick = async () => {
txtAddress.value = `${location.city}, ${location.state}`;
});
};
// check for change in full screen triggered by browser and run local functions
const fullScreenResizeCheck = () => {
if (fullScreenResizeCheck.wasFull && !document.fullscreenElement) {
// leaving full screen
exitFullScreenVisibilityChanges();
}
if (!fullScreenResizeCheck.wasFull && document.fullscreenElement) {
// entering full screen
// can't do much here because a UI interaction is required to change the full screen div element
}
// store state of fullscreen element for next change detection
fullScreenResizeCheck.wasFull = !!document.fullscreenElement;
};

View File

@@ -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) {
@@ -94,7 +94,7 @@ class Almanac extends WeatherDisplay {
if (iteration % 2 === 0) test = (lastPhase, testPhase) => lastPhase > threshold && testPhase <= threshold;
do {
// store last phase
// store last phase
const lastPhase = phase;
// calculate new phase after step
moonDate = moonDate.plus(step);
@@ -103,7 +103,7 @@ class Almanac extends WeatherDisplay {
if (phase > 0.9) phase -= 1.0;
// compare
if (test(lastPhase, phase)) {
// last iteration is three, return value
// last iteration is three, return value
if (iteration >= 3) break;
// iterate recursively
return this.getMoonTransition(threshold, phaseName, moonDate, iteration + 1);
@@ -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 = {};
@@ -160,15 +160,15 @@ class Almanac extends WeatherDisplay {
const imageName = (type) => {
switch (type) {
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
case 'Full':
return 'images/2/Full-Moon.gif';
case 'Last':
return 'images/2/Last-Quarter.gif';
case 'New':
return 'images/2/New-Moon.gif';
case 'First':
default:
return 'images/2/First-Quarter.gif';
}
};

View File

@@ -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
@@ -56,7 +56,9 @@ class CurrentWeather extends WeatherDisplay {
|| observations.features[0].properties.windSpeed.value === null
|| observations.features[0].properties.textDescription === null
|| observations.features[0].properties.textDescription === ''
|| observations.features[0].properties.icon === null) {
|| observations.features[0].properties.icon === null
|| observations.features[0].properties.dewpoint.value === null
|| observations.features[0].properties.barometricPressure.value === null) {
observations = undefined;
throw new Error(`Unable to get observations: ${station.properties.stationIdentifier}, trying next station`);
}
@@ -157,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 = '';
@@ -184,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;
};

View File

@@ -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) => {

View File

@@ -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(' ');

View File

@@ -10,6 +10,12 @@ const hazardLevels = {
Severe: 5,
};
const hazardModifiers = {
'Hurricane Warning': 2,
'Tornado Warning': 3,
'Severe Thunderstorm Warning': 1,
};
class Hazards extends WeatherDisplay {
constructor(navId, elemId, defaultActive) {
// special height and width for scrolling
@@ -34,8 +40,9 @@ class Hazards extends WeatherDisplay {
url.searchParams.append('limit', 5);
const alerts = await json(url, { retryCount: 3, stillWaiting: () => this.stillWaiting() });
const unsortedAlerts = alerts.features ?? [];
const sortedAlerts = unsortedAlerts.sort((a, b) => (hazardLevels[b.properties.severity] ?? 0) - (hazardLevels[a.properties.severity] ?? 0));
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown');
const hasImmediate = unsortedAlerts.reduce((acc, hazard) => acc || hazard.properties.urgency === 'Immediate', false);
const sortedAlerts = unsortedAlerts.sort((a, b) => (calcSeverity(b.properties.severity, b.properties.event)) - (calcSeverity(a.properties.severity, a.properties.event)));
const filteredAlerts = sortedAlerts.filter((hazard) => hazard.properties.severity !== 'Unknown' && (!hasImmediate || (hazard.properties.urgency === 'Immediate')));
this.data = filteredAlerts;
// show alert indicator
@@ -115,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;
@@ -134,7 +141,26 @@ class Hazards extends WeatherDisplay {
this.getDataCallbacks.push(() => resolve(this.data));
});
}
// after we roll through the hazards once, don't display again until the next refresh (10 minutes)
screenIndexFromBaseCount() {
const superValue = super.screenIndexFromBaseCount();
// false is returned when we reach the end of the scroll
if (superValue === false) {
// set total screens to zero to take this out of the rotation
this.timing.totalScreens = 0;
}
// return the value as expected
return superValue;
}
}
const calcSeverity = (severity, event) => {
// base severity plus some modifiers for specific types of warnings
const baseSeverity = hazardLevels[severity] ?? 0;
const modifiedSeverity = hazardModifiers[event] ?? 0;
return baseSeverity + modifiedSeverity;
};
// register display
registerDisplay(new Hazards(0, 'hazards', true));

View File

@@ -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'));

View File

@@ -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;
@@ -109,7 +109,7 @@ class Hourly extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// calculate scroll offset and don't go past end
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').getBoundingClientRect().height - 289, (count - 150));
let offsetY = Math.min(this.elem.querySelector('.hourly-lines').offsetHeight - 289, (count - 150));
// don't let offset go negative
if (offsetY < 0) offsetY = 0;
@@ -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],

View File

@@ -1,4 +1,3 @@
/* eslint-disable unicorn/consistent-function-scoping */
/* spell-checker: disable */
const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
@@ -21,136 +20,138 @@ const getWeatherRegionalIconFromIconLink = (link, _isNightTime) => {
// find the icon
switch (conditionName + (isNightTime ? '-n' : '')) {
case 'skc':
case 'hot':
case 'haze':
return addPath('Sunny.gif');
case 'skc':
case 'hot':
case 'haze':
return addPath('Sunny.gif');
case 'skc-n':
case 'nskc':
case 'nskc-n':
case 'cold-n':
return addPath('Clear-1992.gif');
case 'skc-n':
case 'nskc':
case 'nskc-n':
case 'cold-n':
return addPath('Clear-1992.gif');
case 'bkn':
return addPath('Mostly-Cloudy-1994-2.gif');
case 'bkn':
return addPath('Mostly-Cloudy-1994-2.gif');
case 'bkn-n':
case 'few-n':
case 'nfew-n':
case 'nfew':
return addPath('Partly-Clear-1994-2.gif');
case 'bkn-n':
case 'few-n':
case 'nfew-n':
case 'nfew':
return addPath('Partly-Clear-1994-2.gif');
case 'sct':
case 'few':
return addPath('Partly-Cloudy.gif');
case 'sct':
case 'few':
return addPath('Partly-Cloudy.gif');
case 'sct-n':
case 'nsct':
case 'nsct-n':
return addPath('Mostly-Clear.gif');
case 'sct-n':
case 'nsct':
case 'nsct-n':
return addPath('Mostly-Clear.gif');
case 'ovc':
case 'ovc-n':
return addPath('Cloudy.gif');
case 'ovc':
case 'ovc-n':
return addPath('Cloudy.gif');
case 'fog':
case 'fog-n':
return addPath('Fog.gif');
case 'fog':
case 'fog-n':
return addPath('Fog.gif');
case 'rain_sleet':
return addPath('Sleet.gif');
case 'rain_sleet':
return addPath('Sleet.gif');
case 'rain_showers':
case 'rain_showers_high':
return addPath('Scattered-Showers-1994-2.gif');
case 'rain_showers':
case 'rain_showers_high':
return addPath('Scattered-Showers-1994-2.gif');
case 'rain_showers-n':
case 'rain_showers_high-n':
return addPath('Scattered-Showers-Night-1994-2.gif');
case 'rain_showers-n':
case 'rain_showers_high-n':
return addPath('Scattered-Showers-Night-1994-2.gif');
case 'rain':
case 'rain-n':
return addPath('Rain-1992.gif');
case 'rain':
case 'rain-n':
return addPath('Rain-1992.gif');
// case 'snow':
// return addPath('Light-Snow.gif');
// break;
// case 'snow':
// return addPath('Light-Snow.gif');
// break;
// case 'cc_snowshowers.gif':
// //case "heavy-snow.gif":
// return addPath('AM-Snow-1994.gif');
// break;
// case 'cc_snowshowers.gif':
// //case "heavy-snow.gif":
// return addPath('AM-Snow-1994.gif');
// break;
case 'snow':
case 'snow-n':
if (value > 50) return addPath('Heavy-Snow-1994-2.gif');
return addPath('Light-Snow.gif');
case 'snow':
case 'snow-n':
if (value > 50) return addPath('Heavy-Snow-1994-2.gif');
return addPath('Light-Snow.gif');
case 'rain_snow':
case 'rain_snow-n':
return addPath('Rain-Snow-1992.gif');
case 'rain_snow':
case 'rain_snow-n':
return addPath('Rain-Snow-1992.gif');
case 'snow_fzra':
case 'snow_fzra-n':
return addPath('Freezing-Rain-Snow-1992.gif');
case 'snow_fzra':
case 'snow_fzra-n':
return addPath('Freezing-Rain-Snow-1992.gif');
case 'fzra':
case 'fzra-n':
case 'rain_fzra':
case 'rain_fzra-n':
return addPath('Freezing-Rain-1992.gif');
case 'fzra':
case 'fzra-n':
case 'rain_fzra':
case 'rain_fzra-n':
return addPath('Freezing-Rain-1992.gif');
case 'snow_sleet':
case 'snow_sleet-n':
return addPath('Snow and Sleet.gif');
case 'snow_sleet':
case 'snow_sleet-n':
return addPath('Snow and Sleet.gif');
case 'sleet':
case 'sleet-n':
return addPath('Sleet.gif');
case 'sleet':
case 'sleet-n':
return addPath('Sleet.gif');
case 'tsra_sct':
case 'tsra':
return addPath('Scattered-Tstorms-1994-2.gif');
case 'tsra_sct':
case 'tsra':
return addPath('Scattered-Tstorms-1994-2.gif');
case 'tsra_sct-n':
case 'tsra-n':
return addPath('Scattered-Tstorms-Night-1994-2.gif');
case 'tsra_sct-n':
case 'tsra-n':
return addPath('Scattered-Tstorms-Night-1994-2.gif');
case 'tsra_hi':
case 'tsra_hi-n':
case 'hurricane':
case 'tropical_storm':
return addPath('Thunderstorm.gif');
case 'tsra_hi':
case 'tsra_hi-n':
case 'hurricane':
case 'tropical_storm':
case 'hurricane-n':
case 'tropical_storm-n':
return addPath('Thunderstorm.gif');
case 'wind':
case 'wind_few':
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
case 'wind-n':
case 'wind_few-n':
case 'wind_bkn-n':
case 'wind_ovc-n':
return addPath('Wind.gif');
case 'wind':
case 'wind_few':
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
case 'wind-n':
case 'wind_few-n':
case 'wind_bkn-n':
case 'wind_ovc-n':
return addPath('Wind.gif');
case 'wind_skc':
return addPath('Sunny-Wind-1994.gif');
case 'wind_skc':
return addPath('Sunny-Wind-1994.gif');
case 'wind_skc-n':
case 'wind_sct-n':
return addPath('Clear-Wind-1994.gif');
case 'wind_skc-n':
case 'wind_sct-n':
return addPath('Clear-Wind-1994.gif');
case 'blizzard':
case 'blizzard-n':
return addPath('Blowing Snow.gif');
case 'blizzard':
case 'blizzard-n':
return addPath('Blowing Snow.gif');
case 'cold':
return addPath('cold.gif');
case 'cold':
return addPath('cold.gif');
default:
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
return false;
default:
console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`);
return false;
}
};
@@ -176,120 +177,122 @@ const getWeatherIconFromIconLink = (link, _isNightTime) => {
// find the icon
switch (conditionName + (isNightTime ? '-n' : '')) {
case 'skc':
case 'hot':
case 'haze':
case 'cold':
return addPath('CC_Clear1.gif');
case 'skc':
case 'hot':
case 'haze':
case 'cold':
return addPath('CC_Clear1.gif');
case 'skc-n':
case 'nskc':
case 'nskc-n':
case 'cold-n':
return addPath('CC_Clear0.gif');
case 'skc-n':
case 'nskc':
case 'nskc-n':
case 'cold-n':
return addPath('CC_Clear0.gif');
case 'sct':
case 'few':
case 'bkn':
return addPath('CC_PartlyCloudy1.gif');
case 'sct':
case 'few':
case 'bkn':
return addPath('CC_PartlyCloudy1.gif');
case 'bkn-n':
case 'few-n':
case 'nfew-n':
case 'nfew':
case 'sct-n':
case 'nsct':
case 'nsct-n':
return addPath('CC_PartlyCloudy0.gif');
case 'bkn-n':
case 'few-n':
case 'nfew-n':
case 'nfew':
case 'sct-n':
case 'nsct':
case 'nsct-n':
return addPath('CC_PartlyCloudy0.gif');
case 'ovc':
case 'novc':
case 'ovc-n':
return addPath('CC_Cloudy.gif');
case 'ovc':
case 'novc':
case 'ovc-n':
return addPath('CC_Cloudy.gif');
case 'fog':
case 'fog-n':
return addPath('CC_Fog.gif');
case 'fog':
case 'fog-n':
return addPath('CC_Fog.gif');
case 'rain_sleet':
case 'rain_sleet-n':
case 'sleet':
case 'sleet-n':
return addPath('Sleet.gif');
case 'rain_sleet':
case 'rain_sleet-n':
case 'sleet':
case 'sleet-n':
return addPath('Sleet.gif');
case 'rain_showers':
case 'rain_showers_high':
return addPath('CC_Showers.gif');
case 'rain_showers':
case 'rain_showers_high':
return addPath('CC_Showers.gif');
case 'rain_showers-n':
case 'rain_showers_high-n':
return addPath('CC_Showers.gif');
case 'rain_showers-n':
case 'rain_showers_high-n':
return addPath('CC_Showers.gif');
case 'rain':
case 'rain-n':
return addPath('CC_Rain.gif');
case 'rain':
case 'rain-n':
return addPath('CC_Rain.gif');
// case 'snow':
// return addPath('Light-Snow.gif');
// break;
// case 'snow':
// return addPath('Light-Snow.gif');
// break;
// case 'cc_snowshowers.gif':
// //case "heavy-snow.gif":
// return addPath('AM-Snow-1994.gif');
// break;
// case 'cc_snowshowers.gif':
// //case "heavy-snow.gif":
// return addPath('AM-Snow-1994.gif');
// break;
case 'snow':
case 'snow-n':
if (value > 50) return addPath('CC_Snow.gif');
return addPath('CC_SnowShowers.gif');
case 'snow':
case 'snow-n':
if (value > 50) return addPath('CC_Snow.gif');
return addPath('CC_SnowShowers.gif');
case 'rain_snow':
return addPath('CC_RainSnow.gif');
case 'rain_snow':
return addPath('CC_RainSnow.gif');
case 'snow_fzra':
case 'snow_fzra-n':
case 'fzra':
case 'fzra-n':
case 'rain_fzra':
case 'rain_fzra-n':
return addPath('CC_FreezingRain.gif');
case 'snow_fzra':
case 'snow_fzra-n':
case 'fzra':
case 'fzra-n':
case 'rain_fzra':
case 'rain_fzra-n':
return addPath('CC_FreezingRain.gif');
case 'snow_sleet':
return addPath('Snow-Sleet.gif');
case 'snow_sleet':
return addPath('Snow-Sleet.gif');
case 'tsra_sct':
case 'tsra':
return addPath('EF_ScatTstorms.gif');
case 'tsra_sct':
case 'tsra':
return addPath('EF_ScatTstorms.gif');
case 'tsra_sct-n':
case 'tsra-n':
return addPath('CC_TStorm.gif');
case 'tsra_sct-n':
case 'tsra-n':
return addPath('CC_TStorm.gif');
case 'tsra_hi':
case 'tsra_hi-n':
case 'hurricane':
case 'tropical_storm':
return addPath('CC_TStorm.gif');
case 'tsra_hi':
case 'tsra_hi-n':
case 'hurricane':
case 'tropical_storm':
case 'hurricane-n':
case 'tropical_storm-n':
return addPath('CC_TStorm.gif');
case 'wind_few':
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
case 'wind_skc':
case 'wind_few-n':
case 'wind_bkn-n':
case 'wind_ovc-n':
case 'wind_skc-n':
case 'wind_sct-n':
return addPath('CC_Windy.gif');
case 'wind_few':
case 'wind_sct':
case 'wind_bkn':
case 'wind_ovc':
case 'wind_skc':
case 'wind_few-n':
case 'wind_bkn-n':
case 'wind_ovc-n':
case 'wind_skc-n':
case 'wind_sct-n':
return addPath('CC_Windy.gif');
case 'blizzard':
case 'blizzard-n':
return addPath('Blowing-Snow.gif');
case 'blizzard':
case 'blizzard-n':
return addPath('Blowing-Snow.gif');
default:
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
return false;
default:
console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`);
return false;
}
};

View File

@@ -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,

View File

@@ -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(),

View File

@@ -238,30 +238,30 @@ const setPlaying = (newValue) => {
// handle all navigation buttons
const handleNavButton = (button) => {
switch (button) {
case 'play':
setPlaying(true);
break;
case 'playToggle':
setPlaying(!playing);
break;
case 'stop':
setPlaying(false);
break;
case 'next':
setPlaying(false);
navTo(msg.command.nextFrame);
break;
case 'previous':
setPlaying(false);
navTo(msg.command.previousFrame);
break;
case 'menu':
setPlaying(false);
progress.showCanvas();
hideAllCanvases();
break;
default:
console.error(`Unknown navButton ${button}`);
case 'play':
setPlaying(true);
break;
case 'playToggle':
setPlaying(!playing);
break;
case 'stop':
setPlaying(false);
break;
case 'next':
setPlaying(false);
navTo(msg.command.nextFrame);
break;
case 'previous':
setPlaying(false);
navTo(msg.command.previousFrame);
break;
case 'menu':
setPlaying(false);
progress.showCanvas();
hideAllCanvases();
break;
default:
console.error(`Unknown navButton ${button}`);
}
};

View File

@@ -157,7 +157,7 @@ class Radar extends WeatherDisplay {
minute,
}, {
zone: 'UTC',
}).setZone();
}).setZone(timeZone());
} else {
time = DateTime.fromHTTP(response.headers.get('last-modified')).setZone(timeZone());
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -4,20 +4,32 @@ document.addEventListener('DOMContentLoaded', () => {
init();
});
const settings = {};
// default speed
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'],
[0.75, 'Fast'],
[1.0, 'Normal'],
[1.25, 'Slow'],
[1.5, 'Very Slow'],
]);
settings.units = new Setting('units', 'Units', 'select', 'us', unitChange, true, [
['us', 'US'],
['si', 'Metric'],
]);
// generate checkboxes
const checkboxes = Object.values(settings).map((d) => d.generateCheckbox());
// generate html objects
const settingHtml = Object.values(settings).map((d) => d.generate());
// write to page
const settingsSection = document.querySelector('#settings');
settingsSection.innerHTML = '';
settingsSection.append(...checkboxes);
settingsSection.append(...settingHtml);
};
const wideScreenChange = (value) => {
@@ -39,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;

View File

@@ -7,7 +7,13 @@ const specialMappings = {
const init = () => {
// add action to existing link
document.querySelector('#share-link').addEventListener('click', createLink);
const shareLink = document.querySelector('#share-link');
shareLink.addEventListener('click', createLink);
// if navigator.clipboard does not exist, change text
if (!navigator?.clipboard) {
shareLink.textContent = 'Get Permalink';
}
};
const createLink = async (e) => {
@@ -25,6 +31,14 @@ const createLink = async (e) => {
}
});
// get all select boxes
const selects = document.querySelectorAll('select');
[...selects].forEach((elem) => {
if (elem?.id) {
queryStringElements[elem.id] = elem?.value ?? 0;
}
});
// add the location string
queryStringElements.latLonQuery = localStorage.getItem('latLonQuery');
queryStringElements.latLon = localStorage.getItem('latLon');
@@ -33,10 +47,23 @@ const createLink = async (e) => {
const url = new URL(`?${queryString}`, document.location.href);
// send to proper function based on availability of clipboard
if (navigator?.clipboard) {
copyToClipboard(url);
} else {
writeLinkToPage(url);
}
};
const copyToClipboard = async (url) => {
try {
// write to clipboard
await navigator.clipboard.writeText(url.toString());
// alert user
const confirmSpan = document.querySelector('#share-link-copied');
confirmSpan.style.display = 'inline';
// hide confirm text after 5 seconds
setTimeout(() => {
confirmSpan.style.display = 'none';
}, 5000);
@@ -45,6 +72,18 @@ const createLink = async (e) => {
}
};
const writeLinkToPage = (url) => {
// get elements
const shareLinkInstructions = document.querySelector('#share-link-instructions');
const shareLinkUrl = shareLinkInstructions.querySelector('#share-link-url');
// populate url and display
shareLinkUrl.value = url;
shareLinkInstructions.style.display = 'inline';
// highlight for convenience
shareLinkUrl.focus();
shareLinkUrl.select();
};
const parseQueryString = () => {
// return memoized result
if (parseQueryString.params) return parseQueryString.params;

View File

@@ -9,20 +9,20 @@ const STATUS = {
const calcStatusClass = (statusCode) => {
switch (statusCode) {
case STATUS.loading:
return 'loading';
case STATUS.loaded:
return 'press-here';
case STATUS.failed:
return 'failed';
case STATUS.noData:
return 'no-data';
case STATUS.disabled:
return 'disabled';
case STATUS.retrying:
return 'retrying';
default:
return '';
case STATUS.loading:
return 'loading';
case STATUS.loaded:
return 'press-here';
case STATUS.failed:
return 'failed';
case STATUS.noData:
return 'no-data';
case STATUS.disabled:
return 'disabled';
case STATUS.retrying:
return 'retrying';
default:
return '';
}
};

View File

@@ -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
@@ -131,7 +136,7 @@ class TravelForecast extends WeatherDisplay {
// base count change callback
baseCountChange(count) {
// calculate scroll offset and don't go past end
let offsetY = Math.min(this.elem.querySelector('.travel-lines').getBoundingClientRect().height - 289, (count - 150));
let offsetY = Math.min(this.elem.querySelector('.travel-lines').offsetHeight - 289, (count - 150));
// don't let offset go negative
if (offsetY < 0) offsetY = 0;

View File

@@ -21,7 +21,7 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
if (params.cors === true) corsUrl = rewriteUrl(_url);
const url = new URL(corsUrl, `${window.location.origin}/`);
// match the security protocol when not on localhost
url.protocol = window.location.hostname === 'localhost' ? url.protocol : window.location.protocol;
// url.protocol = window.location.hostname === 'localhost' ? url.protocol : window.location.protocol;
// add parameters if necessary
if (params.data) {
Object.keys(params.data).forEach((key) => {
@@ -39,14 +39,14 @@ const fetchAsync = async (_url, responseType, _params = {}) => {
if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`);
// return the requested response
switch (responseType) {
case 'json':
return response.json();
case 'text':
return response.text();
case 'blob':
return response.blob();
default:
return response;
case 'json':
return response.json();
case 'text':
return response.text();
case 'blob':
return response.blob();
default:
return response;
}
};
@@ -84,11 +84,11 @@ const delay = (time, func, ...args) => new Promise((resolve) => {
const retryDelay = (retryNumber) => {
switch (retryNumber) {
case 1: return 1000;
case 2: return 2000;
case 3: return 5000;
case 4: return 10_000;
default: return 30_000;
case 1: return 1000;
case 2: return 2000;
case 3: return 5000;
case 4: return 10_000;
default: return 30_000;
}
};

View File

@@ -3,23 +3,31 @@ import { parseQueryString } from '../share.mjs';
const SETTINGS_KEY = 'Settings';
class Setting {
constructor(shortName, name, type, defaultValue, changeAction, sticky) {
constructor(shortName, name, type, defaultValue, changeAction, sticky, values) {
// store values
this.shortName = shortName;
this.name = name;
this.defaultValue = defaultValue;
this.myValue = defaultValue;
this.type = type;
this.type = type ?? 'checkbox';
this.sticky = sticky;
this.values = values;
// a default blank change function is provided
this.changeAction = changeAction ?? (() => { });
// get value from url
const urlValue = parseQueryString()?.[`settings-${shortName}-checkbox`];
const urlValue = parseQueryString()?.[`settings-${shortName}-${type}`];
let urlState;
if (urlValue !== undefined) {
if (type === 'checkbox' && urlValue !== undefined) {
urlState = urlValue === 'true';
}
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();
@@ -28,7 +36,50 @@ class Setting {
}
// call the change function on startup
this.changeAction(this.myValue);
switch (type) {
case 'select':
this.selectChange({ target: { value: this.myValue } });
break;
case 'checkbox':
default:
this.checkboxChange({ target: { checked: this.myValue } });
}
}
generateSelect() {
// create a radio button set in the selected displays area
const label = document.createElement('label');
label.for = `settings-${this.shortName}-select`;
label.id = `settings-${this.shortName}-label`;
const span = document.createElement('span');
span.innerHTML = `${this.name} `;
label.append(span);
const select = document.createElement('select');
select.id = `settings-${this.shortName}-select`;
select.name = `settings-${this.shortName}-select`;
select.addEventListener('change', (e) => this.selectChange(e));
this.values.forEach(([value, text]) => {
const option = document.createElement('option');
if (typeof value === 'number') {
option.value = value.toFixed(2);
} else {
option.value = value;
}
option.innerHTML = text;
select.append(option);
});
label.append(select);
this.element = label;
// set the initial value
this.selectHighlight(this.myValue);
return label;
}
generateCheckbox() {
@@ -48,7 +99,7 @@ class Setting {
label.append(checkbox, span);
this.checkbox = label;
this.element = label;
return label;
}
@@ -62,6 +113,19 @@ class Setting {
this.changeAction(this.myValue);
}
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
this.changeAction(this.myValue);
}
storeToLocalStorage(value) {
if (!this.sticky) return;
const allSettingsString = localStorage?.getItem(SETTINGS_KEY) ?? '{}';
@@ -77,12 +141,12 @@ class Setting {
const storedValue = JSON.parse(allSettings)?.[this.shortName];
if (storedValue !== undefined) {
switch (this.type) {
case 'boolean':
return storedValue;
case 'int':
return storedValue;
default:
return null;
case 'boolean':
return storedValue;
case 'select':
return storedValue;
default:
return null;
}
}
}
@@ -99,12 +163,36 @@ class Setting {
set value(newValue) {
// update the state
this.myValue = newValue;
this.checkbox.checked = newValue;
switch (this.type) {
case 'select':
this.selectHighlight(newValue);
break;
case 'checkbox':
default:
this.element.checked = newValue;
}
this.storeToLocalStorage(this.myValue);
// call change action
this.changeAction(this.myValue);
}
selectHighlight(newValue) {
// set the dropdown to the provided value
this.element.querySelectorAll('option').forEach((elem) => {
elem.selected = (newValue?.toFixed?.(2) === elem.value) || (newValue === elem.value);
});
}
generate() {
switch (this.type) {
case 'select':
return this.generateSelect();
case 'checkbox':
default:
return this.generateCheckbox();
}
}
}
export default Setting;

View File

@@ -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,
};

View File

@@ -6,6 +6,7 @@ import {
msg, displayNavMessage, isPlaying, updateStatus, timeZone,
} from './navigation.mjs';
import { parseQueryString } from './share.mjs';
import settings from './settings.mjs';
class WeatherDisplay {
constructor(navId, elemId, name, defaultEnabled) {
@@ -169,7 +170,7 @@ class WeatherDisplay {
// auto clock refresh
if (!this.dateTimeInterval) {
// only draw if canvas is active to conserve battery
setInterval(() => this.active && this.drawCurrentDateTime(), 100);
this.dateTimeInterval = setInterval(() => this.active && this.drawCurrentDateTime(), 100);
}
}
}
@@ -358,7 +359,7 @@ class WeatherDisplay {
// start and stop base counter
startNavCount() {
if (!this.navInterval) this.navInterval = setInterval(() => this.navBaseTime(), this.timing.baseDelay);
if (!this.navInterval) this.navInterval = setInterval(() => this.navBaseTime(), this.timing.baseDelay * settings.speed.value);
}
resetNavBaseCount() {

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -35,6 +35,7 @@
var yDiff = null;
var timeDown = null;
var startEl = null;
var touchCount = 0;
/**
* Fires swiped event if swipe detected on touchend
@@ -84,6 +85,7 @@
var eventData = {
dir: eventType.replace(/swiped-/, ''),
touchType: (changedTouches[0] || {}).touchType || 'direct',
fingers: touchCount, // Number of fingers used
xStart: parseInt(xDown, 10),
xEnd: parseInt((changedTouches[0] || {}).clientX || -1, 10),
yStart: parseInt(yDown, 10),
@@ -102,7 +104,6 @@
yDown = null;
timeDown = null;
}
/**
* Records current location on touchstart event
* @param {object} e - browser event object
@@ -120,6 +121,7 @@
yDown = e.touches[0].clientY;
xDiff = 0;
yDiff = 0;
touchCount = e.touches.length;
}
/**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -353,7 +353,8 @@ body {
margin-bottom: 15px;
}
#enabledDisplays, #settings {
#enabledDisplays,
#settings {
margin-bottom: 15px;
@include u.status-colors();
@@ -411,7 +412,12 @@ body {
align-items: center;
justify-content: center;
align-content: center;
&.no-cursor {
cursor: none;
}
}
.kiosk #divTwc {
justify-content: unset;
}
@@ -447,10 +453,10 @@ body {
.visible {
visibility: visible;
opacity: 1;
transition: opacity 1s linear;
transition: opacity 0.1s linear;
}
.hidden {
#divTwc:fullscreen .hidden {
visibility: hidden;
opacity: 0;
transition: visibility 0s 1s, opacity 1s linear
@@ -728,7 +734,12 @@ body {
display: none;
}
#share-link-instructions {
display: none;
}
.kiosk {
#divQuery,
>.info,
>.heading,

View File

@@ -10,7 +10,7 @@
<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" />
@@ -45,7 +45,7 @@
<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>
@@ -151,10 +151,16 @@
<div id='settings'>
</div>
<div class='heading'>Sharing</div>
<div class='info'>
<a href='' id='share-link'>Copy Permalink</a> <span id="share-link-copied">Link copied to clipboard!</span>
<div id="share-link-instructions">
Copy this long URL:
<input type='text' id="share-link-url"></div>
</div>
</div>
<div class='heading'>Forecast Information</div>
<div id="divInfo">
Location: <span id="spanCity"></span> <span id="spanState"></span><br />
Station Id: <span id="spanStationId"></span><br />

View File

@@ -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"
}
},
}