diff --git a/.vscode/launch.json b/.vscode/launch.json index 0697965..acd1201 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,18 +4,17 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ - { "name": "Frontend", "request": "launch", - "type": "pwa-chrome", + "type": "chrome", "url": "http://localhost:8080", "webRoot": "${workspaceFolder}/server", "skipFiles": [ "/**", "**/*.min.js", "**/vendor/**" - ] + ], }, { "name": "Data:stations", @@ -40,7 +39,10 @@ "compounds": [ { "name": "Compound", - "configurations": ["Frontend", "Server"] + "configurations": [ + "Frontend", + "Server" + ] } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 84b57c5..88e7166 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,21 @@ { "cSpell.enableFiletypes": [ "javascript" - ] + ], + "liveSassCompile.settings.formats": [ + { + "format": "compressed", + "extensionName": ".css", + "savePath": "/server/styles", + "savePathSegmentKeys": null, + "savePathReplaceSegmentsWith": null + } + ], + "search.exclude": { + "**/node_modules": true, + "**/bower_components": true, + "**/*.code-search": true, + "**/compiled.css": true, + "**/*.min.js": true, + }, } \ No newline at end of file diff --git a/README.md b/README.md index 5a2c6a5..3b83f82 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,24 @@ There are a lot of CORS considerations and issues with api.weather.gov that are ``` git clone https://github.com/netbymatt/ws4kp.git cd ws4kp +npm i node index.js ``` Open your web browser: http://localhost:8080/ +## Updates in 5.0 +The change to 5.0 changes from drawing the weather graphics on canvas elements and instead uses HTML and CSS to style all of the weather graphics. A lot of other changes and fixes were implemented at the same time. + +* Replace all canvas elements with HTML and CSS +* City and airport names are better parsed to only show the city name. +* Remove the dependency on libgif-js +* Use browser for text wrapping where necessary +* Some new weather icons +* Refresh only on slideshow repeat +* Removed Almanac 30-day outlook +* Fixed startup issue when current conditions are unavailable +* + ## Why the fork? The fork is a result of wanting a more manageable, modern code base to work with. Part of it is an exercise in my education in JavaScript. There are several technical changes that were made behind the scenes. diff --git a/dist/index.html b/dist/index.html index 7d1b3e5..741efed 100644 --- a/dist/index.html +++ b/dist/index.html @@ -1 +1,389 @@ -WeatherStar 4000+

WeatherStar 4000+
Enter your location above to continue

More information
Selected displays
Location:
Station Id:
Radar Id:
Zone Id:
Last Update: (None)
Units:
\ No newline at end of file + + + + + + WeatherStar 4000+ + + + + + + + + + + + + + + + +
+
+
+
+

+ +
+
+
+
+
WeatherStar 4000+
+
Enter your location above to continue
+
+
+
+
+ +
+
WeatherStar
+
4000+ v5.0.0
+
+
+
+
+
+
+
+
Current Conditions
+ +
+
+
+
+
+
+
+
+
+
+
+
+ +
Hourly Forecast
+
+
+
+
+
+
TEMP
+ +
WIND
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ +
+
Travel Forecast
+
For
+
+
+
+
+
+
+
LOW
+
HIGH
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
Current
+
Conditions
+
+ +
+
+
+
+
+
+
+
+
Wind:
+
+
+
+
+
+
+
+
Humidity:
+
+
+
+
Dewpoint:
+
+
+
+
Ceiling:
+
+
+
+
Visibility:
+
+
+
+
Pressure:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
Local
+
Forecast
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
Latest
+
Observations
+
+ +
+
+
+
+
°F
+
°C
+
Weather
+
Wind
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
Regional
+
Observations
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
Almanac
+
+
+
+
+
+
+
+
Monday
+
Tuesday
+
+
+
Sunrise:
+
6:24 am
+
6:25 am
+
+
+
Sunset:
+
6:24 am
+
6:25 am
+
+
+
+
Moon Data:
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
Extended
+
Forecast
+
+
+
+
+
+
+
+
+
+
+
+
+
Lo
+
+
+
+
Hi
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
Local
+
Radar
+
+
+
+
PRECIP
+
+
Light
+
+
+
+
+
+
+
+
+
+
+
Heavy
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+
More information
+
Selected displays
+
+
Location:
Station Id:
Radar Id:
Zone Id:
+
Last Update: (None)
+
Units:
+ + + \ No newline at end of file diff --git a/dist/resources/ws.min.css b/dist/resources/ws.min.css index 7d2f910..4559c11 100644 --- a/dist/resources/ws.min.css +++ b/dist/resources/ws.min.css @@ -1 +1 @@ -@font-face{font-family:Star4000;src:url(../fonts/Star4000.woff) format('woff')}body{font-family:Star4000}button,input{font-family:Star4000}#imgGetGps{height:13px;vertical-align:middle}#txtAddress{width:490px;font-size:16pt}#btnClearQuery,#btnGetGps,#btnGetLatLng{font-size:16pt}.autocomplete-suggestions{background-color:#fff;border:1px solid #000}.autocomplete-suggestion{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:16pt}.autocomplete-selected{background-color:#00f;color:#fff}#divTwc{display:block;background-color:#000;color:#fff;width:100%;max-width:640px}#divTwcLeft{display:none;text-align:right;flex-direction:column;vertical-align:middle}#divTwcLeft>div{flex:1;padding-right:12px;display:flex;flex-direction:column;justify-content:center}#divTwcRight{text-align:left;display:none;flex-direction:column;vertical-align:middle}#divTwcRight>div{flex:1;padding-left:12px;display:flex;flex-direction:column;justify-content:center}#divTwcBottom{display:flex;flex-direction:row;background-color:#000;color:#fff;width:100%}#divTwcBottom>div{padding-left:6px;padding-right:6px}#divTwcBottomLeft{flex:1;text-align:left}#divTwcBottomMiddle{flex:0;text-align:center}#divTwcBottomRight{flex:1;text-align:right}#divTwcNavContainer{display:none}#divTwcNav{width:100%;display:flex;flex-direction:row;background-color:#000;color:#fff;max-width:640px}#divTwcNav>div{padding-left:6px;padding-right:6px}#divTwcNavLeft{flex:1;text-align:left}#divTwcNavMiddle{flex:0;text-align:center}#divTwcNavRight{flex:1;text-align:right}#imgPause1x,#imgPause2x{visibility:hidden;position:absolute}.HideCursor{cursor:none!important}#txtScrollText{width:475px}@font-face{font-family:Star4000;src:url(../fonts/Star4000.woff) format('woff')}@font-face{font-family:"Star 4 Radar";src:url('../fonts/Star 4 Radar.woff') format('woff')}@font-face{font-family:'Star4000 Extended';src:url('../fonts/Star4000 Extended.woff') format('woff')}@font-face{font-family:Star4000LCN;src:url(../fonts/Star4000LCN.woff) format('woff')}@font-face{font-family:'Star4000 Large Compressed';src:url('../fonts/Star4000 Large Compressed.woff') format('woff')}@font-face{font-family:'Star4000 Large';src:url('../fonts/Star4000 Large.woff') format('woff')}@font-face{font-family:'Star4000 Small';src:url('../fonts/Star4000 Small.woff') format('woff')}#display{font-family:Star4000;margin:0;width:100%}jsgif{display:none}#Star4000{font-family:Star4000}#Star4000Extended{font-family:'Star4000 Extended'}#Star4000LargeCompressed{font-family:'Star4000 Large Compressed'}#Star4000Large{font-family:'Star4000 Large'}#Star4000LargeCompressedNumbers{font-family:Star4000LCN}#Star4000Small{font-family:'Star4000 Small'}#Star4Radar{font-family:'Star 4 Radar'}#container{position:relative;width:100%;height:100%;max-height:480;background-image:url(../images/BackGround1_1.png)}#divTwc:fullscreen #container{background-image:none}#loading{width:640px;height:480px;max-width:100%;text-shadow:4px 4px #000;display:flex;align-items:center;text-align:center;justify-content:center}#loading .title{font-family:Star4000 Large;font-size:36px;color:#ff0;margin-bottom:40px}#loading .instructions{font-size:18pt}#container canvas{width:100%}.heading{font-weight:700;margin-top:15px}#enabledDisplays{margin-bottom:15px}#enabledDisplays label{display:block;max-width:300px}#divTwcBottom img{zoom:150%}#divTwc:fullscreen{display:flex;align-items:center;justify-content:center;align-content:center}#divTwc:fullscreen #display{position:relative}#divTwc:fullscreen #divTwcBottom{display:flex;flex-direction:row;background-color:rgb(0 0 0 / .5);color:#fff;width:100%;position:absolute;bottom:0}@media screen and (orientation:portrait){#divTwc:fullscreen canvas{width:100vw;max-width:100vw;height:auto}#divTwc:fullscreen #container{width:100vw;height:auto;max-height:unset;max-width:unset}}@media screen and (orientation:landscape){#divTwc:fullscreen canvas{height:100vh;max-height:100vh;width:auto}#divTwc:fullscreen #container{height:100vh;width:auto;max-width:100vw;max-height:unset}}.navButton{cursor:pointer}.visible{visibility:visible;opacity:1;transition:opacity 1s linear}.hidden{visibility:hidden;opacity:0;transition:visibility 0s 1s,opacity 1s linear} \ No newline at end of file +@font-face{font-family:"Star4000";src:url("../fonts/Star4000.woff") format("woff")}body{font-family:"Star4000"}input,button{font-family:"Star4000"}#imgGetGps{height:13px;vertical-align:middle}#txtAddress{width:490px;font-size:16pt}#btnGetGps,#btnGetLatLng,#btnClearQuery{font-size:16pt}.autocomplete-suggestions{background-color:#fff;border:1px solid #000}.autocomplete-suggestion{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-size:16pt}.autocomplete-selected{background-color:blue;color:#fff}#divTwc{display:block;background-color:#000;color:#fff;width:100%;max-width:640px}#divTwcLeft{display:none;text-align:right;flex-direction:column;vertical-align:middle}#divTwcLeft>div{flex:1;padding-right:12px;display:flex;flex-direction:column;justify-content:center}#divTwcRight{text-align:left;display:none;flex-direction:column;vertical-align:middle}#divTwcRight>div{flex:1;padding-left:12px;display:flex;flex-direction:column;justify-content:center}#divTwcBottom{display:flex;flex-direction:row;background-color:#000;color:#fff;width:100%}#divTwcBottom>div{padding-left:6px;padding-right:6px}#divTwcBottomLeft{flex:1;text-align:left}#divTwcBottomMiddle{flex:0;text-align:center}#divTwcBottomRight{flex:1;text-align:right}#divTwcNavContainer{display:none}#divTwcNav{width:100%;display:flex;flex-direction:row;background-color:#000;color:#fff;max-width:640px}#divTwcNav>div{padding-left:6px;padding-right:6px}#divTwcNavLeft{flex:1;text-align:left}#divTwcNavMiddle{flex:0;text-align:center}#divTwcNavRight{flex:1;text-align:right}#imgPause1x,#imgPause2x{visibility:hidden;position:absolute}.HideCursor{cursor:none !important}#txtScrollText{width:475px}@font-face{font-family:"Star4000";src:url("../fonts/Star4000.woff") format("woff")}@font-face{font-family:"Star 4 Radar";src:url("../fonts/Star 4 Radar.woff") format("woff")}@font-face{font-family:"Star4000 Extended";src:url("../fonts/Star4000 Extended.woff") format("woff")}@font-face{font-family:"Star4000LCN";src:url("../fonts/Star4000LCN.woff") format("woff")}@font-face{font-family:"Star4000 Large Compressed";src:url("../fonts/Star4000 Large Compressed.woff") format("woff")}@font-face{font-family:"Star4000 Large";src:url("../fonts/Star4000 Large.ttf") format("truetype")}@font-face{font-family:"Star4000 Small";src:url("../fonts/Star4000 Small.woff") format("woff")}#display{font-family:"Star4000";margin:0 0 0 0;width:100%}jsgif{display:none}#Star4000{font-family:"Star4000"}#Star4000Extended{font-family:"Star4000 Extended"}#Star4000LargeCompressed{font-family:"Star4000 Large Compressed"}#Star4000Large{font-family:"Star4000 Large"}#Star4000LargeCompressedNumbers{font-family:"Star4000LCN"}#Star4000Small{font-family:"Star4000 Small"}#Star4Radar{font-family:"Star 4 Radar"}#container{position:relative;width:100%;height:100%;background-image:url(../images/BackGround1_1.png)}#divTwc:-webkit-full-screen #container{background-image:none;width:unset;height:unset}#divTwc:-ms-fullscreen #container{background-image:none;width:unset;height:unset}#divTwc:fullscreen #container{background-image:none;width:unset;height:unset}#loading{width:640px;height:480px;max-width:100%;text-shadow:4px 4px #000;display:flex;align-items:center;text-align:center;justify-content:center}#loading .title{font-family:Star4000 Large;font-size:36px;color:#ff0;margin-bottom:40px}#loading .instructions{font-size:18pt}#container canvas{width:100%}.heading{font-weight:bold;margin-top:15px}#enabledDisplays{margin-bottom:15px}#enabledDisplays label{display:block;max-width:300px}#divTwcBottom img{zoom:150%}#divTwc:-webkit-full-screen{display:flex;align-items:center;justify-content:center;align-content:center}#divTwc:-ms-fullscreen{display:flex;align-items:center;justify-content:center;align-content:center}#divTwc:fullscreen{display:flex;align-items:center;justify-content:center;align-content:center}#divTwc:-webkit-full-screen #display{position:relative}#divTwc:-ms-fullscreen #display{position:relative}#divTwc:fullscreen #display{position:relative}#divTwc:-webkit-full-screen #divTwcBottom{display:flex;flex-direction:row;background-color:rgba(0,0,0,.5);color:#fff;width:100%;position:absolute;bottom:0px}#divTwc:-ms-fullscreen #divTwcBottom{display:flex;flex-direction:row;background-color:rgba(0,0,0,.5);color:#fff;width:100%;position:absolute;bottom:0px}#divTwc:fullscreen #divTwcBottom{display:flex;flex-direction:row;background-color:rgba(0,0,0,.5);color:#fff;width:100%;position:absolute;bottom:0px}.navButton{cursor:pointer}.visible{visibility:visible;opacity:1;transition:opacity 1s linear}.hidden{visibility:hidden;opacity:0;transition:visibility 0s 1s,opacity 1s linear}.weather-display{width:640px;height:480px;overflow:hidden;position:relative;background-image:url(../images/BackGround1_1.png);height:0px}.weather-display.show{height:480px}.weather-display .template{display:none}.weather-display .header{width:640px;height:60px;padding-top:30px}.weather-display .header .title{color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;font-family:"Star4000";font-size:24pt;position:absolute;width:250px}.weather-display .header .title.single{left:170px;top:25px}.weather-display .header .title.dual{left:170px}.weather-display .header .title.dual>div{position:absolute}.weather-display .header .title.dual .top{top:-3px}.weather-display .header .title.dual .bottom{top:26px}.weather-display .header .logo{top:30px;left:50px;position:absolute;z-index:10}.weather-display .header .noaa-logo{position:absolute;top:39px;left:356px}.weather-display .header .title.single{top:40px}.weather-display .header .date-time{white-space:pre;color:#fff;font-family:"Star4000 Small";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;left:415px;width:170px;text-align:right;position:absolute}.weather-display .header .date-time.date{padding-top:22px}.weather-display .main{position:relative}.weather-display .main.has-scroll{width:640px;height:310px;overflow:hidden}.weather-display .main.has-box{margin-left:64px;margin-right:64px;width:calc(100% - 128px)}.weather-display .scroll{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;width:640px;height:70px;overflow:hidden;margin-top:10px}.weather-display .scroll .fixed{font-family:"Star4000";font-size:24pt;margin-left:55px}.weather-display .main.current-weather.main .col{height:50px;width:255px;display:inline-block;margin-top:10px;position:absolute;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.current-weather.main .col.left{font-family:"Star4000 Extended";font-size:24pt}.weather-display .main.current-weather.main .col.right{right:0px;font-family:"Star4000 Large";font-size:16pt;font-weight:bold}.weather-display .main.current-weather.main .col.right .row{margin-bottom:12px}.weather-display .main.current-weather.main .col.right .row .label,.weather-display .main.current-weather.main .col.right .row .value{display:inline-block}.weather-display .main.current-weather.main .col.right .row .label{margin-left:20px}.weather-display .main.current-weather.main .col.right .row .value{float:right;margin-right:10px}.weather-display .main.current-weather.main .center{text-align:center}.weather-display .main.current-weather.main .temp{font-family:"Star4000 Large";font-size:24pt}.weather-display .main.current-weather.main .icon{height:100px}.weather-display .main.current-weather.main .icon img{max-width:126px}.weather-display .main.current-weather.main .wind-container{margin-bottom:10px}.weather-display .main.current-weather.main .wind-container>div{width:45%;display:inline-block;margin:0px}.weather-display .main.current-weather.main .wind-container .wind-label{margin-left:5px}.weather-display .main.current-weather.main .wind-container .wind{text-align:right}.weather-display .main.current-weather.main .wind-gusts{margin-left:5px}.weather-display .main.current-weather.main .location{color:#ff0;margin-bottom:10px}#extended-forecast-html.weather-display{background-image:url("../images/BackGround2_1.png")}.weather-display .main.extended-forecast .day-container{margin-top:16px;margin-left:27px}.weather-display .main.extended-forecast .day{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;padding:5px;height:285px;width:155px;display:inline-block;margin:0px 15px;font-family:"Star4000";font-size:24pt}.weather-display .main.extended-forecast .day .date{text-transform:uppercase;text-align:center;color:#ff0}.weather-display .main.extended-forecast .day .condition{text-align:center;height:74px;margin-top:10px}.weather-display .main.extended-forecast .day .icon{text-align:center;height:75px}.weather-display .main.extended-forecast .day .icon img{max-height:75px}.weather-display .main.extended-forecast .day .temperatures{width:100%;margin-top:5px}.weather-display .main.extended-forecast .day .temperatures .temperature-block{display:inline-block;width:44%;vertical-align:top}.weather-display .main.extended-forecast .day .temperatures .temperature-block>div{text-align:center}.weather-display .main.extended-forecast .day .temperatures .temperature-block .value{font-family:"Star4000 Large";margin-top:4px}.weather-display .main.extended-forecast .day .temperatures .temperature-block.lo .label{color:#8080ff}.weather-display .main.extended-forecast .day .temperatures .temperature-block.hi .label{color:#ff0}.weather-display .main.hourly.main{overflow-y:hidden}.weather-display .main.hourly.main .column-headers{background-color:#200057;height:20px;position:absolute;width:100%}.weather-display .main.hourly.main .column-headers{position:-webkit-sticky;position:sticky;top:0px;z-index:5}.weather-display .main.hourly.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;color:#ff0;position:absolute;top:-14px;z-index:5;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.hourly.main .column-headers .temp{left:355px}.weather-display .main.hourly.main .column-headers .like{left:435px}.weather-display .main.hourly.main .column-headers .wind{left:535px}.weather-display .main.hourly.main .hourly-lines{min-height:338px;padding-top:10px;background:repeating-linear-gradient(0deg, #001040 0px, #102080 136px, #102080 202px, #001040 338px)}.weather-display .main.hourly.main .hourly-lines .hourly-row{font-family:"Star4000 Large";font-size:24pt;height:72px;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative}.weather-display .main.hourly.main .hourly-lines .hourly-row>div{position:absolute;white-space:pre;top:8px}.weather-display .main.hourly.main .hourly-lines .hourly-row .hour{left:25px}.weather-display .main.hourly.main .hourly-lines .hourly-row .icon{left:255px;width:70px;text-align:center;top:unset}.weather-display .main.hourly.main .hourly-lines .hourly-row .temp{left:355px}.weather-display .main.hourly.main .hourly-lines .hourly-row .like{left:425px}.weather-display .main.hourly.main .hourly-lines .hourly-row .wind{left:505px;width:100px;text-align:right}.weather-display .main.travel.main{overflow-y:hidden}.weather-display .main.travel.main .column-headers{background-color:#200057;height:20px;position:absolute;width:100%}.weather-display .main.travel.main .column-headers{position:-webkit-sticky;position:sticky;top:0px;z-index:5}.weather-display .main.travel.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;color:#ff0;position:absolute;top:-14px;z-index:5;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.travel.main .column-headers .temp{width:50px;text-align:center}.weather-display .main.travel.main .column-headers .temp.low{left:455px}.weather-display .main.travel.main .column-headers .temp.high{left:510px;width:60px}.weather-display .main.travel.main .travel-lines{min-height:338px;padding-top:10px;background:repeating-linear-gradient(0deg, #001040 0px, #102080 136px, #102080 202px, #001040 338px)}.weather-display .main.travel.main .travel-lines .travel-row{font-family:"Star4000 Large";font-size:24pt;height:72px;color:#ff0;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative}.weather-display .main.travel.main .travel-lines .travel-row>div{position:absolute;white-space:pre;top:8px}.weather-display .main.travel.main .travel-lines .travel-row .city{left:80px}.weather-display .main.travel.main .travel-lines .travel-row .icon{left:330px;width:70px;text-align:center;top:unset}.weather-display .main.travel.main .travel-lines .travel-row .icon img{max-width:47px}.weather-display .main.travel.main .travel-lines .travel-row .temp{width:50px;text-align:center}.weather-display .main.travel.main .travel-lines .travel-row .temp.low{left:455px}.weather-display .main.travel.main .travel-lines .travel-row .temp.high{left:510px;width:60px}.weather-display .latest-observations.main{overflow-y:hidden}.weather-display .latest-observations.main .column-headers{height:20px;position:absolute;width:100%}.weather-display .latest-observations.main .column-headers{top:0px}.weather-display .latest-observations.main .column-headers div{display:inline-block;font-family:"Star4000 Small";font-size:24pt;position:absolute;top:-14px;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .latest-observations.main .column-headers .temp{display:none}.weather-display .latest-observations.main .column-headers .temp.show{display:inline-block}.weather-display .latest-observations.main .temp{left:230px}.weather-display .latest-observations.main .weather{left:280px}.weather-display .latest-observations.main .wind{left:430px}.weather-display .latest-observations.main .observation-lines{min-height:338px;padding-top:10px}.weather-display .latest-observations.main .observation-lines .observation-row{font-family:"Star4000";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;position:relative;height:40px}.weather-display .latest-observations.main .observation-lines .observation-row>div{position:absolute;top:8px}.weather-display .latest-observations.main .observation-lines .observation-row .wind{white-space:pre;text-align:right}.weather-display .local-forecast .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:280px;overflow:hidden}.weather-display .local-forecast .forecasts{position:relative}.weather-display .local-forecast .forecast{font-family:"Star4000";font-size:24pt;text-transform:uppercase;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;min-height:280px;line-height:40px}.weather-display .progress{text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;font-family:"Star4000 Extended";font-size:19pt}.weather-display .progress .container{position:relative;top:15px;margin:0px 10px;box-sizing:border-box;height:310px;overflow:hidden}.weather-display .progress .container .item{position:relative}.weather-display .progress .container .item .name{white-space:nowrap}.weather-display .progress .container .item .name::after{content:"........................................................................"}.weather-display .progress .container .item .links{position:absolute;text-align:right;right:0px;top:0px}.weather-display .progress .container .item .links>div{background-color:#26235a;display:none;padding-left:4px}.weather-display .progress .container .item .links .loading{color:#ff0}.weather-display .progress .container .item .links .press-here{color:lime;cursor:pointer}.weather-display .progress .container .item .links .failed{color:red}.weather-display .progress .container .item .links .no-data{color:silver}.weather-display .progress .container .item .links .disabled{color:silver}.weather-display .progress .container .item .links.loading .loading{display:block}.weather-display .progress .container .item .links.press-here .press-here{display:block}.weather-display .progress .container .item .links.failed .failed{display:block}.weather-display .progress .container .item .links.no-data .no-data{display:block}.weather-display .progress .container .item .links.disabled .disabled{display:block}@-webkit-keyframes progress-scroll{0%{background-position:-40px 0}100%{background-position:40px 0}}@keyframes progress-scroll{0%{background-position:-40px 0}100%{background-position:40px 0}}#progress-html.weather-display .scroll .progress-bar-container{border:2px solid #000;background-color:#fff;margin:20px auto;width:524px;position:relative;display:none}#progress-html.weather-display .scroll .progress-bar-container.show{display:block}#progress-html.weather-display .scroll .progress-bar-container .progress-bar{height:20px;margin:2px;width:520px;background:repeating-linear-gradient(90deg, #09246f 0px, #09246f 5px, #364ac0 5px, #364ac0 10px, #4f99f9 10px, #4f99f9 15px, #8ffdfa 15px, #8ffdfa 20px, #4f99f9 20px, #4f99f9 25px, #364ac0 25px, #364ac0 30px, #09246f 30px, #09246f 40px);-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-name:progress-scroll;animation-name:progress-scroll;-webkit-animation-timing-function:steps(8, end);animation-timing-function:steps(8, end)}#progress-html.weather-display .scroll .progress-bar-container .cover{position:absolute;top:0px;right:0px;background-color:#fff;width:100%;height:24px;transition:width 1s steps(6)}#radar-html.weather-display{background-image:url("../images/BackGround4_1.png")}#radar-html.weather-display .header{height:83px}#radar-html.weather-display .header .title.dual{color:#fff;font-family:"Arial",sans-serif;font-weight:bold;font-size:28pt;left:155px}#radar-html.weather-display .header .title.dual .top{top:-4px}#radar-html.weather-display .header .title.dual .bottom{top:31px}#radar-html.weather-display .header .right{position:absolute;right:0px;width:360px;margin-top:2px;font-family:"Star4000";font-size:18pt;font-weight:bold;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000;text-align:center}#radar-html.weather-display .header .right .scale>div{display:inline-block}#radar-html.weather-display .header .right .scale-table{display:table-row;border-collapse:collapse}#radar-html.weather-display .header .right .scale-table .box{display:table-cell;border:2px solid #000;width:17px;height:24px;padding:0}#radar-html.weather-display .header .right .scale-table .box-1{background-color:#31d216}#radar-html.weather-display .header .right .scale-table .box-2{background-color:#1c8a12}#radar-html.weather-display .header .right .scale-table .box-3{background-color:#145a0f}#radar-html.weather-display .header .right .scale-table .box-4{background-color:#0a280a}#radar-html.weather-display .header .right .scale-table .box-5{background-color:#c4b346}#radar-html.weather-display .header .right .scale-table .box-6{background-color:#be4813}#radar-html.weather-display .header .right .scale-table .box-7{background-color:#ab0e0e}#radar-html.weather-display .header .right .scale-table .box-8{background-color:#731f04}#radar-html.weather-display .header .right .scale .text{position:relative;top:-5px}#radar-html.weather-display .header .right .time{position:relative;font-weight:normal;top:-14px;font-family:"Star4000 Small";font-size:24pt}.weather-display .main.radar{overflow:hidden;height:367px}.weather-display .main.radar .container .scroll-area{position:relative}#regional-forecast-html.weather-display{background-image:url("../images/BackGround5_1.png")}.weather-display .main.regional-forecast{position:relative}.weather-display .main.regional-forecast .map{position:absolute}.weather-display .main.regional-forecast .location{position:absolute;width:140px;margin-left:-40px;margin-top:-35px}.weather-display .main.regional-forecast .location>div{position:absolute;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.regional-forecast .location .icon{top:26px;left:44px}.weather-display .main.regional-forecast .location .icon img{max-height:32px}.weather-display .main.regional-forecast .location .temp{font-family:"Star4000 Large";font-size:28px;color:#ff0;top:28px;text-align:right;width:40px}.weather-display .main.regional-forecast .location .city{font-family:Star4000;font-size:20px}#almanac-html.weather-display{background-image:url("../images/BackGround3_1.png")}.weather-display .main.almanac{font-family:"Star4000";font-size:24pt;text-shadow:3px 3px 0 #000,-1.5px -1.5px 0 #000,0 -1.5px 0 #000,1.5px -1.5px 0 #000,1.5px 0 0 #000,1.5px 1.5px 0 #000,0 1.5px 0 #000,-1.5px 1.5px 0 #000,-1.5px 0 0 #000}.weather-display .main.almanac .sun{display:table;margin-left:50px;height:100px}.weather-display .main.almanac .sun>div{display:table-row;position:relative}.weather-display .main.almanac .sun>div>div{display:table-cell}.weather-display .main.almanac .sun .days{color:#ff0;text-align:right;top:-5px}.weather-display .main.almanac .sun .days .day{padding-right:10px}.weather-display .main.almanac .sun .times{text-align:right}.weather-display .main.almanac .sun .times .sun-time{width:200px}.weather-display .main.almanac .sun .times.times-1{top:-10px}.weather-display .main.almanac .sun .times.times-2{top:-15px}.weather-display .main.almanac .moon{position:relative;top:-10px;padding:0px 60px}.weather-display .main.almanac .moon .title{color:#ff0}.weather-display .main.almanac .moon .day{display:inline-block;text-align:center;width:130px}.weather-display .main.almanac .moon .day .icon{padding-left:10px}.weather-display .main.almanac .moon .day .date{position:relative;top:-10px}/*# sourceMappingURL=main.css.map */ \ No newline at end of file diff --git a/dist/resources/ws.min.js b/dist/resources/ws.min.js index b12c222..b1d72be 100644 --- a/dist/resources/ws.min.js +++ b/dist/resources/ws.min.js @@ -11,7 +11,7 @@ * * Date: 2021-03-02T17:08Z */ -!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,(function(e,t){"use strict";var n=[],r=Object.getPrototypeOf,i=n.slice,a=n.flat?function(e){return n.flat.call(e)}:function(e){return n.concat.apply([],e)},o=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,A=c.toString,d=A.call(Object),h={},f=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},p=function(e){return null!=e&&e===e.window},g=e.document,m={type:!0,src:!0,nonce:!0,noModule:!0};function v(e,t,n){var r,i,a=(n=n||g).createElement("script");if(a.text=e,t)for(r in m)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&a.setAttribute(r,i);n.head.appendChild(a).parentNode.removeChild(a)}function y(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?u[l.call(e)]||"object":typeof e}var w="3.6.0",x=function(e,t){return new x.fn.init(e,t)};function b(e){var t=!!e&&"length"in e&&e.length,n=y(e);return!f(e)&&!p(e)&&("array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e)}x.fn=x.prototype={jquery:w,constructor:x,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=x.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return x.each(this,e)},map:function(e){return this.pushStack(x.map(this,(function(t,n){return e.call(t,n,t)})))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(x.grep(this,(function(e,t){return(t+1)%2})))},odd:function(){return this.pushStack(x.grep(this,(function(e,t){return t%2})))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n0&&t-1 in e)}S.fn=S.prototype={jquery:w,constructor:S,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=S.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return S.each(this,e)},map:function(e){return this.pushStack(S.map(this,(function(t,n){return e.call(t,n,t)})))},slice:function(){return this.pushStack(i.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(S.grep(this,(function(e,t){return(t+1)%2})))},odd:function(){return this.pushStack(S.grep(this,(function(e,t){return t%2})))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|[\\x20\\t\\r\\n\\f])[\\x20\\t\\r\\n\\f]*"),U=new RegExp(R+"|>"),Z=new RegExp(P),G=new RegExp("^"+V+"$"),z={ID:new RegExp("^#("+V+")"),CLASS:new RegExp("^\\.("+V+")"),TAG:new RegExp("^("+V+"|[*])"),ATTR:new RegExp("^"+H),PSEUDO:new RegExp("^"+P),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\([\\x20\\t\\r\\n\\f]*(even|odd|(([+-]|)(\\d*)n|)[\\x20\\t\\r\\n\\f]*(?:([+-]|)[\\x20\\t\\r\\n\\f]*(\\d+)|))[\\x20\\t\\r\\n\\f]*\\)|)","i"),bool:new RegExp("^(?:"+j+")$","i"),needsContext:new RegExp("^[\\x20\\t\\r\\n\\f]*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\([\\x20\\t\\r\\n\\f]*((?:-\\d)?\\d*)[\\x20\\t\\r\\n\\f]*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,J=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,$=/^[^{]+\{\s*\[native \w/,K=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}[\\x20\\t\\r\\n\\f]?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"�":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},ae=function(){d()},oe=we((function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()}),{dir:"parentNode",next:"legend"});try{L.apply(D=N.call(x.childNodes),x.childNodes),D[x.childNodes.length].nodeType}catch(e){L={apply:D.length?function(e,t){B.apply(e,N.call(t))}:function(e,t){for(var n=e.length,r=0;e[n++]=t[r++];);e.length=n-1}}}function se(e,t,r,i){var a,s,l,c,A,f,m,v=t&&t.ownerDocument,x=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==x&&9!==x&&11!==x)return r;if(!i&&(d(t),t=t||h,p)){if(11!==x&&(A=K.exec(e)))if(a=A[1]){if(9===x){if(!(l=t.getElementById(a)))return r;if(l.id===a)return r.push(l),r}else if(v&&(l=v.getElementById(a))&&y(t,l)&&l.id===a)return r.push(l),r}else{if(A[2])return L.apply(r,t.getElementsByTagName(e)),r;if((a=A[3])&&n.getElementsByClassName&&t.getElementsByClassName)return L.apply(r,t.getElementsByClassName(a)),r}if(n.qsa&&!I[e+" "]&&(!g||!g.test(e))&&(1!==x||"object"!==t.nodeName.toLowerCase())){if(m=e,v=t,1===x&&(U.test(e)||_.test(e))){for((v=ee.test(e)&&me(t.parentNode)||t)===t&&n.scope||((c=t.getAttribute("id"))?c=c.replace(re,ie):t.setAttribute("id",c=w)),s=(f=o(e)).length;s--;)f[s]=(c?"#"+c:":scope")+" "+ye(f[s]);m=f.join(",")}try{return L.apply(r,v.querySelectorAll(m)),r}catch(t){I(e,!0)}finally{c===w&&t.removeAttribute("id")}}}return u(e.replace(q,"$1"),t,r,i)}function ue(){var e=[];return function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}}function le(e){return e[w]=!0,e}function ce(e){var t=h.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function Ae(e,t){for(var n=e.split("|"),i=n.length;i--;)r.attrHandle[n[i]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)for(;n=n.nextSibling;)if(n===t)return-1;return e?1:-1}function he(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}function fe(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}function pe(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&oe(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function ge(e){return le((function(t){return t=+t,le((function(n,r){for(var i,a=e([],n.length,t),o=a.length;o--;)n[i=a[o]]&&(n[i]=!(r[i]=n[i]))}))}))}function me(e){return e&&void 0!==e.getElementsByTagName&&e}for(t in n=se.support={},a=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},d=se.setDocument=function(e){var t,i,o=e?e.ownerDocument||e:x;return o!=h&&9===o.nodeType&&o.documentElement?(f=(h=o).documentElement,p=!a(h),x!=h&&(i=h.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",ae,!1):i.attachEvent&&i.attachEvent("onunload",ae)),n.scope=ce((function(e){return f.appendChild(e).appendChild(h.createElement("div")),void 0!==e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length})),n.attributes=ce((function(e){return e.className="i",!e.getAttribute("className")})),n.getElementsByTagName=ce((function(e){return e.appendChild(h.createComment("")),!e.getElementsByTagName("*").length})),n.getElementsByClassName=$.test(h.getElementsByClassName),n.getById=ce((function(e){return f.appendChild(e).id=w,!h.getElementsByName||!h.getElementsByName(w).length})),n.getById?(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&p){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&p){var n,r,i,a=t.getElementById(e);if(a){if((n=a.getAttributeNode("id"))&&n.value===e)return[a];for(i=t.getElementsByName(e),r=0;a=i[r++];)if((n=a.getAttributeNode("id"))&&n.value===e)return[a]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,a=t.getElementsByTagName(e);if("*"===e){for(;n=a[i++];)1===n.nodeType&&r.push(n);return r}return a},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&p)return t.getElementsByClassName(e)},m=[],g=[],(n.qsa=$.test(h.querySelectorAll))&&(ce((function(e){var t;f.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&g.push("[*^$]=[\\x20\\t\\r\\n\\f]*(?:''|\"\")"),e.querySelectorAll("[selected]").length||g.push("\\[[\\x20\\t\\r\\n\\f]*(?:value|"+j+")"),e.querySelectorAll("[id~="+w+"-]").length||g.push("~="),(t=h.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||g.push("\\[[\\x20\\t\\r\\n\\f]*name[\\x20\\t\\r\\n\\f]*=[\\x20\\t\\r\\n\\f]*(?:''|\"\")"),e.querySelectorAll(":checked").length||g.push(":checked"),e.querySelectorAll("a#"+w+"+*").length||g.push(".#.+[+~]"),e.querySelectorAll("\\\f"),g.push("[\\r\\n\\f]")})),ce((function(e){e.innerHTML="";var t=h.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&g.push("name[\\x20\\t\\r\\n\\f]*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&g.push(":enabled",":disabled"),f.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&g.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),g.push(",.*:")}))),(n.matchesSelector=$.test(v=f.matches||f.webkitMatchesSelector||f.mozMatchesSelector||f.oMatchesSelector||f.msMatchesSelector))&&ce((function(e){n.disconnectedMatch=v.call(e,"*"),v.call(e,"[s!='']:x"),m.push("!=",P)})),g=g.length&&new RegExp(g.join("|")),m=m.length&&new RegExp(m.join("|")),t=$.test(f.compareDocumentPosition),y=t||$.test(f.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)for(;t=t.parentNode;)if(t===e)return!0;return!1},F=t?function(e,t){if(e===t)return A=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e==h||e.ownerDocument==x&&y(x,e)?-1:t==h||t.ownerDocument==x&&y(x,t)?1:c?O(c,e)-O(c,t):0:4&r?-1:1)}:function(e,t){if(e===t)return A=!0,0;var n,r=0,i=e.parentNode,a=t.parentNode,o=[e],s=[t];if(!i||!a)return e==h?-1:t==h?1:i?-1:a?1:c?O(c,e)-O(c,t):0;if(i===a)return de(e,t);for(n=e;n=n.parentNode;)o.unshift(n);for(n=t;n=n.parentNode;)s.unshift(n);for(;o[r]===s[r];)r++;return r?de(o[r],s[r]):o[r]==x?-1:s[r]==x?1:0},h):h},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(d(e),n.matchesSelector&&p&&!I[t+" "]&&(!m||!m.test(t))&&(!g||!g.test(t)))try{var r=v.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){I(t,!0)}return se(t,h,null,[e]).length>0},se.contains=function(e,t){return(e.ownerDocument||e)!=h&&d(e),y(e,t)},se.attr=function(e,t){(e.ownerDocument||e)!=h&&d(e);var i=r.attrHandle[t.toLowerCase()],a=i&&E.call(r.attrHandle,t.toLowerCase())?i(e,t,!p):void 0;return void 0!==a?a:n.attributes||!p?e.getAttribute(t):(a=e.getAttributeNode(t))&&a.specified?a.value:null},se.escape=function(e){return(e+"").replace(re,ie)},se.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},se.uniqueSort=function(e){var t,r=[],i=0,a=0;if(A=!n.detectDuplicates,c=!n.sortStable&&e.slice(0),e.sort(F),A){for(;t=e[a++];)t===e[a]&&(i=r.push(a));for(;i--;)e.splice(r[i],1)}return c=null,e},i=se.getText=function(e){var t,n="",r=0,a=e.nodeType;if(a){if(1===a||9===a||11===a){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===a||4===a)return e.nodeValue}else for(;t=e[r++];)n+=i(t);return n},r=se.selectors={cacheLength:50,createPseudo:le,match:z,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return z.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&Z.test(n)&&(t=o(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=S[e+" "];return t||(t=new RegExp("(^|[\\x20\\t\\r\\n\\f])"+e+"("+R+"|$)"))&&S(e,(function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")}))},ATTR:function(e,t,n){return function(r){var i=se.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(W," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var a="nth"!==e.slice(0,3),o="last"!==e.slice(-4),s="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,A,d,h,f,p=a!==o?"nextSibling":"previousSibling",g=t.parentNode,m=s&&t.nodeName.toLowerCase(),v=!u&&!s,y=!1;if(g){if(a){for(;p;){for(d=t;d=d[p];)if(s?d.nodeName.toLowerCase()===m:1===d.nodeType)return!1;f=p="only"===e&&!f&&"nextSibling"}return!0}if(f=[o?g.firstChild:g.lastChild],o&&v){for(y=(h=(l=(c=(A=(d=g)[w]||(d[w]={}))[d.uniqueID]||(A[d.uniqueID]={}))[e]||[])[0]===b&&l[1])&&l[2],d=h&&g.childNodes[h];d=++h&&d&&d[p]||(y=h=0)||f.pop();)if(1===d.nodeType&&++y&&d===t){c[e]=[b,h,y];break}}else if(v&&(y=h=(l=(c=(A=(d=t)[w]||(d[w]={}))[d.uniqueID]||(A[d.uniqueID]={}))[e]||[])[0]===b&&l[1]),!1===y)for(;(d=++h&&d&&d[p]||(y=h=0)||f.pop())&&((s?d.nodeName.toLowerCase()!==m:1!==d.nodeType)||!++y||(v&&((c=(A=d[w]||(d[w]={}))[d.uniqueID]||(A[d.uniqueID]={}))[e]=[b,y]),d!==t)););return(y-=i)===r||y%r==0&&y/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||se.error("unsupported pseudo: "+e);return i[w]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?le((function(e,n){for(var r,a=i(e,t),o=a.length;o--;)e[r=O(e,a[o])]=!(n[r]=a[o])})):function(e){return i(e,0,n)}):i}},pseudos:{not:le((function(e){var t=[],n=[],r=s(e.replace(q,"$1"));return r[w]?le((function(e,t,n,i){for(var a,o=r(e,null,i,[]),s=e.length;s--;)(a=o[s])&&(e[s]=!(t[s]=a))})):function(e,i,a){return t[0]=e,r(t,null,a,n),t[0]=null,!n.pop()}})),has:le((function(e){return function(t){return se(e,t).length>0}})),contains:le((function(e){return e=e.replace(te,ne),function(t){return(t.textContent||i(t)).indexOf(e)>-1}})),lang:le((function(e){return G.test(e||"")||se.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=p?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}})),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===f},focus:function(e){return e===h.activeElement&&(!h.hasFocus||h.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:pe(!1),disabled:pe(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return X.test(e.nodeName)},input:function(e){return J.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:ge((function(){return[0]})),last:ge((function(e,t){return[t-1]})),eq:ge((function(e,t,n){return[n<0?n+t:n]})),even:ge((function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e})),gt:ge((function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){for(var i=e.length;i--;)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n,r,i){for(var a,o=[],s=0,u=e.length,l=null!=t;s-1&&(a[l]=!(o[l]=A))}}else m=be(m===o?m.splice(f,m.length):m),i?i(null,o,m,u):L.apply(o,m)}))}function Se(e){for(var t,n,i,a=e.length,o=r.relative[e[0].type],s=o||r.relative[" "],u=o?1:0,c=we((function(e){return e===t}),s,!0),A=we((function(e){return O(t,e)>-1}),s,!0),d=[function(e,n,r){var i=!o&&(r||n!==l)||((t=n).nodeType?c(e,n,r):A(e,n,r));return t=null,i}];u1&&xe(d),u>1&&ye(e.slice(0,u-1).concat({value:" "===e[u-2].type?"*":""})).replace(q,"$1"),n,u0,i=e.length>0,a=function(a,o,s,u,c){var A,f,g,m=0,v="0",y=a&&[],w=[],x=l,C=a||i&&r.find.TAG("*",c),S=b+=null==x?1:Math.random()||.1,T=C.length;for(c&&(l=o==h||o||c);v!==T&&null!=(A=C[v]);v++){if(i&&A){for(f=0,o||A.ownerDocument==h||(d(A),s=!p);g=e[f++];)if(g(A,o||h,s)){u.push(A);break}c&&(b=S)}n&&((A=!g&&A)&&m--,a&&y.push(A))}if(m+=v,n&&v!==m){for(f=0;g=t[f++];)g(y,w,o,s);if(a){if(m>0)for(;v--;)y[v]||w[v]||(w[v]=M.call(u));w=be(w)}L.apply(u,w),c&&!a&&w.length>0&&m+t.length>1&&se.uniqueSort(u)}return c&&(b=S,l=x),y};return n?le(a):a}(a,i)),s.selector=e}return s},u=se.select=function(e,t,n,i){var a,u,l,c,A,d="function"==typeof e&&e,h=!i&&o(e=d.selector||e);if(n=n||[],1===h.length){if((u=h[0]=h[0].slice(0)).length>2&&"ID"===(l=u[0]).type&&9===t.nodeType&&p&&r.relative[u[1].type]){if(!(t=(r.find.ID(l.matches[0].replace(te,ne),t)||[])[0]))return n;d&&(t=t.parentNode),e=e.slice(u.shift().value.length)}for(a=z.needsContext.test(e)?0:u.length;a--&&(l=u[a],!r.relative[c=l.type]);)if((A=r.find[c])&&(i=A(l.matches[0].replace(te,ne),ee.test(u[0].type)&&me(t.parentNode)||t))){if(u.splice(a,1),!(e=i.length&&ye(u)))return L.apply(n,i),n;break}}return(d||s(e,h))(i,t,!p,n,!t||ee.test(e)&&me(t.parentNode)||t),n},n.sortStable=w.split("").sort(F).join("")===w,n.detectDuplicates=!!A,d(),n.sortDetached=ce((function(e){return 1&e.compareDocumentPosition(h.createElement("fieldset"))})),ce((function(e){return e.innerHTML="","#"===e.firstChild.getAttribute("href")}))||Ae("type|href|height|width",(function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)})),n.attributes&&ce((function(e){return e.innerHTML="",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")}))||Ae("value",(function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue})),ce((function(e){return null==e.getAttribute("disabled")}))||Ae(j,(function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null})),se}(e);x.find=C,x.expr=C.selectors,x.expr[":"]=x.expr.pseudos,x.uniqueSort=x.unique=C.uniqueSort,x.text=C.getText,x.isXMLDoc=C.isXML,x.contains=C.contains,x.escapeSelector=C.escape;var S=function(e,t,n){for(var r=[],i=void 0!==n;(e=e[t])&&9!==e.nodeType;)if(1===e.nodeType){if(i&&x(e).is(n))break;r.push(e)}return r},T=function(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n},k=x.expr.match.needsContext;function I(e,t){return e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}var F=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function E(e,t,n){return f(t)?x.grep(e,(function(e,r){return!!t.call(e,r,e)!==n})):t.nodeType?x.grep(e,(function(e){return e===t!==n})):"string"!=typeof t?x.grep(e,(function(e){return s.call(t,e)>-1!==n})):x.filter(t,e,n)}x.filter=function(e,t,n){var r=t[0];return n&&(e=":not("+e+")"),1===t.length&&1===r.nodeType?x.find.matchesSelector(r,e)?[r]:[]:x.find.matches(e,x.grep(t,(function(e){return 1===e.nodeType})))},x.fn.extend({find:function(e){var t,n,r=this.length,i=this;if("string"!=typeof e)return this.pushStack(x(e).filter((function(){for(t=0;t1?x.uniqueSort(n):n},filter:function(e){return this.pushStack(E(this,e||[],!1))},not:function(e){return this.pushStack(E(this,e||[],!0))},is:function(e){return!!E(this,"string"==typeof e&&k.test(e)?x(e):e||[],!1).length}});var D,M=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(x.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&e.length>=3?[null,e,null]:M.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof x?t[0]:t,x.merge(this,x.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:g,!0)),F.test(r[1])&&x.isPlainObject(t))for(r in t)f(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=g.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):f(e)?void 0!==n.ready?n.ready(e):e(x):x.makeArray(e,this)}).prototype=x.fn,D=x(g);var B=/^(?:parents|prev(?:Until|All))/,L={children:!0,contents:!0,next:!0,prev:!0};function N(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}x.fn.extend({has:function(e){var t=x(e,this),n=t.length;return this.filter((function(){for(var e=0;e-1:1===n.nodeType&&x.find.matchesSelector(n,e))){a.push(n);break}return this.pushStack(a.length>1?x.uniqueSort(a):a)},index:function(e){return e?"string"==typeof e?s.call(x(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(x.uniqueSort(x.merge(this.get(),x(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),x.each({parent:function(e){var t=e.parentNode;return t&&11!==t.nodeType?t:null},parents:function(e){return S(e,"parentNode")},parentsUntil:function(e,t,n){return S(e,"parentNode",n)},next:function(e){return N(e,"nextSibling")},prev:function(e){return N(e,"previousSibling")},nextAll:function(e){return S(e,"nextSibling")},prevAll:function(e){return S(e,"previousSibling")},nextUntil:function(e,t,n){return S(e,"nextSibling",n)},prevUntil:function(e,t,n){return S(e,"previousSibling",n)},siblings:function(e){return T((e.parentNode||{}).firstChild,e)},children:function(e){return T(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(I(e,"template")&&(e=e.content||e),x.merge([],e.childNodes))}},(function(e,t){x.fn[e]=function(n,r){var i=x.map(this,t,n);return"Until"!==e.slice(-5)&&(r=n),r&&"string"==typeof r&&(i=x.filter(r,i)),this.length>1&&(L[e]||x.uniqueSort(i),B.test(e)&&i.reverse()),this.pushStack(i)}}));var O=/[^\x20\t\r\n\f]+/g;function j(e){return e}function R(e){throw e}function V(e,t,n,r){var i;try{e&&f(i=e.promise)?i.call(e).done(t).fail(n):e&&f(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n.apply(void 0,[e])}}x.Callbacks=function(e){e="string"==typeof e?function(e){var t={};return x.each(e.match(O)||[],(function(e,n){t[n]=!0})),t}(e):x.extend({},e);var t,n,r,i,a=[],o=[],s=-1,u=function(){for(i=i||e.once,r=t=!0;o.length;s=-1)for(n=o.shift();++s-1;)a.splice(n,1),n<=s&&s--})),this},has:function(e){return e?x.inArray(e,a)>-1:a.length>0},empty:function(){return a&&(a=[]),this},disable:function(){return i=o=[],a=n="",this},disabled:function(){return!a},lock:function(){return i=o=[],n||t||(a=n=""),this},locked:function(){return!!i},fireWith:function(e,n){return i||(n=[e,(n=n||[]).slice?n.slice():n],o.push(n),t||u()),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!r}};return l},x.extend({Deferred:function(t){var n=[["notify","progress",x.Callbacks("memory"),x.Callbacks("memory"),2],["resolve","done",x.Callbacks("once memory"),x.Callbacks("once memory"),0,"resolved"],["reject","fail",x.Callbacks("once memory"),x.Callbacks("once memory"),1,"rejected"]],r="pending",i={state:function(){return r},always:function(){return a.done(arguments).fail(arguments),this},catch:function(e){return i.then(null,e)},pipe:function(){var e=arguments;return x.Deferred((function(t){x.each(n,(function(n,r){var i=f(e[r[4]])&&e[r[4]];a[r[1]]((function(){var e=i&&i.apply(this,arguments);e&&f(e.promise)?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+"With"](this,i?[e]:arguments)}))})),e=null})).promise()},then:function(t,r,i){var a=0;function o(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=a&&(r!==R&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(x.Deferred.getStackHook&&(c.stackTrace=x.Deferred.getStackHook()),e.setTimeout(c))}}return x.Deferred((function(e){n[0][3].add(o(0,e,f(i)?i:j,e.notifyWith)),n[1][3].add(o(0,e,f(t)?t:j)),n[2][3].add(o(0,e,f(r)?r:R))})).promise()},promise:function(e){return null!=e?x.extend(e,i):i}},a={};return x.each(n,(function(e,t){var o=t[2],s=t[5];i[t[1]]=o.add,s&&o.add((function(){r=s}),n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),o.add(t[3].fire),a[t[0]]=function(){return a[t[0]+"With"](this===a?void 0:this,arguments),this},a[t[0]+"With"]=o.fireWith})),i.promise(a),t&&t.call(a,a),a},when:function(e){var t=arguments.length,n=t,r=Array(n),a=i.call(arguments),o=x.Deferred(),s=function(e){return function(n){r[e]=this,a[e]=arguments.length>1?i.call(arguments):n,--t||o.resolveWith(r,a)}};if(t<=1&&(V(e,o.done(s(n)).resolve,o.reject,!t),"pending"===o.state()||f(a[n]&&a[n].then)))return o.then();for(;n--;)V(a[n],s(n),o.reject);return o.promise()}});var H=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;x.Deferred.exceptionHook=function(t,n){e.console&&e.console.warn&&t&&H.test(t.name)&&e.console.warn("jQuery.Deferred exception: "+t.message,t.stack,n)},x.readyException=function(t){e.setTimeout((function(){throw t}))};var P=x.Deferred();function W(){g.removeEventListener("DOMContentLoaded",W),e.removeEventListener("load",W),x.ready()}x.fn.ready=function(e){return P.then(e).catch((function(e){x.readyException(e)})),this},x.extend({isReady:!1,readyWait:1,ready:function(e){(!0===e?--x.readyWait:x.isReady)||(x.isReady=!0,!0!==e&&--x.readyWait>0||P.resolveWith(g,[x]))}}),x.ready.then=P.then,"complete"===g.readyState||"loading"!==g.readyState&&!g.documentElement.doScroll?e.setTimeout(x.ready):(g.addEventListener("DOMContentLoaded",W),e.addEventListener("load",W));var q=function(e,t,n,r,i,a,o){var s=0,u=e.length,l=null==n;if("object"===y(n))for(s in i=!0,n)q(e,t,s,n[s],!0,a,o);else if(void 0!==r&&(i=!0,f(r)||(o=!0),l&&(o?(t.call(e,r),t=null):(l=t,t=function(e,t,n){return l.call(x(e),n)})),t))for(;s1,null,!0)},removeData:function(e){return this.each((function(){J.remove(this,e)}))}}),x.extend({queue:function(e,t,n){var r;if(e)return t=(t||"fx")+"queue",r=Y.get(e,t),n&&(!r||Array.isArray(n)?r=Y.access(e,t,x.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||"fx";var n=x.queue(e,t),r=n.length,i=n.shift(),a=x._queueHooks(e,t);"inprogress"===i&&(i=n.shift(),r--),i&&("fx"===t&&n.unshift("inprogress"),delete a.stop,i.call(e,(function(){x.dequeue(e,t)}),a)),!r&&a&&a.empty.fire()},_queueHooks:function(e,t){var n=t+"queueHooks";return Y.get(e,n)||Y.access(e,n,{empty:x.Callbacks("once memory").add((function(){Y.remove(e,[t+"queue",n])}))})}}),x.fn.extend({queue:function(e,t){var n=2;return"string"!=typeof e&&(t=e,e="fx",n--),arguments.length\x20\t\r\n\f]*)/i,pe=/^$|^module$|\/(?:java|ecma)script/i;Ae=g.createDocumentFragment().appendChild(g.createElement("div")),(de=g.createElement("input")).setAttribute("type","radio"),de.setAttribute("checked","checked"),de.setAttribute("name","t"),Ae.appendChild(de),h.checkClone=Ae.cloneNode(!0).cloneNode(!0).lastChild.checked,Ae.innerHTML="",h.noCloneChecked=!!Ae.cloneNode(!0).lastChild.defaultValue,Ae.innerHTML="",h.option=!!Ae.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function me(e,t){var n;return n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||"*"):void 0!==e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&I(e,t)?x.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var ye=/<|&#?\w+;/;function we(e,t,n,r,i){for(var a,o,s,u,l,c,A=t.createDocumentFragment(),d=[],h=0,f=e.length;h-1)i&&i.push(a);else if(l=ie(a),o=me(A.appendChild(a),"script"),l&&ve(o),n)for(c=0;a=o[c++];)pe.test(a.type||"")&&n.push(a);return A}var xe=/^([^.]*)(?:\.(.+)|)/;function be(){return!0}function Ce(){return!1}function Se(e,t){return e===function(){try{return g.activeElement}catch(e){}}()==("focus"===t)}function Te(e,t,n,r,i,a){var o,s;if("object"==typeof t){for(s in"string"!=typeof n&&(r=r||n,n=void 0),t)Te(e,s,n,r,t[s],a);return e}if(null==r&&null==i?(i=n,r=n=void 0):null==i&&("string"==typeof n?(i=r,r=void 0):(i=r,r=n,n=void 0)),!1===i)i=Ce;else if(!i)return e;return 1===a&&(o=i,i=function(e){return x().off(e),o.apply(this,arguments)},i.guid=o.guid||(o.guid=x.guid++)),e.each((function(){x.event.add(this,t,i,r,n)}))}function ke(e,t,n){n?(Y.set(e,t,!1),x.event.add(e,t,{namespace:!1,handler:function(e){var r,a,o=Y.get(this,t);if(1&e.isTrigger&&this[t]){if(o.length)(x.event.special[t]||{}).delegateType&&e.stopPropagation();else if(o=i.call(arguments),Y.set(this,t,o),r=n(this,t),this[t](),o!==(a=Y.get(this,t))||r?Y.set(this,t,!1):a={},o!==a)return e.stopImmediatePropagation(),e.preventDefault(),a&&a.value}else o.length&&(Y.set(this,t,{value:x.event.trigger(x.extend(o[0],x.Event.prototype),o.slice(1),this)}),e.stopImmediatePropagation())}})):void 0===Y.get(e,t)&&x.event.add(e,t,be)}x.event={global:{},add:function(e,t,n,r,i){var a,o,s,u,l,c,A,d,h,f,p,g=Y.get(e);if(G(e))for(n.handler&&(n=(a=n).handler,i=a.selector),i&&x.find.matchesSelector(re,i),n.guid||(n.guid=x.guid++),(u=g.events)||(u=g.events=Object.create(null)),(o=g.handle)||(o=g.handle=function(t){return void 0!==x&&x.event.triggered!==t.type?x.event.dispatch.apply(e,arguments):void 0}),l=(t=(t||"").match(O)||[""]).length;l--;)h=p=(s=xe.exec(t[l])||[])[1],f=(s[2]||"").split(".").sort(),h&&(A=x.event.special[h]||{},h=(i?A.delegateType:A.bindType)||h,A=x.event.special[h]||{},c=x.extend({type:h,origType:p,data:r,handler:n,guid:n.guid,selector:i,needsContext:i&&x.expr.match.needsContext.test(i),namespace:f.join(".")},a),(d=u[h])||((d=u[h]=[]).delegateCount=0,A.setup&&!1!==A.setup.call(e,r,f,o)||e.addEventListener&&e.addEventListener(h,o)),A.add&&(A.add.call(e,c),c.handler.guid||(c.handler.guid=n.guid)),i?d.splice(d.delegateCount++,0,c):d.push(c),x.event.global[h]=!0)},remove:function(e,t,n,r,i){var a,o,s,u,l,c,A,d,h,f,p,g=Y.hasData(e)&&Y.get(e);if(g&&(u=g.events)){for(l=(t=(t||"").match(O)||[""]).length;l--;)if(h=p=(s=xe.exec(t[l])||[])[1],f=(s[2]||"").split(".").sort(),h){for(A=x.event.special[h]||{},d=u[h=(r?A.delegateType:A.bindType)||h]||[],s=s[2]&&new RegExp("(^|\\.)"+f.join("\\.(?:.*\\.|)")+"(\\.|$)"),o=a=d.length;a--;)c=d[a],!i&&p!==c.origType||n&&n.guid!==c.guid||s&&!s.test(c.namespace)||r&&r!==c.selector&&("**"!==r||!c.selector)||(d.splice(a,1),c.selector&&d.delegateCount--,A.remove&&A.remove.call(e,c));o&&!d.length&&(A.teardown&&!1!==A.teardown.call(e,f,g.handle)||x.removeEvent(e,h,g.handle),delete u[h])}else for(h in u)x.event.remove(e,h+t[l],n,r,!0);x.isEmptyObject(u)&&Y.remove(e,"handle events")}},dispatch:function(e){var t,n,r,i,a,o,s=new Array(arguments.length),u=x.event.fix(e),l=(Y.get(this,"events")||Object.create(null))[u.type]||[],c=x.event.special[u.type]||{};for(s[0]=u,t=1;t=1))for(;l!==this;l=l.parentNode||this)if(1===l.nodeType&&("click"!==e.type||!0!==l.disabled)){for(a=[],o={},n=0;n-1:x.find(i,this,null,[l]).length),o[i]&&a.push(r);a.length&&s.push({elem:l,handlers:a})}return l=this,u\s*$/g;function De(e,t){return I(e,"table")&&I(11!==t.nodeType?t:t.firstChild,"tr")&&x(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Be(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,a,o,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n1&&"string"==typeof g&&!h.checkClone&&Fe.test(g))return e.each((function(i){var a=e.eq(i);m&&(t[0]=g.call(this,i,a.html())),Oe(a,t,n,r)}));if(d&&(o=(i=we(t,e[0].ownerDocument,!1,e,r)).firstChild,1===i.childNodes.length&&(i=o),o||r)){for(u=(s=x.map(me(i,"script"),Me)).length;A0&&ve(o,!u&&me(e,"script")),s},cleanData:function(e){for(var t,n,r,i=x.event.special,a=0;void 0!==(n=e[a]);a++)if(G(n)){if(t=n[Y.expando]){if(t.events)for(r in t.events)i[r]?x.event.remove(n,r):x.removeEvent(n,r,t.handle);n[Y.expando]=void 0}n[J.expando]&&(n[J.expando]=void 0)}}}),x.fn.extend({detach:function(e){return je(this,e,!0)},remove:function(e){return je(this,e)},text:function(e){return q(this,(function(e){return void 0===e?x.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=e)}))}),null,e,arguments.length)},append:function(){return Oe(this,arguments,(function(e){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||De(this,e).appendChild(e)}))},prepend:function(){return Oe(this,arguments,(function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=De(this,e);t.insertBefore(e,t.firstChild)}}))},before:function(){return Oe(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this)}))},after:function(){return Oe(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this.nextSibling)}))},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(x.cleanData(me(e,!1)),e.textContent="");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map((function(){return x.clone(this,e,t)}))},html:function(e){return q(this,(function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if("string"==typeof e&&!Ie.test(e)&&!ge[(fe.exec(e)||["",""])[1].toLowerCase()]){e=x.htmlPrefilter(e);try{for(;n=0&&(u+=Math.max(0,Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-a-u-s-.5))||0),u}function Ke(e,t,n){var r=Ve(e),i=(!h.boxSizingReliable()||n)&&"border-box"===x.css(e,"boxSizing",!1,r),a=i,o=We(e,t,r),s="offset"+t[0].toUpperCase()+t.slice(1);if(Re.test(o)){if(!n)return o;o="auto"}return(!h.boxSizingReliable()&&i||!h.reliableTrDimensions()&&I(e,"tr")||"auto"===o||!parseFloat(o)&&"inline"===x.css(e,"display",!1,r))&&e.getClientRects().length&&(i="border-box"===x.css(e,"boxSizing",!1,r),(a=s in e)&&(o=e[s])),(o=parseFloat(o)||0)+$e(e,t,n||(i?"border":"content"),a,r,o)+"px"}function et(e,t,n,r,i){return new et.prototype.init(e,t,n,r,i)}x.extend({cssHooks:{opacity:{get:function(e,t){if(t){var n=We(e,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,columnCount:!0,fillOpacity:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{},style:function(e,t,n,r){if(e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,a,o,s=Z(t),u=ze.test(t),l=e.style;if(u||(t=Ze(s)),o=x.cssHooks[t]||x.cssHooks[s],void 0===n)return o&&"get"in o&&void 0!==(i=o.get(e,!1,r))?i:l[t];"string"===(a=typeof n)&&(i=te.exec(n))&&i[1]&&(n=se(e,t,i),a="number"),null!=n&&n==n&&("number"!==a||u||(n+=i&&i[3]||(x.cssNumber[s]?"":"px")),h.clearCloneStyle||""!==n||0!==t.indexOf("background")||(l[t]="inherit"),o&&"set"in o&&void 0===(n=o.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,a,o,s=Z(t);return ze.test(t)||(t=Ze(s)),(o=x.cssHooks[t]||x.cssHooks[s])&&"get"in o&&(i=o.get(e,!0,n)),void 0===i&&(i=We(e,t,r)),"normal"===i&&t in Je&&(i=Je[t]),""===n||n?(a=parseFloat(i),!0===n||isFinite(a)?a||0:i):i}}),x.each(["height","width"],(function(e,t){x.cssHooks[t]={get:function(e,n,r){if(n)return!Ge.test(x.css(e,"display"))||e.getClientRects().length&&e.getBoundingClientRect().width?Ke(e,t,r):He(e,Ye,(function(){return Ke(e,t,r)}))},set:function(e,n,r){var i,a=Ve(e),o=!h.scrollboxSize()&&"absolute"===a.position,s=(o||r)&&"border-box"===x.css(e,"boxSizing",!1,a),u=r?$e(e,t,r,s,a):0;return s&&o&&(u-=Math.ceil(e["offset"+t[0].toUpperCase()+t.slice(1)]-parseFloat(a[t])-$e(e,t,"border",!1,a)-.5)),u&&(i=te.exec(n))&&"px"!==(i[3]||"px")&&(e.style[t]=n,n=x.css(e,t)),Xe(0,n,u)}}})),x.cssHooks.marginLeft=qe(h.reliableMarginLeft,(function(e,t){if(t)return(parseFloat(We(e,"marginLeft"))||e.getBoundingClientRect().left-He(e,{marginLeft:0},(function(){return e.getBoundingClientRect().left})))+"px"})),x.each({margin:"",padding:"",border:"Width"},(function(e,t){x.cssHooks[e+t]={expand:function(n){for(var r=0,i={},a="string"==typeof n?n.split(" "):[n];r<4;r++)i[e+ne[r]+t]=a[r]||a[r-2]||a[0];return i}},"margin"!==e&&(x.cssHooks[e+t].set=Xe)})),x.fn.extend({css:function(e,t){return q(this,(function(e,t,n){var r,i,a={},o=0;if(Array.isArray(t)){for(r=Ve(e),i=t.length;o1)}}),x.Tween=et,et.prototype={constructor:et,init:function(e,t,n,r,i,a){this.elem=e,this.prop=n,this.easing=i||x.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=a||(x.cssNumber[n]?"":"px")},cur:function(){var e=et.propHooks[this.prop];return e&&e.get?e.get(this):et.propHooks._default.get(this)},run:function(e){var t,n=et.propHooks[this.prop];return this.options.duration?this.pos=t=x.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):et.propHooks._default.set(this),this}},et.prototype.init.prototype=et.prototype,et.propHooks={_default:{get:function(e){var t;return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=x.css(e.elem,e.prop,""))&&"auto"!==t?t:0},set:function(e){x.fx.step[e.prop]?x.fx.step[e.prop](e):1!==e.elem.nodeType||!x.cssHooks[e.prop]&&null==e.elem.style[Ze(e.prop)]?e.elem[e.prop]=e.now:x.style(e.elem,e.prop,e.now+e.unit)}}},et.propHooks.scrollTop=et.propHooks.scrollLeft={set:function(e){e.elem.nodeType&&e.elem.parentNode&&(e.elem[e.prop]=e.now)}},x.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:"swing"},x.fx=et.prototype.init,x.fx.step={};var tt,nt,rt=/^(?:toggle|show|hide)$/,it=/queueHooks$/;function at(){nt&&(!1===g.hidden&&e.requestAnimationFrame?e.requestAnimationFrame(at):e.setTimeout(at,x.fx.interval),x.fx.tick())}function ot(){return e.setTimeout((function(){tt=void 0})),tt=Date.now()}function st(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i["margin"+(n=ne[r])]=i["padding"+n]=e;return t&&(i.opacity=i.width=e),i}function ut(e,t,n){for(var r,i=(lt.tweeners[t]||[]).concat(lt.tweeners["*"]),a=0,o=i.length;a1)},removeAttr:function(e){return this.each((function(){x.removeAttr(this,e)}))}}),x.extend({attr:function(e,t,n){var r,i,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return void 0===e.getAttribute?x.prop(e,t,n):(1===a&&x.isXMLDoc(e)||(i=x.attrHooks[t.toLowerCase()]||(x.expr.match.bool.test(t)?ct:void 0)),void 0!==n?null===n?void x.removeAttr(e,t):i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n+""),n):i&&"get"in i&&null!==(r=i.get(e,t))?r:null==(r=x.find.attr(e,t))?void 0:r)},attrHooks:{type:{set:function(e,t){if(!h.radioValue&&"radio"===t&&I(e,"input")){var n=e.value;return e.setAttribute("type",t),n&&(e.value=n),t}}}},removeAttr:function(e,t){var n,r=0,i=t&&t.match(O);if(i&&1===e.nodeType)for(;n=i[r++];)e.removeAttribute(n)}}),ct={set:function(e,t,n){return!1===t?x.removeAttr(e,n):e.setAttribute(n,n),n}},x.each(x.expr.match.bool.source.match(/\w+/g),(function(e,t){var n=At[t]||x.find.attr;At[t]=function(e,t,r){var i,a,o=t.toLowerCase();return r||(a=At[o],At[o]=i,i=null!=n(e,t,r)?o:null,At[o]=a),i}}));var dt=/^(?:input|select|textarea|button)$/i,ht=/^(?:a|area)$/i;function ft(e){return(e.match(O)||[]).join(" ")}function pt(e){return e.getAttribute&&e.getAttribute("class")||""}function gt(e){return Array.isArray(e)?e:"string"==typeof e&&e.match(O)||[]}x.fn.extend({prop:function(e,t){return q(this,x.prop,e,t,arguments.length>1)},removeProp:function(e){return this.each((function(){delete this[x.propFix[e]||e]}))}}),x.extend({prop:function(e,t,n){var r,i,a=e.nodeType;if(3!==a&&8!==a&&2!==a)return 1===a&&x.isXMLDoc(e)||(t=x.propFix[t]||t,i=x.propHooks[t]),void 0!==n?i&&"set"in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&"get"in i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=x.find.attr(e,"tabindex");return t?parseInt(t,10):dt.test(e.nodeName)||ht.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),h.optSelected||(x.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),x.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){x.propFix[this.toLowerCase()]=this})),x.fn.extend({addClass:function(e){var t,n,r,i,a,o,s,u=0;if(f(e))return this.each((function(t){x(this).addClass(e.call(this,t,pt(this)))}));if((t=gt(e)).length)for(;n=this[u++];)if(i=pt(n),r=1===n.nodeType&&" "+ft(i)+" "){for(o=0;a=t[o++];)r.indexOf(" "+a+" ")<0&&(r+=a+" ");i!==(s=ft(r))&&n.setAttribute("class",s)}return this},removeClass:function(e){var t,n,r,i,a,o,s,u=0;if(f(e))return this.each((function(t){x(this).removeClass(e.call(this,t,pt(this)))}));if(!arguments.length)return this.attr("class","");if((t=gt(e)).length)for(;n=this[u++];)if(i=pt(n),r=1===n.nodeType&&" "+ft(i)+" "){for(o=0;a=t[o++];)for(;r.indexOf(" "+a+" ")>-1;)r=r.replace(" "+a+" "," ");i!==(s=ft(r))&&n.setAttribute("class",s)}return this},toggleClass:function(e,t){var n=typeof e,r="string"===n||Array.isArray(e);return"boolean"==typeof t&&r?t?this.addClass(e):this.removeClass(e):f(e)?this.each((function(n){x(this).toggleClass(e.call(this,n,pt(this),t),t)})):this.each((function(){var t,i,a,o;if(r)for(i=0,a=x(this),o=gt(e);t=o[i++];)a.hasClass(t)?a.removeClass(t):a.addClass(t);else void 0!==e&&"boolean"!==n||((t=pt(this))&&Y.set(this,"__className__",t),this.setAttribute&&this.setAttribute("class",t||!1===e?"":Y.get(this,"__className__")||""))}))},hasClass:function(e){var t,n,r=0;for(t=" "+e+" ";n=this[r++];)if(1===n.nodeType&&(" "+ft(pt(n))+" ").indexOf(t)>-1)return!0;return!1}});var mt=/\r/g;x.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=f(e),this.each((function(n){var i;1===this.nodeType&&(null==(i=r?e.call(this,n,x(this).val()):e)?i="":"number"==typeof i?i+="":Array.isArray(i)&&(i=x.map(i,(function(e){return null==e?"":e+""}))),(t=x.valHooks[this.type]||x.valHooks[this.nodeName.toLowerCase()])&&"set"in t&&void 0!==t.set(this,i,"value")||(this.value=i))}))):i?(t=x.valHooks[i.type]||x.valHooks[i.nodeName.toLowerCase()])&&"get"in t&&void 0!==(n=t.get(i,"value"))?n:"string"==typeof(n=i.value)?n.replace(mt,""):null==n?"":n:void 0}}),x.extend({valHooks:{option:{get:function(e){var t=x.find.attr(e,"value");return null!=t?t:ft(x.text(e))}},select:{get:function(e){var t,n,r,i=e.options,a=e.selectedIndex,o="select-one"===e.type,s=o?null:[],u=o?a+1:i.length;for(r=a<0?u:o?a:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),a}}}}),x.each(["radio","checkbox"],(function(){x.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=x.inArray(x(e).val(),t)>-1}},h.checkOn||(x.valHooks[this].get=function(e){return null===e.getAttribute("value")?"on":e.value})})),h.focusin="onfocusin"in e;var vt=/^(?:focusinfocus|focusoutblur)$/,yt=function(e){e.stopPropagation()};x.extend(x.event,{trigger:function(t,n,r,i){var a,o,s,u,l,A,d,h,m=[r||g],v=c.call(t,"type")?t.type:t,y=c.call(t,"namespace")?t.namespace.split("."):[];if(o=h=s=r=r||g,3!==r.nodeType&&8!==r.nodeType&&!vt.test(v+x.event.triggered)&&(v.indexOf(".")>-1&&(y=v.split("."),v=y.shift(),y.sort()),l=v.indexOf(":")<0&&"on"+v,(t=t[x.expando]?t:new x.Event(v,"object"==typeof t&&t)).isTrigger=i?2:3,t.namespace=y.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+y.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:x.makeArray(n,[t]),d=x.event.special[v]||{},i||!d.trigger||!1!==d.trigger.apply(r,n))){if(!i&&!d.noBubble&&!p(r)){for(u=d.delegateType||v,vt.test(u+v)||(o=o.parentNode);o;o=o.parentNode)m.push(o),s=o;s===(r.ownerDocument||g)&&m.push(s.defaultView||s.parentWindow||e)}for(a=0;(o=m[a++])&&!t.isPropagationStopped();)h=o,t.type=a>1?u:d.bindType||v,(A=(Y.get(o,"events")||Object.create(null))[t.type]&&Y.get(o,"handle"))&&A.apply(o,n),(A=l&&o[l])&&A.apply&&G(o)&&(t.result=A.apply(o,n),!1===t.result&&t.preventDefault());return t.type=v,i||t.isDefaultPrevented()||d._default&&!1!==d._default.apply(m.pop(),n)||!G(r)||l&&f(r[v])&&!p(r)&&((s=r[l])&&(r[l]=null),x.event.triggered=v,t.isPropagationStopped()&&h.addEventListener(v,yt),r[v](),t.isPropagationStopped()&&h.removeEventListener(v,yt),x.event.triggered=void 0,s&&(r[l]=s)),t.result}},simulate:function(e,t,n){var r=x.extend(new x.Event,n,{type:e,isSimulated:!0});x.event.trigger(r,null,t)}}),x.fn.extend({trigger:function(e,t){return this.each((function(){x.event.trigger(e,t,this)}))},triggerHandler:function(e,t){var n=this[0];if(n)return x.event.trigger(e,t,n,!0)}}),h.focusin||x.each({focus:"focusin",blur:"focusout"},(function(e,t){var n=function(e){x.event.simulate(t,e.target,x.event.fix(e))};x.event.special[t]={setup:function(){var r=this.ownerDocument||this.document||this,i=Y.access(r,t);i||r.addEventListener(e,n,!0),Y.access(r,t,(i||0)+1)},teardown:function(){var r=this.ownerDocument||this.document||this,i=Y.access(r,t)-1;i?Y.access(r,t,i):(r.removeEventListener(e,n,!0),Y.remove(r,t))}}}));var wt=e.location,xt={guid:Date.now()},bt=/\?/;x.parseXML=function(t){var n,r;if(!t||"string"!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,"text/xml")}catch(e){}return r=n&&n.getElementsByTagName("parsererror")[0],n&&!r||x.error("Invalid XML: "+(r?x.map(r.childNodes,(function(e){return e.textContent})).join("\n"):t)),n};var Ct=/\[\]$/,St=/\r?\n/g,Tt=/^(?:submit|button|image|reset|file)$/i,kt=/^(?:input|select|textarea|keygen)/i;function It(e,t,n,r){var i;if(Array.isArray(t))x.each(t,(function(t,i){n||Ct.test(e)?r(e,i):It(e+"["+("object"==typeof i&&null!=i?t:"")+"]",i,n,r)}));else if(n||"object"!==y(t))r(e,t);else for(i in t)It(e+"["+i+"]",t[i],n,r)}x.param=function(e,t){var n,r=[],i=function(e,t){var n=f(t)?t():t;r[r.length]=encodeURIComponent(e)+"="+encodeURIComponent(null==n?"":n)};if(null==e)return"";if(Array.isArray(e)||e.jquery&&!x.isPlainObject(e))x.each(e,(function(){i(this.name,this.value)}));else for(n in e)It(n,e[n],t,i);return r.join("&")},x.fn.extend({serialize:function(){return x.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var e=x.prop(this,"elements");return e?x.makeArray(e):this})).filter((function(){var e=this.type;return this.name&&!x(this).is(":disabled")&&kt.test(this.nodeName)&&!Tt.test(e)&&(this.checked||!he.test(e))})).map((function(e,t){var n=x(this).val();return null==n?null:Array.isArray(n)?x.map(n,(function(e){return{name:t.name,value:e.replace(St,"\r\n")}})):{name:t.name,value:n.replace(St,"\r\n")}})).get()}});var Ft=/%20/g,Et=/#.*$/,Dt=/([?&])_=[^&]*/,Mt=/^(.*?):[ \t]*([^\r\n]*)$/gm,Bt=/^(?:GET|HEAD)$/,Lt=/^\/\//,Nt={},Ot={},jt="*/".concat("*"),Rt=g.createElement("a");function Vt(e){return function(t,n){"string"!=typeof t&&(n=t,t="*");var r,i=0,a=t.toLowerCase().match(O)||[];if(f(n))for(;r=a[i++];)"+"===r[0]?(r=r.slice(1)||"*",(e[r]=e[r]||[]).unshift(n)):(e[r]=e[r]||[]).push(n)}}function Ht(e,t,n,r){var i={},a=e===Ot;function o(s){var u;return i[s]=!0,x.each(e[s]||[],(function(e,s){var l=s(t,n,r);return"string"!=typeof l||a||i[l]?a?!(u=l):void 0:(t.dataTypes.unshift(l),o(l),!1)})),u}return o(t.dataTypes[0])||!i["*"]&&o("*")}function Pt(e,t){var n,r,i=x.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&x.extend(!0,e,r),e}Rt.href=wt.href,x.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:wt.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(wt.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":jt,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":x.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Pt(Pt(e,x.ajaxSettings),t):Pt(x.ajaxSettings,e)},ajaxPrefilter:Vt(Nt),ajaxTransport:Vt(Ot),ajax:function(t,n){"object"==typeof t&&(n=t,t=void 0),n=n||{};var r,i,a,o,s,u,l,c,A,d,h=x.ajaxSetup({},n),f=h.context||h,p=h.context&&(f.nodeType||f.jquery)?x(f):x.event,m=x.Deferred(),v=x.Callbacks("once memory"),y=h.statusCode||{},w={},b={},C="canceled",S={readyState:0,getResponseHeader:function(e){var t;if(l){if(!o)for(o={};t=Mt.exec(a);)o[t[1].toLowerCase()+" "]=(o[t[1].toLowerCase()+" "]||[]).concat(t[2]);t=o[e.toLowerCase()+" "]}return null==t?null:t.join(", ")},getAllResponseHeaders:function(){return l?a:null},setRequestHeader:function(e,t){return null==l&&(e=b[e.toLowerCase()]=b[e.toLowerCase()]||e,w[e]=t),this},overrideMimeType:function(e){return null==l&&(h.mimeType=e),this},statusCode:function(e){var t;if(e)if(l)S.always(e[S.status]);else for(t in e)y[t]=[y[t],e[t]];return this},abort:function(e){var t=e||C;return r&&r.abort(t),T(0,t),this}};if(m.promise(S),h.url=((t||h.url||wt.href)+"").replace(Lt,wt.protocol+"//"),h.type=n.method||n.type||h.method||h.type,h.dataTypes=(h.dataType||"*").toLowerCase().match(O)||[""],null==h.crossDomain){u=g.createElement("a");try{u.href=h.url,u.href=u.href,h.crossDomain=Rt.protocol+"//"+Rt.host!=u.protocol+"//"+u.host}catch(e){h.crossDomain=!0}}if(h.data&&h.processData&&"string"!=typeof h.data&&(h.data=x.param(h.data,h.traditional)),Ht(Nt,h,n,S),l)return S;for(A in(c=x.event&&h.global)&&0==x.active++&&x.event.trigger("ajaxStart"),h.type=h.type.toUpperCase(),h.hasContent=!Bt.test(h.type),i=h.url.replace(Et,""),h.hasContent?h.data&&h.processData&&0===(h.contentType||"").indexOf("application/x-www-form-urlencoded")&&(h.data=h.data.replace(Ft,"+")):(d=h.url.slice(i.length),h.data&&(h.processData||"string"==typeof h.data)&&(i+=(bt.test(i)?"&":"?")+h.data,delete h.data),!1===h.cache&&(i=i.replace(Dt,"$1"),d=(bt.test(i)?"&":"?")+"_="+xt.guid+++d),h.url=i+d),h.ifModified&&(x.lastModified[i]&&S.setRequestHeader("If-Modified-Since",x.lastModified[i]),x.etag[i]&&S.setRequestHeader("If-None-Match",x.etag[i])),(h.data&&h.hasContent&&!1!==h.contentType||n.contentType)&&S.setRequestHeader("Content-Type",h.contentType),S.setRequestHeader("Accept",h.dataTypes[0]&&h.accepts[h.dataTypes[0]]?h.accepts[h.dataTypes[0]]+("*"!==h.dataTypes[0]?", "+jt+"; q=0.01":""):h.accepts["*"]),h.headers)S.setRequestHeader(A,h.headers[A]);if(h.beforeSend&&(!1===h.beforeSend.call(f,S,h)||l))return S.abort();if(C="abort",v.add(h.complete),S.done(h.success),S.fail(h.error),r=Ht(Ot,h,n,S)){if(S.readyState=1,c&&p.trigger("ajaxSend",[S,h]),l)return S;h.async&&h.timeout>0&&(s=e.setTimeout((function(){S.abort("timeout")}),h.timeout));try{l=!1,r.send(w,T)}catch(e){if(l)throw e;T(-1,e)}}else T(-1,"No Transport");function T(t,n,o,u){var A,d,g,w,b,C=n;l||(l=!0,s&&e.clearTimeout(s),r=void 0,a=u||"",S.readyState=t>0?4:0,A=t>=200&&t<300||304===t,o&&(w=function(e,t,n){for(var r,i,a,o,s=e.contents,u=e.dataTypes;"*"===u[0];)u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader("Content-Type"));if(r)for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}if(u[0]in n)a=u[0];else{for(i in n){if(!u[0]||e.converters[i+" "+u[0]]){a=i;break}o||(o=i)}a=a||o}if(a)return a!==u[0]&&u.unshift(a),n[a]}(h,S,o)),!A&&x.inArray("script",h.dataTypes)>-1&&x.inArray("json",h.dataTypes)<0&&(h.converters["text script"]=function(){}),w=function(e,t,n,r){var i,a,o,s,u,l={},c=e.dataTypes.slice();if(c[1])for(o in e.converters)l[o.toLowerCase()]=e.converters[o];for(a=c.shift();a;)if(e.responseFields[a]&&(n[e.responseFields[a]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=a,a=c.shift())if("*"===a)a=u;else if("*"!==u&&u!==a){if(!(o=l[u+" "+a]||l["* "+a]))for(i in l)if((s=i.split(" "))[1]===a&&(o=l[u+" "+s[0]]||l["* "+s[0]])){!0===o?o=l[i]:!0!==l[i]&&(a=s[0],c.unshift(s[1]));break}if(!0!==o)if(o&&e.throws)t=o(t);else try{t=o(t)}catch(e){return{state:"parsererror",error:o?e:"No conversion from "+u+" to "+a}}}return{state:"success",data:t}}(h,w,S,A),A?(h.ifModified&&((b=S.getResponseHeader("Last-Modified"))&&(x.lastModified[i]=b),(b=S.getResponseHeader("etag"))&&(x.etag[i]=b)),204===t||"HEAD"===h.type?C="nocontent":304===t?C="notmodified":(C=w.state,d=w.data,A=!(g=w.error))):(g=C,!t&&C||(C="error",t<0&&(t=0))),S.status=t,S.statusText=(n||C)+"",A?m.resolveWith(f,[d,C,S]):m.rejectWith(f,[S,C,g]),S.statusCode(y),y=void 0,c&&p.trigger(A?"ajaxSuccess":"ajaxError",[S,h,A?d:g]),v.fireWith(f,[S,C]),c&&(p.trigger("ajaxComplete",[S,h]),--x.active||x.event.trigger("ajaxStop")))}return S},getJSON:function(e,t,n){return x.get(e,t,n,"json")},getScript:function(e,t){return x.get(e,void 0,t,"script")}}),x.each(["get","post"],(function(e,t){x[t]=function(e,n,r,i){return f(n)&&(i=i||r,r=n,n=void 0),x.ajax(x.extend({url:e,type:t,dataType:i,data:n,success:r},x.isPlainObject(e)&&e))}})),x.ajaxPrefilter((function(e){var t;for(t in e.headers)"content-type"===t.toLowerCase()&&(e.contentType=e.headers[t]||"")})),x._evalUrl=function(e,t,n){return x.ajax({url:e,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(e){x.globalEval(e,t,n)}})},x.fn.extend({wrapAll:function(e){var t;return this[0]&&(f(e)&&(e=e.call(this[0])),t=x(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map((function(){for(var e=this;e.firstElementChild;)e=e.firstElementChild;return e})).append(this)),this},wrapInner:function(e){return f(e)?this.each((function(t){x(this).wrapInner(e.call(this,t))})):this.each((function(){var t=x(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)}))},wrap:function(e){var t=f(e);return this.each((function(n){x(this).wrapAll(t?e.call(this,n):e)}))},unwrap:function(e){return this.parent(e).not("body").each((function(){x(this).replaceWith(this.childNodes)})),this}}),x.expr.pseudos.hidden=function(e){return!x.expr.pseudos.visible(e)},x.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},x.ajaxSettings.xhr=function(){try{return new e.XMLHttpRequest}catch(e){}};var Wt={0:200,1223:204},qt=x.ajaxSettings.xhr();h.cors=!!qt&&"withCredentials"in qt,h.ajax=qt=!!qt,x.ajaxTransport((function(t){var n,r;if(h.cors||qt&&!t.crossDomain)return{send:function(i,a){var o,s=t.xhr();if(s.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(o in t.xhrFields)s[o]=t.xhrFields[o];for(o in t.mimeType&&s.overrideMimeType&&s.overrideMimeType(t.mimeType),t.crossDomain||i["X-Requested-With"]||(i["X-Requested-With"]="XMLHttpRequest"),i)s.setRequestHeader(o,i[o]);n=function(e){return function(){n&&(n=r=s.onload=s.onerror=s.onabort=s.ontimeout=s.onreadystatechange=null,"abort"===e?s.abort():"error"===e?"number"!=typeof s.status?a(0,"error"):a(s.status,s.statusText):a(Wt[s.status]||s.status,s.statusText,"text"!==(s.responseType||"text")||"string"!=typeof s.responseText?{binary:s.response}:{text:s.responseText},s.getAllResponseHeaders()))}},s.onload=n(),r=s.onerror=s.ontimeout=n("error"),void 0!==s.onabort?s.onabort=r:s.onreadystatechange=function(){4===s.readyState&&e.setTimeout((function(){n&&r()}))},n=n("abort");try{s.send(t.hasContent&&t.data||null)}catch(e){if(n)throw e}},abort:function(){n&&n()}}})),x.ajaxPrefilter((function(e){e.crossDomain&&(e.contents.script=!1)})),x.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(e){return x.globalEval(e),e}}}),x.ajaxPrefilter("script",(function(e){void 0===e.cache&&(e.cache=!1),e.crossDomain&&(e.type="GET")})),x.ajaxTransport("script",(function(e){var t,n;if(e.crossDomain||e.scriptAttrs)return{send:function(r,i){t=x(" - - Image tag attributes: - - rel:animated_src - If this url is specified, it's loaded into the player instead of src. - This allows a preview frame to be shown until animated gif data is streamed into the canvas - - rel:auto_play - Defaults to 1 if not specified. If set to zero, a call to the play() method is needed - - Constructor options args - - gif Required. The DOM element of an img tag. - loop_mode Optional. Setting this to false will force disable looping of the gif. - auto_play Optional. Same as the rel:auto_play attribute above, this arg overrides the img tag info. - max_width Optional. Scale images over max_width down to max_width. Helpful with mobile. - on_end Optional. Add a callback for when the gif reaches the end of a single loop (one iteration). The first argument passed will be the gif HTMLElement. - loop_delay Optional. The amount of time to pause (in ms) after each single loop (iteration). - draw_while_loading Optional. Determines whether the gif will be drawn to the canvas whilst it is loaded. - show_progress_bar Optional. Only applies when draw_while_loading is set to true. - - Instance methods - - // loading - load( callback ) Loads the gif specified by the src or rel:animated_src sttributie of the img tag into a canvas element and then calls callback if one is passed - load_url( src, callback ) Loads the gif file specified in the src argument into a canvas element and then calls callback if one is passed - - // play controls - play - Start playing the gif - pause - Stop playing the gif - move_to(i) - Move to frame i of the gif - move_relative(i) - Move i frames ahead (or behind if i < 0) - - // getters - get_canvas The canvas element that the gif is playing in. Handy for assigning event handlers to. - get_playing Whether or not the gif is currently playing - get_loading Whether or not the gif has finished loading/parsing - get_auto_play Whether or not the gif is set to play automatically - get_length The number of frames in the gif - get_current_frame The index of the currently displayed frame of the gif - - For additional customization (viewport inside iframe) these params may be passed: - c_w, c_h - width and height of canvas - vp_t, vp_l, vp_ w, vp_h - top, left, width and height of the viewport - - A bonus: few articles to understand what is going on - http://enthusiasms.org/post/16976438906 - http://www.matthewflickinger.com/lab/whatsinagif/bits_and_bytes.asp - http://humpy77.deviantart.com/journal/Frame-Delay-Times-for-Animated-GIFs-214150546 - -*/ -(function (root, factory) -{ - if (typeof define === 'function' && define.amd) - { - define([], factory); - } else if (typeof exports === 'object') - { - module.exports = factory(); - } else - { - root.SuperGif = factory(); - } -}(this, function () -{ - // Generic functions - var bitsToNum = function (ba) - { - return ba.reduce(function (s, n) - { - return s * 2 + n; - }, 0); - }; - - var byteToBitArr = function (bite) - { - var a = []; - for (var i = 7; i >= 0; i--) - { - a.push(!!(bite & (1 << i))); - } - return a; - }; - - // Stream - /** - * @constructor - */ - // Make compiler happy. - var Stream = function (data) - { - this.data = data; - this.len = this.data.length; - this.pos = 0; - - this.readByte = function () - { - if (this.pos >= this.data.length) - { - throw new Error('Attempted to read past end of stream.'); - } - if (data instanceof Uint8Array) - return data[this.pos++]; - else - return data.charCodeAt(this.pos++) & 0xFF; - }; - - this.readBytes = function (n) - { - var bytes = []; - for (var i = 0; i < n; i++) - { - bytes.push(this.readByte()); - } - return bytes; - }; - - this.read = function (n) - { - var s = ''; - for (var i = 0; i < n; i++) - { - s += String.fromCharCode(this.readByte()); - } - return s; - }; - - this.readUnsigned = function () - { // Little-endian. - var a = this.readBytes(2); - return (a[1] << 8) + a[0]; - }; - }; - - var lzwDecode = function (minCodeSize, data) - { - // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String? - var pos = 0; // Maybe this streaming thing should be merged with the Stream? - var readCode = function (size) - { - var code = 0; - for (var i = 0; i < size; i++) - { - if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) - { - code |= 1 << i; - } - pos++; - } - return code; - }; - - var output = []; - - var clearCode = 1 << minCodeSize; - var eoiCode = clearCode + 1; - - var codeSize = minCodeSize + 1; - - var dict = []; - - var clear = function () - { - dict = []; - codeSize = minCodeSize + 1; - for (var i = 0; i < clearCode; i++) - { - dict[i] = [i]; - } - dict[clearCode] = []; - dict[eoiCode] = null; - - }; - - var code; - var last; - - while (true) - { - last = code; - code = readCode(codeSize); - - if (code === clearCode) - { - clear(); - continue; - } - if (code === eoiCode) break; - - if (code < dict.length) - { - if (last !== clearCode) - { - dict.push(dict[last].concat(dict[code][0])); - } - } - else - { - if (code !== dict.length) throw new Error('Invalid LZW code.'); - dict.push(dict[last].concat(dict[last][0])); - } - output.push.apply(output, dict[code]); - - if (dict.length === (1 << codeSize) && codeSize < 12) - { - // If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long. - codeSize++; - } - } - - // I don't know if this is technically an error, but some GIFs do it. - //if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.'); - return output; - }; - - - // The actual parsing; returns an object with properties. - var parseGIF = function (st, handler) - { - handler || (handler = {}); - - // LZW (GIF-specific) - var parseCT = function (entries) - { // Each entry is 3 bytes, for RGB. - var ct = []; - for (var i = 0; i < entries; i++) - { - ct.push(st.readBytes(3)); - } - return ct; - }; - - var readSubBlocks = function () - { - var size, data; - data = ''; - do - { - size = st.readByte(); - data += st.read(size); - } while (size !== 0); - return data; - }; - - var parseHeader = function () - { - var hdr = {}; - hdr.sig = st.read(3); - hdr.ver = st.read(3); - if (hdr.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely. - hdr.width = st.readUnsigned(); - hdr.height = st.readUnsigned(); - - var bits = byteToBitArr(st.readByte()); - hdr.gctFlag = bits.shift(); - hdr.colorRes = bitsToNum(bits.splice(0, 3)); - hdr.sorted = bits.shift(); - hdr.gctSize = bitsToNum(bits.splice(0, 3)); - - hdr.bgColor = st.readByte(); - hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64 - if (hdr.gctFlag) - { - hdr.gct = parseCT(1 << (hdr.gctSize + 1)); - } - handler.hdr && handler.hdr(hdr); - }; - - var parseExt = function (block) - { - var parseGCExt = function (block) - { - var blockSize = st.readByte(); // Always 4 - var bits = byteToBitArr(st.readByte()); - block.reserved = bits.splice(0, 3); // Reserved; should be 000. - block.disposalMethod = bitsToNum(bits.splice(0, 3)); - block.userInput = bits.shift(); - block.transparencyGiven = bits.shift(); - - block.delayTime = st.readUnsigned(); - - block.transparencyIndex = st.readByte(); - - block.terminator = st.readByte(); - - handler.gce && handler.gce(block); - }; - - var parseComExt = function (block) - { - block.comment = readSubBlocks(); - handler.com && handler.com(block); - }; - - var parsePTExt = function (block) - { - // No one *ever* uses this. If you use it, deal with parsing it yourself. - var blockSize = st.readByte(); // Always 12 - block.ptHeader = st.readBytes(12); - block.ptData = readSubBlocks(); - handler.pte && handler.pte(block); - }; - - var parseAppExt = function (block) - { - var parseNetscapeExt = function (block) - { - var blockSize = st.readByte(); // Always 3 - block.unknown = st.readByte(); // ??? Always 1? What is this? - block.iterations = st.readUnsigned(); - block.terminator = st.readByte(); - handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block); - }; - - var parseUnknownAppExt = function (block) - { - block.appData = readSubBlocks(); - // FIXME: This won't work if a handler wants to match on any identifier. - handler.app && handler.app[block.identifier] && handler.app[block.identifier](block); - }; - - var blockSize = st.readByte(); // Always 11 - block.identifier = st.read(8); - block.authCode = st.read(3); - switch (block.identifier) - { - case 'NETSCAPE': - parseNetscapeExt(block); - break; - default: - parseUnknownAppExt(block); - break; - } - }; - - var parseUnknownExt = function (block) - { - block.data = readSubBlocks(); - handler.unknown && handler.unknown(block); - }; - - block.label = st.readByte(); - switch (block.label) - { - case 0xF9: - block.extType = 'gce'; - parseGCExt(block); - break; - case 0xFE: - block.extType = 'com'; - parseComExt(block); - break; - case 0x01: - block.extType = 'pte'; - parsePTExt(block); - break; - case 0xFF: - block.extType = 'app'; - parseAppExt(block); - break; - default: - block.extType = 'unknown'; - parseUnknownExt(block); - break; - } - }; - - var parseImg = function (img) - { - var deinterlace = function (pixels, width) - { - // Of course this defeats the purpose of interlacing. And it's *probably* - // the least efficient way it's ever been implemented. But nevertheless... - var newPixels = new Array(pixels.length); - var rows = pixels.length / width; - var cpRow = function (toRow, fromRow) - { - var fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width); - newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels)); - }; - - // See appendix E. - var offsets = [0, 4, 2, 1]; - var steps = [8, 8, 4, 2]; - - var fromRow = 0; - for (var pass = 0; pass < 4; pass++) - { - for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) - { - cpRow(toRow, fromRow) - fromRow++; - } - } - - return newPixels; - }; - - img.leftPos = st.readUnsigned(); - img.topPos = st.readUnsigned(); - img.width = st.readUnsigned(); - img.height = st.readUnsigned(); - - var bits = byteToBitArr(st.readByte()); - img.lctFlag = bits.shift(); - img.interlaced = bits.shift(); - img.sorted = bits.shift(); - img.reserved = bits.splice(0, 2); - img.lctSize = bitsToNum(bits.splice(0, 3)); - - if (img.lctFlag) - { - img.lct = parseCT(1 << (img.lctSize + 1)); - } - - img.lzwMinCodeSize = st.readByte(); - - var lzwData = readSubBlocks(); - - img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData); - - if (img.interlaced) - { // Move - img.pixels = deinterlace(img.pixels, img.width); - } - - handler.img && handler.img(img); - }; - - var parseBlock = function () - { - var block = {}; - block.sentinel = st.readByte(); - - switch (String.fromCharCode(block.sentinel)) - { // For ease of matching - case '!': - block.type = 'ext'; - parseExt(block); - break; - case ',': - block.type = 'img'; - parseImg(block); - break; - case ';': - block.type = 'eof'; - handler.eof && handler.eof(block); - break; - default: - throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0. - } - - if (block.type !== 'eof') setTimeout(parseBlock, 0); - }; - - var parse = function () - { - parseHeader(); - setTimeout(parseBlock, 0); - }; - - parse(); - }; - - var SuperGif = function (opts) - { - var options = { - //viewport position - vp_l: 0, - vp_t: 0, - vp_w: null, - vp_h: null, - //canvas sizes - c_w: null, - c_h: null - }; - for (var i in opts) { options[i] = opts[i] } - if (options.vp_w && options.vp_h) options.is_vp = true; - - var stream; - var hdr; - - var loadError = null; - var loading = false; - - var transparency = null; - var delay = null; - var disposalMethod = null; - var disposalRestoreFromIdx = null; - var lastDisposalMethod = null; - var frame = null; - var lastImg = null; - - var playing = true; - var forward = true; - - var ctx_scaled = false; - - var frames = []; - var frameOffsets = []; // elements have .x and .y properties - - var src = options.src; - - if (!options.gif) - { - options.gif = document.createElement("img"); - options.gif.setAttribute("rel:animated_src", src); - //options.gif.setAttribute("rel:auto_play", "1"); - options.gif.src = ""; - } - - var gif = options.gif; - if (typeof options.auto_play == 'undefined') - options.auto_play = (!gif.getAttribute('rel:auto_play') || gif.getAttribute('rel:auto_play') == '1'); - - var onEndListener = (options.hasOwnProperty('on_end') ? options.on_end : null); - var loopDelay = (options.hasOwnProperty('loop_delay') ? options.loop_delay : 0); - var overrideLoopMode = (options.hasOwnProperty('loop_mode') ? options.loop_mode : 'auto'); - var drawWhileLoading = (options.hasOwnProperty('draw_while_loading') ? options.draw_while_loading : true); - var showProgressBar = drawWhileLoading ? (options.hasOwnProperty('show_progress_bar') ? options.show_progress_bar : true) : false; - var progressBarHeight = (options.hasOwnProperty('progressbar_height') ? options.progressbar_height : 25); - var progressBarBackgroundColor = (options.hasOwnProperty('progressbar_background_color') ? options.progressbar_background_color : 'rgba(255,255,255,0.4)'); - var progressBarForegroundColor = (options.hasOwnProperty('progressbar_foreground_color') ? options.progressbar_foreground_color : 'rgba(255,0,22,.8)'); - - var clear = function () - { - transparency = null; - delay = null; - lastDisposalMethod = disposalMethod; - disposalMethod = null; - frame = null; - }; - - // XXX: There's probably a better way to handle catching exceptions when - // callbacks are involved. - var doParse = function () - { - try - { - parseGIF(stream, handler); - } - catch (err) - { - doLoadError('parse'); - } - }; - - var doText = function (text) - { - toolbar.innerHTML = text; // innerText? Escaping? Whatever. - toolbar.style.visibility = 'visible'; - }; - - var setSizes = function (w, h) - { - canvas.width = w * get_canvas_scale(); - canvas.height = h * get_canvas_scale(); - toolbar.style.minWidth = (w * get_canvas_scale()) + 'px'; - - tmpCanvas.width = w; - tmpCanvas.height = h; - tmpCanvas.style.width = w + 'px'; - tmpCanvas.style.height = h + 'px'; - tmpCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0); - }; - - var setFrameOffset = function (frame, offset) - { - if (!frameOffsets[frame]) - { - frameOffsets[frame] = offset; - return; - } - if (typeof offset.x !== 'undefined') - { - frameOffsets[frame].x = offset.x; - } - if (typeof offset.y !== 'undefined') - { - frameOffsets[frame].y = offset.y; - } - }; - - var doShowProgress = function (pos, length, draw) - { - if (draw && showProgressBar) - { - var height = progressBarHeight; - var left, mid, top, width; - if (options.is_vp) - { - if (!ctx_scaled) - { - top = (options.vp_t + options.vp_h - height); - height = height; - left = options.vp_l; - mid = left + (pos / length) * options.vp_w; - width = canvas.width; - } else - { - top = (options.vp_t + options.vp_h - height) / get_canvas_scale(); - height = height / get_canvas_scale(); - left = (options.vp_l / get_canvas_scale()); - mid = left + (pos / length) * (options.vp_w / get_canvas_scale()); - width = canvas.width / get_canvas_scale(); - } - //some debugging, draw rect around viewport - if (false) - { - if (!ctx_scaled) - { - var l = options.vp_l, t = options.vp_t; - var w = options.vp_w, h = options.vp_h; - } else - { - var l = options.vp_l / get_canvas_scale(), t = options.vp_t / get_canvas_scale(); - var w = options.vp_w / get_canvas_scale(), h = options.vp_h / get_canvas_scale(); - } - ctx.rect(l, t, w, h); - ctx.stroke(); - } - } - else - { - top = (canvas.height - height) / (ctx_scaled ? get_canvas_scale() : 1); - mid = ((pos / length) * canvas.width) / (ctx_scaled ? get_canvas_scale() : 1); - width = canvas.width / (ctx_scaled ? get_canvas_scale() : 1); - height /= ctx_scaled ? get_canvas_scale() : 1; - } - - ctx.fillStyle = progressBarBackgroundColor; - ctx.fillRect(mid, top, width - mid, height); - - ctx.fillStyle = progressBarForegroundColor; - ctx.fillRect(0, top, mid, height); - } - }; - - var doLoadError = function (originOfError) - { - var drawError = function () - { - ctx.fillStyle = 'black'; - ctx.fillRect(0, 0, options.c_w ? options.c_w : hdr.width, options.c_h ? options.c_h : hdr.height); - ctx.strokeStyle = 'red'; - ctx.lineWidth = 3; - ctx.moveTo(0, 0); - ctx.lineTo(options.c_w ? options.c_w : hdr.width, options.c_h ? options.c_h : hdr.height); - ctx.moveTo(0, options.c_h ? options.c_h : hdr.height); - ctx.lineTo(options.c_w ? options.c_w : hdr.width, 0); - ctx.stroke(); - }; - - loadError = originOfError; - hdr = { - width: gif.width, - height: gif.height - }; // Fake header. - frames = []; - drawError(); - }; - - var doHdr = function (_hdr) - { - hdr = _hdr; - setSizes(hdr.width, hdr.height) - }; - - var doGCE = function (gce) - { - pushFrame(); - clear(); - transparency = gce.transparencyGiven ? gce.transparencyIndex : null; - delay = gce.delayTime; - disposalMethod = gce.disposalMethod; - // We don't have much to do with the rest of GCE. - }; - - var pushFrame = function () - { - if (!frame) return; - frames.push({ - data: frame.getImageData(0, 0, hdr.width, hdr.height), - delay: delay - }); - frameOffsets.push({ x: 0, y: 0 }); - }; - - var doImg = function (img) - { - if (!frame) frame = tmpCanvas.getContext('2d'); - - var currIdx = frames.length; - - //ct = color table, gct = global color table - var ct = img.lctFlag ? img.lct : hdr.gct; // TODO: What if neither exists? - - /* - Disposal method indicates the way in which the graphic is to - be treated after being displayed. - - Values : 0 - No disposal specified. The decoder is - not required to take any action. - 1 - Do not dispose. The graphic is to be left - in place. - 2 - Restore to background color. The area used by the - graphic must be restored to the background color. - 3 - Restore to previous. The decoder is required to - restore the area overwritten by the graphic with - what was there prior to rendering the graphic. - - Importantly, "previous" means the frame state - after the last disposal of method 0, 1, or 2. - */ - if (currIdx > 0) - { - if (lastDisposalMethod === 3) - { - // Restore to previous - // If we disposed every frame including first frame up to this point, then we have - // no composited frame to restore to. In this case, restore to background instead. - if (disposalRestoreFromIdx !== null) - { - frame.putImageData(frames[disposalRestoreFromIdx].data, 0, 0); - } else - { - frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height); - } - } else - { - disposalRestoreFromIdx = currIdx - 1; - } - - if (lastDisposalMethod === 2) - { - // Restore to background color - // Browser implementations historically restore to transparent; we do the same. - // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079 - frame.clearRect(lastImg.leftPos, lastImg.topPos, lastImg.width, lastImg.height); - } - } - // else, Undefined/Do not dispose. - // frame contains final pixel data from the last frame; do nothing - - //Get existing pixels for img region after applying disposal method - var imgData = frame.getImageData(img.leftPos, img.topPos, img.width, img.height); - - //apply color table colors - img.pixels.forEach(function (pixel, i) - { - // imgData.data === [R,G,B,A,R,G,B,A,...] - if (pixel !== transparency) - { - imgData.data[i * 4 + 0] = ct[pixel][0]; - imgData.data[i * 4 + 1] = ct[pixel][1]; - imgData.data[i * 4 + 2] = ct[pixel][2]; - imgData.data[i * 4 + 3] = 255; // Opaque. - } - }); - - frame.putImageData(imgData, img.leftPos, img.topPos); - - if (!ctx_scaled) - { - ctx.scale(get_canvas_scale(), get_canvas_scale()); - ctx_scaled = true; - } - - // We could use the on-page canvas directly, except that we draw a progress - // bar for each image chunk (not just the final image). - if (drawWhileLoading) - { - ctx.drawImage(tmpCanvas, 0, 0); - drawWhileLoading = options.auto_play; - } - - lastImg = img; - }; - - var player = (function () - { - var i = -1; - var iterationCount = 0; - - var showingInfo = false; - var pinned = false; - - /** - * Gets the index of the frame "up next". - * @returns {number} - */ - var getNextFrameNo = function () - { - var delta = (forward ? 1 : -1); - return (i + delta + frames.length) % frames.length; - }; - - var stepFrame = function (amount) - { // XXX: Name is confusing. - i = i + amount; - - putFrame(); - }; - - var step = (function () - { - var stepping = false; - - var completeLoop = function () - { - if (onEndListener !== null) - onEndListener(gif); - iterationCount++; - - if (overrideLoopMode !== false || iterationCount < 0) - { - doStep(); - } else - { - stepping = false; - playing = false; - } - }; - - var doStep = function () - { - stepping = playing; - if (!stepping) return; - - stepFrame(1); - var delay = frames[i].delay * 10; - if (!delay) delay = 100; // FIXME: Should this even default at all? What should it be? - - var nextFrameNo = getNextFrameNo(); - if (nextFrameNo === 0) - { - delay += loopDelay; - setTimeout(completeLoop, delay); - } else - { - setTimeout(doStep, delay); - } - }; - - return function () - { - if (!stepping) setTimeout(doStep, 0); - }; - }()); - - var putFrame = function () - { - var offset; - i = parseInt(i, 10); - - if (i > frames.length - 1) - { - i = 0; - } - - if (i < 0) - { - i = 0; - } - - offset = frameOffsets[i]; - - tmpCanvas.getContext("2d").putImageData(frames[i].data, offset.x, offset.y); - ctx.globalCompositeOperation = "copy"; - ctx.drawImage(tmpCanvas, 0, 0); - - options.canvas.getContext("2d").drawImage(canvas, options.x, options.y) - }; - - var play = function () - { - playing = true; - step(); - }; - - var pause = function () - { - playing = false; - }; - - - return { - init: function () - { - if (loadError) return; - - if (!(options.c_w && options.c_h)) - { - ctx.scale(get_canvas_scale(), get_canvas_scale()); - } - - if (options.auto_play) - { - step(); - } - else - { - i = 0; - putFrame(); - } - }, - step: step, - play: play, - pause: pause, - playing: playing, - move_relative: stepFrame, - current_frame: function () { return i; }, - length: function () { return frames.length }, - move_to: function (frame_idx) - { - i = frame_idx; - putFrame(); - } - } - }()); - - var doDecodeProgress = function (draw) - { - doShowProgress(stream.pos, stream.data.length, draw); - }; - - var doNothing = function () { }; - /** - * @param{boolean=} draw Whether to draw progress bar or not; this is not idempotent because of translucency. - * Note that this means that the text will be unsynchronized with the progress bar on non-frames; - * but those are typically so small (GCE etc.) that it doesn't really matter. TODO: Do this properly. - */ - var withProgress = function (fn, draw) - { - return function (block) - { - fn(block); - doDecodeProgress(draw); - }; - }; - - - var handler = { - hdr: withProgress(doHdr), - gce: withProgress(doGCE), - com: withProgress(doNothing), - // I guess that's all for now. - app: { - // TODO: Is there much point in actually supporting iterations? - NETSCAPE: withProgress(doNothing) - }, - img: withProgress(doImg, true), - eof: function (block) - { - //toolbar.style.display = ''; - pushFrame(); - doDecodeProgress(false); - if (!(options.c_w && options.c_h)) - { - canvas.width = hdr.width * get_canvas_scale(); - canvas.height = hdr.height * get_canvas_scale(); - } - player.init(); - loading = false; - if (load_callback) - { - load_callback(gif); - } - - } - }; - - var init = function () - { - var parent = gif.parentNode; - - var div = document.createElement('div'); - canvas = document.createElement('canvas'); - ctx = canvas.getContext('2d'); - toolbar = document.createElement('div'); - - tmpCanvas = document.createElement('canvas'); - - div.width = canvas.width = gif.width; - div.height = canvas.height = gif.height; - toolbar.style.minWidth = gif.width + 'px'; - - div.className = 'jsgif'; - toolbar.className = 'jsgif_toolbar'; - div.style.display = 'none'; - div.appendChild(canvas); - div.appendChild(toolbar); - - if (parent) - { - parent.insertBefore(div, gif); - parent.removeChild(gif); - } - - if (options.c_w && options.c_h) setSizes(options.c_w, options.c_h); - initialized = true; - }; - - var get_canvas_scale = function () - { - var scale; - if (options.max_width && hdr && hdr.width > options.max_width) - { - scale = options.max_width / hdr.width; - } - else if (options.max_height && hdr && hdr.height > options.max_height) - { - scale = options.max_height / hdr.height; - } - else - { - scale = 1; - } - return scale; - } - - var canvas, ctx, toolbar, tmpCanvas; - var initialized = false; - var load_callback = false; - var first_time = true; - - var load_setup = function (callback) - { - if (loading) return false; - if (callback) load_callback = callback; - else load_callback = false; - - loading = true; - frames = []; - clear(); - disposalRestoreFromIdx = null; - lastDisposalMethod = null; - frame = null; - lastImg = null; - - return true; - } - - return { - // play controls - play: player.play, - pause: player.pause, - move_relative: player.move_relative, - move_to: player.move_to, - - // getters for instance vars - get_playing: function () { return playing }, - get_canvas: function () { return canvas }, - get_canvas_scale: function () { return get_canvas_scale() }, - get_loading: function () { return loading }, - get_auto_play: function () { return options.auto_play }, - get_length: function () { return player.length() }, - get_current_frame: function () { return player.current_frame() }, - load_url: function (src, callback) - { - if (!load_setup(callback)) return; - - var h = new XMLHttpRequest(); - // new browsers (XMLHttpRequest2-compliant) - h.open('GET', src, true); - - if ('overrideMimeType' in h) - { - h.overrideMimeType('text/plain; charset=x-user-defined'); - } - - // old browsers (XMLHttpRequest-compliant) - else if ('responseType' in h) - { - h.responseType = 'arraybuffer'; - } - - // IE9 (Microsoft.XMLHTTP-compliant) - else - { - h.setRequestHeader('Accept-Charset', 'x-user-defined'); - } - - h.onloadstart = function () - { - // Wait until connection is opened to replace the gif element with a canvas to avoid a blank img - if (!initialized) init(); - }; - h.onload = function (e) - { - if (this.status != 200) - { - doLoadError('xhr - response'); - } - // emulating response field for IE9 - if (!('response' in this)) - { - this.response = new VBArray(this.responseText).toArray().map(String.fromCharCode).join(''); - } - var data = this.response; - if (data.toString().indexOf("ArrayBuffer") > 0) - { - data = new Uint8Array(data); - } - - stream = new Stream(data); - setTimeout(doParse, 0); - }; - h.onprogress = function (e) - { - if (e.lengthComputable) doShowProgress(e.loaded, e.total, true); - }; - h.onerror = function () { doLoadError('xhr'); }; - h.send(); - }, - load: function (callback) - { - this.load_url(gif.getAttribute('rel:animated_src') || gif.src, callback); - }, - load_raw: function (arr, callback) - { - if (!load_setup(callback)) return; - if (!initialized) init(); - stream = new Stream(arr); - setTimeout(doParse, 0); - }, - set_frame_offset: setFrameOffset, - - // mjb - getX: function () - { - return options.x; - }, - setX: function (value) - { - options.x = value; - }, - getY: function () - { - return options.y; - }, - setY: function (value) - { - options.y = value; - }, - setFirstTime: function () - { - first_time = true; - } - - }; - }; - - return SuperGif; -})); - diff --git a/server/scripts/modules/almanac.js b/server/scripts/modules/almanac.js index 3bc80ee..73bec99 100644 --- a/server/scripts/modules/almanac.js +++ b/server/scripts/modules/almanac.js @@ -1,50 +1,35 @@ // display sun and moon data -/* globals WeatherDisplay, utils, STATUS, draw, SunCalc, luxon */ +/* globals WeatherDisplay, utils, STATUS, SunCalc, luxon */ // eslint-disable-next-line no-unused-vars class Almanac extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Almanac'); + super(navId, elemId, 'Almanac', true); // pre-load background images (returns promises) this.backgroundImage0 = utils.image.load('images/BackGround3_1.png'); - this.backgroundImage1 = utils.image.load('images/BackGround1_1.png'); - // load all images in parallel (returns promises) - this.moonImages = [ - utils.image.load('images/2/Full-Moon.gif'), - utils.image.load('images/2/Last-Quarter.gif'), - utils.image.load('images/2/New-Moon.gif'), - utils.image.load('images/2/First-Quarter.gif'), - ]; + // preload the moon images + utils.image.preload('images/2/Full-Moon.gif'); + utils.image.preload('images/2/Last-Quarter.gif'); + utils.image.preload('images/2/New-Moon.gif'); + utils.image.preload('images/2/First-Quarter.gif'); - this.timing.totalScreens = 2; + this.timing.totalScreens = 1; } async getData(_weatherParameters) { super.getData(_weatherParameters); const weatherParameters = _weatherParameters ?? this.weatherParameters; - // get images for outlook - const imagePromises = [ - utils.image.load('https://www.cpc.ncep.noaa.gov/products/predictions/30day/off14_temp.gif', true), - utils.image.load('https://www.cpc.ncep.noaa.gov/products/predictions/30day/off14_prcp.gif', true), - ]; - // get sun/moon data const { sun, moon } = this.calcSunMoonData(weatherParameters); - // process images for outlook - const [outlookTemp, outlookPrecip] = await Promise.all(imagePromises); - - const outlook = Almanac.parseOutlooks(weatherParameters.latitude, weatherParameters.longitude, outlookTemp, outlookPrecip); - // store the data this.data = { sun, moon, - outlook, }; // update status this.setStatus(STATUS.loaded); @@ -127,115 +112,6 @@ class Almanac extends WeatherDisplay { return { phase: phaseName, date: moonDate }; } - // use the color of the pixel to determine the outlook - static parseOutlooks(lat, lon, temp, precip) { - const { DateTime } = luxon; - const month = DateTime.local(); - const thisMonth = month.toLocaleString({ month: 'short' }); - const nextMonth = month.plus({ months: 1 }).toLocaleString({ month: 'short' }); - - // draw the images on the canvases - const tempContext = utils.image.drawLocalCanvas(temp); - const precipContext = utils.image.drawLocalCanvas(precip); - - // get the color from each canvas - const tempColor = Almanac.getOutlookColor(lat, lon, tempContext); - const precipColor = Almanac.getOutlookColor(lat, lon, precipContext); - - return { - thisMonth, - nextMonth, - temperature: Almanac.getOutlookTemperatureIndicator(tempColor), - precipitation: Almanac.getOutlookPrecipitationIndicator(precipColor), - }; - } - - static getOutlookColor(lat, lon, context) { - let x = 0; - let y = 0; - - // The height is in the range of latitude 75'N (top) - 15'N (bottom) - y = ((75 - lat) / 53) * 707; - - if (lat < 48.83) { - y -= Math.abs(48.83 - lat) * 2.9; - } - if (lon < -100.46) { - y -= Math.abs(-100.46 - lon) * 1.7; - } else { - y -= Math.abs(-100.46 - lon) * 1.7; - } - - // The width is in the range of the longitude ??? - x = ((-155 - lon) / -110) * 719; // -155 - -40 - - if (lon < -100.46) { - x -= Math.abs(-100.46 - lon) * 1; - - if (lat > 40) { - x += Math.abs(40 - lat) * 4; - } else { - x -= Math.abs(40 - lat) * 4; - } - } else { - x += Math.abs(-100.46 - lon) * 2; - - if (lat < 36 && lon > -90) { - x += Math.abs(36 - lat) * 8; - } else { - x -= Math.abs(36 - lat) * 6; - } - } - - // The further left and right from lat 45 and lon -97 the y increases - x = Math.round(x); - y = Math.round(y); - - // Determine if there is any "non-white" colors around the area. - // Search a 16x16 region. - for (let colorX = x - 8; colorX <= x + 8; colorX += 1) { - for (let colorY = y - 8; colorY <= y + 8; colorY += 1) { - const pixelColor = Almanac.getPixelColor(context, colorX, colorY); - if ((pixelColor.r !== 0 && pixelColor.g !== 0 && pixelColor.b !== 0) - || (pixelColor.r !== 255 && pixelColor.g !== 255 && pixelColor.b !== 255)) { - return pixelColor; - } - } - } - - return false; - } - - // get rgb values of a pixel - static getPixelColor(context, x, y) { - const pixelData = context.getImageData(x, y, 1, 1).data; - return { - r: pixelData[0], - g: pixelData[1], - b: pixelData[2], - }; - } - - // get temperature outlook from color - static getOutlookTemperatureIndicator(pixelColor) { - if (pixelColor.b > pixelColor.r) { - return 'Below Normal'; - } if (pixelColor.r > pixelColor.b) { - return 'Above Normal'; - } - return 'Normal'; - } - - // get precipitation outlook from color - static getOutlookPrecipitationIndicator(pixelColor) { - if (pixelColor.g > pixelColor.r) { - return 'Above Normal'; - } if (pixelColor.r > pixelColor.g) { - return 'Below Normal'; - } - return 'Normal'; - } - async drawCanvas() { super.drawCanvas(); const info = this.data; @@ -243,81 +119,47 @@ class Almanac extends WeatherDisplay { const Today = DateTime.local(); const Tomorrow = Today.plus({ days: 1 }); - // extract moon images - const [FullMoonImage, LastMoonImage, NewMoonImage, FirstMoonImage] = await Promise.all(this.moonImages); + // 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(); - switch (this.screenIndex) { - case 1: { - this.context.drawImage(await this.backgroundImage1, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2); - draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2); + const days = info.moon.map((MoonPhase) => { + const fill = {}; - draw.titleText(this.context, 'Almanac', 'Outlook'); + const date = MoonPhase.date.toLocaleString({ month: 'short', day: 'numeric' }); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 320, 180, '30 Day Outlook', 2, 'center'); + fill.date = date; + fill.type = MoonPhase.phase; + fill.icon = { type: 'img', src: Almanac.imageName(MoonPhase.Phase) }; - const DateRange = `MID-${info.outlook.thisMonth.toUpperCase()} TO MID-${info.outlook.nextMonth.toUpperCase()}`; - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 320, 220, DateRange, 2, 'center'); + return this.fillTemplate('day', fill); + }); - const Temperature = info.outlook.temperature; - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 300, `Temperatures: ${Temperature}`, 2); - - const Precipitation = info.outlook.precipitation; - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 380, `Precipitation: ${Precipitation}`, 2); - break; - } - case 0: - default: - // sun and moon data - this.context.drawImage(await this.backgroundImage0, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.horizontalGradientSingle(this.context, 0, 90, 640, 190, draw.sideColor1, draw.sideColor2); - - draw.titleText(this.context, 'Almanac', 'Astronomical'); - - draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 320, 120, Today.toLocaleString({ weekday: 'long' }), 2, 'center'); - draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 500, 120, Tomorrow.toLocaleString({ weekday: 'long' }), 2, 'center'); - - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 150, 'Sunrise:', 2); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 270, 150, DateTime.fromJSDate(info.sun[0].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 450, 150, DateTime.fromJSDate(info.sun[1].sunrise).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2); - - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 70, 180, ' Sunset:', 2); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 270, 180, DateTime.fromJSDate(info.sun[0].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 450, 180, DateTime.fromJSDate(info.sun[1].sunset).toLocaleString(DateTime.TIME_SIMPLE).toLowerCase(), 2); - - draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 70, 220, 'Moon Data:', 2); - - info.moon.forEach((MoonPhase, Index) => { - const date = MoonPhase.date.toLocaleString({ month: 'short', day: 'numeric' }); - - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + Index * 130, 260, MoonPhase.phase, 2, 'center'); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + Index * 130, 390, date, 2, 'center'); - - const image = (() => { - switch (MoonPhase.phase) { - case 'Full': - return FullMoonImage; - case 'Last': - return LastMoonImage; - case 'New': - return NewMoonImage; - case 'First': - default: - return FirstMoonImage; - } - })(); - this.context.drawImage(image, 75 + Index * 130, 270); - }); - break; - } + const daysContainer = this.elem.querySelector('.moon .days'); + daysContainer.innerHTML = ''; + daysContainer.append(...days); this.finishDraw(); } + static 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'; + } + } + // make sun and moon data available outside this class // promise allows for data to be requested before it is available async getSun() { diff --git a/server/scripts/modules/currentweather.js b/server/scripts/modules/currentweather.js index b47351e..7a5d419 100644 --- a/server/scripts/modules/currentweather.js +++ b/server/scripts/modules/currentweather.js @@ -1,10 +1,10 @@ // current weather conditions display -/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation */ +/* globals WeatherDisplay, utils, STATUS, icons, UNITS, navigation */ // eslint-disable-next-line no-unused-vars class CurrentWeather extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Current Conditions'); + super(navId, elemId, 'Current Conditions', true); // pre-load background image (returns promise) this.backgroundImage = utils.image.load('images/BackGround1_1.png'); } @@ -69,7 +69,7 @@ class CurrentWeather extends WeatherDisplay { 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); + data.Ceiling = Math.round(observations.cloudLayers[0]?.base?.value ?? 0); data.CeilingUnit = 'm.'; data.Visibility = Math.round(observations.visibility.value / 1000); data.VisibilityUnit = ' km.'; @@ -111,94 +111,43 @@ class CurrentWeather extends WeatherDisplay { async drawCanvas() { super.drawCanvas(); + const fill = {}; // parse each time to deal with a change in units if necessary const data = this.parseData(); - this.context.drawImage(await this.backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2); - draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2); - - draw.titleText(this.context, 'Current', 'Conditions'); - - draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 170, 135, data.Temperature + String.fromCharCode(176), 2); + fill.temp = data.Temperature + String.fromCharCode(176); let Conditions = data.observations.textDescription; if (Conditions.length > 15) { Conditions = this.shortConditions(Conditions); } - draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 195, 170, Conditions, 2, 'center'); + fill.condition = Conditions; - draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 80, 330, 'Wind:', 2); - draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 300, 330, `${data.WindDirection} ${data.WindSpeed}`, 2, 'right'); + fill.wind = data.WindDirection.padEnd(3, '') + data.WindSpeed.toString().padStart(3, ' '); + if (data.WindGust) fill['wind-gusts'] = `Gusts to ${data.WindGust}`; - if (data.WindGust) draw.text(this.context, 'Star4000 Extended', '24pt', '#FFFFFF', 80, 375, `Gusts to ${data.WindGust}`, 2); + fill.location = utils.string.locationCleanup(this.data.station.properties.name).substr(0, 20); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFF00', 315, 120, this.data.station.properties.name.substr(0, 20), 2); - - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 165, 'Humidity:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 165, `${data.Humidity}%`, 2, 'right'); - - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 205, 'Dewpoint:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 205, data.DewPoint + String.fromCharCode(176), 2, 'right'); - - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 245, 'Ceiling:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 245, (data.Ceiling === '' ? 'Unlimited' : data.Ceiling + data.CeilingUnit), 2, 'right'); - - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 285, 'Visibility:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 285, data.Visibility + data.VisibilityUnit, 2, 'right'); - - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 325, 'Pressure:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 535, 325, data.Pressure, 2, 'right'); - - switch (data.PressureDirection) { - case 'R': - // Shadow - draw.triangle(this.context, '#000000', 552, 302, 542, 312, 562, 312); - draw.box(this.context, '#000000', 549, 312, 6, 15); - - // Border - draw.triangle(this.context, '#000000', 550, 300, 540, 310, 560, 310); - draw.box(this.context, '#000000', 547, 310, 6, 15); - - // Fill - draw.triangle(this.context, '#FFFF00', 550, 301, 541, 309, 559, 309); - draw.box(this.context, '#FFFF00', 548, 309, 4, 15); - break; - case 'F': - // Shadow - draw.triangle(this.context, '#000000', 552, 327, 542, 317, 562, 317); - draw.box(this.context, '#000000', 549, 302, 6, 15); - - // Border - draw.triangle(this.context, '#000000', 550, 325, 540, 315, 560, 315); - draw.box(this.context, '#000000', 547, 300, 6, 15); - - // Fill - draw.triangle(this.context, '#FFFF00', 550, 324, 541, 314, 559, 314); - draw.box(this.context, '#FFFF00', 548, 301, 4, 15); - break; - default: - } + fill.humidity = `${data.Humidity}%`; + fill.dewpoint = data.DewPoint + String.fromCharCode(176); + fill.ceiling = (data.Ceiling === 0 ? 'Unlimited' : data.Ceiling + data.CeilingUnit); + fill.visibility = data.Visibility + data.VisibilityUnit; + fill.pressure = `${data.Pressure} ${data.PressureDirection}`; if (data.observations.heatIndex.value && data.HeatIndex !== data.Temperature) { - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Heat Index:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, data.HeatIndex + String.fromCharCode(176), 2, 'right'); + fill['heat-index-label'] = 'Heat Index:'; + fill['heat-index'] = data.HeatIndex + String.fromCharCode(176); } else if (data.observations.windChill.value && data.WindChill !== '' && data.WindChill < data.Temperature) { - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 340, 365, 'Wind Chill:', 2); - draw.text(this.context, 'Star4000 Large', 'bold 16pt', '#FFFFFF', 560, 365, data.WindChill + String.fromCharCode(176), 2, 'right'); + fill['heat-index-label'] = 'Wind Chill:'; + fill['heat-index'] = data.WindChill + String.fromCharCode(176); } - // get main icon - this.gifs.push(await utils.image.superGifAsync({ - src: data.Icon, - auto_play: true, - canvas: this.canvas, - x: 140, - y: 175, - max_width: 126, - })); + fill.icon = { type: 'img', src: data.Icon }; + + const area = this.elem.querySelector('.main'); + + area.innerHTML = ''; + area.append(this.fillTemplate('weather', fill)); this.finishDraw(); } diff --git a/server/scripts/modules/currentweatherscroll.js b/server/scripts/modules/currentweatherscroll.js index 7e6efe5..5ac9042 100644 --- a/server/scripts/modules/currentweatherscroll.js +++ b/server/scripts/modules/currentweatherscroll.js @@ -1,4 +1,4 @@ -/* globals draw, navigation */ +/* globals navigation, utils */ // eslint-disable-next-line no-unused-vars const currentWeatherScroll = (() => { @@ -6,25 +6,13 @@ const currentWeatherScroll = (() => { const degree = String.fromCharCode(176); // local variables - let context; // currently active context - let blankDrawArea; // original state of context let interval; let screenIndex = 0; // start drawing conditions // reset starts from the first item in the text scroll list - const start = (_context) => { - // see if there is a context available - if (!_context) return; + const start = () => { // store see if the context is new - if (_context !== context) { - // clean the outgoing context - cleanLastContext(); - // store the new blank context - blankDrawArea = _context.getImageData(0, 405, 640, 75); - } - // store the context locally - context = _context; // set up the interval if needed if (!interval) { @@ -36,17 +24,10 @@ const currentWeatherScroll = (() => { }; const stop = (reset) => { - cleanLastContext(); if (interval) interval = clearInterval(interval); if (reset) screenIndex = 0; }; - const cleanLastContext = () => { - if (blankDrawArea) context.putImageData(blankDrawArea, 0, 405); - blankDrawArea = undefined; - context = undefined; - }; - // increment interval, roll over const incrementInterval = () => { screenIndex = (screenIndex + 1) % (screens.length); @@ -61,16 +42,13 @@ const currentWeatherScroll = (() => { // nothing to do if there's no data yet if (!data) return; - // clean up any old text - context.putImageData(blankDrawArea, 0, 405); - drawCondition(screens[screenIndex](data)); }; // the "screens" are stored in an array for easy addition and removal const screens = [ // station name - (data) => `Conditions at ${data.station.properties.name.substr(0, 20)}`, + (data) => `Conditions at ${utils.string.locationCleanup(data.station.properties.name).substr(0, 20)}`, // temperature (data) => { @@ -109,7 +87,10 @@ const currentWeatherScroll = (() => { // internal draw function with preset parameters const drawCondition = (text) => { - draw.text(context, 'Star4000', '24pt', '#ffffff', 70, 430, text, 2); + // update all html scroll elements + utils.elem.forEach('.weather-display .scroll .fixed', (elem) => { + elem.innerHTML = text; + }); }; // return the api diff --git a/server/scripts/modules/extendedforecast.js b/server/scripts/modules/extendedforecast.js index 246d5e1..ca64ead 100644 --- a/server/scripts/modules/extendedforecast.js +++ b/server/scripts/modules/extendedforecast.js @@ -1,18 +1,15 @@ // display extended forecast graphically // technically uses the same data as the local forecast, we'll let the browser do the caching of that -/* globals WeatherDisplay, utils, STATUS, UNITS, draw, icons, navigation, luxon */ +/* globals WeatherDisplay, utils, STATUS, UNITS, icons, navigation, luxon */ // eslint-disable-next-line no-unused-vars class ExtendedForecast extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Extended Forecast'); + super(navId, elemId, 'Extended Forecast', true); // set timings this.timing.totalScreens = 2; - - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround2_1.png'); } async getData(_weatherParameters) { @@ -85,14 +82,18 @@ class ExtendedForecast extends WeatherDisplay { } static shortenExtendedForecastText(long) { - let short = long; - short = short.replace(/ and /g, ' '); - short = short.replace(/Slight /g, ''); - short = short.replace(/Chance /g, ''); - short = short.replace(/Very /g, ''); - short = short.replace(/Patchy /g, ''); - short = short.replace(/Areas /g, ''); - short = short.replace(/Dense /g, ''); + const regexList = [ + [/ and /ig, ' '], + [/Slight /ig, ''], + [/Chance /ig, ''], + [/Very /ig, ''], + [/Patchy /ig, ''], + [/Areas /ig, ''], + [/Dense /ig, ''], + [/Thunderstorm/g, 'T\'Storm'], + ]; + // run all regexes + const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long); let conditions = short.split(' '); if (short.indexOf('then') !== -1) { @@ -113,12 +114,12 @@ class ExtendedForecast extends WeatherDisplay { short2 = ''; } } - short = short1; + let result = short1; if (short2 !== '') { - short += ` ${short2}`; + result += ` ${short2}`; } - return [short, short1, short2]; + return result; } async drawCanvas() { @@ -128,45 +129,32 @@ class ExtendedForecast extends WeatherDisplay { // grab the first three or second set of three array elements const forecast = this.data.slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3); - const backgroundImage = await this.backgroundImage; + // create each day template + const days = forecast.map((Day) => { + const fill = {}; + fill.date = Day.dayName; - this.context.drawImage(backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.horizontalGradientSingle(this.context, 0, 90, 640, 399, draw.sideColor1, draw.sideColor2); - this.context.drawImage(backgroundImage, 38, 100, 174, 297, 38, 100, 174, 297); - this.context.drawImage(backgroundImage, 232, 100, 174, 297, 232, 100, 174, 297); - this.context.drawImage(backgroundImage, 426, 100, 174, 297, 426, 100, 174, 297); - - draw.titleText(this.context, 'Extended', 'Forecast'); - - await Promise.all(forecast.map(async (Day, Index) => { - const offset = Index * 195; - draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 100 + offset, 135, Day.dayName.toUpperCase(), 2); - draw.text(this.context, 'Star4000', '24pt', '#8080FF', 85 + offset, 345, 'Lo', 2, 'center'); - draw.text(this.context, 'Star4000', '24pt', '#FFFF00', 165 + offset, 345, 'Hi', 2, 'center'); let { low } = Day; if (low !== undefined) { if (navigation.units() === UNITS.metric) low = utils.units.fahrenheitToCelsius(low); - draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 85 + offset, 385, low, 2, 'center'); + fill['value-lo'] = Math.round(low); } let { high } = Day; if (navigation.units() === UNITS.metric) high = utils.units.fahrenheitToCelsius(high); - draw.text(this.context, 'Star4000 Large', '24pt', '#FFFFFF', 165 + offset, 385, high, 2, 'center'); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + offset, 270, Day.text[1], 2, 'center'); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 120 + offset, 310, Day.text[2], 2, 'center'); + fill['value-hi'] = Math.round(high); + fill.condition = Day.text; // draw the icon - this.gifs.push(await utils.image.superGifAsync({ - src: Day.icon, - auto_play: true, - canvas: this.canvas, - x: 70 + Index * 195, - y: 150, - max_height: 75, - })); - })); + fill.icon = { type: 'img', src: Day.icon }; + // return the filled template + return this.fillTemplate('day', fill); + }); + + // empty and update the container + const dayContainer = this.elem.querySelector('.day-container'); + dayContainer.innerHTML = ''; + dayContainer.append(...days); this.finishDraw(); } } diff --git a/server/scripts/modules/hourly.js b/server/scripts/modules/hourly.js index 1f4c85c..a8aadab 100644 --- a/server/scripts/modules/hourly.js +++ b/server/scripts/modules/hourly.js @@ -1,22 +1,17 @@ // hourly forecast list -/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, icons, luxon */ +/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon */ // eslint-disable-next-line no-unused-vars class Hourly extends WeatherDisplay { constructor(navId, elemId, defaultActive) { // special height and width for scrolling super(navId, elemId, 'Hourly Forecast', defaultActive); - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround6_1.png'); - - // height of one hour in the forecast - this.hourHeight = 72; // set up the timing this.timing.baseDelay = 20; // 24 hours = 6 pages const pages = 4; // first page is already displayed, last page doesn't happen - const timingStep = this.hourHeight * 4; + const timingStep = 75 * 4; this.timing.delay = [150 + timingStep]; // add additional pages for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep); @@ -35,6 +30,7 @@ class Hourly extends WeatherDisplay { console.error('Get hourly forecast failed'); console.error(e.status, e.responseJSON); this.setStatus(STATUS.failed); + return; } this.data = await Hourly.parseForecast(forecast.properties); @@ -114,52 +110,26 @@ class Hourly extends WeatherDisplay { } async drawLongCanvas() { - // create the "long" canvas if necessary - if (!this.longCanvas) { - this.longCanvas = document.createElement('canvas'); - this.longCanvas.width = 640; - this.longCanvas.height = 24 * this.hourHeight; - this.longContext = this.longCanvas.getContext('2d'); - this.longCanvasGifs = []; - } - - // stop all gifs - this.longCanvasGifs.forEach((gif) => gif.pause()); - // delete the gifs - this.longCanvasGifs.length = 0; - - // clean up existing gifs - this.gifs.forEach((gif) => gif.pause()); - // delete the gifs - this.gifs.length = 0; - - this.longContext.clearRect(0, 0, this.longCanvas.width, this.longCanvas.height); - - // draw the "long" canvas with all cities - draw.box(this.longContext, 'rgb(35, 50, 112)', 0, 0, 640, 24 * this.hourHeight); - - for (let i = 0; i <= 4; i += 1) { - const y = i * 346; - draw.horizontalGradient(this.longContext, 0, y, 640, y + 346, '#102080', '#001040'); - } + // get the list element and populate + const list = this.elem.querySelector('.hourly-lines'); + list.innerHTML = ''; const startingHour = luxon.DateTime.local(); - await Promise.all(this.data.map(async (data, index) => { - // calculate base y value - const y = 50 + this.hourHeight * index; - + const lines = this.data.map((data, index) => { + const fillValues = {}; // hour const hour = startingHour.plus({ hours: index }); const formattedHour = hour.toLocaleString({ weekday: 'short', hour: 'numeric' }); - draw.text(this.longContext, 'Star4000 Large Compressed', '24pt', '#FFFF00', 80, y, formattedHour, 2); + 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); - draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', 390, y, temperature, 2, 'center'); + fillValues.temp = temperature; // only plot apparent temperature if there is a difference - if (temperature !== feelsLike) draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', 470, y, feelsLike, 2, 'center'); + // if (temperature !== feelsLike) line.querySelector('.like').innerHTML = feelsLike; + if (temperature !== feelsLike) fillValues.like = feelsLike; // wind let wind = 'Calm'; @@ -167,44 +137,25 @@ class Hourly extends WeatherDisplay { const windSpeed = Math.round(data.windSpeed).toString(); wind = data.windDirection + (Array(6 - data.windDirection.length - windSpeed.length).join(' ')) + windSpeed; } - draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', 580, y, wind, 2, 'center'); + fillValues.wind = wind; - this.longCanvasGifs.push(await utils.image.superGifAsync({ - src: data.icon, - auto_play: true, - canvas: this.longCanvas, - x: 290, - y: y - 35, - max_width: 47, - })); - })); + // image + fillValues.icon = { type: 'img', src: data.icon }; + + return this.fillTemplate('hourly-row', fillValues); + }); + + list.append(...lines); } - async drawCanvas() { - // there are technically 2 canvases: the standard canvas and the extra-long canvas that contains the complete - // list of cities. The second canvas is copied into the standard canvas to create the scroll + drawCanvas() { super.drawCanvas(); - - // draw the standard context - this.context.drawImage(await this.backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - - draw.titleText(this.context, 'Hourly Forecast'); - - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 390, 105, 'TEMP', 2, 'center'); - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 470, 105, 'LIKE', 2, 'center'); - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 580, 105, 'WIND', 2, 'center'); - - // copy the scrolled portion of the canvas for the initial run before the scrolling starts - this.context.drawImage(this.longCanvas, 0, 0, 640, 289, 0, 110, 640, 289); - this.finishDraw(); } - async showCanvas() { - // special to travel forecast to draw the remainder of the canvas - await this.drawCanvas(); + showCanvas() { + // special to hourly to draw the remainder of the canvas + this.drawCanvas(); super.showCanvas(); } @@ -215,17 +166,14 @@ class Hourly extends WeatherDisplay { // base count change callback baseCountChange(count) { - // get a fresh canvas - const longCanvas = this.getLongCanvas(); - // calculate scroll offset and don't go past end - let offsetY = Math.min(longCanvas.height - 289, (count - 150)); + let offsetY = Math.min(this.elem.querySelector('.hourly-lines').getBoundingClientRect().height - 289, (count - 150)); // don't let offset go negative if (offsetY < 0) offsetY = 0; // copy the scrolled portion of the canvas - this.context.drawImage(longCanvas, 0, offsetY, 640, 289, 0, 110, 640, 289); + this.elem.querySelector('.main').scrollTo(0, offsetY); } static getTravelCitiesDayName(cities) { @@ -241,9 +189,4 @@ class Hourly extends WeatherDisplay { return dayName; }, ''); } - - // necessary to get the lastest long canvas when scrolling - getLongCanvas() { - return this.longCanvas; - } } diff --git a/server/scripts/modules/icons.js b/server/scripts/modules/icons.js index c0c6c87..f9496e9 100644 --- a/server/scripts/modules/icons.js +++ b/server/scripts/modules/icons.js @@ -29,6 +29,7 @@ const icons = (() => { case 'skc-n': case 'nskc': case 'nskc-n': + case 'cold-n': return addPath('Clear-1992.gif'); case 'bkn': @@ -135,6 +136,9 @@ const icons = (() => { case 'blizzard': return addPath('Blowing Snow.gif'); + case 'cold': + return addPath('cold.gif'); + default: console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`); return false; @@ -142,6 +146,8 @@ const icons = (() => { }; const getWeatherIconFromIconLink = (link, _isNightTime) => { + if (!link) return false; + // internal function to add path to returned icon const addPath = (icon) => `images/${icon}`; // extract day or night if not provided @@ -164,11 +170,13 @@ const icons = (() => { 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 'sct': diff --git a/server/scripts/modules/latestobservations.js b/server/scripts/modules/latestobservations.js index b85ebb7..baaa0d5 100644 --- a/server/scripts/modules/latestobservations.js +++ b/server/scripts/modules/latestobservations.js @@ -1,12 +1,10 @@ // current weather conditions display -/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, StationInfo */ +/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, StationInfo */ // eslint-disable-next-line no-unused-vars class LatestObservations extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Latest Observations'); - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround1_1.png'); + super(navId, elemId, 'Latest Observations', true); // constants this.MaximumRegionalStations = 7; @@ -67,25 +65,15 @@ class LatestObservations extends WeatherDisplay { // sort array by station name const sortedConditions = conditions.sort((a, b) => ((a.Name < b.Name) ? -1 : 1)); - this.context.drawImage(await this.backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2); - draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2); - - draw.titleText(this.context, 'Latest', 'Observations'); - if (navigation.units() === UNITS.english) { - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 295, 105, `${String.fromCharCode(176)}F`, 2); + this.elem.querySelector('.column-headers .temp.english').classList.add('show'); + this.elem.querySelector('.column-headers .temp.metric').classList.remove('show'); } else { - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 295, 105, `${String.fromCharCode(176)}C`, 2); + this.elem.querySelector('.column-headers .temp.english').classList.remove('show'); + this.elem.querySelector('.column-headers .temp.metric').classList.add('show'); } - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 345, 105, 'WEATHER', 2); - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFFFF', 495, 105, 'WIND', 2); - let y = 140; - - sortedConditions.forEach((condition) => { + const lines = sortedConditions.map((condition) => { let Temperature = condition.temperature.value; let WindSpeed = condition.windSpeed.value; const windDirection = utils.calc.directionToNSEW(condition.windDirection.value); @@ -94,23 +82,28 @@ class LatestObservations extends WeatherDisplay { Temperature = utils.units.celsiusToFahrenheit(Temperature); WindSpeed = utils.units.kphToMph(WindSpeed); } + WindSpeed = Math.round(WindSpeed); + Temperature = Math.round(Temperature); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 65, y, condition.city.substr(0, 14), 2); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 345, y, LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9), 2); - + const fill = {}; + fill.location = utils.string.locationCleanup(condition.city).substr(0, 14); + fill.temp = Temperature; + fill.weather = LatestObservations.shortenCurrentConditions(condition.textDescription).substr(0, 9); if (WindSpeed > 0) { - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString(), 2); + fill.wind = windDirection + (Array(6 - windDirection.length - WindSpeed.toString().length).join(' ')) + WindSpeed.toString(); } else if (WindSpeed === 'NA') { - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'NA', 2); + fill.wind = 'NA'; } else { - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 495, y, 'Calm', 2); + fill.wind = 'Calm'; } - const x = (325 - (Temperature.toString().length * 15)); - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', x, y, Temperature, 2); - - y += 40; + return this.fillTemplate('observation-row', fill); }); + + const linesContainer = this.elem.querySelector('.observation-lines'); + linesContainer.innerHTML = ''; + linesContainer.append(...lines); + this.finishDraw(); } diff --git a/server/scripts/modules/localforecast.js b/server/scripts/modules/localforecast.js index 5b2e884..bc0612e 100644 --- a/server/scripts/modules/localforecast.js +++ b/server/scripts/modules/localforecast.js @@ -1,17 +1,14 @@ // display text based local forecast -/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation */ +/* globals WeatherDisplay, utils, STATUS, UNITS, navigation */ // eslint-disable-next-line no-unused-vars class LocalForecast extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Local Forecast'); + super(navId, elemId, 'Local Forecast', true); // set timings this.timing.baseDelay = 5000; - - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround1_1.png'); } async getData(_weatherParameters) { @@ -28,14 +25,8 @@ class LocalForecast extends WeatherDisplay { // parse raw data const conditions = LocalForecast.parse(rawData); - // split this forecast into the correct number of screens - const maxRows = 7; - const maxCols = 32; - - this.screenTexts = []; - // read each text - conditions.forEach((condition) => { + this.screenTexts = conditions.map((condition) => { // process the text let text = `${condition.DayName.toUpperCase()}...`; let conditionText = condition.Text; @@ -44,44 +35,23 @@ class LocalForecast extends WeatherDisplay { } text += conditionText.toUpperCase().replace('...', ' '); - text = utils.string.wordWrap(text, maxCols, '\n'); - const lines = text.split('\n'); - const lineCount = lines.length; - let ScreenText = ''; - const maxRowCount = maxRows; - let rowCount = 0; - - // if (PrependAlert) { - // ScreenText = LocalForecastScreenTexts[LocalForecastScreenTexts.length - 1]; - // rowCount = ScreenText.split('\n').length - 1; - // } - - for (let i = 0; i <= lineCount - 1; i += 1) { - if (lines[i] !== '') { - if (rowCount > maxRowCount - 1) { - // if (PrependAlert) { - // LocalForecastScreenTexts[LocalForecastScreenTexts.length - 1] = ScreenText; - // PrependAlert = false; - // } else { - this.screenTexts.push(ScreenText); - // } - ScreenText = ''; - rowCount = 0; - } - - ScreenText += `${lines[i]}\n`; - rowCount += 1; - } - } - // if (PrependAlert) { - // this.screenTexts[this.screenTexts.length - 1] = ScreenText; - // PrependAlert = false; - // } else { - this.screenTexts.push(ScreenText); - // } + return text; }); - this.timing.totalScreens = this.screenTexts.length; + // fill the forecast texts + const templates = this.screenTexts.map((text) => this.fillTemplate('forecast', { text })); + const forecastsElem = this.elem.querySelector('.forecasts'); + forecastsElem.innerHTML = ''; + forecastsElem.append(...templates); + + // increase each forecast height to a multiple of container height + this.pageHeight = forecastsElem.parentNode.getBoundingClientRect().height; + templates.forEach((forecast) => { + const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight; + forecast.style.height = `${newHeight}px`; + }); + + this.timing.totalScreens = forecastsElem.scrollHeight / this.pageHeight; this.calcNavTiming(); this.setStatus(STATUS.loaded); } @@ -105,25 +75,12 @@ class LocalForecast extends WeatherDisplay { } } - // TODO: alerts needs a cleanup - // TODO: second page of screenTexts when needed async drawCanvas() { super.drawCanvas(); - this.context.drawImage(await this.backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2); - draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2); + const top = -this.screenIndex * this.pageHeight; + this.elem.querySelector('.forecasts').style.top = `${top}px`; - draw.titleText(this.context, 'Local ', 'Forecast'); - - // clear existing text - draw.box(this.context, 'rgb(33, 40, 90)', 65, 105, 505, 280); - // Draw the text. - this.screenTexts[this.screenIndex].split('\n').forEach((text, index) => { - draw.text(this.context, 'Star4000', '24pt', '#FFFFFF', 75, 140 + 40 * index, text, 2); - }); this.finishDraw(); } diff --git a/server/scripts/modules/navigation.js b/server/scripts/modules/navigation.js index 8624f70..e4577b4 100644 --- a/server/scripts/modules/navigation.js +++ b/server/scripts/modules/navigation.js @@ -23,7 +23,8 @@ const navigation = (() => { let almanac; const init = async () => { - // nothing to do + // set up resize handler + window.addEventListener('resize', resize); }; const message = (data) => { @@ -87,22 +88,22 @@ const navigation = (() => { // draw the progress canvas and hide others hideAllCanvases(); document.getElementById('loading').style.display = 'none'; - progress = new Progress(-1, 'progress'); + if (!progress) progress = new Progress(-1, 'progress'); await progress.drawCanvas(); progress.showCanvas(); // start loading canvases if necessary if (displays.length === 0) { - currentWeather = new CurrentWeather(0, 'currentWeather'); + currentWeather = new CurrentWeather(0, 'current-weather'); almanac = new Almanac(7, 'almanac'); displays = [ currentWeather, - new LatestObservations(1, 'latestObservations'), + new LatestObservations(1, 'latest-observations'), new Hourly(2, 'hourly'), - new TravelForecast(3, 'travelForecast', false), // not active by default - new RegionalForecast(4, 'regionalForecast'), - new LocalForecast(5, 'localForecast'), - new ExtendedForecast(6, 'extendedForecast'), + new TravelForecast(3, 'travel', false), // not active by default + new RegionalForecast(4, 'regional-forecast'), + new LocalForecast(5, 'local-forecast'), + new ExtendedForecast(6, 'extended-forecast'), almanac, new Radar(8, 'radar'), ]; @@ -177,7 +178,15 @@ const navigation = (() => { progress.hideCanvas(); if (!current) { // special case for no active displays (typically on progress screen) - displays[0].navNext(msg.command.firstFrame); + // find the first ready display + let firstDisplay; + let displayCount = 0; + do { + if (displays[displayCount].status === STATUS.loaded) firstDisplay = displays[displayCount]; + displayCount += 1; + } while (!firstDisplay && displayCount < displays.length); + + firstDisplay.navNext(msg.command.firstFrame); return; } if (direction === msg.command.nextFrame) currentDisplay().navNext(); @@ -266,6 +275,25 @@ const navigation = (() => { return almanac.getSun(); }; + // resize the container on a page resize + const resize = () => { + const widthZoomPercent = window.innerWidth / 640; + const heightZoomPercent = window.innerHeight / 480; + + const scale = Math.min(widthZoomPercent, heightZoomPercent); + + if (scale < 1.0 || document.fullscreenElement) { + document.getElementById('container').style.zoom = scale; + } else { + document.getElementById('container').style.zoom = 1; + } + }; + + // reset all statuses to loading on all displays, used to keep the progress bar accurate during refresh + const resetStatuses = () => { + displays.forEach((display) => { display.status = STATUS.loading; }); + }; + return { init, message, @@ -277,5 +305,7 @@ const navigation = (() => { getDisplay, getCurrentWeather, getSun, + resize, + resetStatuses, }; })(); diff --git a/server/scripts/modules/progress.js b/server/scripts/modules/progress.js index 6e3ca23..ee7ebd4 100644 --- a/server/scripts/modules/progress.js +++ b/server/scripts/modules/progress.js @@ -1,11 +1,11 @@ // regional forecast and observations -/* globals WeatherDisplay, utils, STATUS, draw, navigation */ +/* globals WeatherDisplay, utils, STATUS, navigation */ // eslint-disable-next-line no-unused-vars class Progress extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId); + super(navId, elemId, '', false); // pre-load background image (returns promise) this.backgroundImage = utils.image.load('images/BackGround1_1.png'); @@ -14,101 +14,90 @@ class Progress extends WeatherDisplay { this.timing = false; this.version = document.getElementById('version').innerHTML; + + // setup event listener + this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this)); } async drawCanvas(displays, loadedCount) { super.drawCanvas(); - // set up an event listener - if (!this.eventListener) { - this.eventListener = true; - this.canvas.addEventListener('click', (e) => this.canvasClick(e), false); - } - // get the background image - const backgroundImage = await this.backgroundImage; + // get the progress bar cover (makes percentage) + if (!this.progressCover) this.progressCover = this.elem.querySelector('.scroll .cover'); - // only draw the background once - if (!this.backgroundDrawn) { - this.context.drawImage(backgroundImage, 0, 0, 640, 480, 0, 0, 640, 480); - draw.horizontalGradientSingle(this.context, 0, 90, 52, 399, draw.sideColor1, draw.sideColor2); - draw.horizontalGradientSingle(this.context, 584, 90, 640, 399, draw.sideColor1, draw.sideColor2); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - draw.titleText(this.context, 'WeatherStar', `4000+ ${this.version}`); - } - - this.finishDraw(); // if no displays provided just draw the backgrounds (above) if (!displays) return; - displays.forEach((display, idx) => { - const y = 120 + idx * 29; - const dots = Array(120 - Math.floor(display.name.length * 2.5)).join('.'); - draw.text(this.context, 'Star4000 Extended', '19pt', '#ffffff', 70, y, display.name + dots, 2); + const lines = displays.map((display, index) => { + const fill = {}; - let statusText; - let statusColor; + fill.name = display.name; + + let statusClass; switch (display.status) { case STATUS.loading: - statusText = 'Loading'; - statusColor = '#ffff00'; + statusClass = 'loading'; break; case STATUS.loaded: - statusText = 'Press Here'; - statusColor = '#00ff00'; - this.context.drawImage(backgroundImage, 440, y - 20, 75, 25, 440, y - 20, 75, 25); + statusClass = 'press-here'; break; case STATUS.failed: - statusText = 'Failed'; - statusColor = '#ff0000'; + statusClass = 'failed'; break; case STATUS.noData: - statusText = 'No Data'; - statusColor = '#C0C0C0'; - draw.box(this.context, 'rgb(33, 40, 90)', 475, y - 15, 75, 15); + statusClass = 'no-data'; break; case STATUS.disabled: - statusText = 'Disabled'; - statusColor = '#C0C0C0'; - this.context.drawImage(backgroundImage, 470, y - 20, 45, 25, 470, y - 20, 45, 25); + statusClass = 'disabled'; break; default: } - // Erase any dots that spill into the status text. - this.context.drawImage(backgroundImage, 475, y - 20, 165, 30, 475, y - 20, 165, 30); - draw.text(this.context, 'Star4000 Extended', '19pt', statusColor, 565, y, statusText, 2, 'end'); - }); + + // make the line + const line = this.fillTemplate('item', fill); + // because of timing, this might get called before the template is loaded + if (!line) return false; + + // update the status + const links = line.querySelector('.links'); + links.classList.remove('loading'); + links.classList.add(statusClass); + links.dataset.index = index; + return line; + }).filter((d) => d); + + // get the container and update + const container = this.elem.querySelector('.container'); + container.innerHTML = ''; + container.append(...lines); + + this.finishDraw(); // calculate loaded percent const loadedPercent = (loadedCount / displays.length); + this.progressCover.style.width = `${(1.0 - loadedPercent) * 100}%`; if (loadedPercent < 1.0) { - // Draw a box for the progress. - draw.box(this.context, '#000000', 51, 428, 534, 22); - draw.box(this.context, '#ffffff', 53, 430, 530, 18); - // update the progress gif - draw.box(this.context, '#1d7fff', 55, 432, 526 * loadedPercent, 14); + // show the progress bar and set width + this.progressCover.parentNode.classList.add('show'); } else { - // restore the background - this.context.drawImage(backgroundImage, 51, 428, 534, 22, 51, 428, 534, 22); + // hide the progressbar after 1 second (lines up with with width transition animation) + setTimeout(() => this.progressCover.parentNode.classList.remove('show'), 1000); } } - canvasClick(e) { - const x = e.offsetX; - const y = e.offsetY; - // eliminate off canvas and outside area clicks - if (!this.isActive()) return; - if (y < 100 || y > 410) return; - if (x < 440 || x > 570) return; + lineClick(e) { + // get index + const indexRaw = e.target?.parentNode?.dataset?.index; + if (indexRaw === undefined) return; + const index = +indexRaw; // stop playing navigation.message('navButton'); // use the y value to determine an index - const index = Math.floor((y - 100) / 29); const display = navigation.getDisplay(index); if (display && display.status === STATUS.loaded) { display.showCanvas(navigation.msg.command.firstFrame); - this.hideCanvas(); + this.elem.classList.remove('show'); } } } diff --git a/server/scripts/modules/radar.js b/server/scripts/modules/radar.js index fa431d9..fbab92b 100644 --- a/server/scripts/modules/radar.js +++ b/server/scripts/modules/radar.js @@ -1,10 +1,10 @@ // current weather conditions display -/* globals WeatherDisplay, utils, STATUS, draw, luxon */ +/* globals WeatherDisplay, utils, STATUS, luxon */ // eslint-disable-next-line no-unused-vars class Radar extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Local Radar'); + super(navId, elemId, 'Local Radar', true); // set max images this.dopplerRadarImageMax = 6; @@ -31,9 +31,6 @@ class Radar extends WeatherDisplay { { time: 1, si: 4 }, { time: 12, si: 5 }, ]; - - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround4_1.png'); } async getData(_weatherParameters) { @@ -163,7 +160,6 @@ class Radar extends WeatherDisplay { const imgBlob = await utils.image.load(blob); // draw the entire image - workingContext.clearRect(0, 0, width, 1600); workingContext.drawImage(imgBlob, 0, 0, width, 1600); @@ -174,7 +170,7 @@ class Radar extends WeatherDisplay { const cropCanvas = document.createElement('canvas'); cropCanvas.width = 640; cropCanvas.height = 367; - const cropContext = cropCanvas.getContext('2d'); + const cropContext = cropCanvas.getContext('2d', { willReadFrequently: true }); cropContext.imageSmoothingEnabled = false; cropContext.drawImage(workingCanvas, radarSourceX, radarSourceY, (radarOffsetX * 2), (radarOffsetY * 2.33), 0, 0, 640, 367); // clean the image @@ -183,11 +179,20 @@ class Radar extends WeatherDisplay { // merge the radar and map Radar.mergeDopplerRadarImage(context, cropContext); + const elem = this.fillTemplate('frame', { map: { type: 'img', src: canvas.toDataURL() } }); + return { canvas, time, + elem, }; })); + + // put the elements in the container + const scrollArea = this.elem.querySelector('.scroll-area'); + scrollArea.innerHTML = ''; + scrollArea.append(...radarInfo.map((r) => r.elem)); + // set max length this.timing.totalScreens = radarInfo.length; // store the images @@ -199,31 +204,13 @@ class Radar extends WeatherDisplay { async drawCanvas() { super.drawCanvas(); - if (this.screenIndex === -1) return; - this.context.drawImage(await this.backgroundImage, 0, 0); const { DateTime } = luxon; - // Title - draw.text(this.context, 'Arial', 'bold 28pt', '#ffffff', 155, 60, 'Local', 2); - draw.text(this.context, 'Arial', 'bold 28pt', '#ffffff', 155, 95, 'Radar', 2); + const time = this.times[this.screenIndex].toLocaleString(DateTime.TIME_SIMPLE); + const timePadded = time.length >= 8 ? time : ` ${time}`; + this.elem.querySelector('.header .right .time').innerHTML = timePadded; - draw.text(this.context, 'Star4000', 'bold 18pt', '#ffffff', 438, 49, 'PRECIP', 2, 'center'); - draw.text(this.context, 'Star4000', 'bold 18pt', '#ffffff', 298, 73, 'Light', 2); - draw.text(this.context, 'Star4000', 'bold 18pt', '#ffffff', 517, 73, 'Heavy', 2); - - let x = 362; - const y = 52; - draw.box(this.context, '#000000', x - 2, y - 2, 154, 28); - draw.box(this.context, 'rgb(49, 210, 22)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(28, 138, 18)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(20, 90, 15)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(10, 40, 10)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(196, 179, 70)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(190, 72, 19)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(171, 14, 14)', x, y, 17, 24); x += 19; - draw.box(this.context, 'rgb(115, 31, 4)', x, y, 17, 24); x += 19; - - this.context.drawImage(this.data[this.screenIndex], 0, 0, 640, 367, 0, 113, 640, 367); - draw.text(this.context, 'Star4000 Small', '24pt', '#ffffff', 438, 105, this.times[this.screenIndex].toLocaleString(DateTime.TIME_SIMPLE), 2, 'center'); + // scroll to image + this.elem.querySelector('.scroll-area').style.top = `${-this.screenIndex * 371}px`; this.finishDraw(); } diff --git a/server/scripts/modules/regionalforecast.js b/server/scripts/modules/regionalforecast.js index f29697a..04d2722 100644 --- a/server/scripts/modules/regionalforecast.js +++ b/server/scripts/modules/regionalforecast.js @@ -1,15 +1,12 @@ // regional forecast and observations // type 0 = observations, 1 = first forecast, 2 = second forecast -/* globals WeatherDisplay, utils, STATUS, icons, UNITS, draw, navigation, luxon, StationInfo, RegionalCities */ +/* globals WeatherDisplay, utils, STATUS, icons, UNITS, navigation, luxon, StationInfo, RegionalCities */ // eslint-disable-next-line no-unused-vars class RegionalForecast extends WeatherDisplay { constructor(navId, elemId) { - super(navId, elemId, 'Regional Forecast'); - - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround5_1.png'); + super(navId, elemId, 'Regional Forecast', true); // timings this.timing.totalScreens = 3; @@ -19,14 +16,14 @@ class RegionalForecast extends WeatherDisplay { super.getData(_weatherParameters); const weatherParameters = _weatherParameters ?? this.weatherParameters; - // pre-load the base map (returns promise) - let src = 'images/Basemap2.png'; + // pre-load the base map + let baseMap = 'images/Basemap2.png'; if (weatherParameters.state === 'HI') { - src = 'images/HawaiiRadarMap4.png'; + baseMap = 'images/HawaiiRadarMap4.png'; } else if (weatherParameters.state === 'AK') { - src = 'images/AlaskaRadarMap6.png'; + baseMap = 'images/AlaskaRadarMap6.png'; } - this.baseMap = utils.image.load(src); + this.elem.querySelector('.map img').src = baseMap; // map offset const offsetXY = { @@ -34,10 +31,10 @@ class RegionalForecast extends WeatherDisplay { y: 117, }; // get user's location in x/y - const sourceXY = this.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state); + const sourceXY = RegionalForecast.getXYFromLatitudeLongitude(weatherParameters.latitude, weatherParameters.longitude, offsetXY.x, offsetXY.y, weatherParameters.state); // get latitude and longitude limits - const minMaxLatLon = this.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state); + const minMaxLatLon = RegionalForecast.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, offsetXY.x, offsetXY.y, weatherParameters.state); // get a target distance let targetDistance = 2.5; @@ -66,7 +63,7 @@ class RegionalForecast extends WeatherDisplay { }); // get regional forecasts and observations (the two are intertwined due to the design of api.weather.gov) - const regionalForecastPromises = regionalCities.map(async (city) => { + const regionalDataAll = await Promise.all(regionalCities.map(async (city) => { try { // get the point first, then break down into forecast and observations const point = await utils.weather.getPoint(city.lat, city.lon); @@ -77,7 +74,7 @@ class RegionalForecast extends WeatherDisplay { const forecast = await utils.fetch.json(point.properties.forecast); // get XY on map for city - const cityXY = this.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state); + const cityXY = RegionalForecast.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, weatherParameters.state); // wait for the regional observation if it's not done yet const observation = await observationPromise; @@ -105,14 +102,12 @@ class RegionalForecast extends WeatherDisplay { RegionalForecast.buildForecast(forecast.properties.periods[2], city, cityXY), ]; } catch (e) { - console.log(`No regional forecast data for '${city.name}'`); + console.log(`No regional forecast data for '${city.name ?? city.city}'`); console.log(e); return false; } - }); + })); - // wait for the forecasts - const regionalDataAll = await Promise.all(regionalForecastPromises); // filter out any false (unavailable data) const regionalData = regionalDataAll.filter((data) => data); @@ -154,20 +149,21 @@ class RegionalForecast extends WeatherDisplay { // get the observation data const observation = await utils.fetch.json(`${station}/observations/latest`); // preload the image + if (!observation.properties.icon) return false; utils.image.preload(icons.getWeatherRegionalIconFromIconLink(observation.properties.icon, !observation.properties.daytime)); // return the observation return observation.properties; } catch (e) { - console.log(`Unable to get regional observations for ${city.Name}`); + console.log(`Unable to get regional observations for ${city.Name ?? city.city}`); console.error(e.status, e.responseJSON); return false; } } // utility latitude/pixel conversions - getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) { - if (state === 'AK') return this.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY); - if (state === 'HI') return this.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY); + static getXYFromLatitudeLongitude(Latitude, Longitude, OffsetX, OffsetY, state) { + if (state === 'AK') return RegionalForecast.getXYFromLatitudeLongitudeAK(Latitude, Longitude, OffsetX, OffsetY); + if (state === 'HI') return RegionalForecast.getXYFromLatitudeLongitudeHI(Latitude, Longitude, OffsetX, OffsetY); let y = 0; let x = 0; const ImgHeight = 1600; @@ -248,9 +244,9 @@ class RegionalForecast extends WeatherDisplay { return { x, y }; } - getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) { - if (state === 'AK') return this.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY); - if (state === 'HI') return this.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY); + static getMinMaxLatitudeLongitude(X, Y, OffsetX, OffsetY, state) { + if (state === 'AK') return RegionalForecast.getMinMaxLatitudeLongitudeAK(X, Y, OffsetX, OffsetY); + if (state === 'HI') return RegionalForecast.getMinMaxLatitudeLongitudeHI(X, Y, OffsetX, OffsetY); const maxLat = ((Y / 55.2) - 50.5) * -1; const minLat = (((Y + (OffsetY * 2)) / 55.2) - 50.5) * -1; const minLon = (((X * -1) / 41.775) + 127.5) * -1; @@ -283,9 +279,9 @@ class RegionalForecast extends WeatherDisplay { }; } - getXYForCity(City, MaxLatitude, MinLongitude, state) { - if (state === 'AK') this.getXYForCityAK(City, MaxLatitude, MinLongitude); - if (state === 'HI') this.getXYForCityHI(City, MaxLatitude, MinLongitude); + static getXYForCity(City, MaxLatitude, MinLongitude, state) { + if (state === 'AK') RegionalForecast.getXYForCityAK(City, MaxLatitude, MinLongitude); + if (state === 'HI') RegionalForecast.getXYForCityHI(City, MaxLatitude, MinLongitude); let x = (City.lon - MinLongitude) * 57; let y = (MaxLatitude - City.lat) * 70; @@ -328,61 +324,61 @@ class RegionalForecast extends WeatherDisplay { return city.match(/[^-;/\\,]*/)[0].substr(0, 12); } - async drawCanvas() { + drawCanvas() { super.drawCanvas(); // break up data into useful values const { regionalData: data, sourceXY, offsetXY } = this.data; - // fixed offset for all y values when drawing to the map - const mapYOff = 90; - const { DateTime } = luxon; // draw the header graphics - this.context.drawImage(await this.backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); // draw the appropriate title + const titleTop = this.elem.querySelector('.title.dual .top'); + const titleBottom = this.elem.querySelector('.title.dual .bottom'); if (this.screenIndex === 0) { - draw.titleText(this.context, 'Regional', 'Observations'); + titleTop.innerHTML = 'Regional'; + titleBottom.innerHTML = 'Observations'; } else { const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time); // get the name of the day const dayName = forecastDate.toLocaleString({ weekday: 'long' }); + titleTop.innerHTML = 'Forecast for'; // draw the title if (data[0][this.screenIndex].daytime) { - draw.titleText(this.context, 'Forecast for', dayName); + titleBottom.innerHTML = dayName; } else { - draw.titleText(this.context, 'Forecast for', `${dayName} Night`); + titleBottom.innerHTML = `${dayName} Night`; } } // draw the map - this.context.drawImage(await this.baseMap, sourceXY.x, sourceXY.y, (offsetXY.x * 2), (offsetXY.y * 2), 0, mapYOff, 640, 312); - await Promise.all(data.map(async (city) => { + const scale = 640 / (offsetXY.x * 2); + const map = this.elem.querySelector('.map'); + map.style.zoom = scale; + map.style.top = `-${sourceXY.y}px`; + map.style.left = `-${sourceXY.x}px`; + + const cities = data.map((city) => { + const fill = {}; const period = city[this.screenIndex]; - // draw the icon if possible - const icon = icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime); - if (icon) { - this.gifs.push(await utils.image.superGifAsync({ - src: icon, - max_width: 42, - auto_play: true, - canvas: this.canvas, - x: period.x, - y: period.y - 15 + mapYOff, - })); - } - // City Name - draw.text(this.context, 'Star4000', '20px', '#ffffff', period.x - 40, period.y - 15 + mapYOff, period.name, 2); - - // Temperature + fill.icon = { type: 'img', src: icons.getWeatherRegionalIconFromIconLink(period.icon, !period.daytime) }; + fill.city = period.name; let { temperature } = period; if (navigation.units() === UNITS.metric) temperature = Math.round(utils.units.fahrenheitToCelsius(temperature)); - draw.text(this.context, 'Star4000 Large Compressed', '28px', '#ffff00', period.x - (temperature.toString().length * 15), period.y + 20 + mapYOff, temperature, 2); - })); + fill.temp = temperature; + + const elem = this.fillTemplate('location', fill); + elem.style.left = `${period.x}px`; + elem.style.top = `${period.y}px`; + + return elem; + }); + + const locationContainer = this.elem.querySelector('.location-container'); + locationContainer.innerHTML = ''; + locationContainer.append(...cities); this.finishDraw(); } diff --git a/server/scripts/modules/travelforecast.js b/server/scripts/modules/travelforecast.js index e902919..dafe2d2 100644 --- a/server/scripts/modules/travelforecast.js +++ b/server/scripts/modules/travelforecast.js @@ -1,16 +1,11 @@ // travel forecast display -/* globals WeatherDisplay, utils, STATUS, UNITS, draw, navigation, icons, luxon, TravelCities */ +/* globals WeatherDisplay, utils, STATUS, UNITS, navigation, icons, luxon, TravelCities */ // eslint-disable-next-line no-unused-vars class TravelForecast extends WeatherDisplay { constructor(navId, elemId, defaultActive) { // special height and width for scrolling super(navId, elemId, 'Travel Forecast', defaultActive); - // pre-load background image (returns promise) - this.backgroundImage = utils.image.load('images/BackGround6_1.png'); - - // height of one city in the travel forecast - this.cityHeight = 72; // set up the timing this.timing.baseDelay = 20; @@ -18,7 +13,7 @@ class TravelForecast extends WeatherDisplay { const pagesFloat = TravelCities.length / 4; const pages = Math.floor(pagesFloat) - 2; // first page is already displayed, last page doesn't happen const extra = pages % 1; - const timingStep = this.cityHeight * 4; + const timingStep = 75 * 4; this.timing.delay = [150 + timingStep]; // add additional pages for (let i = 0; i < pages; i += 1) this.timing.delay.push(timingStep); @@ -49,7 +44,7 @@ class TravelForecast extends WeatherDisplay { } catch (e) { console.error(`GetTravelWeather for ${city.Name} failed`); console.error(e.status, e.responseJSON); - return { name: city.Name }; + return { name: city.Name, error: true }; } }); @@ -69,47 +64,23 @@ class TravelForecast extends WeatherDisplay { } async drawLongCanvas() { - // create the "long" canvas if necessary - if (!this.longCanvas) { - this.longCanvas = document.createElement('canvas'); - this.longCanvas.width = 640; - this.longCanvas.height = 1728; - this.longContext = this.longCanvas.getContext('2d'); - this.longCanvasGifs = []; - } - - // stop all gifs - this.longCanvasGifs.forEach((gif) => gif.pause()); - // delete the gifs - this.longCanvasGifs.length = 0; + // get the element and populate + const list = this.elem.querySelector('.travel-lines'); + list.innerHTML = ''; // set up variables const cities = this.data; - // clean up existing gifs - this.gifs.forEach((gif) => gif.pause()); - // delete the gifs - this.gifs.length = 0; - - this.longContext.clearRect(0, 0, this.longCanvas.width, this.longCanvas.height); - - // draw the "long" canvas with all cities - draw.box(this.longContext, 'rgb(35, 50, 112)', 0, 0, 640, TravelCities.length * this.cityHeight); - - for (let i = 0; i <= 4; i += 1) { - const y = i * 346; - draw.horizontalGradient(this.longContext, 0, y, 640, y + 346, '#102080', '#001040'); - } - - await Promise.all(cities.map(async (city, index) => { - // calculate base y value - const y = 50 + this.cityHeight * index; + const lines = cities.map((city) => { + if (city.error) return false; + const fillValues = {}; // city name - draw.text(this.longContext, 'Star4000 Large Compressed', '24pt', '#FFFF00', 80, y, city.name, 2); + fillValues.city = city; // check for forecast data if (city.icon) { + fillValues.city = city.name; // get temperatures and convert if necessary let { low, high } = city; @@ -122,25 +93,16 @@ class TravelForecast extends WeatherDisplay { const lowString = Math.round(low).toString(); const highString = Math.round(high).toString(); - const xLow = (500 - (lowString.length * 20)); - draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xLow, y, lowString, 2); + fillValues.low = lowString; + fillValues.high = highString; - const xHigh = (560 - (highString.length * 20)); - draw.text(this.longContext, 'Star4000 Large', '24pt', '#FFFF00', xHigh, y, highString, 2); - - this.longCanvasGifs.push(await utils.image.superGifAsync({ - src: city.icon, - auto_play: true, - canvas: this.longCanvas, - x: 330, - y: y - 35, - max_width: 47, - })); + fillValues.icon = { type: 'img', src: city.icon }; } else { - draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y - 18, 'NO TRAVEL', 2); - draw.text(this.longContext, 'Star4000 Small', '24pt', '#FFFFFF', 400, y, 'DATA AVAILABLE', 2); + fillValues.error = 'NO TRAVEL DATA AVAILABLE'; } - })); + return this.fillTemplate('travel-row', fillValues); + }).filter((d) => d); + list.append(...lines); } async drawCanvas() { @@ -151,18 +113,7 @@ class TravelForecast extends WeatherDisplay { // set up variables const cities = this.data; - // draw the standard context - this.context.drawImage(await this.backgroundImage, 0, 0); - draw.horizontalGradientSingle(this.context, 0, 30, 500, 90, draw.topColor1, draw.topColor2); - draw.triangle(this.context, 'rgb(28, 10, 87)', 500, 30, 450, 90, 500, 90); - - draw.titleText(this.context, 'Travel Forecast', `For ${TravelForecast.getTravelCitiesDayName(cities)}`); - - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 455, 105, 'LOW', 2); - draw.text(this.context, 'Star4000 Small', '24pt', '#FFFF00', 510, 105, 'HIGH', 2); - - // copy the scrolled portion of the canvas for the initial run before the scrolling starts - this.context.drawImage(this.longCanvas, 0, 0, 640, 289, 0, 110, 640, 289); + this.elem.querySelector('.header .title.dual .bottom').innerHTML = `For ${TravelForecast.getTravelCitiesDayName(cities)}`; this.finishDraw(); } @@ -180,17 +131,14 @@ class TravelForecast extends WeatherDisplay { // base count change callback baseCountChange(count) { - // get a fresh canvas - const longCanvas = this.getLongCanvas(); - // calculate scroll offset and don't go past end - let offsetY = Math.min(longCanvas.height - 289, (count - 150)); + let offsetY = Math.min(this.elem.querySelector('.travel-lines').getBoundingClientRect().height - 289, (count - 150)); // don't let offset go negative if (offsetY < 0) offsetY = 0; // copy the scrolled portion of the canvas - this.context.drawImage(longCanvas, 0, offsetY, 640, 289, 0, 110, 640, 289); + this.elem.querySelector('.main').scrollTo(0, offsetY); } static getTravelCitiesDayName(cities) { diff --git a/server/scripts/modules/utilities.js b/server/scripts/modules/utilities.js index 2a1ede2..6b95dfb 100644 --- a/server/scripts/modules/utilities.js +++ b/server/scripts/modules/utilities.js @@ -1,6 +1,5 @@ // radar utilities -/* globals SuperGif */ // eslint-disable-next-line no-unused-vars const utils = (() => { // ****************************** weather data ******************************** @@ -30,40 +29,17 @@ const utils = (() => { } }); - // async version of SuperGif - const superGifAsync = (e) => new Promise((resolve) => { - const gif = new SuperGif(e); - gif.load(() => resolve(gif)); - }); - // preload an image // the goal is to get it in the browser's cache so it is available more quickly when the browser needs it // a list of cached icons is used to avoid hitting the cache multiple times const cachedImages = []; const preload = (src) => { if (cachedImages.includes(src)) return false; - const img = new Image(); - img.scr = src; - cachedImages.push(src); + blob(src); + // cachedImages.push(src); return true; }; - // draw an image on a local canvas and return the context - const drawLocalCanvas = (img) => { - // create a canvas - const canvas = document.createElement('canvas'); - canvas.width = img.width; - canvas.height = img.height; - - // get the context - const context = canvas.getContext('2d'); - context.imageSmoothingEnabled = false; - - // draw the image - context.drawImage(img, 0, 0); - return context; - }; - // *********************************** unit conversions *********************** Math.round2 = (value, decimals) => Number(`${Math.round(`${value}e${decimals}`)}e-${decimals}`); @@ -136,99 +112,21 @@ const utils = (() => { const wrap = (x, m) => ((x % m) + m) % m; // ********************************* strings ********************************************* - const wordWrap = (_str, ...rest) => { - // discuss at: https://locutus.io/php/wordwrap/ - // original by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) - // improved by: Nick Callen - // improved by: Kevin van Zonneveld (https://kvz.io) - // improved by: Sakimori - // revised by: Jonas Raoni Soares Silva (https://www.jsfromhell.com) - // bugfixed by: Michael Grier - // bugfixed by: Feras ALHAEK - // improved by: Rafał Kukawski (https://kukawski.net) - // example 1: wordwrap('Kevin van Zonneveld', 6, '|', true) - // returns 1: 'Kevin|van|Zonnev|eld' - // example 2: wordwrap('The quick brown fox jumped over the lazy dog.', 20, '
\n') - // returns 2: 'The quick brown fox
\njumped over the lazy
\ndog.' - // example 3: wordwrap('Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.') - // returns 3: 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod\ntempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim\nveniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea\ncommodo consequat.' - const intWidth = rest[0] ?? 75; - const strBreak = rest[1] ?? '\n'; - const cut = rest[2] ?? false; + const locationCleanup = (input) => { + // regexes to run + const regexes = [ + // "Chicago / West Chicago", removes before slash + /^[A-Za-z ]+ \/ /, + // "Chicago/Waukegan" removes before slash + /^[A-Za-z ]+\//, + // "Chicago, Chicago O'hare" removes before comma + /^[A-Za-z ]+, /, + ]; - let i; - let j; - let line; - - let str = _str; - str += ''; - - if (intWidth < 1) { - return str; - } - - const reLineBreaks = /\r\n|\n|\r/; - const reBeginningUntilFirstWhitespace = /^\S*/; - const reLastCharsWithOptionalTrailingWhitespace = /\S*(\s)?$/; - - const lines = str.split(reLineBreaks); - const l = lines.length; - let match; - - // for each line of text - // eslint-disable-next-line no-plusplus - for (i = 0; i < l; lines[i++] += line) { - line = lines[i]; - lines[i] = ''; - - while (line.length > intWidth) { - // get slice of length one char above limit - const slice = line.slice(0, intWidth + 1); - - // remove leading whitespace from rest of line to parse - let ltrim = 0; - // remove trailing whitespace from new line content - let rtrim = 0; - - match = slice.match(reLastCharsWithOptionalTrailingWhitespace); - - // if the slice ends with whitespace - if (match[1]) { - // then perfect moment to cut the line - j = intWidth; - ltrim = 1; - } else { - // otherwise cut at previous whitespace - j = slice.length - match[0].length; - - if (j) { - rtrim = 1; - } - - // but if there is no previous whitespace - // and cut is forced - // cut just at the defined limit - if (!j && cut && intWidth) { - j = intWidth; - } - - // if cut wasn't forced - // cut at next possible whitespace after the limit - if (!j) { - const charsUntilNextWhitespace = (line.slice(intWidth).match(reBeginningUntilFirstWhitespace) || [''])[0]; - - j = slice.length + charsUntilNextWhitespace.length; - } - } - - lines[i] += line.slice(0, j - rtrim); - line = line.slice(j + ltrim); - lines[i] += line.length ? strBreak : ''; - } - } - - return lines.join('\n'); + // run all regexes + return regexes.reduce((value, regex) => value.replace(regex, ''), input); }; + // ********************************* cors ******************************************** // rewrite some urls for local server const rewriteUrl = (_url) => { @@ -255,9 +153,9 @@ const utils = (() => { // build a url, including the rewrite for cors if necessary let corsUrl = _url; if (params.cors === true) corsUrl = rewriteUrl(_url); - const url = new URL(corsUrl); - // match the security protocol - url.protocol = window.location.protocol; + const url = new URL(corsUrl, `${window.location.origin}/`); + // match the security protocol when not on localhost + url.protocol = window.location.hostname !== 'localhost' ? window.location.protocol : url.protocol; // add parameters if necessary if (params.data) { Object.keys(params.data).forEach((key) => { @@ -286,13 +184,18 @@ const utils = (() => { } }; + const elemForEach = (selector, callback) => { + [...document.querySelectorAll(selector)].forEach(callback); + }; + // return an orderly object return { + elem: { + forEach: elemForEach, + }, image: { load: loadImg, - superGifAsync, preload, - drawLocalCanvas, }, weather: { getPoint, @@ -318,7 +221,7 @@ const utils = (() => { wrap, }, string: { - wordWrap, + locationCleanup, }, cors: { rewriteUrl, diff --git a/server/scripts/modules/weatherdisplay.js b/server/scripts/modules/weatherdisplay.js index 874212c..062e1c4 100644 --- a/server/scripts/modules/weatherdisplay.js +++ b/server/scripts/modules/weatherdisplay.js @@ -1,6 +1,6 @@ // base weather display class -/* globals navigation, utils, draw, UNITS, luxon, currentWeatherScroll */ +/* globals navigation, utils, luxon, currentWeatherScroll */ const STATUS = { loading: Symbol('loading'), @@ -31,8 +31,8 @@ class WeatherDisplay { this.navBaseCount = 0; this.screenIndex = -1; // special starting condition - // create the canvas, also stores this.elemId - this.createCanvas(elemId); + // store elemId once + this.storeElemId(elemId); if (elemId !== 'progress') this.addCheckbox(defaultEnabled); if (this.enabled) { @@ -41,6 +41,9 @@ class WeatherDisplay { this.setStatus(STATUS.disabled); } this.startNavCount(); + + // get any templates + this.loadTemplates(); } addCheckbox(defaultEnabled = true) { @@ -92,18 +95,10 @@ class WeatherDisplay { this.loadingStatus = state; } - createCanvas(elemId, width = 640, height = 480) { + storeElemId(elemId) { // only create it once if (this.elemId) return; this.elemId = elemId; - - // create a canvas - const canvas = document.createElement('template'); - canvas.innerHTML = `