diff --git a/.gitignore b/.gitignore index d36a28cc6f4ad9898b6d8b4ae57ed017617073ce..3b4f12f78d9352b81f0778080d5e472f836182ce 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,7 @@ log *.swo /js/tmp/ /conf/was_dev_erose.json +conf/tab_1.json +conf/tab_2.json +conf/tab_3.json + diff --git a/assets/bundle.js b/assets/bundle.js deleted file mode 100644 index 184bb888208ba4ce071e4db2e879829e5a0ad5b0..0000000000000000000000000000000000000000 --- a/assets/bundle.js +++ /dev/null @@ -1,2 +0,0 @@ -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e():"function"==typeof define&&define.amd?define(e):e()}(0,function(){"use strict";function t(t,e){return e={exports:{}},t(e,e.exports),e.exports}var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:{},n=t(function(t){!function(e){function n(t,e){function n(t){return e.bgcolor&&(t.style.backgroundColor=e.bgcolor),e.width&&(t.style.width=e.width+"px"),e.height&&(t.style.height=e.height+"px"),e.style&&Object.keys(e.style).forEach(function(n){t.style[n]=e.style[n]}),t}return e=e||{},s(e),Promise.resolve(t).then(function(t){return u(t,e.filter,!0)}).then(c).then(d).then(n).then(function(n){return g(n,e.width||h.width(t),e.height||h.height(t))})}function i(t,e){return l(t,e||{}).then(function(e){return e.getContext("2d").getImageData(0,0,h.width(t),h.height(t)).data})}function o(t,e){return l(t,e||{}).then(function(t){return t.toDataURL()})}function r(t,e){return e=e||{},l(t,e).then(function(t){return t.toDataURL("image/jpeg",e.quality||1)})}function a(t,e){return l(t,e||{}).then(h.canvasToBlob)}function s(t){void 0===t.imagePlaceholder?w.impl.options.imagePlaceholder=M.imagePlaceholder:w.impl.options.imagePlaceholder=t.imagePlaceholder,void 0===t.cacheBust?w.impl.options.cacheBust=M.cacheBust:w.impl.options.cacheBust=t.cacheBust}function l(t,e){function i(t){var n=document.createElement("canvas");if(n.width=e.width||h.width(t),n.height=e.height||h.height(t),e.bgcolor){var i=n.getContext("2d");i.fillStyle=e.bgcolor,i.fillRect(0,0,n.width,n.height)}return n}return n(t,e).then(h.makeImage).then(h.delay(100)).then(function(e){var n=i(t);return n.getContext("2d").drawImage(e,0,0),n})}function u(t,e,n){function i(t){return t instanceof HTMLCanvasElement?h.makeImage(t.toDataURL()):t.cloneNode(!1)}function o(t,e,n){var i=t.childNodes;return 0===i.length?Promise.resolve(e):function(t,e,n){var i=Promise.resolve();return e.forEach(function(e){i=i.then(function(){return u(e,n)}).then(function(e){e&&t.appendChild(e)})}),i}(e,h.asArray(i),n).then(function(){return e})}function r(t,e){function n(){!function(t,e){t.cssText?e.cssText=t.cssText:function(t,e){h.asArray(t).forEach(function(n){e.setProperty(n,t.getPropertyValue(n),t.getPropertyPriority(n))})}(t,e)}(window.getComputedStyle(t),e.style)}function i(){function n(n){var i=window.getComputedStyle(t,n),o=i.getPropertyValue("content");if(""!==o&&"none"!==o){var r=h.uid();e.className=e.className+" "+r;var a=document.createElement("style");a.appendChild(function(t,e,n){var i="."+t+":"+e,o=n.cssText?function(t){var e=t.getPropertyValue("content");return t.cssText+" content: "+e+";"}(n):function(t){function e(e){return e+": "+t.getPropertyValue(e)+(t.getPropertyPriority(e)?" !important":"")}return h.asArray(t).map(e).join("; ")+";"}(n);return document.createTextNode(i+"{"+o+"}")}(r,n,i)),e.appendChild(a)}}[":before",":after"].forEach(function(t){n(t)})}function o(){t instanceof HTMLTextAreaElement&&(e.innerHTML=t.value),t instanceof HTMLInputElement&&e.setAttribute("value",t.value)}function r(){e instanceof SVGElement&&(e.setAttribute("xmlns","http://www.w3.org/2000/svg"),e instanceof SVGRectElement&&["width","height"].forEach(function(t){var n=e.getAttribute(t);n&&e.style.setProperty(t,n)}))}return e instanceof Element?Promise.resolve().then(n).then(i).then(o).then(r).then(function(){return e}):e}return n||!e||e(t)?Promise.resolve(t).then(i).then(function(n){return o(t,n,e)}).then(function(e){return r(t,e)}):Promise.resolve()}function c(t){return p.resolveAll().then(function(e){var n=document.createElement("style");return t.appendChild(n),n.appendChild(document.createTextNode(e)),t})}function d(t){return f.inlineAll(t).then(function(){return t})}function g(t,e,n){return Promise.resolve(t).then(function(t){return t.setAttribute("xmlns","http://www.w3.org/1999/xhtml"),(new XMLSerializer).serializeToString(t)}).then(h.escapeXhtml).then(function(t){return''+t+""}).then(function(t){return''+t+""}).then(function(t){return"data:image/svg+xml;charset=utf-8,"+t})}var h=function(){function t(){var t="application/font-woff",e="image/jpeg";return{woff:t,woff2:t,ttf:"application/font-truetype",eot:"application/vnd.ms-fontobject",png:"image/png",jpg:e,jpeg:e,gif:"image/gif",tiff:"image/tiff",svg:"image/svg+xml"}}function e(t){var e=/\.([^\.\/]*?)$/g.exec(t);return e?e[1]:""}function n(n){var i=e(n).toLowerCase();return t()[i]||""}function i(t){return-1!==t.search(/^(data:)/)}function o(t){return new Promise(function(e){for(var n=window.atob(t.toDataURL().split(",")[1]),i=n.length,o=new Uint8Array(i),r=0;rthis.mapContainer.style.height?this.orientation="portrait":this.orientation="landscape",this._map.setView(this.originalState.center),this._map.setZoom(this.originalState.zoom),this._map.invalidateSize(),this.options.tileLayer?this._pausePrint(t):this._printOpertion(t)},_pausePrint:function(t){var e=this,n=setInterval(function(){e.options.tileLayer.isLoading()||(clearInterval(n),e._printOpertion(t))},e.options.tileWait)},_printOpertion:function(t){var e=this,o=this.mapContainer.style.width;(this.originalState.widthWasAuto&&"CurrentSize"===t||this.originalState.widthWasPercentage&&"CurrentSize"===t)&&(o=this.originalState.mapWidth),n.toPng(e.mapContainer,{width:parseInt(o),height:parseInt(e.mapContainer.style.height.replace("px"))}).then(function(t){var n=e._dataURItoBlob(t);e.options.exportOnly?i.saveAs(n,e.options.filename+".png"):e._sendToBrowserPrint(t,e.orientation),e._toggleControls(!0),e.outerContainer&&(e.originalState.widthWasAuto?e.mapContainer.style.width="auto":e.originalState.widthWasPercentage?e.mapContainer.style.width=e.originalState.percentageWidth:e.mapContainer.style.width=e.originalState.mapWidth,e.mapContainer.style.height=e.originalState.mapHeight,e._removeOuterContainer(e.mapContainer,e.outerContainer,e.blankDiv),e._map.invalidateSize(),e._map.setView(e.originalState.center),e._map.setZoom(e.originalState.zoom)),e._map.fire("easyPrint-finished")}).catch(function(t){console.error("Print operation failed",t)})},_sendToBrowserPrint:function(t,e){this._page.resizeTo(600,800);var n=this._createNewWindow(t,e,this);this._page.document.body.innerHTML="",this._page.document.write(n),this._page.document.close()},_createSpinner:function(t,e,n){return""+t+"\n
Loading...
'},_createNewWindow:function(t,e,n){return"\n \n \r\n `;\r\n },\r\n\r\n _createOuterContainer: function (mapDiv) {\r\n var outerContainer = document.createElement('div'); \r\n mapDiv.parentNode.insertBefore(outerContainer, mapDiv); \r\n mapDiv.parentNode.removeChild(mapDiv);\r\n outerContainer.appendChild(mapDiv);\r\n outerContainer.style.width = mapDiv.style.width;\r\n outerContainer.style.height = mapDiv.style.height;\r\n outerContainer.style.display = 'inline-block'\r\n outerContainer.style.overflow = 'hidden';\r\n return outerContainer;\r\n },\r\n\r\n _removeOuterContainer: function (mapDiv, outerContainer, blankDiv) {\r\n if (outerContainer.parentNode) {\r\n outerContainer.parentNode.insertBefore(mapDiv, outerContainer);\r\n outerContainer.parentNode.removeChild(blankDiv);\r\n outerContainer.parentNode.removeChild(outerContainer); \r\n }\r\n },\r\n\r\n _addCss: function () {\r\n var css = document.createElement(\"style\");\r\n css.type = \"text/css\";\r\n css.innerHTML = `.leaflet-control-easyPrint-button { \r\n background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDUxMiA1MTIiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMiA1MTI7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPGc+Cgk8cGF0aCBkPSJNMTI4LDMyaDI1NnY2NEgxMjhWMzJ6IE00ODAsMTI4SDMyYy0xNy42LDAtMzIsMTQuNC0zMiwzMnYxNjBjMCwxNy42LDE0LjM5OCwzMiwzMiwzMmg5NnYxMjhoMjU2VjM1Mmg5NiAgIGMxNy42LDAsMzItMTQuNCwzMi0zMlYxNjBDNTEyLDE0Mi40LDQ5Ny42LDEyOCw0ODAsMTI4eiBNMzUyLDQ0OEgxNjBWMjg4aDE5MlY0NDh6IE00ODcuMTk5LDE3NmMwLDEyLjgxMy0xMC4zODcsMjMuMi0yMy4xOTcsMjMuMiAgIGMtMTIuODEyLDAtMjMuMjAxLTEwLjM4Ny0yMy4yMDEtMjMuMnMxMC4zODktMjMuMiwyMy4xOTktMjMuMkM0NzYuODE0LDE1Mi44LDQ4Ny4xOTksMTYzLjE4Nyw0ODcuMTk5LDE3NnoiIGZpbGw9IiMwMDAwMDAiLz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K);\r\n background-size: 16px 16px; \r\n cursor: pointer; \r\n }\r\n .leaflet-control-easyPrint-button-export { \r\n background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTYuMC4wLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgd2lkdGg9IjE2cHgiIGhlaWdodD0iMTZweCIgdmlld0JveD0iMCAwIDQzMy41IDQzMy41IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0MzMuNSA0MzMuNTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8Zz4KCTxnIGlkPSJmaWxlLWRvd25sb2FkIj4KCQk8cGF0aCBkPSJNMzk1LjI1LDE1M2gtMTAyVjBoLTE1M3YxNTNoLTEwMmwxNzguNSwxNzguNUwzOTUuMjUsMTUzeiBNMzguMjUsMzgyLjV2NTFoMzU3di01MUgzOC4yNXoiIGZpbGw9IiMwMDAwMDAiLz4KCTwvZz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8Zz4KPC9nPgo8L3N2Zz4K);\r\n background-size: 16px 16px; \r\n cursor: pointer; \r\n }\r\n .easyPrintHolder a {\r\n background-size: 16px 16px;\r\n cursor: pointer;\r\n }\r\n .easyPrintHolder .CurrentSize{\r\n background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj4KPHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB3aWR0aD0iMTZweCIgdmVyc2lvbj0iMS4xIiBoZWlnaHQ9IjE2cHgiIHZpZXdCb3g9IjAgMCA2NCA2NCIgZW5hYmxlLWJhY2tncm91bmQ9Im5ldyAwIDAgNjQgNjQiPgogIDxnPgogICAgPGcgZmlsbD0iIzFEMUQxQiI+CiAgICAgIDxwYXRoIGQ9Ik0yNS4yNTUsMzUuOTA1TDQuMDE2LDU3LjE0NVY0Ni41OWMwLTEuMTA4LTAuODk3LTIuMDA4LTIuMDA4LTIuMDA4QzAuODk4LDQ0LjU4MiwwLDQ1LjQ4MSwwLDQ2LjU5djE1LjQwMiAgICBjMCwwLjI2MSwwLjA1MywwLjUyMSwwLjE1NSwwLjc2N2MwLjIwMywwLjQ5MiwwLjU5NCwwLjg4MiwxLjA4NiwxLjA4N0MxLjQ4Niw2My45NDcsMS43NDcsNjQsMi4wMDgsNjRoMTUuNDAzICAgIGMxLjEwOSwwLDIuMDA4LTAuODk4LDIuMDA4LTIuMDA4cy0wLjg5OC0yLjAwOC0yLjAwOC0yLjAwOEg2Ljg1NWwyMS4yMzgtMjEuMjRjMC43ODQtMC43ODQsMC43ODQtMi4wNTUsMC0yLjgzOSAgICBTMjYuMDM5LDM1LjEyMSwyNS4yNTUsMzUuOTA1eiIgZmlsbD0iIzAwMDAwMCIvPgogICAgICA8cGF0aCBkPSJtNjMuODQ1LDEuMjQxYy0wLjIwMy0wLjQ5MS0wLjU5NC0wLjg4Mi0xLjA4Ni0xLjA4Ny0wLjI0NS0wLjEwMS0wLjUwNi0wLjE1NC0wLjc2Ny0wLjE1NGgtMTUuNDAzYy0xLjEwOSwwLTIuMDA4LDAuODk4LTIuMDA4LDIuMDA4czAuODk4LDIuMDA4IDIuMDA4LDIuMDA4aDEwLjU1NmwtMjEuMjM4LDIxLjI0Yy0wLjc4NCwwLjc4NC0wLjc4NCwyLjA1NSAwLDIuODM5IDAuMzkyLDAuMzkyIDAuOTA2LDAuNTg5IDEuNDIsMC41ODlzMS4wMjctMC4xOTcgMS40MTktMC41ODlsMjEuMjM4LTIxLjI0djEwLjU1NWMwLDEuMTA4IDAuODk3LDIuMDA4IDIuMDA4LDIuMDA4IDEuMTA5LDAgMi4wMDgtMC44OTkgMi4wMDgtMi4wMDh2LTE1LjQwMmMwLTAuMjYxLTAuMDUzLTAuNTIyLTAuMTU1LTAuNzY3eiIgZmlsbD0iIzAwMDAwMCIvPgogICAgPC9nPgogIDwvZz4KPC9zdmc+Cg==)\r\n }\r\n .easyPrintHolder .page {\r\n background-image: url(data:image/svg+xml;utf8;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pgo8IS0tIEdlbmVyYXRvcjogQWRvYmUgSWxsdXN0cmF0b3IgMTguMS4xLCBTVkcgRXhwb3J0IFBsdWctSW4gLiBTVkcgVmVyc2lvbjogNi4wMCBCdWlsZCAwKSAgLS0+CjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgdmVyc2lvbj0iMS4xIiBpZD0iQ2FwYV8xIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMCAwIDQ0NC44MzMgNDQ0LjgzMyIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDQ0LjgzMyA0NDQuODMzOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSIgd2lkdGg9IjUxMnB4IiBoZWlnaHQ9IjUxMnB4Ij4KPGc+Cgk8Zz4KCQk8cGF0aCBkPSJNNTUuMjUsNDQ0LjgzM2gzMzQuMzMzYzkuMzUsMCwxNy03LjY1LDE3LTE3VjEzOS4xMTdjMC00LjgxNy0xLjk4My05LjM1LTUuMzgzLTEyLjQ2N0wyNjkuNzMzLDQuNTMzICAgIEMyNjYuNjE3LDEuNywyNjIuMzY3LDAsMjU4LjExNywwSDU1LjI1Yy05LjM1LDAtMTcsNy42NS0xNywxN3Y0MTAuODMzQzM4LjI1LDQzNy4xODMsNDUuOSw0NDQuODMzLDU1LjI1LDQ0NC44MzN6ICAgICBNMzcyLjU4MywxNDYuNDgzdjAuODVIMjU2LjQxN3YtMTA4LjhMMzcyLjU4MywxNDYuNDgzeiBNNzIuMjUsMzRoMTUwLjE2N3YxMzAuMzMzYzAsOS4zNSw3LjY1LDE3LDE3LDE3aDEzMy4xNjd2MjI5LjVINzIuMjVWMzR6ICAgICIgZmlsbD0iIzAwMDAwMCIvPgoJPC9nPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+CjxnPgo8L2c+Cjwvc3ZnPgo=);\r\n }\r\n .easyPrintHolder .A4Landscape { \r\n transform: rotate(-90deg);\r\n }\r\n\r\n .leaflet-control-easyPrint-button{\r\n display: inline-block;\r\n }\r\n .easyPrintHolder{\r\n margin-top:-31px;\r\n margin-bottom: -5px;\r\n margin-left: 30px;\r\n padding-left: 0px;\r\n display: none;\r\n }\r\n\r\n .easyPrintSizeMode {\r\n display: inline-block;\r\n }\r\n .easyPrintHolder .easyPrintSizeMode a {\r\n border-radius: 0px;\r\n }\r\n\r\n .easyPrintHolder .easyPrintSizeMode:last-child a{\r\n border-top-right-radius: 2px;\r\n border-bottom-right-radius: 2px;\r\n margin-left: -1px;\r\n }\r\n\r\n .easyPrintPortrait:hover, .easyPrintLandscape:hover{\r\n background-color: #757570;\r\n cursor: pointer;\r\n }`;\r\n document.body.appendChild(css);\r\n },\r\n\r\n _dataURItoBlob: function (dataURI) {\r\n var byteString = atob(dataURI.split(',')[1]);\r\n var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];\r\n var ab = new ArrayBuffer(byteString.length);\r\n var dw = new DataView(ab);\r\n for(var i = 0; i < byteString.length; i++) {\r\n dw.setUint8(i, byteString.charCodeAt(i));\r\n }\r\n return new Blob([ab], {type: mimeString});\r\n },\r\n\r\n _togglePageSizeButtons: function (e) {\r\n var holderStyle = this.holder.style\r\n var linkStyle = this.link.style\r\n if (e.type === 'mouseover') {\r\n holderStyle.display = 'block';\r\n linkStyle.borderTopRightRadius = '0'\r\n linkStyle.borderBottomRightRadius = '0'\r\n } else {\r\n holderStyle.display = 'none';\r\n linkStyle.borderTopRightRadius = '2px'\r\n linkStyle.borderBottomRightRadius = '2px' \r\n }\r\n },\r\n\r\n _toggleControls: function (show) {\r\n var controlContainer = document.getElementsByClassName(\"leaflet-control-container\")[0];\r\n if (show) return controlContainer.style.display = 'block';\r\n controlContainer.style.display = 'none';\r\n },\r\n\r\n _a4PageSize: {\r\n height: 715,\r\n width: 1045\r\n }\r\n\r\n});\r\n\r\nL.easyPrint = function(options) {\r\n return new L.Control.EasyPrint(options);\r\n};\r\n\r\n"],"names":["a","b","c","bgcolor","style","backgroundColor","width","height","Object","keys","forEach","g","Promise","resolve","then","i","filter","j","k","l","q","h","getContext","getImageData","data","d","toDataURL","e","quality","f","canvasToBlob","imagePlaceholder","v","impl","options","u","cacheBust","document","createElement","fillStyle","fillRect","makeImage","delay","drawImage","HTMLCanvasElement","cloneNode","childNodes","length","appendChild","asArray","cssText","setProperty","getPropertyValue","getPropertyPriority","window","getComputedStyle","uid","className","map","join","createTextNode","HTMLTextAreaElement","innerHTML","value","HTMLInputElement","setAttribute","SVGElement","SVGRectElement","getAttribute","Element","s","resolveAll","t","inlineAll","XMLSerializer","serializeToString","escapeXhtml","woff","woff2","ttf","eot","png","jpg","jpeg","gif","tiff","svg","exec","toLowerCase","search","atob","split","Uint8Array","charCodeAt","Blob","type","toBlob","implementation","createHTMLDocument","head","body","href","Image","onload","onerror","src","test","Date","getTime","readyState","status","FileReader","onloadend","result","readAsDataURL","response","console","error","XMLHttpRequest","onreadystatechange","ontimeout","responseType","timeout","open","send","replace","m","setTimeout","n","push","o","p","r","scrollWidth","scrollHeight","parseFloat","escape","parseExtension","mimeType","dataAsUrl","isDataUrl","resolveUrl","getAndEncode","Math","random","pow","toString","slice","RegExp","shouldProcess","readUrls","inline","all","CSSRule","FONT_FACE_RULE","cssRules","bind","log","parentStyleSheet","styleSheets","readAll","HTMLImageElement","newImage","toSvg","toPng","toJpeg","toPixelData","fontFaces","images","util","inliner","module","saveAs","view","navigator","userAgent","doc","get_URL","URL","webkitURL","save_link","createElementNS","can_use_save_link","click","node","event","MouseEvent","dispatchEvent","is_safari","HTMLElement","safari","is_chrome_ios","throw_outside","ex","setImmediate","revoke","file","revoker","revokeObjectURL","remove","dispatch","filesaver","event_types","concat","listener","call","auto_bom","blob","String","fromCharCode","FileSaver","name","no_auto_bom","object_url","this","force","dispatch_all","INIT","createObjectURL","download","DONE","reader","url","location","undefined","FS_proto","prototype","msSaveOrOpenBlob","abort","WRITING","onwritestart","onprogress","onwrite","onabort","onwriteend","self","content","exports","L","Control","EasyPrint","extend","title","mapContainer","_map","getContainer","sizeModes","sizeMode","defaultSizeTitles","Current","_a4PageSize","A4Landscape","A4Portrait","container","DomUtil","create","hidden","_addCss","DomEvent","addListener","_togglePageSizeButtons","btnClass","exportOnly","link","id","holder","btn","printMap","disableClickPropagation","filename","_page","write","_createSpinner","customWindowTitle","customSpinnerClass","spinnerBgCOlor","originalState","getZoom","getCenter","mapWidth","getSize","x","widthWasAuto","includes","percentageWidth","widthWasPercentage","fire","hideControlContainer","_toggleControls","target","_printOpertion","outerContainer","_createOuterContainer","_createImagePlaceholder","plugin","parseInt","mapHeight","dataUrl","blankDiv","parentElement","insertBefore","backgroundImage","position","zIndex","display","_resizeAndPrintMap","catch","opacity","pageSize","item","orientation","setView","center","setZoom","zoom","invalidateSize","tileLayer","_pausePrint","loadingTest","setInterval","isLoading","tileWait","sizemode","widthForExport","_dataURItoBlob","_sendToBrowserPrint","_removeOuterContainer","img","resizeTo","pageContent","_createNewWindow","close","spinnerClass","spinnerColor","mapDiv","parentNode","removeChild","overflow","css","dataURI","byteString","mimeString","ab","ArrayBuffer","dw","DataView","setUint8","holderStyle","linkStyle","borderTopRightRadius","borderBottomRightRadius","show","controlContainer","getElementsByClassName","easyPrint"],"mappings":"kVACC,SAASA,GAAgB,QAASC,GAAED,EAAEC,GAAG,QAASC,GAAEF,GAAG,MAAOC,GAAEE,UAAUH,EAAEI,MAAMC,gBAAgBJ,EAAEE,SAASF,EAAEK,QAAQN,EAAEI,MAAME,MAAML,EAAEK,MAAM,MAAML,EAAEM,SAASP,EAAEI,MAAMG,OAAON,EAAEM,OAAO,MAAMN,EAAEG,OAAOI,OAAOC,KAAKR,EAAEG,OAAOM,QAAQ,SAASR,GAAGF,EAAEI,MAAMF,GAAGD,EAAEG,MAAMF,KAAKF,EAAE,MAAOC,GAAEA,MAAMU,EAAEV,GAAGW,QAAQC,QAAQb,GAAGc,KAAK,SAASd,GAAG,MAAOe,GAAEf,EAAEC,EAAEe,QAAO,KAAMF,KAAKG,GAAGH,KAAKI,GAAGJ,KAAKZ,GAAGY,KAAK,SAASZ,GAAG,MAAOiB,GAAEjB,EAAED,EAAEK,OAAOc,EAAEd,MAAMN,GAAGC,EAAEM,QAAQa,EAAEb,OAAOP,MAAM,QAASE,GAAEF,EAAEC,GAAG,MAAOoB,GAAErB,EAAEC,OAAOa,KAAK,SAASb,GAAG,MAAOA,GAAEqB,WAAW,MAAMC,aAAa,EAAE,EAAEH,EAAEd,MAAMN,GAAGoB,EAAEb,OAAOP,IAAIwB,OAAO,QAASC,GAAEzB,EAAEC,GAAG,MAAOoB,GAAErB,EAAEC,OAAOa,KAAK,SAASd,GAAG,MAAOA,GAAE0B,cAAc,QAASC,GAAE3B,EAAEC,GAAG,MAAOA,GAAEA,MAAMoB,EAAErB,EAAEC,GAAGa,KAAK,SAASd,GAAG,MAAOA,GAAE0B,UAAU,aAAazB,EAAE2B,SAAS,KAAK,QAASC,GAAE7B,EAAEC,GAAG,MAAOoB,GAAErB,EAAEC,OAAOa,KAAKM,EAAEU,cAAc,QAASnB,GAAEX,OAAG,KAAoBA,EAAE+B,iBAAiBC,EAAEC,KAAKC,QAAQH,iBAAiBI,EAAEJ,iBAAiBC,EAAEC,KAAKC,QAAQH,iBAAiB/B,EAAE+B,qBAAiB,KAAoB/B,EAAEoC,UAAUJ,EAAEC,KAAKC,QAAQE,UAAUD,EAAEC,UAAUJ,EAAEC,KAAKC,QAAQE,UAAUpC,EAAEoC,UAAU,QAASf,GAAErB,EAAEE,GAAG,QAASuB,GAAEzB,GAAG,GAAIC,GAAEoC,SAASC,cAAc,SAAU,IAAGrC,EAAEK,MAAMJ,EAAEI,OAAOc,EAAEd,MAAMN,GAAGC,EAAEM,OAAOL,EAAEK,QAAQa,EAAEb,OAAOP,GAAGE,EAAEC,QAAQ,CAAC,GAAIsB,GAAExB,EAAEqB,WAAW,KAAMG,GAAEc,UAAUrC,EAAEC,QAAQsB,EAAEe,SAAS,EAAE,EAAEvC,EAAEK,MAAML,EAAEM,QAAQ,MAAON,GAAE,MAAOA,GAAED,EAAEE,GAAGY,KAAKM,EAAEqB,WAAW3B,KAAKM,EAAEsB,MAAM,MAAM5B,KAAK,SAASb,GAAG,GAAIC,GAAEuB,EAAEzB,EAAG,OAAOE,GAAEoB,WAAW,MAAMqB,UAAU1C,EAAE,EAAE,GAAGC,IAAI,QAASa,GAAEf,EAAEC,EAAEC,GAAG,QAASuB,GAAEzB,GAAG,MAAOA,aAAa4C,mBAAkBxB,EAAEqB,UAAUzC,EAAE0B,aAAa1B,EAAE6C,WAAU,GAAI,QAASlB,GAAE3B,EAAEC,EAAEC,GAAyJ,GAAIyB,GAAE3B,EAAE8C,UAAW,OAAO,KAAInB,EAAEoB,OAAOnC,QAAQC,QAAQZ,GAA7M,SAAWD,EAAEC,EAAEC,GAAG,GAAIuB,GAAEb,QAAQC,SAAU,OAAOZ,GAAES,QAAQ,SAAST,GAAGwB,EAAEA,EAAEX,KAAK,WAAW,MAAOC,GAAEd,EAAEC,KAAKY,KAAK,SAASb,GAAGA,GAAGD,EAAEgD,YAAY/C,OAAOwB,GAA8DxB,EAAEmB,EAAE6B,QAAQtB,GAAGzB,GAAGY,KAAK,WAAW,MAAOb,KAAI,QAAS4B,GAAE7B,EAAEC,GAAG,QAASC,MAAI,SAAWF,EAAEC,GAAsHD,EAAEkD,QAAQjD,EAAEiD,QAAQlD,EAAEkD,QAAzI,SAAWlD,EAAEC,GAAGmB,EAAE6B,QAAQjD,GAAGU,QAAQ,SAASR,GAAGD,EAAEkD,YAAYjD,EAAEF,EAAEoD,iBAAiBlD,GAAGF,EAAEqD,oBAAoBnD,OAAsCF,EAAEC,IAAKqD,OAAOC,iBAAiBvD,GAAGC,EAAEG,OAAO,QAASqB,KAAI,QAASvB,GAAEA,GAAqV,GAAIyB,GAAE2B,OAAOC,iBAAiBvD,EAAEE,GAAG2B,EAAEF,EAAEyB,iBAAiB,UAAW,IAAG,KAAKvB,GAAG,SAASA,EAAE,CAAC,GAAIlB,GAAES,EAAEoC,KAAMvD,GAAEwD,UAAUxD,EAAEwD,UAAU,IAAI9C,CAAE,IAAIU,GAAEgB,SAASC,cAAc,QAASjB,GAAE2B,YAAhgB,SAAWhD,EAAEC,EAAEC,GAA+O,GAAI2B,GAAE,IAAI7B,EAAE,IAAIC,EAAEU,EAAET,EAAEgD,QAAlQ,SAAWlD,GAAG,GAAIC,GAAED,EAAEoD,iBAAiB,UAAW,OAAOpD,GAAEkD,QAAQ,aAAajD,EAAE,KAA0LC,GAAtL,SAAWF,GAAG,QAASC,GAAEA,GAAG,MAAOA,GAAE,KAAKD,EAAEoD,iBAAiBnD,IAAID,EAAEqD,oBAAoBpD,GAAG,cAAc,IAAI,MAAOmB,GAAE6B,QAAQjD,GAAG0D,IAAIzD,GAAG0D,KAAK,MAAM,KAAyCzD,EAAG,OAAOmC,UAASuB,eAAe/B,EAAE,IAAIlB,EAAE,MAAiMA,EAAET,EAAEyB,IAAI1B,EAAE+C,YAAY3B,KAAK,UAAU,UAAUX,QAAQ,SAASV,GAAGE,EAAEF,KAAK,QAAS2B,KAAI3B,YAAa6D,uBAAsB5D,EAAE6D,UAAU9D,EAAE+D,OAAO/D,YAAagE,mBAAkB/D,EAAEgE,aAAa,QAAQjE,EAAE+D,OAAO,QAASlC,KAAI5B,YAAaiE,cAAajE,EAAEgE,aAAa,QAAQ,8BAA8BhE,YAAakE,kBAAiB,QAAQ,UAAUzD,QAAQ,SAASV,GAAG,GAAIE,GAAED,EAAEmE,aAAapE,EAAGE,IAAGD,EAAEG,MAAM+C,YAAYnD,EAAEE,MAAM,MAAOD,aAAaoE,SAAQzD,QAAQC,UAAUC,KAAKZ,GAAGY,KAAKW,GAAGX,KAAKa,GAAGb,KAAKe,GAAGf,KAAK,WAAW,MAAOb,KAAIA,EAAE,MAAOC,KAAID,GAAGA,EAAED,GAAGY,QAAQC,QAAQb,GAAGc,KAAKW,GAAGX,KAAK,SAASZ,GAAG,MAAOyB,GAAE3B,EAAEE,EAAED,KAAKa,KAAK,SAASb,GAAG,MAAO4B,GAAE7B,EAAEC,KAAKW,QAAQC,UAAU,QAASI,GAAEjB,GAAG,MAAOsE,GAAEC,aAAazD,KAAK,SAASb,GAAG,GAAIC,GAAEmC,SAASC,cAAc,QAAS,OAAOtC,GAAEgD,YAAY9C,GAAGA,EAAE8C,YAAYX,SAASuB,eAAe3D,IAAID,IAAI,QAASkB,GAAElB,GAAG,MAAOwE,GAAEC,UAAUzE,GAAGc,KAAK,WAAW,MAAOd,KAAI,QAASmB,GAAEnB,EAAEC,EAAEC,GAAG,MAAOU,SAAQC,QAAQb,GAAGc,KAAK,SAASd,GAAG,MAAOA,GAAEiE,aAAa,QAAQ,iCAAgC,GAAKS,gBAAeC,kBAAkB3E,KAAKc,KAAKM,EAAEwD,aAAa9D,KAAK,SAASd,GAAG,MAAM,yDAAyDA,EAAE,qBAAqBc,KAAK,SAASd,GAAG,MAAM,kDAAkDC,EAAE,aAAaC,EAAE,KAAKF,EAAE,WAAWc,KAAK,SAASd,GAAG,MAAM,oCAAoCA,IAA+8J,GAAIoB,GAA/8J,WAAa,QAASpB,KAAI,GAAIA,GAAE,wBAAwBC,EAAE,YAAa,QAAO4E,KAAK7E,EAAE8E,MAAM9E,EAAE+E,IAAI,4BAA4BC,IAAI,gCAAgCC,IAAI,YAAYC,IAAIjF,EAAEkF,KAAKlF,EAAEmF,IAAI,YAAYC,KAAK,aAAaC,IAAI,iBAAiB,QAASrF,GAAED,GAAG,GAAIC,GAAE,kBAAkBsF,KAAKvF,EAAG,OAAOC,GAAEA,EAAE,GAAG,GAAG,QAASC,GAAEA,GAAG,GAAIuB,GAAExB,EAAEC,GAAGsF,aAAc,OAAOxF,KAAIyB,IAAI,GAAG,QAASA,GAAEzB,GAAG,OAA+B,IAAxBA,EAAEyF,OAAO,YAAiB,QAAS9D,GAAE3B,GAAG,MAAO,IAAIY,SAAQ,SAASX,GAAG,IAAI,GAAIC,GAAEoD,OAAOoC,KAAK1F,EAAE0B,YAAYiE,MAAM,KAAK,IAAIlE,EAAEvB,EAAE6C,OAAOpB,EAAE,GAAIiE,YAAWnE,GAAGI,EAAE,EAAEA,EAAEJ,EAAEI,IAAIF,EAAEE,GAAG3B,EAAE2F,WAAWhE,EAAG5B,GAAE,GAAI6F,OAAMnE,IAAIoE,KAAK,iBAAiB,QAASlE,GAAE7B,GAAG,MAAOA,GAAEgG,OAAO,GAAIpF,SAAQ,SAASX,GAAGD,EAAEgG,OAAO/F,KAAK0B,EAAE3B,GAAG,QAASW,GAAEX,EAAEC,GAAG,GAAIC,GAAEmC,SAAS4D,eAAeC,qBAAqBzE,EAAEvB,EAAEoC,cAAc,OAAQpC,GAAEiG,KAAKnD,YAAYvB,EAAG,IAAIE,GAAEzB,EAAEoC,cAAc,IAAK,OAAOpC,GAAEkG,KAAKpD,YAAYrB,GAAGF,EAAE4E,KAAKpG,EAAE0B,EAAE0E,KAAKrG,EAAE2B,EAAE0E,KAAoJ,QAAStF,GAAEf,GAAG,MAAO,IAAIY,SAAQ,SAASX,EAAEC,GAAG,GAAIuB,GAAE,GAAI6E,MAAM7E,GAAE8E,OAAO,WAAWtG,EAAEwB,IAAIA,EAAE+E,QAAQtG,EAAEuB,EAAEgF,IAAIzG,IAAI,QAASiB,GAAEjB,GAAG,GAAIC,GAAE,GAAI,OAAO+B,GAAEC,KAAKC,QAAQE,YAAYpC,IAAI,KAAK0G,KAAK1G,GAAG,IAAI,MAAK,GAAK2G,OAAMC,WAAW,GAAIhG,SAAQ,SAASV,GAAG,QAASuB,KAAI,GAAG,IAAId,EAAEkG,WAAW,CAAC,GAAG,MAAMlG,EAAEmG,OAAO,YAAYzF,EAAEnB,EAAEmB,GAAGQ,EAAE,0BAA0B7B,EAAE,aAAaW,EAAEmG,QAAS,IAAI7G,GAAE,GAAI8G,WAAW9G,GAAE+G,UAAU,WAAW,GAAIhH,GAAEC,EAAEgH,OAAOtB,MAAM,KAAK,EAAGzF,GAAEF,IAAIC,EAAEiH,cAAcvG,EAAEwG,WAAW,QAASxF,KAAIN,EAAEnB,EAAEmB,GAAGQ,EAAE,cAAc5B,EAAE,uCAAuCD,GAAG,QAAS6B,GAAE7B,GAAGoH,QAAQC,MAAMrH,GAAGE,EAAE,IAAI,GAAIS,GAAE,GAAI2G,eAAe3G,GAAE4G,mBAAmB9F,EAAEd,EAAE6G,UAAU7F,EAAEhB,EAAE8G,aAAa,OAAO9G,EAAE+G,QAAQzH,EAAEU,EAAEgH,KAAK,MAAM3H,GAAE,GAAIW,EAAEiH,MAAO,IAAIvG,EAAE,IAAGW,EAAEC,KAAKC,QAAQH,iBAAiB,CAAC,GAAIhB,GAAEiB,EAAEC,KAAKC,QAAQH,iBAAiB4D,MAAM,IAAK5E,IAAGA,EAAE,KAAKM,EAAEN,EAAE,OAAO,QAASG,GAAElB,EAAEC,GAAG,MAAM,QAAQA,EAAE,WAAWD,EAAE,QAASmB,GAAEnB,GAAG,MAAOA,GAAE6H,QAAQ,2BAA2B,QAAQ,QAASC,GAAE9H,GAAG,MAAO,UAASC,GAAG,MAAO,IAAIW,SAAQ,SAASV,GAAG6H,WAAW,WAAW7H,EAAED,IAAID,MAAM,QAASgI,GAAEhI,GAAG,IAAI,GAAIC,MAAKC,EAAEF,EAAE+C,OAAOtB,EAAE,EAAEA,EAAEvB,EAAEuB,IAAIxB,EAAEgI,KAAKjI,EAAEyB,GAAI,OAAOxB,GAAE,QAASiI,GAAElI,GAAG,MAAOA,GAAE6H,QAAQ,KAAK,OAAOA,QAAQ,MAAM,OAAO,QAASM,GAAEnI,GAAG,GAAIC,GAAEmI,EAAEpI,EAAE,qBAAqBE,EAAEkI,EAAEpI,EAAE,qBAAsB,OAAOA,GAAEqI,YAAYpI,EAAEC,EAAE,QAASkB,GAAEpB,GAAG,GAAIC,GAAEmI,EAAEpI,EAAE,oBAAoBE,EAAEkI,EAAEpI,EAAE,sBAAuB,OAAOA,GAAEsI,aAAarI,EAAEC,EAAE,QAASkI,GAAEpI,EAAEC,GAAG,GAAIC,GAAEoD,OAAOC,iBAAiBvD,GAAGoD,iBAAiBnD,EAAG,OAAOsI,YAAWrI,EAAE2H,QAAQ,KAAK,KAAK,OAAOW,OAAOrH,EAAEsH,eAAexI,EAAEyI,SAASxI,EAAEyI,UAAUzH,EAAE0H,UAAUnH,EAAEK,aAAaD,EAAEgH,WAAWlI,EAAEmI,aAAa7H,EAAEuC,IAAlsD,WAAa,GAAIxD,GAAE,CAAE,OAAO,YAAgG,MAAM,IAA3F,WAAa,OAAO,QAAQ+I,KAAKC,SAASD,KAAKE,IAAI,GAAG,IAAI,GAAGC,SAAS,KAAKC,OAAO,MAAiBnJ,QAAgkD0C,MAAMoF,EAAE7E,QAAQ+E,EAAEpD,YAAYsD,EAAEzF,UAAU1B,EAAET,MAAM6H,EAAE5H,OAAOa,MAAi2EgH,EAA91E,WAAa,QAASpI,GAAEA,GAAG,OAAsB,IAAfA,EAAEyF,OAAO9D,GAAQ,QAAS1B,GAAED,GAAG,IAAI,GAAIC,GAAEC,KAAK,QAAQD,EAAE0B,EAAE4D,KAAKvF,KAAKE,EAAE+H,KAAKhI,EAAE,GAAI,OAAOC,GAAEc,OAAO,SAAShB,GAAG,OAAOoB,EAAEwH,UAAU5I,KAAK,QAASE,GAAEF,EAAEC,EAAEC,EAAEuB,GAAG,QAASE,GAAE3B,GAAG,MAAO,IAAIoJ,QAAO,kBAAkBhI,EAAEoH,OAAOxI,GAAG,eAAe,KAAK,MAAOY,SAAQC,QAAQZ,GAAGa,KAAK,SAASd,GAAG,MAAOE,GAAEkB,EAAEyH,WAAW7I,EAAEE,GAAGF,IAAIc,KAAKW,GAAGL,EAAE0H,cAAchI,KAAK,SAASd,GAAG,MAAOoB,GAAEuH,UAAU3I,EAAEoB,EAAEsH,SAASzI,MAAMa,KAAK,SAASZ,GAAG,MAAOF,GAAE6H,QAAQlG,EAAE1B,GAAG,KAAKC,EAAE,QAAQ,QAASuB,GAAEA,EAAEE,EAAEE,GAA4B,MAAzB,YAAa,OAAO7B,EAAEyB,MAAcb,QAAQC,QAAQY,GAAGb,QAAQC,QAAQY,GAAGX,KAAKb,GAAGa,KAAK,SAASd,GAAG,GAAIC,GAAEW,QAAQC,QAAQY,EAAG,OAAOzB,GAAEU,QAAQ,SAASV,GAAGC,EAAEA,EAAEa,KAAK,SAASb,GAAG,MAAOC,GAAED,EAAED,EAAE2B,EAAEE,OAAO5B,IAAI,GAAI0B,GAAE,6BAA8B,QAAO8C,UAAUhD,EAAE4H,cAAcrJ,EAAEiC,MAAMqH,SAASrJ,EAAEsJ,OAAOrJ,OAA+kDoE,EAA3kD,WAAa,QAAStE,KAAI,MAAOC,GAAEoC,UAAUvB,KAAK,SAASd,GAAG,MAAOY,SAAQ4I,IAAIxJ,EAAE0D,IAAI,SAAS1D,GAAG,MAAOA,GAAEa,eAAeC,KAAK,SAASd,GAAG,MAAOA,GAAE2D,KAAK,QAAQ,QAAS1D,KAAI,QAASD,GAAEA,GAAG,MAAOA,GAAEgB,OAAO,SAAShB,GAAG,MAAOA,GAAE+F,OAAO0D,QAAQC,iBAAiB1I,OAAO,SAAShB,GAAG,MAAOoI,GAAEiB,cAAcrJ,EAAEI,MAAMgD,iBAAiB,UAAU,QAASnD,GAAED,GAAG,GAAIC,KAAK,OAAOD,GAAEU,QAAQ,SAASV,GAAG,IAAIoB,EAAE6B,QAAQjD,EAAE2J,cAAcjJ,QAAQT,EAAEgI,KAAK2B,KAAK3J,IAAI,MAAMC,GAAGkH,QAAQyC,IAAI,sCAAsC7J,EAAEqG,KAAKnG,EAAEgJ,eAAejJ,EAAE,QAASC,GAAEF,GAAG,OAAOa,QAAQ,WAAW,GAAIZ,IAAGD,EAAE8J,sBAAsBzD,IAAK,OAAO+B,GAAE3D,UAAUzE,EAAEkD,QAAQjD,IAAIwG,IAAI,WAAW,MAAOzG,GAAEI,MAAMgD,iBAAiB,SAAS,MAAOxC,SAAQC,QAAQO,EAAE6B,QAAQZ,SAAS0H,cAAcjJ,KAAKb,GAAGa,KAAKd,GAAGc,KAAK,SAASd,GAAG,MAAOA,GAAE0D,IAAIxD,KAAK,OAAOqE,WAAWvE,EAAEiC,MAAM+H,QAAQ/J,OAAixBuE,EAA7wB,WAAa,QAASxE,GAAEA,GAAG,QAASC,GAAEA,GAAG,MAAOmB,GAAEwH,UAAU5I,EAAEyG,KAAK7F,QAAQC,UAAUD,QAAQC,QAAQb,EAAEyG,KAAK3F,KAAKb,GAAGmB,EAAE0H,cAAchI,KAAK,SAASb,GAAG,MAAOmB,GAAEuH,UAAU1I,EAAEmB,EAAEsH,SAAS1I,EAAEyG,QAAQ3F,KAAK,SAASb,GAAG,MAAO,IAAIW,SAAQ,SAASV,EAAEuB,GAAGzB,EAAEuG,OAAOrG,EAAEF,EAAEwG,QAAQ/E,EAAEzB,EAAEyG,IAAIxG,MAAM,OAAOsJ,OAAOtJ,GAAG,QAASA,GAAEC,GAAqO,MAAOA,aAAamE,SAAtP,SAAWrE,GAAG,GAAIC,GAAED,EAAEI,MAAMgD,iBAAiB,aAAc,OAAOnD,GAAEmI,EAAE3D,UAAUxE,GAAGa,KAAK,SAASb,GAAGD,EAAEI,MAAM+C,YAAY,aAAalD,EAAED,EAAEI,MAAMiD,oBAAoB,iBAAiBvC,KAAK,WAAW,MAAOd,KAAIY,QAAQC,QAAQb,IAAiCE,GAAGY,KAAK,WAAW,MAAOZ,aAAa+J,kBAAiBjK,EAAEE,GAAGqJ,SAAS3I,QAAQ4I,IAAIpI,EAAE6B,QAAQ/C,EAAE4C,YAAYY,IAAI,SAAS1D,GAAG,MAAOC,GAAED,QAAQY,QAAQC,QAAQX,GAAG,OAAOuE,UAAUxE,EAAEgC,MAAMiI,SAASlK,OAAgCmC,GAAGJ,qBAAiB,GAAOK,WAAU,GAAIJ,GAAGmI,MAAMlK,EAAEmK,MAAM3I,EAAE4I,OAAO1I,EAAEqE,OAAOnE,EAAEyI,YAAYpK,EAAE+B,MAAMsI,UAAUjG,EAAEkG,OAAOhG,EAAEiG,KAAKrJ,EAAEsJ,QAAQtC,EAAElG,YAAayI,WAA0C3I,uBCcvgS,GAAI4I,GAASA,GAAW,SAASC,GAGhC,SAAoB,KAATA,GAA6C,mBAAdC,YAA6B,eAAepE,KAAKoE,UAAUC,YAArG,CAGA,GACGC,GAAMH,EAAKxI,SAEX4I,EAAU,WACX,MAAOJ,GAAKK,KAAOL,EAAKM,WAAaN,GAEpCO,EAAYJ,EAAIK,gBAAgB,+BAAgC,KAChEC,EAAoB,YAAcF,GAClCG,EAAQ,SAASC,GAClB,GAAIC,GAAQ,GAAIC,YAAW,QAC3BF,GAAKG,cAAcF,IAElBG,EAAY,eAAelF,KAAKmE,EAAKgB,cAAgBhB,EAAKiB,OAC1DC,EAAe,eAAerF,KAAKoE,UAAUC,WAC7CiB,EAAgB,SAASC,IACzBpB,EAAKqB,cAAgBrB,EAAK9C,YAAY,WACtC,KAAMkE,IACJ,IAKFE,EAAS,SAASC,GACnB,GAAIC,GAAU,WACO,gBAATD,GACVnB,IAAUqB,gBAAgBF,GAE1BA,EAAKG,SAGPxE,YAAWsE,EATiB,MAW3BG,EAAW,SAASC,EAAWC,EAAajB,GAC7CiB,KAAiBC,OAAOD,EAExB,KADA,GAAI3L,GAAI2L,EAAY3J,OACbhC,KAAK,CACX,GAAI6L,GAAWH,EAAU,KAAOC,EAAY3L,GAC5C,IAAwB,kBAAb6L,GACV,IACCA,EAASC,KAAKJ,EAAWhB,GAASgB,GACjC,MAAOR,GACRD,EAAcC,MAKhBa,EAAW,SAASC,GAGrB,MAAI,6EAA6ErG,KAAKqG,EAAKhH,MACnF,GAAID,OAAMkH,OAAOC,aAAa,OAASF,IAAQhH,KAAMgH,EAAKhH,OAE3DgH,GAENG,EAAY,SAASH,EAAMI,EAAMC,GAC7BA,IACJL,EAAOD,EAASC,GAGjB,IAIGM,GAHAZ,EAAYa,KACZvH,EAAOgH,EAAKhH,KACZwH,EA3CoB,6BA2CZxH,EAERyH,EAAe,WAChBhB,EAASC,EAAW,qCAAqC9G,MAAM,MAuCjE,IAFA8G,EAAU5F,WAAa4F,EAAUgB,KAE7BnC,EAUH,MATA+B,GAAapC,IAAUyC,gBAAgBX,OACvChF,YAAW,WACVqD,EAAU/E,KAAOgH,EACjBjC,EAAUuC,SAAWR,EACrB5B,EAAMH,GACNoC,IACArB,EAAOkB,GACPZ,EAAU5F,WAAa4F,EAAUmB,QA5CrB,WACZ,IAAK7B,GAAkBwB,GAAS3B,IAAef,EAAK9D,WAAY,CAE/D,GAAI8G,GAAS,GAAI9G,WAWjB,OAVA8G,GAAO7G,UAAY,WAClB,GAAI8G,GAAM/B,EAAgB8B,EAAO5G,OAAS4G,EAAO5G,OAAOY,QAAQ,eAAgB,wBACpEgD,GAAKlD,KAAKmG,EAAK,YAChBjD,EAAKkD,SAAS1H,KAAOyH,GAChCA,MAAIE,GACJvB,EAAU5F,WAAa4F,EAAUmB,KACjCJ,KAEDK,EAAO3G,cAAc6F,QACrBN,EAAU5F,WAAa4F,EAAUgB,MAOlC,GAHKJ,IACJA,EAAapC,IAAUyC,gBAAgBX,IAEpCQ,EACH1C,EAAKkD,SAAS1H,KAAOgH,MACf,CACOxC,EAAKlD,KAAK0F,EAAY,YAGlCxC,EAAKkD,SAAS1H,KAAOgH,GAGvBZ,EAAU5F,WAAa4F,EAAUmB,KACjCJ,IACArB,EAAOkB,OAoBRY,EAAWf,EAAUgB,UACrBtD,EAAS,SAASmC,EAAMI,EAAMC,GAC/B,MAAO,IAAIF,GAAUH,EAAMI,GAAQJ,EAAKI,MAAQ,WAAYC,GAI9D,OAAyB,mBAAdtC,YAA6BA,UAAUqD,iBAC1C,SAASpB,EAAMI,EAAMC,GAM3B,MALAD,GAAOA,GAAQJ,EAAKI,MAAQ,WAEvBC,IACJL,EAAOD,EAASC,IAEVjC,UAAUqD,iBAAiBpB,EAAMI,KAI1Cc,EAASG,MAAQ,aACjBH,EAASpH,WAAaoH,EAASR,KAAO,EACtCQ,EAASI,QAAU,EACnBJ,EAASL,KAAO,EAEhBK,EAAS5G,MACT4G,EAASK,aACTL,EAASM,WACTN,EAASO,QACTP,EAASQ,QACTR,EAASzH,QACTyH,EAASS,WACR,KAEM9D,KAEY,mBAAT+D,OAAwBA,MACb,mBAAXrL,SAA0BA,QACjCgK,EAAKsB,QAM4BjE,GAAOkE,UAC1ClE,iBAAwBC,ICnL1BkE,GAAEC,QAAQC,UAAYF,EAAEC,QAAQE,uBAErB,qBACG,qBACE,oBACF,kBACE,UACJ,WACE,0BACY,oBACH3L,OAAOjB,SAAS6M,qBACnB,6BACI,sCAET,2BACI,0BACD,sBAIT,gBACAC,aAAe7B,KAAK8B,KAAKC,oBACzBnN,QAAQoN,UAAYhC,KAAKpL,QAAQoN,UAAU5L,IAAI,SAAU6L,SAC3C,YAAbA,QAEMjC,KAAKpL,QAAQsN,kBAAkBC,kBAC1B,eAGE,gBAAbF,UAEQjC,KAAKoC,YAAYnP,aAClB+M,KAAKoC,YAAYpP,WAClBgN,KAAKpL,QAAQsN,kBAAkBG,sBAC1B,oBAGE,eAAbJ,UAEQjC,KAAKoC,YAAYpP,YAClBgN,KAAKoC,YAAYnP,YAClB+M,KAAKpL,QAAQsN,kBAAkBI,qBAC1B,mBAGRL,GACNjC,SAECuC,GAAYf,EAAEgB,QAAQC,OAAO,MAAO,6DACnCzC,KAAKpL,QAAQ8N,OAAQ,MACnBC,YAEHC,SAASC,YAAYN,EAAW,YAAavC,KAAK8C,uBAAwB9C,QAC1E4C,SAASC,YAAYN,EAAW,WAAYvC,KAAK8C,uBAAwB9C,SAEvE+C,GAAW,kCACX/C,MAAKpL,QAAQoO,aAAYD,GAAsB,gBAE9CE,KAAOzB,EAAEgB,QAAQC,OAAO,IAAKM,EAAUR,QACvCU,KAAKC,GAAK,wBACVD,KAAKrB,MAAQ5B,KAAKpL,QAAQgN,WAC1BuB,OAAS3B,EAAEgB,QAAQC,OAAO,KAAM,kBAAmBF,QAEnD3N,QAAQoN,UAAU5O,QAAQ,SAAU6O,MACnCmB,GAAM5B,EAAEgB,QAAQC,OAAO,KAAM,oBAAqBzC,KAAKmD,UACvDvB,MAAQK,EAASpC,IACV2B,GAAEgB,QAAQC,OAAO,IAAKR,EAAS9L,UAAWiN,KACnDR,SAASC,YAAYO,EAAK,QAASpD,KAAKqD,SAAUrD,OACnDA,QAED4C,SAASU,wBAAwBf,SAE9BA,aAGC,SAAUpE,EAAOoF,GACrBA,SACG3O,QAAQ2O,SAAWA,GAErBvD,KAAKpL,QAAQoO,kBACXQ,MAAQxN,OAAOqE,KAAK,GAAI,SAAU,wHAClCmJ,MAAMzO,SAAS0O,MAAMzD,KAAK0D,eAAe1D,KAAKpL,QAAQ+O,kBAAmB3D,KAAKpL,QAAQgP,mBAAoB5D,KAAKpL,QAAQiP,uBAEzHC,wBACO9D,KAAK6B,aAAa/O,MAAME,oBACpB,sBACM,YACTgN,KAAK6B,aAAa/O,MAAMG,YAC7B+M,KAAK8B,KAAKiC,iBACR/D,KAAK8B,KAAKkC,aAEgB,SAAhChE,KAAK8D,cAAcG,eAChBH,cAAcG,SAAWjE,KAAK8B,KAAKoC,UAAUC,EAAK,UAClDL,cAAcM,cAAe,GACzBpE,KAAK8D,cAAcG,SAASI,SAAS,YACzCP,cAAcQ,gBAAkBtE,KAAK8D,cAAcG,cACnDH,cAAcS,oBAAqB,OACnCT,cAAcG,SAAWjE,KAAK8B,KAAKoC,UAAUC,EAAK,WAEpDrC,KAAK0C,KAAK,mBAAqBrG,MAAOA,IACtC6B,KAAKpL,QAAQ8N,aACXI,wBAAwBrK,KAAM,OAEjCuH,KAAKpL,QAAQ6P,2BACVC,qBAEHzC,GAA4B,gBAAV9D,GAAqBA,EAAMwG,OAAOxO,UAAYgI,KACnD,gBAAb8D,QACKjC,MAAK4E,eAAe3C,QAExB4C,eAAiB7E,KAAK8E,sBAAsB9E,KAAK6B,cAClD7B,KAAK8D,cAAcM,oBAChBS,eAAe/R,MAAME,MAAQgN,KAAK8D,cAAcG,eAElDc,wBAAwB9C,4BAGN,SAAUA,MAC7B+C,GAAShF,OACFlD,MAAMkD,KAAK6B,oBACXoD,SAASjF,KAAK8D,cAAcG,SAAS1J,QAAQ,cAC5C0K,SAASjF,KAAK8D,cAAcoB,UAAU3K,QAAQ,SAEvD/G,KAAK,SAAU2R,KACPC,SAAWrQ,SAASC,cAAc,UACrCoQ,GAAWJ,EAAOI,WACfP,eAAeQ,cAAcC,aAAaF,EAAUJ,EAAOH,kBACzD1O,UAAY,aACZrD,MAAMyS,gBAAkB,QAAUJ,EAAU,OAC5CrS,MAAM0S,SAAW,aACjB1S,MAAM2S,OAAS,OACf3S,MAAM4S,QAAU,YAChB5S,MAAME,MAAQgS,EAAOlB,cAAcG,WACnCnR,MAAMG,OAAS+R,EAAOlB,cAAcoB,YACtCS,mBAAmB1D,KAE3B2D,MAAM,SAAU7L,WACLA,MAAM,8BAA+BA,yBAIjC,SAAUkI,QACvB4C,eAAe/R,MAAM+S,QAAU,KAChCC,GAAW9F,KAAKpL,QAAQoN,UAAUtO,OAAO,SAAUqS,SAC9CA,GAAK5P,YAAc8L,MAEjB6D,EAAS,QACfjE,aAAa/O,MAAME,MAAQ8S,EAAS9S,MAAQ,UAC5C6O,aAAa/O,MAAMG,OAAS6S,EAAS7S,OAAS,KAC/C+M,KAAK6B,aAAa/O,MAAME,MAAQgN,KAAK6B,aAAa/O,MAAMG,YACrD+S,YAAc,gBAEdA,YAAc,iBAEhBlE,KAAKmE,QAAQjG,KAAK8D,cAAcoC,aAChCpE,KAAKqE,QAAQnG,KAAK8D,cAAcsC,WAChCtE,KAAKuE,iBACNrG,KAAKpL,QAAQ0R,eACVC,YAAYtE,QAEZ2C,eAAe3C,gBAIX,SAAUA,MACjB+C,GAAShF,KACTwG,EAAcC,YAAY,WACxBzB,EAAOpQ,QAAQ0R,UAAUI,4BACbF,KACP5B,eAAe3C,KAEvB+C,EAAOpQ,QAAQ+R,0BAGJ,SAAUC,MACpB5B,GAAShF,KACT6G,EAAiB7G,KAAK6B,aAAa/O,MAAME,OACzCgN,KAAK8D,cAAcM,cAA6B,gBAAbwC,GAA8B5G,KAAK8D,cAAcS,oBAAmC,gBAAbqC,OAC3F5G,KAAK8D,cAAcG,YAE3BnH,MAAMkI,EAAOnD,oBACboD,SAAS4B,UACR5B,SAASD,EAAOnD,aAAa/O,MAAMG,OAAOsH,QAAQ,SAE3D/G,KAAK,SAAU2R,MACR1F,GAAOuF,EAAO8B,eAAe3B,EAC7BH,GAAOpQ,QAAQoO,aACP1F,OAAOmC,EAAMuF,EAAOpQ,QAAQ2O,SAAW,UAE1CwD,oBAAoB5B,EAASH,EAAOgB,eAEtCtB,iBAAgB,GAEnBM,EAAOH,iBACLG,EAAOlB,cAAcM,eAChBvC,aAAa/O,MAAME,MAAQ,OACzBgS,EAAOlB,cAAcS,qBACvB1C,aAAa/O,MAAME,MAAQgS,EAAOlB,cAAcQ,kBAGhDzC,aAAa/O,MAAME,MAAQgS,EAAOlB,cAAcG,WAElDpC,aAAa/O,MAAMG,OAAS+R,EAAOlB,cAAcoB,YACjD8B,sBAAsBhC,EAAOnD,aAAcmD,EAAOH,eAAgBG,EAAOI,YACzEtD,KAAKuE,mBACLvE,KAAKmE,QAAQjB,EAAOlB,cAAcoC,UAClCpE,KAAKqE,QAAQnB,EAAOlB,cAAcsC,SAEpCtE,KAAK0C,KAAK,wBAEpBoB,MAAM,SAAU7L,WACLA,MAAM,yBAA0BA,0BAI3B,SAAUkN,EAAKjB,QAC7BxC,MAAM0D,SAAS,IAAK,QACrBC,GAAcnH,KAAKoH,iBAAiBH,EAAKjB,EAAahG,WACrDwD,MAAMzO,SAAS+D,KAAKtC,UAAY,QAChCgN,MAAMzO,SAAS0O,MAAM0D,QACrB3D,MAAMzO,SAASsS,wBAGN,SAAUzF,EAAO0F,EAAcC,SACtC,sBAAuB3F,oEAEV2F,83DAsEND,uDAGE,SAAUL,EAAKjB,EAAahB,SACrC,gJAGiBgB,kOAMNiB,gFAGG,SAAUO,MAC3B3C,GAAiB9P,SAASC,cAAc,gBACrCyS,WAAWnC,aAAaT,EAAgB2C,KACxCC,WAAWC,YAAYF,KACf9R,YAAY8R,KACZ1U,MAAME,MAAQwU,EAAO1U,MAAME,QAC3BF,MAAMG,OAASuU,EAAO1U,MAAMG,SAC5BH,MAAM4S,QAAU,iBAChB5S,MAAM6U,SAAW,SACzB9C,yBAGc,SAAU2C,EAAQ3C,EAAgBO,GACnDP,EAAe4C,eACFA,WAAWnC,aAAakC,EAAQ3C,KAChC4C,WAAWC,YAAYtC,KACvBqC,WAAWC,YAAY7C,aAIjC,cACH+C,GAAM7S,SAASC,cAAc,WAC7ByD,KAAO,aACPjC,i9MAoDKsC,KAAKpD,YAAYkS,mBAGZ,SAAUC,OAKpB,GAJAC,GAAa1P,KAAKyP,EAAQxP,MAAM,KAAK,IACrC0P,EAAaF,EAAQxP,MAAM,KAAK,GAAGA,MAAM,KAAK,GAAGA,MAAM,KAAK,GAC5D2P,EAAK,GAAIC,aAAYH,EAAWrS,QAChCyS,EAAK,GAAIC,UAASH,GACdvU,EAAI,EAAGA,EAAIqU,EAAWrS,OAAQhC,MAC/B2U,SAAS3U,EAAGqU,EAAWvP,WAAW9E,UAElC,IAAI+E,OAAMwP,IAAMvP,KAAMsP,4BAGP,SAAU1T,MAC5BgU,GAAcrI,KAAKmD,OAAOrQ,MAC1BwV,EAAYtI,KAAKiD,KAAKnQ,KACX,eAAXuB,EAAEoE,QACQiN,QAAU,UACZ6C,qBAAuB,MACvBC,wBAA0B,QAExB9C,QAAU,SACZ6C,qBAAuB,QACvBC,wBAA0B,wBAIvB,SAAUC,MACrBC,GAAmB3T,SAAS4T,uBAAuB,6BAA6B,MAChFF,EAAM,MAAOC,GAAiB5V,MAAM4S,QAAU,UACjC5S,MAAM4S,QAAU,4BAIzB,UACD,QAKXlE,EAAEoH,UAAY,SAAShU,SACd,IAAI4M,GAAEC,QAAQC,UAAU9M"} \ No newline at end of file diff --git a/assets/custom-functions.js b/assets/custom-functions.js index f7fa9f3fc16eabc4288dab52f255c86ebfd12fda..3369b9fc5460627b0d1672aee046cc5160940eda 100644 --- a/assets/custom-functions.js +++ b/assets/custom-functions.js @@ -1,13 +1,13 @@ window.forecastTab = Object.assign({}, window.forecastTab, { forecastMaps: { styleHandle: function(feature, context){ - // get props from hideout + // Get props from hideout const {bounds, colorscale, style, colorProp} = context.props.hideout; - // get value the determines the color - const value = feature.properties[colorProp]; + // Get value the determines the color + const value = feature.properties[colorProp]; for (let i = 0; i < bounds.length; ++i) { if (value > bounds[i]) { - // set the fill color according to the class + // Set the fill color according to the class style.fillColor = colorscale[i]; } } @@ -15,23 +15,23 @@ window.forecastTab = Object.assign({}, window.forecastTab, { }, pointToLayer: function(feature, latlng, context){ const {min, max, colorscale, circleOptions, colorProp} = context.props.hideout; - // set color based on color prop. + // Set color based on color prop. circleOptions.fillColor = feature.properties[colorProp]; - // sender a simple circle marker. + // Sender a simple circle marker. return L.circleMarker(latlng, circleOptions); }, bindTooltip: function(feature, layer, context) { const props = feature.properties; - // delete props.cluster; + // Delete props.cluster; layer.bindTooltip(JSON.stringify(props.value), { opacity: 1.0 }) } }, wasMaps: { styleHandle: function(feature, context){ - // get props from hideout + // Get props from hideout const {bounds, colorscale, style, colorProp} = context.props.hideout; - // get value the determines the color - const value = feature.properties[colorProp]; + // Get value the determines the color + const value = feature.properties[colorProp]; style.fillColor = colorscale[value]; return style; }, @@ -40,9 +40,15 @@ window.forecastTab = Object.assign({}, window.forecastTab, { window.evaluationTab = Object.assign({}, window.evaluationTab, { evaluationMaps: { - pointToLayer: function(feature, latlng, context){ - const {circleOptions} = context.props.hideout; - // sender a simple circle marker. + pointToLayer: function(feature, latlng, context){ + if (typeof context.props.hideout.colorProp !== 'undefined') { + var {colorscale, circleOptions, colorProp} = context.props.hideout; + const value = feature.properties[colorProp]; + circleOptions.fillColor = colorscale[value]; + } else { + var {circleOptions} = context.props.hideout; + } + // Sender a simple circle marker. return L.circleMarker(latlng, circleOptions); }, } @@ -54,7 +60,7 @@ window.observationsTab = Object.assign({}, window.observationsTab, { const {colorscale, circleOptions, colorProp} = context.props.hideout; const value = feature.properties[colorProp]; circleOptions.fillColor = colorscale[value]; - // sender a simple circle marker. + // Sender a simple circle marker. return L.circleMarker(latlng, circleOptions); }, } @@ -75,234 +81,7 @@ $(document).ready(function () { }); }); -// Add logos for the animated gifs -$(document).ready(function () { - $(document).on('click', "#model-slider-graph", function () { - const logos = document.getElementById('logos'); - if(!logos) { - var img = new Image(); - img.src = './assets/images/logoline.png'; - img.style.position = 'absolute'; - img.style.width = '40rem'; - img.style.height = '5rem'; - img.style.top = '0'; - img.style.left = '23rem'; - img.style.zIndex = '999'; - img.setAttribute('id','logos'); - img.style.display = 'none'; - - // Get the element to overlay the image on - var div = document.querySelectorAll('.leaflet-map-pane')[0]; - - // Append the image to the element - div.appendChild(img); - } - }); -}); - -// MAKE FULLSCREEN ICON SMALLER WHEN APP IS FULLSCREEN -function changeFullscreenIcon() { - const isFullscreen = window.innerWidth === screen.width && window.innerHeight === screen.height; - const button = document.getElementById('fullscreen-tab'); - if (isFullscreen) { - button.classList.remove('small'); - } else { - button.classList.add('small'); - } -} - -// SEND FULLSCREEN REQUEST TO PARENT WINDOW -$(document).ready(function () { - $(document).on('click', "#fullscreen-tab", function () { - parent.postMessage('fullscreen', '*'); - changeFullscreenIcon(); - }) -}); - -// DETECT IFRAME AND REMOVE FULLSCREEN BUTTON -$(window).on('load', function removeFullscreen () { - const tab = document.getElementById('fullscreen-tab'); - if (tab != null) { - if (window.self === window.top){ - tab.style.visibility = "hidden"; - } - } else { - setTimeout(removeFullscreen, 5); - } -}); - -// Move datepicker above the time series area -$(document).ready(function () { - $(document).on('click', ".SingleDatePickerInput_clearDate, .SingleDatePicker", function moveDatePicker () { - const element = document.getElementsByClassName('DayPicker')[0]; - //we still want the menu to go below on eval/vis for modis - const evalVis = document.getElementById('eval-date'); - if (evalVis != null) { - return - }else if (element != null) { - element.style.position = 'absolute'; - element.style.bottom = '78px'; - }else { - setTimeout(moveDatePicker, 20); - }; - }); -}); - // COLLAPSE NAVBAR HAMBURGER RESIZE LARGER $(window).on('resize', function () { if (window.innerWidth > 1045) $('.navbar-collapse').removeClass('show') }) - -// ADD SPACES IN DATE INPUT IN BETWEEN DAY MONTH AND YEAR -$(document).ready(function() { - $(document).on('keydown input', '#date', function(e) { - var input = this.value; - var key = e.key || String.fromCharCode(e.keyCode || e.which || e.charCode); - - var caretPosition = this.selectionStart; - - if (key === 'Backspace') { - var previousChar = input[caretPosition - 1]; - - // Allow deletion of spaces - if (previousChar === ' ' && (caretPosition === 3 || caretPosition === 7)) { - this.value = input.slice(0, caretPosition - 2) + input.slice(caretPosition); - this.setSelectionRange(caretPosition - 2, caretPosition - 2); - e.preventDefault(); - return; - } - } - - if (input.length === 11) return; - - var values = input.split(' '); - var output = values.map(function(v, i) { - if (v.length === 2 && i < 1) { - return v + ' '; - } else if (v.length === 3 && i < 2) { - return v + ' '; - } else { - return v; - } - }); - - this.value = output.join('').substr(0, 11); - - // Adjust caret position after modifying the input value - var newCaretPosition = caretPosition - (input.length - this.value.length); - this.setSelectionRange(newCaretPosition, newCaretPosition); - }); -}); - -//================== Stats table carets =============================================== -// The aeronet stats table changes the position/index of the regions when they -// are clicked, so we cannot easily assign a class to the regions that will maintain on click, -// so we must remove the carets on click, and then add them back after the table has finished - -// changing it's state - -// ADD FUNCTION TO WAIT FOR TABLE TO FINISH CHANGING -function waitForMutation(selector, func) { - return new Promise(resolve => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)); - }; - const observer = new MutationObserver(mutations => { - if (document.querySelector(selector)) { - resolve(document.querySelector(selector)); - // console.log(mutations); - func(mutations); - } - }); - observer.observe(document.body, { - childList: true, - subtree: true, - attributeOldValue: true, - characterDataOldValue: true - }); - }); -} - -//FIND THE REGIONS TO REMOVE THE CARETS BEFORE FLIPPING THEM -function targetRegions(mutations) { - //Carets will populate in the wrong rows after click, so we must remove them - for (const element of mutations) { - if (['Mediterranean','NAfrica', 'MiddleEast'].includes(element.oldValue)) { - //GET ELEMENTS PARENT NODE 2 ABOVE AS IT CONTAINS THE TARGET CLASS - const target = element.target.parentNode.parentNode; - target.classList.remove('table_caret_up'); - target.classList.remove('table_caret_down'); - }; - } - // Now add the carets back in where appropriate - flipCarets(); -} - -//CREATE FUNCTION TO FIND REGIONS AND FLIP CARETS AS NEEDED -function flipCarets() { - var areas = "td:contains('Europe'), td:contains('Mediterranean'),td:contains('NAfrica'),td:contains('MiddleEast')"; - $(areas).addClass('table_caret_down'); - //Each table has the four regions. Split the returned areas into tables, and then address caret flips - for(var i = 0; i < ($(areas).length/4); i++){ - var medIndex = parseInt($("td:contains('Mediterranean')").eq(i).attr('data-dash-row')); - var nafricaIndex = parseInt($("td:contains('NAfrica')").eq(i).attr('data-dash-row')); - var middleEastIndex = parseInt($("td:contains('MiddleEast')").eq(i).attr('data-dash-row')); - var totalIndex = parseInt($("td:contains('Total')").eq(i).attr('data-dash-row')); - //for each table, check if the caret should be flipped - //if the next region is not at the next index, the dropdown is open - //trigger the caret to be flipped - if (medIndex !== 1) { - $("td:contains('Europe')").eq(i).addClass('table_caret_up'); - }; - if ((medIndex + 1) !== middleEastIndex) { - $("td:contains('Mediterranean')").eq(i).addClass('table_caret_up'); - }; - if ((middleEastIndex + 1) !== nafricaIndex) { - $("td:contains('MiddleEast')").eq(i).addClass('table_caret_up'); - }; - if ((nafricaIndex + 1) !== totalIndex) { - $("td:contains('NAfrica')").eq(i).addClass('table_caret_up'); - }; - }; -}; - -//ADD FUNCTION TO WAIT FOR CLICKS ON AREAS THAT SHOULD TRIGGER CARET FLIPS -$(document).ready(function () { - $(document).on('click', "td:contains('Europe'), td:contains('Mediterranean'),td:contains('NAfrica'),td:contains('MiddleEast'), #scores-apply, #evaluation-tab", function () { - waitForMutation('td.dash-cell.column-0', targetRegions); - }) -}) -//================== Stats table carets END =============================================== - - -//==================Functions to resize colorbar ====================== - -function setWidthForColorbars(nothing) { - const colorbars = document.querySelectorAll('.leaflet-control-colorbar'); - colorbars.forEach(colorbar => { - const info = document.querySelector('.info').offsetWidth; - colorbar.style.maxWidth = info.toString() + 'px'; - }); -} - -$(document).ready(function () { - $(document).on('click', "#models-apply, .DayPicker, .SingleDatePickerInput_clearDate", function () { - setTimeout(setWidthForColorbars, 500); - }) -}) - -$(document).ready(function () { - setTimeout(setWidthForColorbars, 1500); -}); -//==================END Funcfions to resize colorbar ================== - -// //================= Function to clear datepicker without resetting ================================= -$(document).ready(function () { - $(document).on('click', "#clear_button", function () { - const date = document.getElementById('date'); - date.focus(); - date.click(); - date.value = ''; - }) -}) - diff --git a/assets/datepicker.js b/assets/datepicker.js new file mode 100644 index 0000000000000000000000000000000000000000..18cc0c0c58d06eaf42305e1e7519c21c598cee9b --- /dev/null +++ b/assets/datepicker.js @@ -0,0 +1,68 @@ +// ADD SPACES IN DATE INPUT IN BETWEEN DAY MONTH AND YEAR +$(document).ready(function() { + $(document).on('keydown input', '.DateInput_input', function(e) { + var input = this.value; + var key = e.key || String.fromCharCode(e.keyCode || e.which || e.charCode); + + var caretPosition = this.selectionStart; + + if (key === 'Backspace') { + var previousChar = input[caretPosition - 1]; + + // Allow deletion of spaces + if (previousChar === ' ' && (caretPosition === 3 || caretPosition === 7)) { + this.value = input.slice(0, caretPosition - 2) + input.slice(caretPosition); + this.setSelectionRange(caretPosition - 2, caretPosition - 2); + e.preventDefault(); + return; + } + } + + if (input.length === 11) return; + + var values = input.split(' '); + var output = values.map(function(v, i) { + if (v.length === 2 && i < 1) { + return v + ' '; + } else if (v.length === 3 && i < 2) { + return v + ' '; + } else { + return v; + } + }); + + this.value = output.join('').substr(0, 11); + + // Adjust caret position after modifying the input value + var newCaretPosition = caretPosition - (input.length - this.value.length); + this.setSelectionRange(newCaretPosition, newCaretPosition); + }); +}); + +// MOVE DATEPICKER ABOVE THE TIMESERIES AREA +$(document).ready(function () { + $(document).on('click', ".SingleDatePickerInput_clearDate, .SingleDatePicker", function moveDatePicker () { + const element = document.getElementsByClassName('DayPicker')[0]; + // We still want the menu to go below on eval/vis for modis + const evalVis = document.getElementById('eval-date'); + if (evalVis != null) { + return + }else if (element != null) { + element.style.position = 'absolute'; + element.style.bottom = '78px'; + }else { + setTimeout(moveDatePicker, 20); + }; + }); +}); + +// CLEAR DATEPICKER WITHOUT RESETTING +$(document).ready(function () { + $(document).on('click', "#clear_button", function () { + const date = document.getElementById('date'); + date.focus(); + date.click(); + date.value = ''; + }) +}) + diff --git a/assets/download-img.js b/assets/download-img.js index 503e4b1638b3c5090f9224c93f0d6a337da6b0f2..710a8f1f3c26dae4a3e200df3c1ef79869eb2713 100644 --- a/assets/download-img.js +++ b/assets/download-img.js @@ -1,7 +1,8 @@ $(document).ready(function () { $(document).on('click', "#btn-frame-download", function () { - //first add the logos - addLogos(); + // First add the logos + // Adjust colorbar, as it is appearing covered by the info bar + makeChanges(); var element = document.getElementById("graph-collection"); // global variable if (element == null) { element = document.getElementById("prob-graph"); // global variable @@ -10,7 +11,8 @@ $(document).ready(function () { } } getCanvas(element); - removeLogos(); + // The changes need to be remove to ensure the map appears normal + removeChanges(); }); }); @@ -21,7 +23,7 @@ function getCanvas(element) { allowTaint: true, useCORS: true, async: true, - // windowWidth: element.offsetWidth + 90, + windowWidth: element.offsetWidth + 90, windowHeight: element.offsetHeight, logging: true, imageTimeout: 0, @@ -41,20 +43,20 @@ function saveAs(uri, filename) { link.href = uri; link.download = filename; - //Firefox requires the link to be in the body + // Firefox requires the link to be in the body document.body.appendChild(link); - //simulate click + // Simulate click link.click(); - //remove the link when done + // Remove the link when done document.body.removeChild(link); } else { window.open(uri); } } -//Add logos top map for screen shot +// ADD LOGOS AT THE TOP OF THE MAP FOR THE SCREENSHOT function addLogos() { const logos = document.getElementById('logos'); if (logos){ @@ -81,7 +83,31 @@ function addLogos() { }; } +// REMOVE LOGOS AFTER SCREENSHOT IS TAKEN function removeLogos() { var logos = document.getElementById('logos'); logos.remove(); }; + +// PUSH DOWN COLORBAR FOR SCREENSHOT +function makeChanges() { + // Push down + const colorbar = document.querySelector('.leaflet-control-colorbar'); + colorbar.style.paddingTop = '60px'; + // Add the logos for the screenshot + addLogos(); + // Remove the attribution in the bottom right corner + const attribution = document.querySelector('.leaflet-control-attribution'); + attribution.style.display = "none"; +} + +// RETURN COLORBAR TO INITIAL POSITION AFTER SCREENSHOT +function removeChanges() { + // Put colorbar back in place + const colorbar = document.querySelector('.leaflet-control-colorbar'); + colorbar.style.paddingTop = '0px'; + // Remove added logos + removeLogos(); +} + + diff --git a/assets/fullscreen.js b/assets/fullscreen.js new file mode 100644 index 0000000000000000000000000000000000000000..c1febe6e1b7d9d8b7dc0032a2c44599ead3f599f --- /dev/null +++ b/assets/fullscreen.js @@ -0,0 +1,31 @@ +// MAKE FULLSCREEN ICON SMALLER WHEN APP IS FULLSCREEN +function changeFullscreenIcon() { + const isFullscreen = window.innerWidth === screen.width && window.innerHeight === screen.height; + const button = document.getElementById('fullscreen-tab'); + if (isFullscreen) { + button.classList.remove('small'); + } else { + button.classList.add('small'); + } +} + +// SEND FULLSCREEN REQUEST TO PARENT WINDOW +$(document).ready(function () { + $(document).on('click', "#fullscreen-tab", function () { + parent.postMessage('fullscreen', '*'); + changeFullscreenIcon(); + }) +}); + +// DETECT IFRAME AND REMOVE FULLSCREEN BUTTON +$(window).on('load', function removeFullscreen () { + const tab = document.getElementById('fullscreen-tab'); + if (tab != null) { + if (window.self === window.top){ + tab.style.visibility = "hidden"; + } + } else { + setTimeout(removeFullscreen, 5); + } +}); + diff --git a/assets/gif_logos.js b/assets/gif_logos.js new file mode 100644 index 0000000000000000000000000000000000000000..2a29e16ae10732ccd484e08999dc79fc9a6ecd2e --- /dev/null +++ b/assets/gif_logos.js @@ -0,0 +1,24 @@ +// ADD LOGOS FOR THE ANIMATED GIF +$(document).ready(function () { + $(document).on('click', "#model-slider-graph", function () { + const logos = document.getElementById('logos'); + if(!logos) { + var img = new Image(); + img.src = './assets/images/logoline.png'; + img.style.position = 'absolute'; + img.style.width = '40rem'; + img.style.height = '5rem'; + img.style.top = '0'; + img.style.left = '23rem'; + img.style.zIndex = '999'; + img.setAttribute('id','logos'); + img.style.display = 'none'; + + // Get the element to overlay the image on + var div = document.querySelectorAll('.leaflet-map-pane')[0]; + + // Append the image to the element + div.appendChild(img); + } + }); +}); diff --git a/assets/html2canvas.min.js b/assets/html2canvas.min.js deleted file mode 100644 index aed6bfd70defa322824e5cba11b3ad5d6061ee28..0000000000000000000000000000000000000000 --- a/assets/html2canvas.min.js +++ /dev/null @@ -1,20 +0,0 @@ -/*! - * html2canvas 1.4.1 - * Copyright (c) 2022 Niklas von Hertzen - * Released under MIT License - */ -!function(A,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(A="undefined"!=typeof globalThis?globalThis:A||self).html2canvas=e()}(this,function(){"use strict"; -/*! ***************************************************************************** - Copyright (c) Microsoft Corporation. - - Permission to use, copy, modify, and/or distribute this software for any - purpose with or without fee is hereby granted. - - THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH - REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY - AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, - INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM - LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR - OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - PERFORMANCE OF THIS SOFTWARE. - ***************************************************************************** */var r=function(A,e){return(r=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(A,e){A.__proto__=e}||function(A,e){for(var t in e)Object.prototype.hasOwnProperty.call(e,t)&&(A[t]=e[t])})(A,e)};function A(A,e){if("function"!=typeof e&&null!==e)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");function t(){this.constructor=A}r(A,e),A.prototype=null===e?Object.create(e):(t.prototype=e.prototype,new t)}var h=function(){return(h=Object.assign||function(A){for(var e,t=1,r=arguments.length;ts[0]&&e[1]>10),s%1024+56320)),(B+1===t||16384>5],this.data[e=(e<<2)+(31&A)];if(A<=65535)return e=this.index[2048+(A-55296>>5)],this.data[e=(e<<2)+(31&A)];if(A>11)],e=this.index[e+=A>>5&63],this.data[e=(e<<2)+(31&A)];if(A<=1114111)return this.data[this.highValueIndex]}return this.errorValue},l);function l(A,e,t,r,B,n){this.initialValue=A,this.errorValue=e,this.highStart=t,this.highValueIndex=r,this.index=B,this.data=n}for(var C="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u="undefined"==typeof Uint8Array?[]:new Uint8Array(256),F=0;F>4,i[o++]=(15&t)<<4|r>>2,i[o++]=(3&r)<<6|63&B;return n}(y="KwAAAAAAAAAACA4AUD0AADAgAAACAAAAAAAIABAAGABAAEgAUABYAGAAaABgAGgAYgBqAF8AZwBgAGgAcQB5AHUAfQCFAI0AlQCdAKIAqgCyALoAYABoAGAAaABgAGgAwgDKAGAAaADGAM4A0wDbAOEA6QDxAPkAAQEJAQ8BFwF1AH0AHAEkASwBNAE6AUIBQQFJAVEBWQFhAWgBcAF4ATAAgAGGAY4BlQGXAZ8BpwGvAbUBvQHFAc0B0wHbAeMB6wHxAfkBAQIJAvEBEQIZAiECKQIxAjgCQAJGAk4CVgJeAmQCbAJ0AnwCgQKJApECmQKgAqgCsAK4ArwCxAIwAMwC0wLbAjAA4wLrAvMC+AIAAwcDDwMwABcDHQMlAy0DNQN1AD0DQQNJA0kDSQNRA1EDVwNZA1kDdQB1AGEDdQBpA20DdQN1AHsDdQCBA4kDkQN1AHUAmQOhA3UAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AKYDrgN1AHUAtgO+A8YDzgPWAxcD3gPjA+sD8wN1AHUA+wMDBAkEdQANBBUEHQQlBCoEFwMyBDgEYABABBcDSARQBFgEYARoBDAAcAQzAXgEgASIBJAEdQCXBHUAnwSnBK4EtgS6BMIEyAR1AHUAdQB1AHUAdQCVANAEYABgAGAAYABgAGAAYABgANgEYADcBOQEYADsBPQE/AQEBQwFFAUcBSQFLAU0BWQEPAVEBUsFUwVbBWAAYgVgAGoFcgV6BYIFigWRBWAAmQWfBaYFYABgAGAAYABgAKoFYACxBbAFuQW6BcEFwQXHBcEFwQXPBdMF2wXjBeoF8gX6BQIGCgYSBhoGIgYqBjIGOgZgAD4GRgZMBmAAUwZaBmAAYABgAGAAYABgAGAAYABgAGAAYABgAGIGYABpBnAGYABgAGAAYABgAGAAYABgAGAAYAB4Bn8GhQZgAGAAYAB1AHcDFQSLBmAAYABgAJMGdQA9A3UAmwajBqsGqwaVALMGuwbDBjAAywbSBtIG1QbSBtIG0gbSBtIG0gbdBuMG6wbzBvsGAwcLBxMHAwcbByMHJwcsBywHMQcsB9IGOAdAB0gHTgfSBkgHVgfSBtIG0gbSBtIG0gbSBtIG0gbSBiwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdgAGAALAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAdbB2MHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB2kH0gZwB64EdQB1AHUAdQB1AHUAdQB1AHUHfQdgAIUHjQd1AHUAlQedB2AAYAClB6sHYACzB7YHvgfGB3UAzgfWBzMB3gfmB1EB7gf1B/0HlQENAQUIDQh1ABUIHQglCBcDLQg1CD0IRQhNCEEDUwh1AHUAdQBbCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIaQhjCGQIZQhmCGcIaAhpCGMIZAhlCGYIZwhoCGkIYwhkCGUIZghnCGgIcAh3CHoIMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIgggwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAALAcsBywHLAcsBywHLAcsBywHLAcsB4oILAcsB44I0gaWCJ4Ipgh1AHUAqgiyCHUAdQB1AHUAdQB1AHUAdQB1AHUAtwh8AXUAvwh1AMUIyQjRCNkI4AjoCHUAdQB1AO4I9gj+CAYJDgkTCS0HGwkjCYIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiCCIIIggiAAIAAAAFAAYABgAGIAXwBgAHEAdQBFAJUAogCyAKAAYABgAEIA4ABGANMA4QDxAMEBDwE1AFwBLAE6AQEBUQF4QkhCmEKoQrhCgAHIQsAB0MLAAcABwAHAAeDC6ABoAHDCwMMAAcABwAHAAdDDGMMAAcAB6MM4wwjDWMNow3jDaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAGgAaABoAEjDqABWw6bDqABpg6gAaABoAHcDvwOPA+gAaABfA/8DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DvwO/A78DpcPAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcAB9cPKwkyCToJMAB1AHUAdQBCCUoJTQl1AFUJXAljCWcJawkwADAAMAAwAHMJdQB2CX4JdQCECYoJjgmWCXUAngkwAGAAYABxAHUApgn3A64JtAl1ALkJdQDACTAAMAAwADAAdQB1AHUAdQB1AHUAdQB1AHUAowYNBMUIMAAwADAAMADICcsJ0wnZCRUE4QkwAOkJ8An4CTAAMAB1AAAKvwh1AAgKDwoXCh8KdQAwACcKLgp1ADYKqAmICT4KRgowADAAdQB1AE4KMAB1AFYKdQBeCnUAZQowADAAMAAwADAAMAAwADAAMAAVBHUAbQowADAAdQC5CXUKMAAwAHwBxAijBogEMgF9CoQKiASMCpQKmgqIBKIKqgquCogEDQG2Cr4KxgrLCjAAMADTCtsKCgHjCusK8Qr5CgELMAAwADAAMAB1AIsECQsRC3UANAEZCzAAMAAwADAAMAB1ACELKQswAHUANAExCzkLdQBBC0kLMABRC1kLMAAwADAAMAAwADAAdQBhCzAAMAAwAGAAYABpC3ELdwt/CzAAMACHC4sLkwubC58Lpwt1AK4Ltgt1APsDMAAwADAAMAAwADAAMAAwAL4LwwvLC9IL1wvdCzAAMADlC+kL8Qv5C/8LSQswADAAMAAwADAAMAAwADAAMAAHDDAAMAAwADAAMAAODBYMHgx1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1ACYMMAAwADAAdQB1AHUALgx1AHUAdQB1AHUAdQA2DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AD4MdQBGDHUAdQB1AHUAdQB1AEkMdQB1AHUAdQB1AFAMMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQBYDHUAdQB1AF8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUA+wMVBGcMMAAwAHwBbwx1AHcMfwyHDI8MMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAYABgAJcMMAAwADAAdQB1AJ8MlQClDDAAMACtDCwHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsB7UMLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHdQB1AHUAdQB1AHUAdQB1AHUAdQB1AHUAdQB1AA0EMAC9DDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAsBywHLAcsBywHLAcsBywHLQcwAMEMyAwsBywHLAcsBywHLAcsBywHLAcsBywHzAwwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwAHUAdQB1ANQM2QzhDDAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMABgAGAAYABgAGAAYABgAOkMYADxDGAA+AwADQYNYABhCWAAYAAODTAAMAAwADAAFg1gAGAAHg37AzAAMAAwADAAYABgACYNYAAsDTQNPA1gAEMNPg1LDWAAYABgAGAAYABgAGAAYABgAGAAUg1aDYsGVglhDV0NcQBnDW0NdQ15DWAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAlQCBDZUAiA2PDZcNMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAnw2nDTAAMAAwADAAMAAwAHUArw23DTAAMAAwADAAMAAwADAAMAAwADAAMAB1AL8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAB1AHUAdQB1AHUAdQDHDTAAYABgAM8NMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA1w11ANwNMAAwAD0B5A0wADAAMAAwADAAMADsDfQN/A0EDgwOFA4wABsOMAAwADAAMAAwADAAMAAwANIG0gbSBtIG0gbSBtIG0gYjDigOwQUuDsEFMw7SBjoO0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGQg5KDlIOVg7SBtIGXg5lDm0OdQ7SBtIGfQ6EDooOjQ6UDtIGmg6hDtIG0gaoDqwO0ga0DrwO0gZgAGAAYADEDmAAYAAkBtIGzA5gANIOYADaDokO0gbSBt8O5w7SBu8O0gb1DvwO0gZgAGAAxA7SBtIG0gbSBtIGYABgAGAAYAAED2AAsAUMD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHJA8sBywHLAcsBywHLAccDywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywPLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAc0D9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAccD9IG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIGFA8sBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHLAcsBywHPA/SBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gbSBtIG0gYUD0QPlQCVAJUAMAAwADAAMACVAJUAlQCVAJUAlQCVAEwPMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAA//8EAAQABAAEAAQABAAEAAQABAANAAMAAQABAAIABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQACgATABcAHgAbABoAHgAXABYAEgAeABsAGAAPABgAHABLAEsASwBLAEsASwBLAEsASwBLABgAGAAeAB4AHgATAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABYAGwASAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWAA0AEQAeAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAFAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJABYAGgAbABsAGwAeAB0AHQAeAE8AFwAeAA0AHgAeABoAGwBPAE8ADgBQAB0AHQAdAE8ATwAXAE8ATwBPABYAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AHgAeAFAATwBAAE8ATwBPAEAATwBQAFAATwBQAB4AHgAeAB4AHgAeAB0AHQAdAB0AHgAdAB4ADgBQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgBQAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAJAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAkACQAJAAkACQAJAAkABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAFAAHgAeAB4AKwArAFAAUABQAFAAGABQACsAKwArACsAHgAeAFAAHgBQAFAAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUAAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAYAA0AKwArAB4AHgAbACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAB4ABAAEAB4ABAAEABMABAArACsAKwArACsAKwArACsAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAKwArACsAKwBWAFYAVgBWAB4AHgArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AGgAaABoAGAAYAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQAEwAEACsAEwATAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABLAEsASwBLAEsASwBLAEsASwBLABoAGQAZAB4AUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQABMAUAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABABQAFAABAAEAB4ABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUAAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAFAABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQAUABQAB4AHgAYABMAUAArACsABAAbABsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAFAABAAEAAQABAAEAFAABAAEAAQAUAAEAAQABAAEAAQAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArACsAHgArAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAUAAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEAA0ADQBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUAArACsAKwBQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABABQACsAKwArACsAKwArACsAKwAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUAAaABoAUABQAFAAUABQAEwAHgAbAFAAHgAEACsAKwAEAAQABAArAFAAUABQAFAAUABQACsAKwArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQACsAUABQACsAKwAEACsABAAEAAQABAAEACsAKwArACsABAAEACsAKwAEAAQABAArACsAKwAEACsAKwArACsAKwArACsAUABQAFAAUAArAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLAAQABABQAFAAUAAEAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsAKwAEAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAArACsAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AGwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAKwArACsAKwArAAQABAAEACsAKwArACsAUABQACsAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAAQAUAArAFAAUABQAFAAUABQACsAKwArAFAAUABQACsAUABQAFAAUAArACsAKwBQAFAAKwBQACsAUABQACsAKwArAFAAUAArACsAKwBQAFAAUAArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArAAQABAAEAAQABAArACsAKwAEAAQABAArAAQABAAEAAQAKwArAFAAKwArACsAKwArACsABAArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAHgAeAB4AHgAeAB4AGwAeACsAKwArACsAKwAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAUABQAFAAKwArACsAKwArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwAOAFAAUABQAFAAUABQAFAAHgBQAAQABAAEAA4AUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAKwArAAQAUAAEAAQABAAEAAQABAAEACsABAAEAAQAKwAEAAQABAAEACsAKwArACsAKwArACsABAAEACsAKwArACsAKwArACsAUAArAFAAUAAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAFAABAAEAAQABAAEAAQABAArAAQABAAEACsABAAEAAQABABQAB4AKwArACsAKwBQAFAAUAAEAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQABoAUABQAFAAUABQAFAAKwAEAAQABAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQACsAUAArACsAUABQAFAAUABQAFAAUAArACsAKwAEACsAKwArACsABAAEAAQABAAEAAQAKwAEACsABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArAAQABAAeACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAXAAqACoAKgAqACoAKgAqACsAKwArACsAGwBcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAeAEsASwBLAEsASwBLAEsASwBLAEsADQANACsAKwArACsAKwBcAFwAKwBcACsAXABcAFwAXABcACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAXAArAFwAXABcAFwAXABcAFwAXABcAFwAKgBcAFwAKgAqACoAKgAqACoAKgAqACoAXAArACsAXABcAFwAXABcACsAXAArACoAKgAqACoAKgAqACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwBcAFwAXABcAFAADgAOAA4ADgAeAA4ADgAJAA4ADgANAAkAEwATABMAEwATAAkAHgATAB4AHgAeAAQABAAeAB4AHgAeAB4AHgBLAEsASwBLAEsASwBLAEsASwBLAFAAUABQAFAAUABQAFAAUABQAFAADQAEAB4ABAAeAAQAFgARABYAEQAEAAQAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQADQAEAAQABAAEAAQADQAEAAQAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAA0ADQAeAB4AHgAeAB4AHgAEAB4AHgAeAB4AHgAeACsAHgAeAA4ADgANAA4AHgAeAB4AHgAeAAkACQArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgBcAEsASwBLAEsASwBLAEsASwBLAEsADQANAB4AHgAeAB4AXABcAFwAXABcAFwAKgAqACoAKgBcAFwAXABcACoAKgAqAFwAKgAqACoAXABcACoAKgAqACoAKgAqACoAXABcAFwAKgAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKgAqAFwAKgBLAEsASwBLAEsASwBLAEsASwBLACoAKgAqACoAKgAqAFAAUABQAFAAUABQACsAUAArACsAKwArACsAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAKwBQACsAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsABAAEAAQAHgANAB4AHgAeAB4AHgAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUAArACsADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAWABEAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQANAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAANAA0AKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUAArAAQABAArACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqAA0ADQAVAFwADQAeAA0AGwBcACoAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwAeAB4AEwATAA0ADQAOAB4AEwATAB4ABAAEAAQACQArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAHgArACsAKwATABMASwBLAEsASwBLAEsASwBLAEsASwBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAXABcAFwAXABcACsAKwArACsAKwArACsAKwArACsAKwBcAFwAXABcAFwAXABcAFwAXABcAFwAXAArACsAKwArAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAXAArACsAKwAqACoAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAArACsAHgAeAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcACoAKgAqACoAKgAqACoAKgAqACoAKwAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKwArAAQASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACoAKgAqACoAKgAqACoAXAAqACoAKgAqACoAKgArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABABQAFAAUABQAFAAUABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwANAA0AHgANAA0ADQANAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAEAAQABAAEAAQAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwAeAB4AHgAeAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArAA0ADQANAA0ADQBLAEsASwBLAEsASwBLAEsASwBLACsAKwArAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAA0ADQBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUAAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArAAQABAAEAB4ABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAAQAUABQAFAAUABQAFAABABQAFAABAAEAAQAUAArACsAKwArACsABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQACsAUAArAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAFAAUABQACsAHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQACsAKwAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQACsAHgAeAB4AHgAeAB4AHgAOAB4AKwANAA0ADQANAA0ADQANAAkADQANAA0ACAAEAAsABAAEAA0ACQANAA0ADAAdAB0AHgAXABcAFgAXABcAFwAWABcAHQAdAB4AHgAUABQAFAANAAEAAQAEAAQABAAEAAQACQAaABoAGgAaABoAGgAaABoAHgAXABcAHQAVABUAHgAeAB4AHgAeAB4AGAAWABEAFQAVABUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ADQAeAA0ADQANAA0AHgANAA0ADQAHAB4AHgAeAB4AKwAEAAQABAAEAAQABAAEAAQABAAEAFAAUAArACsATwBQAFAAUABQAFAAHgAeAB4AFgARAE8AUABPAE8ATwBPAFAAUABQAFAAUAAeAB4AHgAWABEAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArABsAGwAbABsAGwAbABsAGgAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGgAbABsAGwAbABoAGwAbABoAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbABsAGwAbAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAHgAeAFAAGgAeAB0AHgBQAB4AGgAeAB4AHgAeAB4AHgAeAB4AHgBPAB4AUAAbAB4AHgBQAFAAUABQAFAAHgAeAB4AHQAdAB4AUAAeAFAAHgBQAB4AUABPAFAAUAAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAHgBQAFAAUABQAE8ATwBQAFAAUABQAFAATwBQAFAATwBQAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAUABQAFAATwBPAE8ATwBPAE8ATwBPAE8ATwBQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABPAB4AHgArACsAKwArAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHQAdAB4AHgAeAB0AHQAeAB4AHQAeAB4AHgAdAB4AHQAbABsAHgAdAB4AHgAeAB4AHQAeAB4AHQAdAB0AHQAeAB4AHQAeAB0AHgAdAB0AHQAdAB0AHQAeAB0AHgAeAB4AHgAeAB0AHQAdAB0AHgAeAB4AHgAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB4AHgAeAB0AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAeAB0AHQAdAB0AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAdAB4AHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAWABEAHgAeAB4AHgAeAB4AHQAeAB4AHgAeAB4AHgAeACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAWABEAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAFAAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAeAB4AHQAdAB0AHQAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB0AHQAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB0AHQAeAB4AHQAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AHQAdAB0AHgAeAB0AHgAeAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlAB4AHQAdAB4AHgAdAB4AHgAeAB4AHQAdAB4AHgAeAB4AJQAlAB0AHQAlAB4AJQAlACUAIAAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAeAB4AHgAeAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHgAdAB0AHQAeAB0AJQAdAB0AHgAdAB0AHgAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHQAdAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAdAB0AHQAdACUAHgAlACUAJQAdACUAJQAdAB0AHQAlACUAHQAdACUAHQAdACUAJQAlAB4AHQAeAB4AHgAeAB0AHQAlAB0AHQAdAB0AHQAdACUAJQAlACUAJQAdACUAJQAgACUAHQAdACUAJQAlACUAJQAlACUAJQAeAB4AHgAlACUAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB0AHgAeAB4AFwAXABcAFwAXABcAHgATABMAJQAeAB4AHgAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARABYAEQAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAWABEAFgARABYAEQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAWABEAFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AFgARAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAdAB0AHQAdAB0AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAFAAUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAEAAQABAAeAB4AKwArACsAKwArABMADQANAA0AUAATAA0AUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUAANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAA0ADQANAA0ADQANAA0ADQAeAA0AFgANAB4AHgAXABcAHgAeABcAFwAWABEAFgARABYAEQAWABEADQANAA0ADQATAFAADQANAB4ADQANAB4AHgAeAB4AHgAMAAwADQANAA0AHgANAA0AFgANAA0ADQANAA0ADQANAA0AHgANAB4ADQANAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArACsAKwArACsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArAA0AEQARACUAJQBHAFcAVwAWABEAFgARABYAEQAWABEAFgARACUAJQAWABEAFgARABYAEQAWABEAFQAWABEAEQAlAFcAVwBXAFcAVwBXAFcAVwBXAAQABAAEAAQABAAEACUAVwBXAFcAVwA2ACUAJQBXAFcAVwBHAEcAJQAlACUAKwBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBRAFcAUQBXAFEAVwBXAFcAVwBXAFcAUQBXAFcAVwBXAFcAVwBRAFEAKwArAAQABAAVABUARwBHAFcAFQBRAFcAUQBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFEAVwBRAFcAUQBXAFcAVwBXAFcAVwBRAFcAVwBXAFcAVwBXAFEAUQBXAFcAVwBXABUAUQBHAEcAVwArACsAKwArACsAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwAlACUAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACsAKwArACsAKwArACsAKwArACsAKwArAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAUQBRAFEAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBPAE8ATwBPAE8ATwBPAE8AJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADQATAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABLAEsASwBLAEsASwBLAEsASwBLAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAABAAEAAQABAAeAAQABAAEAAQABAAEAAQABAAEAAQAHgBQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUABQAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAeAA0ADQANAA0ADQArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AUAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAB4AHgAeAB4AHgAeAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AUABQAFAAUABQAFAAUABQAFAAUABQAAQAUABQAFAABABQAFAAUABQAAQAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAeAB4AHgAeAAQAKwArACsAUABQAFAAUABQAFAAHgAeABoAHgArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAADgAOABMAEwArACsAKwArACsAKwArACsABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwANAA0ASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUAAeAB4AHgBQAA4AUABQAAQAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArAB4AWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYAFgAWABYACsAKwArAAQAHgAeAB4AHgAeAB4ADQANAA0AHgAeAB4AHgArAFAASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArAB4AHgBcAFwAXABcAFwAKgBcAFwAXABcAFwAXABcAFwAXABcAEsASwBLAEsASwBLAEsASwBLAEsAXABcAFwAXABcACsAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAFAAUABQAAQAUABQAFAAUABQAFAAUABQAAQABAArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAHgANAA0ADQBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKgAqACoAXAAqACoAKgBcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXAAqAFwAKgAqACoAXABcACoAKgBcAFwAXABcAFwAKgAqAFwAKgBcACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFwAXABcACoAKgBQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAA0ADQBQAFAAUAAEAAQAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQADQAEAAQAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAVABVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBUAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVAFUAVQBVACsAKwArACsAKwArACsAKwArACsAKwArAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAWQBZAFkAKwArACsAKwBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAWgBaAFoAKwArACsAKwAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYABgAGAAYAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAKwArACsAKwArAFYABABWAFYAVgBWAFYAVgBWAFYAVgBWAB4AVgBWAFYAVgBWAFYAVgBWAFYAVgBWAFYAVgArAFYAVgBWAFYAVgArAFYAKwBWAFYAKwBWAFYAKwBWAFYAVgBWAFYAVgBWAFYAVgBWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAEQAWAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAaAB4AKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAGAARABEAGAAYABMAEwAWABEAFAArACsAKwArACsAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACUAJQAlACUAJQAWABEAFgARABYAEQAWABEAFgARABYAEQAlACUAFgARACUAJQAlACUAJQAlACUAEQAlABEAKwAVABUAEwATACUAFgARABYAEQAWABEAJQAlACUAJQAlACUAJQAlACsAJQAbABoAJQArACsAKwArAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAcAKwATACUAJQAbABoAJQAlABYAEQAlACUAEQAlABEAJQBXAFcAVwBXAFcAVwBXAFcAVwBXABUAFQAlACUAJQATACUAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXABYAJQARACUAJQAlAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAWACUAEQAlABYAEQARABYAEQARABUAVwBRAFEAUQBRAFEAUQBRAFEAUQBRAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAEcARwArACsAVwBXAFcAVwBXAFcAKwArAFcAVwBXAFcAVwBXACsAKwBXAFcAVwBXAFcAVwArACsAVwBXAFcAKwArACsAGgAbACUAJQAlABsAGwArAB4AHgAeAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwAEAAQABAAQAB0AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsADQANAA0AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAA0AUABQAFAAUAArACsAKwArAFAAUABQAFAAUABQAFAAUAANAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwArAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwBQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwANAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAB4AUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAUABQAFAAUABQAAQABAAEACsABAAEACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAKwBQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAA0ADQANAA0ADQANAA0ADQAeACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAArACsAKwArAFAAUABQAFAAUAANAA0ADQANAA0ADQAUACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsADQANAA0ADQANAA0ADQBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAB4AHgAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArAAQABAANACsAKwBQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAB4AHgAeAB4AHgArACsAKwArACsAKwAEAAQABAAEAAQABAAEAA0ADQAeAB4AHgAeAB4AKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwAeACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEACsASwBLAEsASwBLAEsASwBLAEsASwANAA0ADQANAFAABAAEAFAAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAeAA4AUAArACsAKwArACsAKwArACsAKwAEAFAAUABQAFAADQANAB4ADQAEAAQABAAEAB4ABAAEAEsASwBLAEsASwBLAEsASwBLAEsAUAAOAFAADQANAA0AKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAANAA0AHgANAA0AHgAEACsAUABQAFAAUABQAFAAUAArAFAAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAA0AKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsABAAEAAQABAArAFAAUABQAFAAUABQAFAAUAArACsAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQACsABAAEAFAABAAEAAQABAAEAAQABAArACsABAAEACsAKwAEAAQABAArACsAUAArACsAKwArACsAKwAEACsAKwArACsAKwBQAFAAUABQAFAABAAEACsAKwAEAAQABAAEAAQABAAEACsAKwArAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsABAAEAAQABAAEAAQABABQAFAAUABQAA0ADQANAA0AHgBLAEsASwBLAEsASwBLAEsASwBLAA0ADQArAB4ABABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAFAAUAAeAFAAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABAAEAAQADgANAA0AEwATAB4AHgAeAA0ADQANAA0ADQANAA0ADQANAA0ADQANAA0ADQANAFAAUABQAFAABAAEACsAKwAEAA0ADQAeAFAAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAFAAKwArACsAKwArACsAKwBLAEsASwBLAEsASwBLAEsASwBLACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAXABcAFwAKwArACoAKgAqACoAKgAqACoAKgAqACoAKgAqACoAKgAqACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBcAFwADQANAA0AKgBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAKwArAFAAKwArAFAAUABQAFAAUABQAFAAUAArAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQAKwAEAAQAKwArAAQABAAEAAQAUAAEAFAABAAEAA0ADQANACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAArACsABAAEAAQABAAEAAQABABQAA4AUAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAABAAEAAQABAAEAAQABAAEAAQABABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAFAABAAEAAQABAAOAB4ADQANAA0ADQAOAB4ABAArACsAKwArACsAKwArACsAUAAEAAQABAAEAAQABAAEAAQABAAEAAQAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAA0ADQANAFAADgAOAA4ADQANACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAEAAQABAAEACsABAAEAAQABAAEAAQABAAEAFAADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAOABMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQACsAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAArACsAKwAEACsABAAEACsABAAEAAQABAAEAAQABABQAAQAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAUABQAFAAUABQAFAAKwBQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAUAArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAABAAEAAQABAAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAaABoAGgAaAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArAA0AUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsADQANAA0ADQANACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABIAEgAQwBDAEMAUABQAFAAUABDAFAAUABQAEgAQwBIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAASABDAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwAJAAkACQAJAAkACQAJABYAEQArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABIAEMAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwANAA0AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArAAQABAAEAAQABAANACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEAA0ADQANAB4AHgAeAB4AHgAeAFAAUABQAFAADQAeACsAKwArACsAKwArACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAANAA0AHgAeACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwAEAFAABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwAEAAQABAAEAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAARwBHABUARwAJACsAKwArACsAKwArACsAKwArACsAKwAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUQBRAFEAKwArACsAKwArACsAKwArACsAKwArACsAKwBRAFEAUQBRACsAKwArACsAKwArACsAKwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUAArACsAHgAEAAQADQAEAAQABAAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAAQABAAEAAQABAAeAB4AHgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAB4AHgAEAAQABAAEAAQABAAEAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4ABAAEAAQAHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwArACsAKwArACsAKwArACsAKwArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAKwArAFAAKwArAFAAUAArACsAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACsAUAArAFAAUABQAFAAUABQAFAAKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwBQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAHgAeAFAAUABQAFAAUAArAFAAKwArACsAUABQAFAAUABQAFAAUAArAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAB4AHgAeAB4AHgAeAB4AHgAeACsAKwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAEsASwBLAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAeAB4AHgAeAB4AHgAeAB4ABAAeAB4AHgAeAB4AHgAeAB4AHgAeAAQAHgAeAA0ADQANAA0AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQAKwAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArAAQABAAEAAQABAAEAAQAKwAEAAQAKwAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwAEAAQABAAEAAQABAAEAFAAUABQAFAAUABQAFAAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwBQAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArABsAUABQAFAAUABQACsAKwBQAFAAUABQAFAAUABQAFAAUAAEAAQABAAEAAQABAAEACsAKwArACsAKwArACsAKwArAB4AHgAeAB4ABAAEAAQABAAEAAQABABQACsAKwArACsASwBLAEsASwBLAEsASwBLAEsASwArACsAKwArABYAFgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAGgBQAFAAUAAaAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAeAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQACsAKwBQAFAAUABQACsAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUAArACsAKwArACsAKwBQACsAKwArACsAUAArAFAAKwBQACsAUABQAFAAKwBQAFAAKwBQACsAKwBQACsAUAArAFAAKwBQACsAUAArAFAAUAArAFAAKwArAFAAUABQAFAAKwBQAFAAUABQAFAAUABQACsAUABQAFAAUAArAFAAUABQAFAAKwBQACsAUABQAFAAUABQAFAAUABQAFAAUAArAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAArACsAKwArACsAUABQAFAAKwBQAFAAUABQAFAAKwBQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwAeAB4AKwArACsAKwArACsAKwArACsAKwArACsAKwArAE8ATwBPAE8ATwBPAE8ATwBPAE8ATwBPAE8AJQAlACUAHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHgAeAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB4AHgAeACUAJQAlAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAdAB0AHQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAKQApACkAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAlACUAJQAlACUAHgAlACUAJQAlACUAIAAgACAAJQAlACAAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACEAIQAhACEAIQAlACUAIAAgACUAJQAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlACUAIAAlACUAJQAlACAAIAAgACUAIAAgACAAJQAlACUAJQAlACUAJQAgACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAlAB4AJQAeACUAJQAlACUAJQAgACUAJQAlACUAHgAlAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAgACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACAAIAAgACAAIAAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeABcAFwAXABUAFQAVAB4AHgAeAB4AJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAgACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlACUAJQAeAB4AHgAeAB4AHgAeAB4AHgAeACUAJQAlACUAJQAlAB4AHgAeAB4AHgAeAB4AHgAlACUAJQAlACUAJQAlACUAHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAgACUAJQAgACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAJQAlACUAJQAlACUAIAAlACUAJQAlACUAJQAlACUAJQAgACAAIAAgACAAIAAgACAAIAAgACUAJQAgACAAIAAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACAAIAAlACAAIAAlACAAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAgACAAIAAlACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAJQAlAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AKwAeAB4AHgAeAB4AHgAeAB4AHgAeAB4AHgArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAEsASwBLAEsASwBLAEsASwBLAEsAKwArACsAKwArACsAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwArAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwAlACUAJQAlACUAJQAlACUAJQAlACUAVwBXACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQBXAFcAVwBXAFcAVwBXAFcAVwBXAFcAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAJQAlACUAKwAEACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArACsAKwArAA=="),L=Array.isArray(m)?function(A){for(var e=A.length,t=[],r=0;r=this._value.length?-1:this._value[A]},XA.prototype.consumeUnicodeRangeToken=function(){for(var A=[],e=this.consumeCodePoint();lA(e)&&A.length<6;)A.push(e),e=this.consumeCodePoint();for(var t=!1;63===e&&A.length<6;)A.push(e),e=this.consumeCodePoint(),t=!0;if(t)return{type:30,start:parseInt(g.apply(void 0,A.map(function(A){return 63===A?48:A})),16),end:parseInt(g.apply(void 0,A.map(function(A){return 63===A?70:A})),16)};var r=parseInt(g.apply(void 0,A),16);if(45===this.peekCodePoint(0)&&lA(this.peekCodePoint(1))){this.consumeCodePoint();for(var e=this.consumeCodePoint(),B=[];lA(e)&&B.length<6;)B.push(e),e=this.consumeCodePoint();return{type:30,start:r,end:parseInt(g.apply(void 0,B),16)}}return{type:30,start:r,end:r}},XA.prototype.consumeIdentLikeToken=function(){var A=this.consumeName();return"url"===A.toLowerCase()&&40===this.peekCodePoint(0)?(this.consumeCodePoint(),this.consumeUrlToken()):40===this.peekCodePoint(0)?(this.consumeCodePoint(),{type:19,value:A}):{type:20,value:A}},XA.prototype.consumeUrlToken=function(){var A=[];if(this.consumeWhiteSpace(),-1===this.peekCodePoint(0))return{type:22,value:""};var e,t=this.peekCodePoint(0);if(39===t||34===t){t=this.consumeStringToken(this.consumeCodePoint());return 0===t.type&&(this.consumeWhiteSpace(),-1===this.peekCodePoint(0)||41===this.peekCodePoint(0))?(this.consumeCodePoint(),{type:22,value:t.value}):(this.consumeBadUrlRemnants(),xA)}for(;;){var r=this.consumeCodePoint();if(-1===r||41===r)return{type:22,value:g.apply(void 0,A)};if(CA(r))return this.consumeWhiteSpace(),-1===this.peekCodePoint(0)||41===this.peekCodePoint(0)?(this.consumeCodePoint(),{type:22,value:g.apply(void 0,A)}):(this.consumeBadUrlRemnants(),xA);if(34===r||39===r||40===r||(0<=(e=r)&&e<=8||11===e||14<=e&&e<=31||127===e))return this.consumeBadUrlRemnants(),xA;if(92===r){if(!hA(r,this.peekCodePoint(0)))return this.consumeBadUrlRemnants(),xA;A.push(this.consumeEscapedCodePoint())}else A.push(r)}},XA.prototype.consumeWhiteSpace=function(){for(;CA(this.peekCodePoint(0));)this.consumeCodePoint()},XA.prototype.consumeBadUrlRemnants=function(){for(;;){var A=this.consumeCodePoint();if(41===A||-1===A)return;hA(A,this.peekCodePoint(0))&&this.consumeEscapedCodePoint()}},XA.prototype.consumeStringSlice=function(A){for(var e="";0>8,r=255&A>>16,A=255&A>>24;return e<255?"rgba("+A+","+r+","+t+","+e/255+")":"rgb("+A+","+r+","+t+")"}function Qe(A,e){if(17===A.type)return A.number;if(16!==A.type)return 0;var t=3===e?1:255;return 3===e?A.number/100*t:Math.round(A.number/100*t)}var ce=function(A,e){return 11===e&&12===A.type||(28===e&&29===A.type||2===e&&3===A.type)},ae={type:17,number:0,flags:4},ge={type:16,number:50,flags:4},we={type:16,number:100,flags:4},Ue=function(A,e){if(16===A.type)return A.number/100*e;if(WA(A))switch(A.unit){case"rem":case"em":return 16*A.number;default:return A.number}return A.number},le=function(A,e){if(15===e.type)switch(e.unit){case"deg":return Math.PI*e.number/180;case"grad":return Math.PI/200*e.number;case"rad":return e.number;case"turn":return 2*Math.PI*e.number}throw new Error("Unsupported angle type")},Ce=function(A){return Math.PI*A/180},ue=function(A,e){if(18===e.type){var t=me[e.name];if(void 0===t)throw new Error('Attempting to parse an unsupported color function "'+e.name+'"');return t(A,e.values)}if(5===e.type){if(3===e.value.length){var r=e.value.substring(0,1),B=e.value.substring(1,2),n=e.value.substring(2,3);return Fe(parseInt(r+r,16),parseInt(B+B,16),parseInt(n+n,16),1)}if(4===e.value.length){var r=e.value.substring(0,1),B=e.value.substring(1,2),n=e.value.substring(2,3),s=e.value.substring(3,4);return Fe(parseInt(r+r,16),parseInt(B+B,16),parseInt(n+n,16),parseInt(s+s,16)/255)}if(6===e.value.length){r=e.value.substring(0,2),B=e.value.substring(2,4),n=e.value.substring(4,6);return Fe(parseInt(r,16),parseInt(B,16),parseInt(n,16),1)}if(8===e.value.length){r=e.value.substring(0,2),B=e.value.substring(2,4),n=e.value.substring(4,6),s=e.value.substring(6,8);return Fe(parseInt(r,16),parseInt(B,16),parseInt(n,16),parseInt(s,16)/255)}}if(20===e.type){e=Le[e.value.toUpperCase()];if(void 0!==e)return e}return Le.TRANSPARENT},Fe=function(A,e,t,r){return(A<<24|e<<16|t<<8|Math.round(255*r)<<0)>>>0},he=function(A,e){e=e.filter($A);if(3===e.length){var t=e.map(Qe),r=t[0],B=t[1],t=t[2];return Fe(r,B,t,1)}if(4!==e.length)return 0;e=e.map(Qe),r=e[0],B=e[1],t=e[2],e=e[3];return Fe(r,B,t,e)};function de(A,e,t){return t<0&&(t+=1),1<=t&&--t,t<1/6?(e-A)*t*6+A:t<.5?e:t<2/3?6*(e-A)*(2/3-t)+A:A}function fe(A,e){return ue(A,JA.create(e).parseComponentValue())}function He(A,e){return A=ue(A,e[0]),(e=e[1])&&te(e)?{color:A,stop:e}:{color:A,stop:null}}function pe(A,t){var e=A[0],r=A[A.length-1];null===e.stop&&(e.stop=ae),null===r.stop&&(r.stop=we);for(var B=[],n=0,s=0;sA.optimumDistance)?{optimumCorner:e,optimumDistance:r}:A},{optimumDistance:s?1/0:-1/0,optimumCorner:null}).optimumCorner}var Ke=function(A,e){var t=e.filter($A),r=t[0],B=t[1],n=t[2],e=t[3],t=(17===r.type?Ce(r.number):le(A,r))/(2*Math.PI),A=te(B)?B.number/100:0,r=te(n)?n.number/100:0,B=void 0!==e&&te(e)?Ue(e,1):1;if(0==A)return Fe(255*r,255*r,255*r,1);n=r<=.5?r*(1+A):r+A-r*A,e=2*r-n,A=de(e,n,t+1/3),r=de(e,n,t),t=de(e,n,t-1/3);return Fe(255*A,255*r,255*t,B)},me={hsl:Ke,hsla:Ke,rgb:he,rgba:he},Le={ALICEBLUE:4042850303,ANTIQUEWHITE:4209760255,AQUA:16777215,AQUAMARINE:2147472639,AZURE:4043309055,BEIGE:4126530815,BISQUE:4293182719,BLACK:255,BLANCHEDALMOND:4293643775,BLUE:65535,BLUEVIOLET:2318131967,BROWN:2771004159,BURLYWOOD:3736635391,CADETBLUE:1604231423,CHARTREUSE:2147418367,CHOCOLATE:3530104575,CORAL:4286533887,CORNFLOWERBLUE:1687547391,CORNSILK:4294499583,CRIMSON:3692313855,CYAN:16777215,DARKBLUE:35839,DARKCYAN:9145343,DARKGOLDENROD:3095837695,DARKGRAY:2846468607,DARKGREEN:6553855,DARKGREY:2846468607,DARKKHAKI:3182914559,DARKMAGENTA:2332068863,DARKOLIVEGREEN:1433087999,DARKORANGE:4287365375,DARKORCHID:2570243327,DARKRED:2332033279,DARKSALMON:3918953215,DARKSEAGREEN:2411499519,DARKSLATEBLUE:1211993087,DARKSLATEGRAY:793726975,DARKSLATEGREY:793726975,DARKTURQUOISE:13554175,DARKVIOLET:2483082239,DEEPPINK:4279538687,DEEPSKYBLUE:12582911,DIMGRAY:1768516095,DIMGREY:1768516095,DODGERBLUE:512819199,FIREBRICK:2988581631,FLORALWHITE:4294635775,FORESTGREEN:579543807,FUCHSIA:4278255615,GAINSBORO:3705462015,GHOSTWHITE:4177068031,GOLD:4292280575,GOLDENROD:3668254975,GRAY:2155905279,GREEN:8388863,GREENYELLOW:2919182335,GREY:2155905279,HONEYDEW:4043305215,HOTPINK:4285117695,INDIANRED:3445382399,INDIGO:1258324735,IVORY:4294963455,KHAKI:4041641215,LAVENDER:3873897215,LAVENDERBLUSH:4293981695,LAWNGREEN:2096890111,LEMONCHIFFON:4294626815,LIGHTBLUE:2916673279,LIGHTCORAL:4034953471,LIGHTCYAN:3774873599,LIGHTGOLDENRODYELLOW:4210742015,LIGHTGRAY:3553874943,LIGHTGREEN:2431553791,LIGHTGREY:3553874943,LIGHTPINK:4290167295,LIGHTSALMON:4288707327,LIGHTSEAGREEN:548580095,LIGHTSKYBLUE:2278488831,LIGHTSLATEGRAY:2005441023,LIGHTSLATEGREY:2005441023,LIGHTSTEELBLUE:2965692159,LIGHTYELLOW:4294959359,LIME:16711935,LIMEGREEN:852308735,LINEN:4210091775,MAGENTA:4278255615,MAROON:2147483903,MEDIUMAQUAMARINE:1724754687,MEDIUMBLUE:52735,MEDIUMORCHID:3126187007,MEDIUMPURPLE:2473647103,MEDIUMSEAGREEN:1018393087,MEDIUMSLATEBLUE:2070474495,MEDIUMSPRINGGREEN:16423679,MEDIUMTURQUOISE:1221709055,MEDIUMVIOLETRED:3340076543,MIDNIGHTBLUE:421097727,MINTCREAM:4127193855,MISTYROSE:4293190143,MOCCASIN:4293178879,NAVAJOWHITE:4292783615,NAVY:33023,OLDLACE:4260751103,OLIVE:2155872511,OLIVEDRAB:1804477439,ORANGE:4289003775,ORANGERED:4282712319,ORCHID:3664828159,PALEGOLDENROD:4008225535,PALEGREEN:2566625535,PALETURQUOISE:2951671551,PALEVIOLETRED:3681588223,PAPAYAWHIP:4293907967,PEACHPUFF:4292524543,PERU:3448061951,PINK:4290825215,PLUM:3718307327,POWDERBLUE:2967529215,PURPLE:2147516671,REBECCAPURPLE:1714657791,RED:4278190335,ROSYBROWN:3163525119,ROYALBLUE:1097458175,SADDLEBROWN:2336560127,SALMON:4202722047,SANDYBROWN:4104413439,SEAGREEN:780883967,SEASHELL:4294307583,SIENNA:2689740287,SILVER:3233857791,SKYBLUE:2278484991,SLATEBLUE:1784335871,SLATEGRAY:1887473919,SLATEGREY:1887473919,SNOW:4294638335,SPRINGGREEN:16744447,STEELBLUE:1182971135,TAN:3535047935,TEAL:8421631,THISTLE:3636451583,TOMATO:4284696575,TRANSPARENT:0,TURQUOISE:1088475391,VIOLET:4001558271,WHEAT:4125012991,WHITE:4294967295,WHITESMOKE:4126537215,YELLOW:4294902015,YELLOWGREEN:2597139199},be={name:"background-clip",initialValue:"border-box",prefix:!1,type:1,parse:function(A,e){return e.map(function(A){if(_A(A))switch(A.value){case"padding-box":return 1;case"content-box":return 2}return 0})}},De={name:"background-color",initialValue:"transparent",prefix:!1,type:3,format:"color"},Ke=function(t,A){var r=Ce(180),B=[];return Ae(A).forEach(function(A,e){if(0===e){e=A[0];if(20===e.type&&-1!==["top","left","right","bottom"].indexOf(e.value))return void(r=se(A));if(ne(e))return void(r=(le(t,e)+Ce(270))%Ce(360))}A=He(t,A);B.push(A)}),{angle:r,stops:B,type:1}},ve="closest-side",xe="farthest-side",Me="closest-corner",Se="farthest-corner",Te="ellipse",Ge="contain",he=function(r,A){var B=0,n=3,s=[],o=[];return Ae(A).forEach(function(A,e){var t=!0;0===e?t=A.reduce(function(A,e){if(_A(e))switch(e.value){case"center":return o.push(ge),!1;case"top":case"left":return o.push(ae),!1;case"right":case"bottom":return o.push(we),!1}else if(te(e)||ee(e))return o.push(e),!1;return A},t):1===e&&(t=A.reduce(function(A,e){if(_A(e))switch(e.value){case"circle":return B=0,!1;case Te:return!(B=1);case Ge:case ve:return n=0,!1;case xe:return!(n=1);case Me:return!(n=2);case"cover":case Se:return!(n=3)}else if(ee(e)||te(e))return(n=!Array.isArray(n)?[]:n).push(e),!1;return A},t)),t&&(A=He(r,A),s.push(A))}),{size:n,shape:B,stops:s,position:o,type:2}},Oe=function(A,e){if(22===e.type){var t={url:e.value,type:0};return A.cache.addImage(e.value),t}if(18!==e.type)throw new Error("Unsupported image type "+e.type);t=ke[e.name];if(void 0===t)throw new Error('Attempting to parse an unsupported image function "'+e.name+'"');return t(A,e.values)};var Ve,ke={"linear-gradient":function(t,A){var r=Ce(180),B=[];return Ae(A).forEach(function(A,e){if(0===e){e=A[0];if(20===e.type&&"to"===e.value)return void(r=se(A));if(ne(e))return void(r=le(t,e))}A=He(t,A);B.push(A)}),{angle:r,stops:B,type:1}},"-moz-linear-gradient":Ke,"-ms-linear-gradient":Ke,"-o-linear-gradient":Ke,"-webkit-linear-gradient":Ke,"radial-gradient":function(B,A){var n=0,s=3,o=[],i=[];return Ae(A).forEach(function(A,e){var t,r=!0;0===e&&(t=!1,r=A.reduce(function(A,e){if(t)if(_A(e))switch(e.value){case"center":return i.push(ge),A;case"top":case"left":return i.push(ae),A;case"right":case"bottom":return i.push(we),A}else(te(e)||ee(e))&&i.push(e);else if(_A(e))switch(e.value){case"circle":return n=0,!1;case Te:return!(n=1);case"at":return!(t=!0);case ve:return s=0,!1;case"cover":case xe:return!(s=1);case Ge:case Me:return!(s=2);case Se:return!(s=3)}else if(ee(e)||te(e))return(s=!Array.isArray(s)?[]:s).push(e),!1;return A},r)),r&&(A=He(B,A),o.push(A))}),{size:s,shape:n,stops:o,position:i,type:2}},"-moz-radial-gradient":he,"-ms-radial-gradient":he,"-o-radial-gradient":he,"-webkit-radial-gradient":he,"-webkit-gradient":function(r,A){var e=Ce(180),B=[],n=1;return Ae(A).forEach(function(A,e){var t,A=A[0];if(0===e){if(_A(A)&&"linear"===A.value)return void(n=1);if(_A(A)&&"radial"===A.value)return void(n=2)}18===A.type&&("from"===A.name?(t=ue(r,A.values[0]),B.push({stop:ae,color:t})):"to"===A.name?(t=ue(r,A.values[0]),B.push({stop:we,color:t})):"color-stop"!==A.name||2===(A=A.values.filter($A)).length&&(t=ue(r,A[1]),A=A[0],ZA(A)&&B.push({stop:{type:16,number:100*A.number,flags:A.flags},color:t})))}),1===n?{angle:(e+Ce(180))%Ce(360),stops:B,type:n}:{size:3,shape:0,stops:B,position:[],type:n}}},Re={name:"background-image",initialValue:"none",type:1,prefix:!1,parse:function(e,A){if(0===A.length)return[];var t=A[0];return 20===t.type&&"none"===t.value?[]:A.filter(function(A){return $A(A)&&!(20===(A=A).type&&"none"===A.value||18===A.type&&!ke[A.name])}).map(function(A){return Oe(e,A)})}},Ne={name:"background-origin",initialValue:"border-box",prefix:!1,type:1,parse:function(A,e){return e.map(function(A){if(_A(A))switch(A.value){case"padding-box":return 1;case"content-box":return 2}return 0})}},Pe={name:"background-position",initialValue:"0% 0%",type:1,prefix:!1,parse:function(A,e){return Ae(e).map(function(A){return A.filter(te)}).map(re)}},Xe={name:"background-repeat",initialValue:"repeat",prefix:!1,type:1,parse:function(A,e){return Ae(e).map(function(A){return A.filter(_A).map(function(A){return A.value}).join(" ")}).map(Je)}},Je=function(A){switch(A){case"no-repeat":return 1;case"repeat-x":case"repeat no-repeat":return 2;case"repeat-y":case"no-repeat repeat":return 3;default:return 0}};(he=Ve=Ve||{}).AUTO="auto",he.CONTAIN="contain";function Ye(A,e){return _A(A)&&"normal"===A.value?1.2*e:17===A.type?e*A.number:te(A)?Ue(A,e):e}var We,Ze,_e={name:"background-size",initialValue:"0",prefix:!(he.COVER="cover"),type:1,parse:function(A,e){return Ae(e).map(function(A){return A.filter(qe)})}},qe=function(A){return _A(A)||te(A)},he=function(A){return{name:"border-"+A+"-color",initialValue:"transparent",prefix:!1,type:3,format:"color"}},je=he("top"),ze=he("right"),$e=he("bottom"),At=he("left"),he=function(A){return{name:"border-radius-"+A,initialValue:"0 0",prefix:!1,type:1,parse:function(A,e){return re(e.filter(te))}}},et=he("top-left"),tt=he("top-right"),rt=he("bottom-right"),Bt=he("bottom-left"),he=function(A){return{name:"border-"+A+"-style",initialValue:"solid",prefix:!1,type:2,parse:function(A,e){switch(e){case"none":return 0;case"dashed":return 2;case"dotted":return 3;case"double":return 4}return 1}}},nt=he("top"),st=he("right"),ot=he("bottom"),it=he("left"),he=function(A){return{name:"border-"+A+"-width",initialValue:"0",type:0,prefix:!1,parse:function(A,e){return WA(e)?e.number:0}}},Qt=he("top"),ct=he("right"),at=he("bottom"),gt=he("left"),wt={name:"color",initialValue:"transparent",prefix:!1,type:3,format:"color"},Ut={name:"direction",initialValue:"ltr",prefix:!1,type:2,parse:function(A,e){return"rtl"!==e?0:1}},lt={name:"display",initialValue:"inline-block",prefix:!1,type:1,parse:function(A,e){return e.filter(_A).reduce(function(A,e){return A|Ct(e.value)},0)}},Ct=function(A){switch(A){case"block":case"-webkit-box":return 2;case"inline":return 4;case"run-in":return 8;case"flow":return 16;case"flow-root":return 32;case"table":return 64;case"flex":case"-webkit-flex":return 128;case"grid":case"-ms-grid":return 256;case"ruby":return 512;case"subgrid":return 1024;case"list-item":return 2048;case"table-row-group":return 4096;case"table-header-group":return 8192;case"table-footer-group":return 16384;case"table-row":return 32768;case"table-cell":return 65536;case"table-column-group":return 131072;case"table-column":return 262144;case"table-caption":return 524288;case"ruby-base":return 1048576;case"ruby-text":return 2097152;case"ruby-base-container":return 4194304;case"ruby-text-container":return 8388608;case"contents":return 16777216;case"inline-block":return 33554432;case"inline-list-item":return 67108864;case"inline-table":return 134217728;case"inline-flex":return 268435456;case"inline-grid":return 536870912}return 0},ut={name:"float",initialValue:"none",prefix:!1,type:2,parse:function(A,e){switch(e){case"left":return 1;case"right":return 2;case"inline-start":return 3;case"inline-end":return 4}return 0}},Ft={name:"letter-spacing",initialValue:"0",prefix:!1,type:0,parse:function(A,e){return!(20===e.type&&"normal"===e.value||17!==e.type&&15!==e.type)?e.number:0}},ht={name:"line-break",initialValue:(he=We=We||{}).NORMAL="normal",prefix:!(he.STRICT="strict"),type:2,parse:function(A,e){return"strict"!==e?We.NORMAL:We.STRICT}},dt={name:"line-height",initialValue:"normal",prefix:!1,type:4},ft={name:"list-style-image",initialValue:"none",type:0,prefix:!1,parse:function(A,e){return 20===e.type&&"none"===e.value?null:Oe(A,e)}},Ht={name:"list-style-position",initialValue:"outside",prefix:!1,type:2,parse:function(A,e){return"inside"!==e?1:0}},pt={name:"list-style-type",initialValue:"none",prefix:!1,type:2,parse:function(A,e){switch(e){case"disc":return 0;case"circle":return 1;case"square":return 2;case"decimal":return 3;case"cjk-decimal":return 4;case"decimal-leading-zero":return 5;case"lower-roman":return 6;case"upper-roman":return 7;case"lower-greek":return 8;case"lower-alpha":return 9;case"upper-alpha":return 10;case"arabic-indic":return 11;case"armenian":return 12;case"bengali":return 13;case"cambodian":return 14;case"cjk-earthly-branch":return 15;case"cjk-heavenly-stem":return 16;case"cjk-ideographic":return 17;case"devanagari":return 18;case"ethiopic-numeric":return 19;case"georgian":return 20;case"gujarati":return 21;case"gurmukhi":case"hebrew":return 22;case"hiragana":return 23;case"hiragana-iroha":return 24;case"japanese-formal":return 25;case"japanese-informal":return 26;case"kannada":return 27;case"katakana":return 28;case"katakana-iroha":return 29;case"khmer":return 30;case"korean-hangul-formal":return 31;case"korean-hanja-formal":return 32;case"korean-hanja-informal":return 33;case"lao":return 34;case"lower-armenian":return 35;case"malayalam":return 36;case"mongolian":return 37;case"myanmar":return 38;case"oriya":return 39;case"persian":return 40;case"simp-chinese-formal":return 41;case"simp-chinese-informal":return 42;case"tamil":return 43;case"telugu":return 44;case"thai":return 45;case"tibetan":return 46;case"trad-chinese-formal":return 47;case"trad-chinese-informal":return 48;case"upper-armenian":return 49;case"disclosure-open":return 50;case"disclosure-closed":return 51;default:return-1}}},he=function(A){return{name:"margin-"+A,initialValue:"0",prefix:!1,type:4}},Et=he("top"),It=he("right"),yt=he("bottom"),Kt=he("left"),mt={name:"overflow",initialValue:"visible",prefix:!1,type:1,parse:function(A,e){return e.filter(_A).map(function(A){switch(A.value){case"hidden":return 1;case"scroll":return 2;case"clip":return 3;case"auto":return 4;default:return 0}})}},Lt={name:"overflow-wrap",initialValue:"normal",prefix:!1,type:2,parse:function(A,e){return"break-word"!==e?"normal":"break-word"}},he=function(A){return{name:"padding-"+A,initialValue:"0",prefix:!1,type:3,format:"length-percentage"}},bt=he("top"),Dt=he("right"),vt=he("bottom"),xt=he("left"),Mt={name:"text-align",initialValue:"left",prefix:!1,type:2,parse:function(A,e){switch(e){case"right":return 2;case"center":case"justify":return 1;default:return 0}}},St={name:"position",initialValue:"static",prefix:!1,type:2,parse:function(A,e){switch(e){case"relative":return 1;case"absolute":return 2;case"fixed":return 3;case"sticky":return 4}return 0}},Tt={name:"text-shadow",initialValue:"none",type:1,prefix:!1,parse:function(n,A){return 1===A.length&&jA(A[0],"none")?[]:Ae(A).map(function(A){for(var e={color:Le.TRANSPARENT,offsetX:ae,offsetY:ae,blur:ae},t=0,r=0;r>5],this.data[e=(e<<2)+(31&A)];if(A<=65535)return e=this.index[2048+(A-55296>>5)],this.data[e=(e<<2)+(31&A)];if(A>11)],e=this.index[e+=A>>5&63],this.data[e=(e<<2)+(31&A)];if(A<=1114111)return this.data[this.highValueIndex]}return this.errorValue},pr);function pr(A,e,t,r,B,n){this.initialValue=A,this.errorValue=e,this.highStart=t,this.highValueIndex=r,this.index=B,this.data=n}for(var Er="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Ir="undefined"==typeof Uint8Array?[]:new Uint8Array(256),yr=0;yr>10),s%1024+56320)),(B+1===t||16384>4,i[o++]=(15&t)<<4|r>>2,i[o++]=(3&r)<<6|63&B;return n}(br="AAAAAAAAAAAAEA4AGBkAAFAaAAACAAAAAAAIABAAGAAwADgACAAQAAgAEAAIABAACAAQAAgAEAAIABAACAAQAAgAEAAIABAAQABIAEQATAAIABAACAAQAAgAEAAIABAAVABcAAgAEAAIABAACAAQAGAAaABwAHgAgACIAI4AlgAIABAAmwCjAKgAsAC2AL4AvQDFAMoA0gBPAVYBWgEIAAgACACMANoAYgFkAWwBdAF8AX0BhQGNAZUBlgGeAaMBlQGWAasBswF8AbsBwwF0AcsBYwHTAQgA2wG/AOMBdAF8AekB8QF0AfkB+wHiAHQBfAEIAAMC5gQIAAsCEgIIAAgAFgIeAggAIgIpAggAMQI5AkACygEIAAgASAJQAlgCYAIIAAgACAAKBQoFCgUTBRMFGQUrBSsFCAAIAAgACAAIAAgACAAIAAgACABdAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABoAmgCrwGvAQgAbgJ2AggAHgEIAAgACADnAXsCCAAIAAgAgwIIAAgACAAIAAgACACKAggAkQKZAggAPADJAAgAoQKkAqwCsgK6AsICCADJAggA0AIIAAgACAAIANYC3gIIAAgACAAIAAgACABAAOYCCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAkASoB+QIEAAgACAA8AEMCCABCBQgACABJBVAFCAAIAAgACAAIAAgACAAIAAgACABTBVoFCAAIAFoFCABfBWUFCAAIAAgACAAIAAgAbQUIAAgACAAIAAgACABzBXsFfQWFBYoFigWKBZEFigWKBYoFmAWfBaYFrgWxBbkFCAAIAAgACAAIAAgACAAIAAgACAAIAMEFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAMgFCADQBQgACAAIAAgACAAIAAgACAAIAAgACAAIAO4CCAAIAAgAiQAIAAgACABAAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAD0AggACAD8AggACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIANYFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAMDvwAIAAgAJAIIAAgACAAIAAgACAAIAAgACwMTAwgACAB9BOsEGwMjAwgAKwMyAwsFYgE3A/MEPwMIAEUDTQNRAwgAWQOsAGEDCAAIAAgACAAIAAgACABpAzQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFOgU0BTUFNgU3BTgFOQU6BTQFNQU2BTcFOAU5BToFNAU1BTYFNwU4BTkFIQUoBSwFCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABtAwgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABMAEwACAAIAAgACAAIABgACAAIAAgACAC/AAgACAAyAQgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACAAIAAwAAgACAAIAAgACAAIAAgACAAIAAAARABIAAgACAAIABQASAAIAAgAIABwAEAAjgCIABsAqAC2AL0AigDQAtwC+IJIQqVAZUBWQqVAZUBlQGVAZUBlQGrC5UBlQGVAZUBlQGVAZUBlQGVAXsKlQGVAbAK6wsrDGUMpQzlDJUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAZUBlQGVAfAKAAuZA64AtwCJALoC6ADwAAgAuACgA/oEpgO6AqsD+AAIAAgAswMIAAgACAAIAIkAuwP5AfsBwwPLAwgACAAIAAgACADRA9kDCAAIAOED6QMIAAgACAAIAAgACADuA/YDCAAIAP4DyQAIAAgABgQIAAgAXQAOBAgACAAIAAgACAAIABMECAAIAAgACAAIAAgACAD8AAQBCAAIAAgAGgQiBCoECAExBAgAEAEIAAgACAAIAAgACAAIAAgACAAIAAgACAA4BAgACABABEYECAAIAAgATAQYAQgAVAQIAAgACAAIAAgACAAIAAgACAAIAFoECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAOQEIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAB+BAcACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAEABhgSMBAgACAAIAAgAlAQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAwAEAAQABAADAAMAAwADAAQABAAEAAQABAAEAAQABHATAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAdQMIAAgACAAIAAgACAAIAMkACAAIAAgAfQMIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACACFA4kDCAAIAAgACAAIAOcBCAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAIcDCAAIAAgACAAIAAgACAAIAAgACAAIAJEDCAAIAAgACADFAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABgBAgAZgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAbAQCBXIECAAIAHkECAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACABAAJwEQACjBKoEsgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAC6BMIECAAIAAgACAAIAAgACABmBAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAxwQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAGYECAAIAAgAzgQIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBd0FXwUIAOIF6gXxBYoF3gT5BQAGCAaKBYoFigWKBYoFigWKBYoFigWKBYoFigXWBIoFigWKBYoFigWKBYoFigWKBYsFEAaKBYoFigWKBYoFigWKBRQGCACKBYoFigWKBQgACAAIANEECAAIABgGigUgBggAJgYIAC4GMwaKBYoF0wQ3Bj4GigWKBYoFigWKBYoFigWKBYoFigWKBYoFigUIAAgACAAIAAgACAAIAAgAigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWKBYoFigWLBf///////wQABAAEAAQABAAEAAQABAAEAAQAAwAEAAQAAgAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAQADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUAAAAFAAUAAAAFAAUAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUAAQAAAAUABQAFAAUABQAFAAAAAAAFAAUAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAFAAUAAQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAAABwAHAAcAAAAHAAcABwAFAAEAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAcABwAFAAUAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAQABAAAAAAAAAAAAAAAFAAUABQAFAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAHAAcAAAAHAAcAAAAAAAUABQAHAAUAAQAHAAEABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwABAAUABQAFAAUAAAAAAAAAAAAAAAEAAQABAAEAAQABAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABQANAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAQABAAEAAQABAAEAAQABAAEAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAABQAHAAUABQAFAAAAAAAAAAcABQAFAAUABQAFAAQABAAEAAQABAAEAAQABAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAEAAQABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUAAAAFAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAUAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAcABwAFAAcABwAAAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUABwAHAAUABQAFAAUAAAAAAAcABwAAAAAABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAABQAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAAAAAAAAAAABQAFAAAAAAAFAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAFAAUABQAFAAUAAAAFAAUABwAAAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABwAFAAUABQAFAAAAAAAHAAcAAAAAAAcABwAFAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAAAAAAAAAHAAcABwAAAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAABQAHAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAUABQAFAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAHAAcABQAHAAcAAAAFAAcABwAAAAcABwAFAAUAAAAAAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAFAAcABwAFAAUABQAAAAUAAAAHAAcABwAHAAcABwAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAHAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAABwAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAUAAAAFAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABwAFAAUABQAFAAUAAAAFAAUAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABwAFAAUABQAFAAUABQAAAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABQAFAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABQAFAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAHAAUABQAFAAUABQAFAAUABwAHAAcABwAHAAcABwAHAAUABwAHAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABwAHAAcABwAFAAUABwAHAAcAAAAAAAAAAAAHAAcABQAHAAcABwAHAAcABwAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAcABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAUABQAFAAUABQAFAAUAAAAFAAAABQAAAAAABQAFAAUABQAFAAUABQAFAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAFAAUAAAAAAAUABQAFAAUABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABwAFAAcABwAHAAcABwAFAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAUABQAFAAUABwAHAAUABQAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABQAFAAcABwAHAAUABwAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAcABQAFAAUABQAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAAAAAABwAFAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAAAAAAAAAFAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAUABQAHAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAUABQAFAAUABQAHAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAcABwAFAAUABQAFAAcABwAFAAUABwAHAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAFAAcABwAFAAUABwAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAFAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAFAAUABQAAAAAABQAFAAAAAAAAAAAAAAAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAcABwAAAAAAAAAAAAAABwAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAcABwAFAAcABwAAAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAFAAUABQAAAAUABQAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABwAFAAUABQAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAUABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAHAAcABQAHAAUABQAAAAAAAAAAAAAAAAAFAAAABwAHAAcABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAHAAcABwAAAAAABwAHAAAAAAAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABwAHAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAFAAUABwAFAAcABwAFAAcABQAFAAcABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAHAAcABQAFAAUABQAAAAAABwAHAAcABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAHAAUABQAFAAUABQAFAAUABQAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABwAFAAcABwAFAAUABQAFAAUABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAcABwAFAAUABQAFAAcABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAUABQAFAAUABQAHAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAFAAUABQAFAAAAAAAFAAUABwAHAAcABwAFAAAAAAAAAAcAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABwAHAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAcABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUAAAAHAAUABQAFAAUABQAFAAUABwAFAAUABwAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUAAAAAAAAABQAAAAUABQAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAcABwAHAAcAAAAFAAUAAAAHAAcABQAHAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAAAAUABQAFAAAAAAAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAFAAUABQAAAAAABQAFAAUABQAFAAUABQAAAAUABQAAAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFAAUABQAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQAFAAUABQAFAAUABQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAFAAUABQAFAAUADgAOAA4ADgAOAA4ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAA8ADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAcABwAHAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAgACAAIAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAMAAwADAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkACQAJAAkAAAAAAAAAAAAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAKAAoACgAAAAAAAAAAAAsADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwACwAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAMAAwADAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAADgAOAA4AAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAAAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4AAAAOAAAAAAAAAAAAAAAAAA4AAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAAAAAAAAAAAA4AAAAOAAAAAAAAAAAADgAOAA4AAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4AAAAAAA4ADgAOAA4ADgAOAA4ADgAOAAAADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4ADgAOAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAOAA4ADgAOAA4AAAAAAAAAAAAAAAAAAAAAAA4ADgAOAA4ADgAOAA4ADgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAOAA4ADgAOAA4ADgAAAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4ADgAOAA4AAAAAAAAAAAA="),xr=Array.isArray(vr)?function(A){for(var e=A.length,t=[],r=0;rs.x||t.y>s.y;return s=t,0===e||A});return A.body.removeChild(e),t}(document);return Object.defineProperty(Xr,"SUPPORT_WORD_BREAKING",{value:A}),A},get SUPPORT_SVG_DRAWING(){var A=function(A){var e=new Image,t=A.createElement("canvas"),A=t.getContext("2d");if(!A)return!1;e.src="data:image/svg+xml,";try{A.drawImage(e,0,0),t.toDataURL()}catch(A){return!1}return!0}(document);return Object.defineProperty(Xr,"SUPPORT_SVG_DRAWING",{value:A}),A},get SUPPORT_FOREIGNOBJECT_DRAWING(){var A="function"==typeof Array.from&&"function"==typeof window.fetch?function(t){var A=t.createElement("canvas"),r=100;A.width=r,A.height=r;var B=A.getContext("2d");if(!B)return Promise.reject(!1);B.fillStyle="rgb(0, 255, 0)",B.fillRect(0,0,r,r);var e=new Image,n=A.toDataURL();e.src=n;e=Nr(r,r,0,0,e);return B.fillStyle="red",B.fillRect(0,0,r,r),Pr(e).then(function(A){B.drawImage(A,0,0);var e=B.getImageData(0,0,r,r).data;B.fillStyle="red",B.fillRect(0,0,r,r);A=t.createElement("div");return A.style.backgroundImage="url("+n+")",A.style.height="100px",Lr(e)?Pr(Nr(r,r,0,0,A)):Promise.reject(!1)}).then(function(A){return B.drawImage(A,0,0),Lr(B.getImageData(0,0,r,r).data)}).catch(function(){return!1})}(document):Promise.resolve(!1);return Object.defineProperty(Xr,"SUPPORT_FOREIGNOBJECT_DRAWING",{value:A}),A},get SUPPORT_CORS_IMAGES(){var A=void 0!==(new Image).crossOrigin;return Object.defineProperty(Xr,"SUPPORT_CORS_IMAGES",{value:A}),A},get SUPPORT_RESPONSE_TYPE(){var A="string"==typeof(new XMLHttpRequest).responseType;return Object.defineProperty(Xr,"SUPPORT_RESPONSE_TYPE",{value:A}),A},get SUPPORT_CORS_XHR(){var A="withCredentials"in new XMLHttpRequest;return Object.defineProperty(Xr,"SUPPORT_CORS_XHR",{value:A}),A},get SUPPORT_NATIVE_TEXT_SEGMENTATION(){var A=!("undefined"==typeof Intl||!Intl.Segmenter);return Object.defineProperty(Xr,"SUPPORT_NATIVE_TEXT_SEGMENTATION",{value:A}),A}},Jr=function(A,e){this.text=A,this.bounds=e},Yr=function(A,e){var t=e.ownerDocument;if(t){var r=t.createElement("html2canvaswrapper");r.appendChild(e.cloneNode(!0));t=e.parentNode;if(t){t.replaceChild(r,e);A=f(A,r);return r.firstChild&&t.replaceChild(r.firstChild,r),A}}return d.EMPTY},Wr=function(A,e,t){var r=A.ownerDocument;if(!r)throw new Error("Node has no owner document");r=r.createRange();return r.setStart(A,e),r.setEnd(A,e+t),r},Zr=function(A){if(Xr.SUPPORT_NATIVE_TEXT_SEGMENTATION){var e=new Intl.Segmenter(void 0,{granularity:"grapheme"});return Array.from(e.segment(A)).map(function(A){return A.segment})}return function(A){for(var e,t=mr(A),r=[];!(e=t.next()).done;)e.value&&r.push(e.value.slice());return r}(A)},_r=function(A,e){return 0!==e.letterSpacing?Zr(A):function(A,e){if(Xr.SUPPORT_NATIVE_TEXT_SEGMENTATION){var t=new Intl.Segmenter(void 0,{granularity:"word"});return Array.from(t.segment(A)).map(function(A){return A.segment})}return jr(A,e)}(A,e)},qr=[32,160,4961,65792,65793,4153,4241],jr=function(A,e){for(var t,r=wA(A,{lineBreak:e.lineBreak,wordBreak:"break-word"===e.overflowWrap?"break-word":e.wordBreak}),B=[];!(t=r.next()).done;)!function(){var A,e;t.value&&(A=t.value.slice(),A=Q(A),e="",A.forEach(function(A){-1===qr.indexOf(A)?e+=g(A):(e.length&&B.push(e),B.push(g(A)),e="")}),e.length&&B.push(e))}();return B},zr=function(A,e,t){var B,n,s,o,i;this.text=$r(e.data,t.textTransform),this.textBounds=(B=A,A=this.text,s=e,A=_r(A,n=t),o=[],i=0,A.forEach(function(A){var e,t,r;n.textDecorationLine.length||0e.height?new d(e.left+(e.width-e.height)/2,e.top,e.height,e.height):e.width"),Ln(this.referenceElement.ownerDocument,t,n),o.replaceChild(o.adoptNode(this.documentElement),o.documentElement),o.close(),A},fn.prototype.createElementClone=function(A){if(Cr(A,2),zB(A))return this.createCanvasClone(A);if(MB(A))return this.createVideoClone(A);if(SB(A))return this.createStyleClone(A);var e=A.cloneNode(!1);return $B(e)&&($B(A)&&A.currentSrc&&A.currentSrc!==A.src&&(e.src=A.currentSrc,e.srcset=""),"lazy"===e.loading&&(e.loading="eager")),TB(e)?this.createCustomElementClone(e):e},fn.prototype.createCustomElementClone=function(A){var e=document.createElement("html2canvascustomelement");return Kn(A.style,e),e},fn.prototype.createStyleClone=function(A){try{var e=A.sheet;if(e&&e.cssRules){var t=[].slice.call(e.cssRules,0).reduce(function(A,e){return e&&"string"==typeof e.cssText?A+e.cssText:A},""),r=A.cloneNode(!1);return r.textContent=t,r}}catch(A){if(this.context.logger.error("Unable to access cssRules property",A),"SecurityError"!==A.name)throw A}return A.cloneNode(!1)},fn.prototype.createCanvasClone=function(e){var A;if(this.options.inlineImages&&e.ownerDocument){var t=e.ownerDocument.createElement("img");try{return t.src=e.toDataURL(),t}catch(A){this.context.logger.info("Unable to inline canvas contents, canvas is tainted",e)}}t=e.cloneNode(!1);try{t.width=e.width,t.height=e.height;var r,B,n=e.getContext("2d"),s=t.getContext("2d");return s&&(!this.options.allowTaint&&n?s.putImageData(n.getImageData(0,0,e.width,e.height),0,0):(!(r=null!==(A=e.getContext("webgl2"))&&void 0!==A?A:e.getContext("webgl"))||!1===(null==(B=r.getContextAttributes())?void 0:B.preserveDrawingBuffer)&&this.context.logger.warn("Unable to clone WebGL context as it has preserveDrawingBuffer=false",e),s.drawImage(e,0,0))),t}catch(A){this.context.logger.info("Unable to clone canvas as it is tainted",e)}return t},fn.prototype.createVideoClone=function(e){var A=e.ownerDocument.createElement("canvas");A.width=e.offsetWidth,A.height=e.offsetHeight;var t=A.getContext("2d");try{return t&&(t.drawImage(e,0,0,A.width,A.height),this.options.allowTaint||t.getImageData(0,0,A.width,A.height)),A}catch(A){this.context.logger.info("Unable to clone video as it is tainted",e)}A=e.ownerDocument.createElement("canvas");return A.width=e.offsetWidth,A.height=e.offsetHeight,A},fn.prototype.appendChildNode=function(A,e,t){XB(e)&&("SCRIPT"===e.tagName||e.hasAttribute(hn)||"function"==typeof this.options.ignoreElements&&this.options.ignoreElements(e))||this.options.copyStyles&&XB(e)&&SB(e)||A.appendChild(this.cloneNode(e,t))},fn.prototype.cloneChildNodes=function(A,e,t){for(var r,B=this,n=(A.shadowRoot||A).firstChild;n;n=n.nextSibling)XB(n)&&rn(n)&&"function"==typeof n.assignedNodes?(r=n.assignedNodes()).length&&r.forEach(function(A){return B.appendChildNode(e,A,t)}):this.appendChildNode(e,n,t)},fn.prototype.cloneNode=function(A,e){if(PB(A))return document.createTextNode(A.data);if(!A.ownerDocument)return A.cloneNode(!1);var t=A.ownerDocument.defaultView;if(t&&XB(A)&&(JB(A)||YB(A))){var r=this.createElementClone(A);r.style.transitionProperty="none";var B=t.getComputedStyle(A),n=t.getComputedStyle(A,":before"),s=t.getComputedStyle(A,":after");this.referenceElement===A&&JB(r)&&(this.clonedReferenceElement=r),jB(r)&&Mn(r);t=this.counters.parse(new Ur(this.context,B)),n=this.resolvePseudoContent(A,r,n,gn.BEFORE);TB(A)&&(e=!0),MB(A)||this.cloneChildNodes(A,r,e),n&&r.insertBefore(n,r.firstChild);s=this.resolvePseudoContent(A,r,s,gn.AFTER);return s&&r.appendChild(s),this.counters.pop(t),(B&&(this.options.copyStyles||YB(A))&&!An(A)||e)&&Kn(B,r),0===A.scrollTop&&0===A.scrollLeft||this.scrolledElements.push([r,A.scrollLeft,A.scrollTop]),(en(A)||tn(A))&&(en(r)||tn(r))&&(r.value=A.value),r}return A.cloneNode(!1)},fn.prototype.resolvePseudoContent=function(o,A,e,t){var i=this;if(e){var r=e.content,Q=A.ownerDocument;if(Q&&r&&"none"!==r&&"-moz-alt-content"!==r&&"none"!==e.display){this.counters.parse(new Ur(this.context,e));var c=new wr(this.context,e),a=Q.createElement("html2canvaspseudoelement");Kn(e,a),c.content.forEach(function(A){if(0===A.type)a.appendChild(Q.createTextNode(A.value));else if(22===A.type){var e=Q.createElement("img");e.src=A.value,e.style.opacity="1",a.appendChild(e)}else if(18===A.type){var t,r,B,n,s;"attr"===A.name?(e=A.values.filter(_A)).length&&a.appendChild(Q.createTextNode(o.getAttribute(e[0].value)||"")):"counter"===A.name?(B=(r=A.values.filter($A))[0],r=r[1],B&&_A(B)&&(t=i.counters.getCounterValue(B.value),s=r&&_A(r)?pt.parse(i.context,r.value):3,a.appendChild(Q.createTextNode(Fn(t,s,!1))))):"counters"===A.name&&(B=(t=A.values.filter($A))[0],s=t[1],r=t[2],B&&_A(B)&&(B=i.counters.getCounterValues(B.value),n=r&&_A(r)?pt.parse(i.context,r.value):3,s=s&&0===s.type?s.value:"",s=B.map(function(A){return Fn(A,n,!1)}).join(s),a.appendChild(Q.createTextNode(s))))}else if(20===A.type)switch(A.value){case"open-quote":a.appendChild(Q.createTextNode(Xt(c.quotes,i.quoteDepth++,!0)));break;case"close-quote":a.appendChild(Q.createTextNode(Xt(c.quotes,--i.quoteDepth,!1)));break;default:a.appendChild(Q.createTextNode(A.value))}}),a.className=Dn+" "+vn;t=t===gn.BEFORE?" "+Dn:" "+vn;return YB(A)?A.className.baseValue+=t:A.className+=t,a}}},fn.destroy=function(A){return!!A.parentNode&&(A.parentNode.removeChild(A),!0)},fn);function fn(A,e,t){if(this.context=A,this.options=t,this.scrolledElements=[],this.referenceElement=e,this.counters=new Bn,this.quoteDepth=0,!e.ownerDocument)throw new Error("Cloned element does not have an owner document");this.documentElement=this.cloneNode(e.ownerDocument.documentElement,!1)}(he=gn=gn||{})[he.BEFORE=0]="BEFORE",he[he.AFTER=1]="AFTER";function Hn(e){return new Promise(function(A){!e.complete&&e.src?(e.onload=A,e.onerror=A):A()})}var pn=function(A,e){var t=A.createElement("iframe");return t.className="html2canvas-container",t.style.visibility="hidden",t.style.position="fixed",t.style.left="-10000px",t.style.top="0px",t.style.border="0",t.width=e.width.toString(),t.height=e.height.toString(),t.scrolling="no",t.setAttribute(hn,"true"),A.body.appendChild(t),t},En=function(A){return Promise.all([].slice.call(A.images,0).map(Hn))},In=function(B){return new Promise(function(e,A){var t=B.contentWindow;if(!t)return A("No window assigned for iframe");var r=t.document;t.onload=B.onload=function(){t.onload=B.onload=null;var A=setInterval(function(){0"),e},Ln=function(A,e,t){A&&A.defaultView&&(e!==A.defaultView.pageXOffset||t!==A.defaultView.pageYOffset)&&A.defaultView.scrollTo(e,t)},bn=function(A){var e=A[0],t=A[1],A=A[2];e.scrollLeft=t,e.scrollTop=A},Dn="___html2canvas___pseudoelement_before",vn="___html2canvas___pseudoelement_after",xn='{\n content: "" !important;\n display: none !important;\n}',Mn=function(A){Sn(A,"."+Dn+":before"+xn+"\n ."+vn+":after"+xn)},Sn=function(A,e){var t=A.ownerDocument;t&&((t=t.createElement("style")).textContent=e,A.appendChild(t))},Tn=(Gn.getOrigin=function(A){var e=Gn._link;return e?(e.href=A,e.href=e.href,e.protocol+e.hostname+e.port):"about:blank"},Gn.isSameOrigin=function(A){return Gn.getOrigin(A)===Gn._origin},Gn.setContext=function(A){Gn._link=A.document.createElement("a"),Gn._origin=Gn.getOrigin(A.location.href)},Gn._origin="about:blank",Gn);function Gn(){}var On=(Vn.prototype.addImage=function(A){var e=Promise.resolve();return this.has(A)||(Yn(A)||Pn(A))&&(this._cache[A]=this.loadImage(A)).catch(function(){}),e},Vn.prototype.match=function(A){return this._cache[A]},Vn.prototype.loadImage=function(s){return a(this,void 0,void 0,function(){var e,r,t,B,n=this;return H(this,function(A){switch(A.label){case 0:return(e=Tn.isSameOrigin(s),r=!Xn(s)&&!0===this._options.useCORS&&Xr.SUPPORT_CORS_IMAGES&&!e,t=!Xn(s)&&!e&&!Yn(s)&&"string"==typeof this._options.proxy&&Xr.SUPPORT_CORS_XHR&&!r,e||!1!==this._options.allowTaint||Xn(s)||Yn(s)||t||r)?(B=s,t?[4,this.proxy(B)]:[3,2]):[2];case 1:B=A.sent(),A.label=2;case 2:return this.context.logger.debug("Added image "+s.substring(0,256)),[4,new Promise(function(A,e){var t=new Image;t.onload=function(){return A(t)},t.onerror=e,(Jn(B)||r)&&(t.crossOrigin="anonymous"),t.src=B,!0===t.complete&&setTimeout(function(){return A(t)},500),0t.width+C?0:Math.max(0,n-C),Math.max(0,s-l),As.TOP_RIGHT):new Zn(t.left+t.width-C,t.top+l),this.bottomRightPaddingBox=0t.width+F+A?0:n-F+A,s-(l+h),As.TOP_RIGHT):new Zn(t.left+t.width-(C+d),t.top+l+h),this.bottomRightContentBox=0A.element.container.styles.zIndex.order?(s=e,!1):0=A.element.container.styles.zIndex.order?(o=e+1,!1):0 { + const info = document.querySelector('.info').offsetWidth; + colorbar.style.maxWidth = info.toString() + 'px'; + }); +} + +$(document).ready(function () { + $(document).on('click', "#models-apply, .DayPicker, .SingleDatePickerInput_clearDate", function () { + setTimeout(setWidthForColorbars, 500); + }) +}) + +$(document).ready(function () { + setTimeout(setWidthForColorbars, 1500); +}); diff --git a/assets/router.js b/assets/router.js new file mode 100644 index 0000000000000000000000000000000000000000..aa6a7631d69f58b50c9dd5fa05f24f7cebaa5e5e --- /dev/null +++ b/assets/router.js @@ -0,0 +1,92 @@ +// ROUTING FUNCTIONS + +// Convert date from DD MON YYYY to YYYYMMDD +function getDate() { + const dict = { + 'Jan': '01', + 'Feb': '02', + 'Mar': '03', + 'Apr': '04', + 'May': '05', + 'Jun': '06', + 'Jul': '07', + 'Aug': '08', + 'Sep': '09', + 'Oct': '10', + 'Nov': '11', + 'Dec': '12' + }; + date = document.getElementById('date').value; + splitDate = date.split(' '); + month = dict[splitDate[1]]; + return 'date=' + splitDate[2] + month + splitDate[0]; +} + +// Send the URL to the CMS +function sendURL(url) { + window.history.pushState("URL from Dashboard", "", url); + parent.postMessage(url, '*'); +} + +// GET THE VALUE SELECTED IN DROPDOWN MENU +function getDropdownValue(selector, key) { + curvar = document.querySelector(selector).innerHTML; + curvar = curvar.split(' ').join('_') + //CREATE KEY/VALUE OUTPUT FOR URL SEACH QUERY FORMAT (EX. FRUIT=APPLE) + return key + '=' + curvar.toLowerCase(); +} + +// GET CHECK BOXES CHECKED +function getCheckedBoxes(selector, key){ + const data = [...document.querySelectorAll(selector)].map(e => e.value); + data.forEach(function(item, index){ + //CREATE KEY/VALUE OUTPUT FOR URL SEACH QUERY FORMAT (EX. FRUIT=APPLE) + this[index] = key + '=' + item + }, data); + return data.join('&'); +} + +// CREATE URL WHEN MODELS-APPLY IS CLICKED +function handleModelsApply () { + curvar = getDropdownValue('.Select-value-label', 'var') + models = getCheckedBoxes('.form-check-input:checked', 'model') + outputDate = getDate(); + url = "?" + curvar + '&' + models + '&' + outputDate; + sendURL(url); + }; + +// CREATE URL WHEN MODELS-APPLY IS CLICKED +function handleWASApply () { + const checked = document.getElementById('was-dropdown'); + var text = checked.querySelector('input[type="radio"]:checked').parentElement.innerText; + text = text.split(' ').join('_').toLowerCase(); + url = "?tab=forecast§ion=was&country=" + text; + sendURL(url); + }; + +// CREATE A DICTIONARY OF IDs TO CREATE URL OUTPUTS +const urlOuputDict = { + "#group-1-toggle": "?tab=forecast§ion=models", + "#group-2-toggle": "?tab=forecast§ion=prob", + "#group-3-toggle": "?tab=forecast§ion=was", + "#forecast-tab": "?tab=forecast", + "#evaluation-tab": "?tab=evaluation", + "#observations-tab": "?tab=observations", + "#nrt-evaluation": "?tab=evaluation§ion=visual_comparison", + "#scores-evaluation": "?tab=evaluation§ion=statistics", + "#rgb": "?tab=observations§ion=eumetsat-rgb", + "#visibility": "?tab=observations§ion=visibility", + "#models-apply": handleModelsApply, + "#was-apply": handleWASApply, +}; + +// LISTEN FOR CLICK ON ID(KEY) LISTED IN URLOUTPUTDICT AND OUTPUT (VALUE) TO BROWSER SEARCH BAR +$(document).ready(function () { + // MATCH ID WITH THE TOOGLEMAP KEY AND OUTPUT URL VALUE + $(document).on('click', Object.keys(urlOuputDict).join(', '), function () { + var selector = "#" + $(this).attr('id'); + var value = urlOuputDict[selector]; + var url = typeof value === 'function' ? value() : value; + sendURL(url); + }); +}); diff --git a/assets/sidebar.css b/assets/sidebar.css index 6b51a62363bf225fd8f7e3f7aca6b012bc23aef5..cb1319d2e78c4112f6e0d65cff764621b1053063 100644 --- a/assets/sidebar.css +++ b/assets/sidebar.css @@ -11,7 +11,7 @@ left: 0; width: 200px; height: 100%; - font-family: "Roboto", sans-serif; + font-family: "Roboto", sans-serif !important; color: var(--blue) !important; background-color: #FAFAFA; overflow: hidden; @@ -37,6 +37,7 @@ padding-top: 0; border-radius: 0px; border: none; + box-shadow: none !important; } #app-sidebar>label { @@ -113,20 +114,23 @@ } /* Custom control: Classes recently added by Francesco */ -.custom-control { +.form-check { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; padding-left: 0; - padding-right: 0; + padding-right: 0.2rem; } -.custom-control-label { +.form-check-label { /* margin-left: 1rem; */ margin-top: 0.1rem; display: block; font-size: small; } -.custom-control-label::before, -.custom-control-label::after { +.form-check-label::before, +.form-check-label::after { left: unset; right: 0.5rem; float: right; @@ -157,11 +161,21 @@ } .card-header>h2>button.btn-link { - white-space: normal; - float: left !important; - text-align: left !important; - display: block; - width: 100%; + white-space: normal; + float: left !important; + text-align: left !important; + display: block; + width: 100%; + text-decoration: none; +} + +.card-header>h2>button.btn-link:hover { + text-decoration: underline; +} + +.card { + border-top: none; + border-bottom: none; } /*.accordion>.card>.card-header:after { @@ -337,7 +351,10 @@ span>#was-apply { } .card-body { - padding: 1rem; + padding: 1rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.175) !important; + border-left: none; + border-right: none; } .caret-span { @@ -362,6 +379,10 @@ span>#was-apply { -webkit-transform: rotate(180deg); } +#group-1-toggle span { + top: .15rem; +} + .Select-arrow-zone { bottom: .3em; right: .5em; @@ -421,7 +442,7 @@ div.tab-parent { bottom: 0; } -/* /* On screens that are less than 700px wide, make the sidebar into a topbar */ */ +/* On screens that are less than 700px wide, make the sidebar into a topbar */ /* @media screen and (max-width: 700px) { */ /* .sidebar { */ /* width: 100%; */ @@ -432,10 +453,11 @@ div.tab-parent { /* div.content {margin-left: 0;} */ /* } */ /**/ + /* On screens that are less than 400px, display the bar vertically, instead of horizontally */ @media screen and (max-width: 400px) { - .sidebar a { - text-align: center; - float: none; - } + .sidebar a { + text-align: center; + float: none; + } } diff --git a/assets/stats_carets.js b/assets/stats_carets.js new file mode 100644 index 0000000000000000000000000000000000000000..f33277b47b2f9133946a263c9bbd06d1642f797c --- /dev/null +++ b/assets/stats_carets.js @@ -0,0 +1,78 @@ +//================== Stats table carets =============================================== +// The aeronet stats table changes the position/index of the regions when they +// are clicked, so we cannot easily assign a class to the regions that will maintain on click, +// so we must remove the carets on click, and then add them back after the table has finished + +// changing its state + +// ADD FUNCTION TO WAIT FOR TABLE TO FINISH CHANGING +function waitForMutation(selector, func) { + return new Promise(resolve => { + if (document.querySelector(selector)) { + return resolve(document.querySelector(selector)); + }; + const observer = new MutationObserver(mutations => { + if (document.querySelector(selector)) { + resolve(document.querySelector(selector)); + // console.log(mutations); + func(mutations); + } + }); + observer.observe(document.body, { + childList: true, + subtree: true, + attributeOldValue: true, + characterDataOldValue: true + }); + }); +} + +// FIND THE REGIONS TO REMOVE THE CARETS BEFORE FLIPPING THEM +function targetRegions(mutations) { + // Carets will populate in the wrong rows after click, so we must remove them + for (const element of mutations) { + if (['Mediterranean','NAfrica', 'MiddleEast'].includes(element.oldValue)) { + // Get elements parent node 2 above as it contains the target class + const target = element.target.parentNode.parentNode; + target.classList.remove('table_caret_up'); + target.classList.remove('table_caret_down'); + }; + } + // Now add the carets back in where appropriate + flipCarets(); +} + +// CREATE FUNCTION TO FIND REGIONS AND FLIP CARETS AS NEEDED +function flipCarets() { + var areas = "td:contains('Europe'), td:contains('Mediterranean'),td:contains('NAfrica'),td:contains('MiddleEast')"; + $(areas).addClass('table_caret_down'); + // Each table has the four regions. Split the returned areas into tables, and then address caret flips + for(var i = 0; i < ($(areas).length/4); i++){ + var medIndex = parseInt($("td:contains('Mediterranean')").eq(i).attr('data-dash-row')); + var nafricaIndex = parseInt($("td:contains('NAfrica')").eq(i).attr('data-dash-row')); + var middleEastIndex = parseInt($("td:contains('MiddleEast')").eq(i).attr('data-dash-row')); + var totalIndex = parseInt($("td:contains('Total')").eq(i).attr('data-dash-row')); + // Ror each table, check if the caret should be flipped + // if the next region is not at the next index, the dropdown is open + // trigger the caret to be flipped + if (medIndex !== 1) { + $("td:contains('Europe')").eq(i).addClass('table_caret_up'); + }; + if ((medIndex + 1) !== middleEastIndex) { + $("td:contains('Mediterranean')").eq(i).addClass('table_caret_up'); + }; + if ((middleEastIndex + 1) !== nafricaIndex) { + $("td:contains('MiddleEast')").eq(i).addClass('table_caret_up'); + }; + if ((nafricaIndex + 1) !== totalIndex) { + $("td:contains('NAfrica')").eq(i).addClass('table_caret_up'); + }; + }; +}; + +// ADD FUNCTION TO WAIT FOR CLICKS ON AREAS THAT SHOULD TRIGGER CARET FLIPS +$(document).ready(function () { + $(document).on('click', "td:contains('Europe'), td:contains('Mediterranean'),td:contains('NAfrica'),td:contains('MiddleEast'), #scores-apply, #evaluation-tab", function () { + waitForMutation('td.dash-cell.column-0', targetRegions); + }) +}) diff --git a/assets/style.css b/assets/style.css index d48b54d7a78c35e014d178f982874edfcbaf2b0b..b2396c1af4bc4718d5b98dbce3b0eb50d66b9541 100644 --- a/assets/style.css +++ b/assets/style.css @@ -6,6 +6,7 @@ body { overflow-y: hidden; + font-family: "Roboto", sans-serif !important; background-color: #212529; } @@ -154,7 +155,7 @@ a.modebar-btn { z-index: 1000 !important; } -.disclaimer>span#forecast-disclaimer>p { +.disclaimer>span#models-disclaimer>p { float: right; margin-right: 1rem; } @@ -504,6 +505,10 @@ div.SingleDatePickerInput { padding: 0 !important; } +.navbar-timebar:has(.show) { + height: auto; +} + .centered-image>.layout-dropdown>#rgb-navbar>.container-fluid>.collapse>.navbar-nav { justify-content: center; } @@ -522,12 +527,14 @@ div.SingleDatePickerInput { padding: 0px !important; } -.navbar-timebar>.show { - height: 18vh !important; -} - .navbar-toggler { height: 5vh; + border-radius: 0px !important; + border-bottom: none !important; +} + +.navbar-toggler:focus { + box-shadow: none !important; } .disclaimer { @@ -605,7 +612,7 @@ div.SingleDatePickerInput { height: 100%; } -.btn-secondary { +.btn-primary { box-shadow: none !important; border: 0 !important; } @@ -1024,3 +1031,7 @@ td.column-0 { transform: rotate(20deg) translate(-50%, 5px) !important; padding-top: 2px; } + +#scores-map-modalbody { + height: 70vh !important; +} \ No newline at end of file diff --git a/assets/url.js b/assets/url.js deleted file mode 100644 index 56d8694c8f0a8e7757a8415782a69d4c06deb381..0000000000000000000000000000000000000000 --- a/assets/url.js +++ /dev/null @@ -1,176 +0,0 @@ -// ==================== ROUTING FUNCTIONS ============================= -//convert date from DD MON YYYY to YYYYMMDD -function getDate() { - const dict = { - 'Jan': '01', - 'Feb': '02', - 'Mar': '03', - 'Apr': '04', - 'May': '05', - 'Jun': '06', - 'Jul': '07', - 'Aug': '08', - 'Sep': '09', - 'Oct': '10', - 'Nov': '11', - 'Dec': '12' - }; - date = document.getElementById('date').value; - splitDate = date.split(' '); - month = dict[splitDate[1]]; - return '&date=' + splitDate[2] + month + splitDate[0]; -} - -// LISTEN FOR MESSAGES FROM CMS -window.addEventListener("message", (event) => { - // CREATE LIST OF ADDRESSES - const hosts = [ - 'http://bscesdust02.bsc.es', - 'http://bscesdust02.bsc.es/products/daily-dust-products', - 'https://dust.aemet.es', - 'https://dust02.bsc.es', - 'https://dust02.bsc.es/products/daily-dust-products', - 'https://dust03.bsc.es' - ]; - if (hosts.includes(event.origin)){ - console.log('DASH: Success!! ' + event.data); - window.history.pushState("Models", "Models", event.data); - return; - }else{ - console.log('DASH: that is not the right address' + event.origin); - return; - } -}, false); - -// NOW ASSESS HOW THE URL SHOULD BE OUTPUT -function sendURL(url) { - window.history.pushState("Models", "Models", url); - parent.postMessage(url, '*'); -} - -//GET THE VARIABLE VALUE -function getVar() { - curvar = document.querySelector('.Select-value-label').innerHTML; - curvar = curvar.split(' ').join('_') - return 'var=' + curvar.toLowerCase() + '&'; -} - -// GET MODELS CHECKED -function getModels(){ - const data = [...document.querySelectorAll('.custom-control-input:checked')].map(e => e.value); - data.forEach(function(item, index){ - this[index] = 'model=' + item - }, data); - return data.join('&'); -} - -// CREATE OUTPUT URL FOR MODELS-APPLY -$(document).ready(function () { - $(document).on('click', "#models-apply", function () { - curvar = getVar() - models = getModels() - outputDate = getDate(); - url = "?" + curvar + models + outputDate; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR TABS -$(document).ready(function () { - $(document).on('click', "#forecast-tab", function () { - url = "?tab=forecast"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR TABS -$(document).ready(function () { - $(document).on('click', "#evaluation-tab", function () { - url = "?tab=evaluation"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR TABS -$(document).ready(function () { - $(document).on('click', "#observations-tab", function () { - url = "?tab=observations"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR GROUP-1-TOGGLE(MODELS) -$(document).ready(function () { - $(document).on('click', "#group-1-toggle", function () { - url = "?tab=forecast§ion=models"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR GROUP-2-TOGGLE(PROBABILITY) -$(document).ready(function () { - $(document).on('click', "#group-2-toggle", function () { - url = "?tab=forecast§ion=prob"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR GROUP-3-TOGGLE(WAS) -$(document).ready(function () { - $(document).on('click', "#group-3-toggle", function () { - url = "?tab=forecast§ion=was"; - sendURL(url); - }) -}); - -// CREATE WAS OUTPUT -$(document).ready(function () { - $(document).on('click', "#was-apply", function () { - const checked = document.getElementById('was-dropdown'); - var text = checked.querySelector('input[type="radio"]:checked').parentElement.innerText; - text = text.split(' ').join('_').toLowerCase(); - url = "?tab=forecast§ion=was&country=" + text; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR NRT-EVALUATION(VISUAL_COMPARISION) -$(document).ready(function () { - $(document).on('click', "#nrt-evaluation", function () { - url = "?tab=evaluation§ion=visual_comparison"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR SCORES-EVALUATION(STATISTICS) -$(document).ready(function () { - $(document).on('click', "#scores-evaluation", function () { - url = "?tab=evaluation§ion=statistics"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR SCORES-APPLY(STATISTICS) -$(document).ready(function () { - $(document).on('click', "#scores-apply", function () { - url = "?tab=evaluation§ion=statistics"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR RGB(EUMETSAT RGB) -$(document).ready(function () { - $(document).on('click', "#rgb", function () { - url = "?tab=observations§ion=eumetsat-rgb"; - sendURL(url); - }) -}); - -// CREATE OUTPUT URL FOR VISIBILITY -$(document).ready(function () { - $(document).on('click', "#visibility", function () { - url = "?tab=observations§ion=visibility"; - sendURL(url); - }) -}); - diff --git a/bin/gen_country_loop.sh b/bin/gen_country_loop.sh index a711c627958807e4bd558e64c006175970492acd..cdc28bf186fd0f4bd93e8c5e95b7c291e822047d 100755 --- a/bin/gen_country_loop.sh +++ b/bin/gen_country_loop.sh @@ -2,7 +2,7 @@ ########################################## # country should be written lowercase, or how appears in coords.conf -# date currently does not function and will return always today's date (or most recent available) +# date is only for labelling and will return always gifs for today's date (or most recent available) # only monarch accepts all variables, the rest get od550_dust and sconc_dust # anim should only be called with true here, as the pngs are automatically deleted # ./bin/gen_country_loop.sh monarch true sconc_dust 20220808 spain @@ -43,10 +43,12 @@ fi curyear=`date '+%Y' -d ${curdate}` curmon=`date '+%m' -d ${curdate}` -repodir='/data/daily_dashboard/comparison/' +# CHANGE REPORDIR WHEN TESTING +repodir='/data/daily_dashboard/comparison' runProcess () { - tmpdir=${HOME}/tmp/${var}/${country} + # CHANGE THIS TMPDIR WHEN TESTING + tmpdir=${HOME}/tmp/${variable} mkdir -p $tmpdir rm -rf $tmpdir/* # SET REPO AND FILENAME FOR NON-COUNTRY SELECTIONS @@ -54,7 +56,8 @@ runProcess () { filename=${curdate}_${model} # SET FOLDER STRUCTURE AND FILENAME FOR SPECIFIC COUNTRIES if [ "$country" != "default" ] && [ "$country" != "monarch_fit" ]; then - tmpdir=${HOME}/tmp/${var}/${country} + # CHANGE THIS TMPDIR WHEN TESTING + tmpdir=${HOME}/tmp/${variable}/${country} mkdir -p $tmpdir rm -rf $tmpdir/* currepo=${repodir}/${model}/${variable}/${curyear}/${curmon}/${country} @@ -74,28 +77,40 @@ for var in od550_dust sconc_dust dust_depd dust_depw dust_load dust_ext_sfc do variable=$var echo "************ $model $anim $curvar $variable $curdate $country****************" - if [ "$curvar" != "$variable" ] && [ "$curvar" != "all" ]; then - continue - fi - # IF VARIABLE ISN'T AOD OR CONCENTRATION, FORCE MONARCH - if [ "$variable" != "od550_dust" ] && [ "$variable" != "sconc_dust" ]; then - model="monarch" - runProcess # IF MODEL INPUT IS ALL, RUN THROUGH ALL MODELS - elif [ "$model" == "all" ]; then + if [ "$model" == "all" ]; then selectedCountry=$country for model in `cat ./conf/models.json | grep '": {' | sed 's/^.*"\(.*\)".*$/\1/g'` do + # SET MONARCH FIT VIEW FOR MONARCH if [ "$model" == "monarch" ] && [ "$country" == "default" ]; then country="monarch_fit" else country=$selectedCountry fi - runProcess + # ONLY MONARCH SHOULD RUN EXTRA VARIABLES + if [ "$variable" != "od550_dust" ] && [ "$variable" != "sconc_dust" ] && [ "$model" != "monarch" ]; then + echo "****** Skipping $model run for remaining variables ******" + continue + else + runProcess + fi done - # PRODUCE GIF FOR DECLARED MODEL AND VARIABLE - else + #NOW RESET MODEL TO ALL TO CONTINUE TO NEXT VARIABLE + model="all" + # PRODUCE GIF FOR DECLARED MODEL AND VARIABLE WHERE SINGLE VARIABLE IS SELECTED + elif [ "$curvar" != "all" ]; then + variable=$curvar runProcess + break + # RUN FOR SPECIFIED MODEL WITH ALL APPLICABLE VARIABLES + else + # ONLY MONARCH SHOULD RUN EXTRA VARIABLES + if [ "$variable" == "dust_depd" ] && [ "$model" != "monarch" ]; then + break + else + runProcess + fi fi wait sleep 1 diff --git a/callback_tools.py b/callback_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..0a85c8dfad8bfddf7b99194876b7d7795ca4700f --- /dev/null +++ b/callback_tools.py @@ -0,0 +1,524 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" Tools module with functions related to plots """ + +from datetime import datetime +from datetime import timedelta +import os +import logging + +from map_handler import MapHandler +from map_handler import ForecastModelsFigureHandler +from map_handler import ForecastProbFigureHandler +from map_handler import ForecastWasFigureHandler +from map_handler import EvaluationGroundFigureHandler +from map_handler import EvaluationSatelliteFigureHandler +from map_handler import EvaluationStatisticsFigureHandler +from map_handler import VisFigureHandler +from map_handler import DEBUG +from map_handler import MODELS +from map_handler import PATHS +from map_handler import END_DATE, DELAY, DELAY_DATE +from timeseries_handler import ForecastModelsTimeSeriesHandler +from timeseries_handler import EvaluationGroundTimeSeriesHandler + +def download_image_link(models, variable, curdate, tstep=0, anim=False): + """ Generates links to animated gifs + + Parameters + ---------- + models : str, list + Model name or list of model names + variable : str + Variable name + curdate : str + Current date + tstep : int, optional + Timestep, by default 0 + anim : bool, optional + Indicates if we want to create an animation or an image, by default False + + Returns + ------- + str + Filename of the image / animation + """ + + logging.debug('CURRDIR %s', os.getcwd()) + filepath = "assets/comparison/{model}/{variable}/{year}/{month}/{curdate}_{model}_{tstep}.{ext}" + + if len(models) == 1: + model = models[0] + logging.debug('DOWNLOAD MODELS %s', model) + else: + logging.debug('DOWNLOAD ALL MODELS') + model = "all" + if anim: + tstep = "loop" + ext = "gif" + logging.debug('DOWNLOAD LOOP') + else: + tstep = "%02d" % tstep + ext = "png" + logging.debug('DOWNLOAD PNG %s', tstep) + + filename = filepath.format( + model=model, + variable=variable.lower(), + year=curdate[:4], + month=curdate[4:6], + curdate=curdate, + tstep=tstep, + ext=ext + ) + logging.debug('DOWNLOAD FILENAME %s', filename) + + return filename + + +def get_eval_timeseries(obs, start_date, end_date, var, idx, station_name, model): + """ Retrieve timeseries in the evaluation visual comparison map + + Parameters + ---------- + obs : str + Observations name + start_date : str + Start date + end_date : str + End date + var : str + Variable name + idx : int + Station index + station_name : str + Station name + model : str, optional + Model name, by default None + + Returns + ------- + go.Figure + Modal window with timeseries + """ + + logging.debug('SERVER: OBS TS init for obs %s ... ', str(obs)) + th = EvaluationGroundTimeSeriesHandler(obs, start_date, end_date, var) + logging.debug('SERVER: OBS TS generation ... ') + + return th.retrieve_timeseries(idx, station_name, model) + + +def get_timeseries(model, date, var, lat, lon, forecast=False): + """ Retrieve timeseries in the forecast models maps + + Parameters + ---------- + model : str + Model name + date : str + Selected date + var : str + Variable name + lat : float + Latitude + lon : float + Longitude + forecast : bool, optional + Indicates if we are using forecast data, by default False + + Returns + ------- + go.Figure + Modal window with timeseries + """ + + logging.debug('SERVER: TS init for models %s ... ', str(model)) + th = ForecastModelsTimeSeriesHandler(model, date, var) + logging.debug('SERVER: TS generation ... ') + + return th.retrieve_timeseries(lat, lon, method='feather', forecast=forecast) + + +def get_single_point(model, date, tstep, var, lat, lon): + """ Retrive data on single point + + Parameters + ---------- + model : str + Model name + date : str + Selected date + tstep : int + Timestep + var : str + Variable name + lat : float + Latitude + lon : float + Longitude + + Returns + ------- + float + Variable value at closest available point from (lat, lon) + """ + + logging.debug('SERVER: SINGLE POINT init for models %s ... ',str(model)) + th = ForecastModelsTimeSeriesHandler(model, date, var) + logging.debug('SERVER: SINGLE POINT generation ... ') + + return th.retrieve_single_point(tstep, lat, lon) + + +def get_evaluation_comparison_aeronet_figure(start_date, end_date, obs, var): + """ Retrieve evaluation visual comparison data and figure layers (ground observations from AERONET) + + Parameters + ---------- + start_date : str + Start date + end_date : str + End date + obs : str + Observations name + var : str + Variable name + + Returns + ------- + pandas.core.frame.DataFrame + Dataframe with stations information + list + Figure layers + """ + + fh = EvaluationGroundFigureHandler(start_date, end_date, obs, var) + + return fh.get_figure_layers() + + +def get_evaluation_comparison_modis_figure(var, obs, tstep=0, selected_date=END_DATE): + """ Retrieve evaluation visual comparison figure layers (satellite observations from MODIS) + + Parameters + ---------- + var : str + Variable name + obs : str + Observations name + tstep : int, optional + Timestep, by default 0 + selected_date : _type_, optional + Selected date, by default END_DATE + + Returns + ------- + list + Figure layers + """ + + fh = EvaluationSatelliteFigureHandler(var, obs, tstep, selected_date) + + return fh.get_figure_layers() + + +def get_evaluation_statistics_figure(network=None, model=None, statistic=None, selection=None): + """ Retrieve evaluation statistics figure layers + + Parameters + ---------- + network : str, optional + Network name, by default None + model : str, optional + Model name, by default None + statistic : str, optional + Statistic name, by default None + model : str, optional + Model name, by default None + selection : str, optional + Period selection, by default None + + Returns + ------- + list + Figure layers + """ + + logging.debug('SERVER: SCORES Figure init ... ') + fh = EvaluationStatisticsFigureHandler(network, statistic, model, selection=selection) + + if network and model and statistic: + logging.debug('SERVER: SCORES Figure generation ... ') + else: + logging.debug('SERVER: NO SCORES Figure') + + return fh.get_figure_layers() + + +def get_currdate_tstep(model_start, model_start_before, current_time_before, delay, selected_date, + tstep=4): + """ Returns current date and timestep + + Parameters + ---------- + model_start : int + Model start hour + model_start_before : int + Model start hour before run + current_time_before : _type_ + Current time before run + delay : bool + Indicates if there is a delay + selected_date : str + Selected date + tstep : int, optional + Timestep, by default 4 + + Returns + ------- + datetime.datetime + Selected date + int + Timestep + str + Timesteps + """ + + # MODELS starting at 00 with 3 days of forecast + logging.debug("Starting at 0 and NOT DELAYED: 3 days forecast!") + cdo_tsteps = "1/25" + delayed = (current_time_before and not delay) or (not current_time_before and delay) + + # MODELS not starting at 00 + if (model_start == 12 and not model_start_before) or \ + (current_time_before and model_start_before == 12): + if delayed: + logging.debug("Starting at 12 and DELAYED: 2 days forecast!") + # DELAYED (2 days): models that starts at 12h considering end_date = current_date - 1 + if tstep < 4: + selected_date = (datetime.strptime(selected_date, "%Y%m%d") - + timedelta(days=1)).strftime("%Y%m%d") + tstep = int(tstep) + 4 + else: + tstep = int(tstep) - 4 + cdo_tsteps = "1/21" + else: + logging.debug("Starting at 12 and NOT DELAYED: 3 days forecast!") + # NOT DELAYED (3 days): models that starts at 12h considering end_date = current_date + selected_date = (datetime.strptime(selected_date, "%Y%m%d") - + timedelta(days=1)).strftime("%Y%m%d") + tstep = int(tstep) + 4 + cdo_tsteps = "5/29" + # MODELS starting at 00 with 2 days of forecast + elif delayed: + logging.debug("Starting at 0 and DELAYED: 2 days forecast!") + cdo_tsteps = "5/25" + + return selected_date, tstep, cdo_tsteps + + +def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, aspect=(1, 1)): + """ Retrieve forecast models figure layers + + Parameters + ---------- + var : str + Variable name + model : str + Model name + tstep : int, optional + Timestep, by default 0 + hour : int, optional + Hour, by default None + selected_date : str, optional + Selected date, by default END_DATE + aspect : tuple, optional + Map aspect, by default (1,1) + + Returns + ------- + list + Figure layers + """ + + logging.debug("*** %s %s %s %s %s *****", model, var, selected_date, tstep, hour) + + try: + selected_date = datetime.strptime( + selected_date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + + if model in MODELS: + model_start = MODELS[model]['start'] + model_start_before = ('start_before' in MODELS[model]) and MODELS[model]['start_before'] or False + current_time_before = (DELAY_DATE and datetime.strptime(selected_date, "%Y%m%d") < datetime.strptime(DELAY_DATE, "%Y%m%d")) + logging.debug(f""" + ***************************** SERVER: Figure init ******************************* + * DELAY: {DELAY}, DELAY_DATE: {DELAY_DATE}, SELECTED DATE: {selected_date}, CURR_BEFORE: {current_time_before} * + * MODEL: {model}, MODEL_START: {model_start}, MODEL_START_DELAYED: {model_start_before} * + ********************************************************************************* + """) + # If current date is later than the delay date and the model_start is 12 + # or current date is before than the delay date and the model_start + selected_date, tstep, _ = get_currdate_tstep(model_start, model_start_before, current_time_before, DELAY, selected_date, tstep) + + logging.debug(' %s %s %s %s %s', var, model, tstep, hour, selected_date) + if var: + logging.debug('SERVER: MODELS Figure init ... ') + fh = ForecastModelsFigureHandler(var=var, model=model, tstep=tstep, hour=hour, selected_date=selected_date) + logging.debug('SERVER: MODELS Figure generation ... ') + return fh.get_figure_layers(tstep=tstep, hour=hour, aspect=aspect) + + logging.debug('SERVER: NO MODELS Figure') + + return ForecastModelsFigureHandler().get_figure_layers() + + +def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): + """ Retrieve probability figure layers + + Parameters + ---------- + var : str, optional + Variable name, by default None + prob : float, optional + Probability threshold, by default None + day : int, optional + Day, by default 0 + selected_date : str, optional + Selected date, by default END_DATE + + Returns + ------- + list + Figure layers + """ + + logging.debug(' %s %s %s',prob, day, selected_date) + try: + selected_date = datetime.strptime( + selected_date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + logging.debug(' %s %s %s', prob, day, selected_date) + + if prob: + logging.debug('SERVER: PROB Figure init ... ') + fh = ForecastProbFigureHandler(var=var, prob=prob, selected_date=selected_date) + logging.debug('SERVER: PROB Figure generation ... ') + return fh.get_figure_layers(day=day) + + logging.debug('SERVER: NO PROB Figure') + + return ForecastProbFigureHandler().get_figure_layers() + + +def get_was_figure(was=None, day=0, selected_date=END_DATE): + """ Retrieve WAS figure layers + + Parameters + ---------- + was : str, optional + Region name, by default None + day : int, optional + Day, by default 0 + selected_date : str, optional + Selected date, by default END_DATE + + Returns + ------- + list + Figure layers + """ + + logging.debug(' %s %s %s', was, day, selected_date) + try: + selected_date = datetime.strptime( + selected_date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + logging.debug(' %s %s %s', was, day, selected_date) + + if was: + logging.debug('SERVER: WAS Figure init ... ') + fh = ForecastWasFigureHandler(was=was, selected_date=selected_date) + logging.debug('SERVER: WAS Figure generation ... ') + return fh.get_figure_layers(day=day) + logging.debug('SERVER: NO WAS Figure') + + return ForecastWasFigureHandler().get_figure_layers() + + +def get_vis_figure(tstep=0, selected_date=END_DATE): + """ Retrieve visibility figure layers + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + selected_date : str, optional + Selected date, by default END_DATE + + Returns + ------- + list + Figure layers + """ + + logging.debug(' %s %s', tstep, selected_date) + try: + selected_date = datetime.strptime( + selected_date, "%Y-%m-%d").strftime("%Y%m%d") + except: + pass + + logging.debug(' %s %s', tstep, selected_date) + + if tstep is not None: + logging.debug('SERVER: VIS Figure init ... ') + fh = VisFigureHandler(selected_date=selected_date) + logging.debug('SERVER: VIS Figure generation ... ') + return fh.get_figure_layers(tstep=tstep) + logging.debug('SERVER: NO VIS Figure') + + return VisFigureHandler().get_figure_layers() + + +def get_figure(selected_date=END_DATE, tstep=0, aspect=(1, 1), center=None, + selected_tiles='carto-positron', zoom=None, layers=None, index=None, tag='empty'): + """ Generate map with figure layers + + Parameters + ---------- + selected_date : str, optional + Selected date, by default END_DATE + tstep : int, optional + Timestep, by default 0 + aspect : tuple, optional + Map aspect, by default (1, 1) + center : list, optional + Map center, by default None + selected_tiles : str, optional + Map tiles, by default 'carto-positron' + zoom : float, optional + Map zoom, by default None + layers : list, optional + Figure layers, by default None + index : str, optional + Map index, by default None + tag : str, optional + Map tag, by default 'empty' + + Returns + ------- + fig : dash_leaflet.Map + Figure + """ + + logging.debug('***** SERVER: Figure generation: CURR_DATE %s TSTEP %s *****', selected_date, tstep) + + fh = MapHandler() + return fh.retrieve_fig(aspect=aspect, center=center, selected_tiles= selected_tiles, + zoom=zoom, tag=tag, index=index, layers=layers) diff --git a/conf/cache.json b/conf/cache.json new file mode 100644 index 0000000000000000000000000000000000000000..8f239237b02b20c66d8852ee605087e96edd6e08 --- /dev/null +++ b/conf/cache.json @@ -0,0 +1,5 @@ +{ + "config": {"DEBUG": true, + "CACHE_TYPE": "FileSystemCache"}, + "timeout": 86400 +} \ No newline at end of file diff --git a/conf/colorbars.json b/conf/colorbars.json new file mode 100644 index 0000000000000000000000000000000000000000..c6ca2daec0abe78e62514fa3962f85f271c74133 --- /dev/null +++ b/conf/colorbars.json @@ -0,0 +1,21 @@ +{ + "model": {"position": "topleft", + "width": 270, + "height": 15, + "style": {"top": "55px"} + }, + "prob": {"position": "topleft", + "width": 330, + "height": 8, + "style": {"top": "65px", + "overflow": "hidden", + "white-space": "nowrap"} + }, + "scores": {"position": "topleft", + "width": 330, + "height": 8, + "style": {"top": "65px", + "overflow": "hidden", + "white-space": "nowrap"} + } +} \ No newline at end of file diff --git a/conf/coords.json b/conf/coords.json index 36ab8f322ea447d587fa59cbf371fbffb1223b1e..73e0c6ebcf54f2ef386ec4272bce1d6a09b0bf82 100644 --- a/conf/coords.json +++ b/conf/coords.json @@ -35,8 +35,8 @@ "lon": "16.5", "width": "1180", "height": "720", - "paddingRight": "35px", - "marginTop": "20px", + "paddingRight": "55px", + "marginTop": "5px", "logos": true } } diff --git a/conf/create_loop_dev.json b/conf/create_loop_dev.json new file mode 100644 index 0000000000000000000000000000000000000000..1efe1e344f53793a2e55b988a8aeaee63aabdfb6 --- /dev/null +++ b/conf/create_loop_dev.json @@ -0,0 +1,4 @@ +{ + "url": "http://127.0.0.1:9000/dashboard/", + "targetFolder": "./js/tmp" +} diff --git a/conf/create_loop_prod.json b/conf/create_loop_prod.json new file mode 100644 index 0000000000000000000000000000000000000000..ec89cfa821e903e26934aed63c1275cc628cca4c --- /dev/null +++ b/conf/create_loop_prod.json @@ -0,0 +1,4 @@ +{ + "url": "http://127.0.0.1:9000/dashboard/", + "targetFolder": "./tmp" +} diff --git a/conf/create_loop_selectors.json b/conf/create_loop_selectors.json new file mode 100644 index 0000000000000000000000000000000000000000..5f95b93e0cd79b53ae563da4034b718c1e5059e2 --- /dev/null +++ b/conf/create_loop_selectors.json @@ -0,0 +1,20 @@ +{ + "graphs":"#graph-collection", + "varDropdown": "#variable-dropdown-forecast", + "selectMenu": ".Select-menu-outer", + "selectedVar": ".VirtualizedSelectOption", + "selectedModels": ".custom-control-input", + "modelsApply": "#models-apply", + "zoomLevel": "#country-zoom", + "lat": "#country-lat", + "lon": "#country-lon", + "focus": "#country-focus", + "sliderDot":"span.rc-slider-dot", + "alertForecast": "#alert-forecast", + "leafletContainer": ".leaflet-container", + "navTimebar": ".navbar-timebar", + "logos": "#logos", + "leafletBar": ".leaflet-bar", + "disclaimer": ".disclaimer" +} + diff --git a/conf/create_loop_setup.json b/conf/create_loop_setup.json new file mode 100644 index 0000000000000000000000000000000000000000..0e799b997e1750a3aa98a9f418eac57ee7331f15 --- /dev/null +++ b/conf/create_loop_setup.json @@ -0,0 +1,10 @@ +{ + "specialFit": "monarch_fit", + "defaultFit": "default", + "tstepCount": 25, + "graphHeight": "93vh", + "disclaimerRight": "1px", + "makeInvisible": "none", + "makeVisible": "block", + "logoDisplay": "inline" +} diff --git a/conf/dash_style.json b/conf/dash_style.json new file mode 100644 index 0000000000000000000000000000000000000000..c3bf7024ecfe6e6b88791323514c1ed52a667f78 --- /dev/null +++ b/conf/dash_style.json @@ -0,0 +1,4 @@ +{ + "graph_height": 90, + "opacity": 0.7 +} \ No newline at end of file diff --git a/conf/disclaimers.json b/conf/disclaimers.json new file mode 100644 index 0000000000000000000000000000000000000000..11fbcf8f571adf0c16686281fe3305cded34a9dc --- /dev/null +++ b/conf/disclaimers.json @@ -0,0 +1,4 @@ +{ + "models": "Dust data ©2023 WMO Barcelona Dust Regional Center.", + "observations": "Aerosol data ©2023 WMO Barcelona Dust Regional Center, NASA." +} \ No newline at end of file diff --git a/conf/info_style.json b/conf/info_style.json new file mode 100644 index 0000000000000000000000000000000000000000..31b568b12a51330f878b148e6eddc6d2dd78d038 --- /dev/null +++ b/conf/info_style.json @@ -0,0 +1,9 @@ +{ + "position": "absolute", + "top": "10px", + "left": "10px", + "zIndex": "1000", + "fontFamily": "'Roboto', sans-serif", + "fontSize": "14px", + "fontWeight": "bold" +} \ No newline at end of file diff --git a/conf/init.json b/conf/init.json new file mode 100644 index 0000000000000000000000000000000000000000..ec57457df3ca86bdfaee220f7174104417691bd4 --- /dev/null +++ b/conf/init.json @@ -0,0 +1,7 @@ +{ + "frequency": 3, + "forecast_max": 72, + "forecast_final_day": 3, + "default_variable": "OD550_DUST", + "default_model": "median" +} \ No newline at end of file diff --git a/conf/modebars.json b/conf/modebars.json index c6b7048a5a775e14e5bc037395c5fc9771a32a01..22a06e662723423ee70e84cff975bfa61a195e86 100644 --- a/conf/modebars.json +++ b/conf/modebars.json @@ -35,14 +35,5 @@ "bgcolor": "rgba(0,0,0,0)", "activecolor": "#A2B0B6" } - }, - "info_style": { - "position": "absolute", - "top": "10px", - "left": "10px", - "zIndex": "1000", - "fontFamily": "'Roboto', sans-serif", - "fontSize": "14px", - "fontWeight": "bold" } } diff --git a/conf/obs.json b/conf/obs.json index c96ef81289eee90079bc0afc3884b413777c674d..4e6a3fc246396673671a00b8389b83dd42ab22f7 100644 --- a/conf/obs.json +++ b/conf/obs.json @@ -1,22 +1,28 @@ { - "aeronet": { - "obs_var": "od550aero", - "flt_var": "ae440-870aero", - "flt_expr": "<0.6", - "mod_var": "OD550_DUST", - "template" : "{}_{}", - "sites": "aeronet_sites.txt", - "path" : "/data/daily_dashboard/obs/aeronet/", - "name": "Aeronet v3 lev1.5", - "start_date": "20120101" - }, - "modis": { - "obs_var": "od550aero", - "mod_var": "OD550_DUST", - "template" : "{}_{}", - "path" : "/data/daily_dashboard/obs/modis/", - "name": "MODIS", - "title": "Optical Depth (550nm)
Valid: %(shour)sh %(sday)s %(smonth)s %(syear)s", - "start_date": "20180101" - } + "aeronet": {"obs_var": "od550aero", + "flt_var": "ae440-870aero", + "flt_expr": "<0.6", + "mod_var": "OD550_DUST", + "template" : "{}_{}", + "sites": "aeronet_sites.txt", + "path" : "/data/daily_dashboard/obs/aeronet/", + "name": "Aeronet v3 lev1.5", + "start_date": "20120101", + "circle_options": {"fillOpacity": 0.7, + "stroke": false, + "fillColor": "#f0b450", + "radius": 8} + }, + "modis": {"obs_var": "od550aero", + "mod_var": "OD550_DUST", + "template" : "{}_{}", + "path" : "/data/daily_dashboard/obs/modis/", + "name": "MODIS", + "title": "Optical Depth (550nm)
Valid: %(shour)sh %(sday)s %(smonth)s %(syear)s", + "start_date": "20180101", + "circle_options": {"fillOpacity": 0.7, + "stroke": false, + "fillColor": "#f0b450", + "radius": 8} + } } diff --git a/conf/paths.json b/conf/paths.json new file mode 100644 index 0000000000000000000000000000000000000000..95b7628dfd6e35d16c85e335656c90ad3745e7d2 --- /dev/null +++ b/conf/paths.json @@ -0,0 +1,5 @@ +{ + "user_guide": "/products/overview/user-guide/@@download", + "netcdf": "/products/data-download", + "gif_link" : "assets/comparison/{model}/{variable}/{year}/{month}/{curdate}_{model}_{tstep}.{ext}" +} diff --git a/conf/prob.json b/conf/prob.json index 81c568759a241c8e5ca398f3ab1a9cefc3aedf6c..6e9ffe20fadaf12d88ca9ef7dfedc1bd6d902cad 100644 --- a/conf/prob.json +++ b/conf/prob.json @@ -1,20 +1,20 @@ { "SCONC_DUST": { - "prob_thresh": ["50", "100", "200", "500"], + "prob_thresh": ["50", "100", "200", "500"], "netcdf_path": "/data/daily_dashboard/prob/sconc_dust/{prob}/netcdf/{date}", "geojson_path": "/data/daily_dashboard/prob/sconc_dust/{prob}/geojson/{date}", - "netcdf_template": "{date}_{var}_Prob{prob}.nc", - "geojson_template": "{{step:02d}}_{date}_{var}.geojson", - "units": "µg/m³", - "title": "Daily Mean of Dust Surface Concentration
Probability of exceeding {prob}µg/m³
ENS members: {members} Run: {rday} {rmonth} {ryear} Valid: {sday} {smonth} {syear}" + "netcdf_template": "{date}_{var}_Prob{prob}.nc", + "geojson_template": "{{step:02d}}_{date}_{var}.geojson", + "units": "µg/m³", + "title": "Daily Mean of Dust Surface Concentration
Probability of exceeding %(prob)sµg/m³
ENS members: %(members)s Run: %(rday)s %(rmonth)s %(ryear)s Valid: %(sday)s %(smonth)s %(syear)s" }, "OD550_DUST": { - "prob_thresh": ["0.1", "0.2", "0.5", "0.8"], + "prob_thresh": ["0.1", "0.2", "0.5", "0.8"], "netcdf_path": "/data/daily_dashboard/prob/od550_dust/{prob}/netcdf/{date}", "geojson_path": "/data/daily_dashboard/prob/od550_dust/{prob}/geojson/{date}", - "netcdf_template": "{date}_{var}_Prob{prob}.nc", - "geojson_template": "{{step:02d}}_{date}_{var}.geojson", - "units": "", - "title": "Daily Mean of Dust AOD
Probability of exceeding {prob}
ENS members: {members} Run: {rday} {rmonth} {ryear} Valid: {sday} {smonth} {syear}" - } + "netcdf_template": "{date}_{var}_Prob{prob}.nc", + "geojson_template": "{{step:02d}}_{date}_{var}.geojson", + "units": "", + "title": "Daily Mean of Dust AOD
Probability of exceeding %(prob)s
ENS members: %(members)s Run: %(rday)s %(rmonth)s %(ryear)s Valid: %(sday)s %(smonth)s %(syear)s" + } } diff --git a/conf/render404.json b/conf/render404.json new file mode 100644 index 0000000000000000000000000000000000000000..769f95f91b50bc3cb2f34ef5de33c27966617b5a --- /dev/null +++ b/conf/render404.json @@ -0,0 +1,16 @@ +{ + "forecast": {"name": "Forecast", + "id": "forecast_link", + "path_prod": "https://dust.aemet.es/products/daily-dust-products/?tab=forecast", + "path_dev": "/?tab=forecast"}, + + "evaluation": {"name": "Evaluation", + "id": "evaluation_link", + "path_prod": "https://dust.aemet.es/products/daily-dust-products/?tab=evaluation", + "path_dev": "/?tab=evaluation"}, + + "observations": {"name": "Observations", + "id": "observations_link", + "path_prod": "https://dust.aemet.es/products/daily-dust-products/?tab=observations", + "path_dev": "/?tab=observations"} +} diff --git a/conf/route.json b/conf/route.json new file mode 100644 index 0000000000000000000000000000000000000000..ca270a940c848aced2f21d7e323c6dbd7cc5f5b9 --- /dev/null +++ b/conf/route.json @@ -0,0 +1,10 @@ +{ + "tab": ["forecast"], + "var": ["OD550_DUST"], + "model": ["median"], + "for_option": ["models"], + "eval_option": ["nrt"], + "obs_option": ["rgb"], + "country": ["burkinafaso"], + "download": [null] +} \ No newline at end of file diff --git a/conf/satellite_image_src.json b/conf/satellite_image_src.json new file mode 100644 index 0000000000000000000000000000000000000000..29c6eea84ef9bc11ad78e85bd6953f729fbaa55e --- /dev/null +++ b/conf/satellite_image_src.json @@ -0,0 +1,6 @@ +{ + "middle_east": "eumetsat/MiddleEast/archive/{date}/MET8_RGBDust_MiddleEast_{date}{tstep:02d}00.gif", + "fulldisc": "eumetsat/FullDiscHD/archive/{date}/FRAME_OIS_RGB-dust-all_{date}{tstep:02d}00.gif", + "fulldisc_obs": "./assets/eumetsat/FullDiscHD/archive/{date}/FRAME_OIS_RGB-dust-all_{date}{tstep:02d}00.gif", + "aod_image":"./assets/metoffice/{date}/MSG_{date}{tstep:02}00_AOD_444x278.gif" +} diff --git a/conf/scores.json b/conf/scores.json new file mode 100644 index 0000000000000000000000000000000000000000..b7abb785602a9055e499e927ce7ecc3c9fc16de1 --- /dev/null +++ b/conf/scores.json @@ -0,0 +1,6 @@ +{ + "extent": [-27, 60, 0, 67], + "title": "%(model_name)s - %(network_name)s - %(statistic_name)s
%(rmonth)s %(ryear)s", + "circle_options": {"stroke": false, + "fillOpacity": 0.7} +} \ No newline at end of file diff --git a/conf/vis.json b/conf/vis.json new file mode 100644 index 0000000000000000000000000000000000000000..4ce65db776c83b699bd9a661c40c6c4ae41f8a32 --- /dev/null +++ b/conf/vis.json @@ -0,0 +1,20 @@ +{ + "path": "/data/daily_dashboard/obs/visibility/{year}/{month}/{year}{month}{day}{tstep0:02d}{tstep1:02d}_visibility.csv", + "freq": 6, + "xlon": [-25, 60], + "ylat": [0, 65], + "ec": "none", + "size": 80, + "colormap": { + "< 1 km": "#714921", + "1 - 2 km": "#da7230", + "2 - 5 km": "#fcd775", + "Haze < 5 km": "CadetBlue" + }, + "values": [0, 1, 2, 3], + "markers": ["o", "o", "o", "^"], + "title": "Visibility reduced by airborne dust
%(rday)s %(rmonth)s %(ryear)s %(tstep0)s-%(tstep1)s UTC", + "circle_options": {"radius": 8, + "stroke": false, + "fillOpacity": 0.7} +} \ No newline at end of file diff --git a/conf/was_prod.json b/conf/was_prod.json index 059b9f02dfa63bb69e479b5ea99121d3ba88e7d7..5bba7785695e85e666e9e82e48d39528eb2f2bc2 100644 --- a/conf/was_prod.json +++ b/conf/was_prod.json @@ -10,11 +10,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "Boucle du Mouhoun" : [328, 411, 533], @@ -31,7 +31,9 @@ "Sahel" : [473, 596, 766], "Sud-Ouest" : [258, 320, 392] }, - "title": "Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "title": "Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} }, "senegal": { @@ -44,11 +46,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "Diourbel" : [365, 464, 655], @@ -68,7 +70,9 @@ "correspondence": { "Dakar": "Thies" }, - "title": "Barcelona Dust Regional Center - Senegal WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "title": "Barcelona Dust Regional Center - Senegal WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} }, "chad": { @@ -81,11 +85,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "BATHA" : [1348, 1716, 2152], @@ -114,7 +118,9 @@ "correspondence": { "NDJAMENA": "HADJER LAMIS" }, - "title": "Barcelona Dust Regional Center - Chad WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "title": "Barcelona Dust Regional Center - Chad WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} }, "mali": { @@ -127,11 +133,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "Gao" : [871, 1100, 1603], @@ -147,7 +153,9 @@ "Bamako": "Koulikoro", "Menaka": "Kidal" }, - "title": "Barcelona Dust Regional Center - Mali WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "title": "Barcelona Dust Regional Center - Mali WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} }, "niger": { @@ -160,11 +168,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "AGADEZ" : [1346, 1607, 2014], @@ -178,7 +186,9 @@ "correspondence": { "NIAMEY": "TILLABERI" }, - "title": "Barcelona Dust Regional Center - Niger WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "title": "Barcelona Dust Regional Center - Niger WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} }, "cabo_verde": { @@ -191,11 +201,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "CaboVerdeBarlavento" : [137, 196, 345], @@ -213,8 +223,10 @@ "SANTA LUZIA" : "CaboVerdeBarlavento", "SAO NICOLAU" : "CaboVerdeBarlavento" }, - "exclude" : [ "CaboVerdeBarlavento", "CaboVerdeSotavento"], - "title": "Barcelona Dust Regional Center - Cape Verde WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "exclude" : [ "CaboVerdeBarlavento", "CaboVerdeSotavento"], + "title": "Barcelona Dust Regional Center - Cape Verde WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} }, "mauritania": { @@ -227,11 +239,11 @@ "var": "SCONC_DUST", "path": "/data/daily_dashboard/was/{was}/{format}/{date}", "template": "{date}_{var}.{format}", - "colors": { - "green" : "Normal", - "gold" : "High", - "darkorange": "Very High", - "red" : "Extremely High" + "colormap": { + "Normal" : "green", + "High" : "gold", + "Very High" : "darkorange", + "Extremely High" : "red" }, "values": { "Adrar" : [742, 921, 1213], @@ -248,6 +260,8 @@ "Trarza" : [945, 1160, 1406], "Tris-Zemmour" : [952, 1189, 1579] }, - "title": "Barcelona Dust Regional Center - Mauritania WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s" + "title": "Barcelona Dust Regional Center - Mauritania WAS.
Expected concentration of airborne dust.
Issued: %(rday)s %(rmonth)s %(ryear)s. Valid: %(sday)s %(smonth)s %(syear)s", + "hideout_style": {"weight": 1, "opacity": 1, "color": "white", "dashArray": "3"}, + "hover_style": {"weight": 2, "color": "white", "dashArray": ""} } } diff --git a/conf/styles.json b/conf_ines_core/map_layers.json similarity index 78% rename from conf/styles.json rename to conf_ines_core/map_layers.json index c770f3cafc9015b91da118e44c91622f3b570cc6..6a1ce10a9643dcaf194a8605896e9b97ecfff0b5 100644 --- a/conf/styles.json +++ b/conf_ines_core/map_layers.json @@ -2,12 +2,12 @@ "carto-positron": { "name": "Light", "url": "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png", - "attribution": "© OpenStreetMap contributors © CARTO" + "attribution": "© OpenStreetMap contributors © CARTO" }, "open-street-map": { "name": "Open street map", "url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - "attribution": "© OpenStreetMap contributors" + "attribution": "© OpenStreetMap contributors" }, "stamen-terrain": { "name": "Terrain", diff --git a/dash_server.py b/dash_server.py index 33f7b3bcd67018aed6b336589e5aace3a3a334bb..0d3d3ef2fd41d819aca3b107c9ce492a7012f9be 100755 --- a/dash_server.py +++ b/dash_server.py @@ -6,24 +6,29 @@ from dash import dcc from dash import html from flask.app import Flask -from data_handler import DEBUG -from data_handler import cache -from data_handler import PATHNAME -from data_handler import HOSTNAME +from map_handler import DASH_LOG_LEVEL +from map_handler import cache +from map_handler import PATHNAME +from map_handler import HOSTNAME from router import * +import logging +logging.basicConfig(level=DASH_LOG_LEVEL) + fontawesome = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css' leaflet = "https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.2.0/leaflet.css" +html2canvas = "https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js" +jQuery = 'https://code.jquery.com/jquery-3.6.0.slim.min.js' srv = Flask(__name__) app = dash.Dash(__name__, - external_scripts=['https://code.jquery.com/jquery-3.6.0.slim.min.js'], + external_scripts=[jQuery, html2canvas], external_stylesheets=[dbc.themes.BOOTSTRAP, dbc.themes.GRID, fontawesome, - leaflet + leaflet, ], url_base_pathname=PATHNAME, server=srv, @@ -40,7 +45,7 @@ cache.init_app(server) try: cache.clear() except Exception as e: - print('CACHE CLEAR ERROR:', str(e)) + logging.error('CACHE CLEAR ERROR:', str(e)) pass app.index_string = """ @@ -86,16 +91,17 @@ app.index_string = """ """ -if DEBUG: print('SERVER: start creating app layout') +logging.debug('SERVER: start creating app layout') #SET APP LAYOUT TO BE POPULATED app.layout = html.Div([dcc.Location(id="url", refresh=False)], id="content") -if DEBUG: print('SERVER: stop creating app layout') +logging.debug('SERVER: stop creating app layout') from tabs.forecast_callbacks import * from tabs.evaluation_callbacks import * from tabs.observations_callbacks import * +from tabs.generic_callbacks import * if __name__ == '__main__': - app.run_server(debug=True, host=HOSTNAME, port=7777) + app.run_server(debug=True, host=HOSTNAME, port=7778) diff --git a/data_handler.py b/data_handler.py deleted file mode 100644 index 0586241132f28bf77017e15be8f1fead8aac7a80..0000000000000000000000000000000000000000 --- a/data_handler.py +++ /dev/null @@ -1,1787 +0,0 @@ -# -*- coding: utf-8 -*- -""" Data Handler """ - -import plotly.graph_objs as go -from dash import html -import dash_leaflet as dl -import dash_leaflet.express as dlx -from dash_extensions.javascript import arrow_function -from dash_extensions.javascript import Namespace -from matplotlib.colors import ListedColormap -import numpy as np -from netCDF4 import Dataset as nc_file -import pandas as pd -import json -import orjson -from datetime import datetime -from dateutil.relativedelta import relativedelta -from collections import OrderedDict -import calendar -import time -import os -from datetime import datetime as dt -from datetime import timedelta - -from utils import concat_dataframes -from utils import retrieve_timeseries -from utils import retrieve_single_point -from utils import get_colorscale - -from pathlib import Path -from flask_caching import Cache -import uuid -import socket - -#SETUP CACHE -cache_dir = "/dev/shm/{}".format(str(uuid.uuid1())) -Path(cache_dir).mkdir(parents=True, exist_ok=True) - -cache_config = { - "DEBUG": True, - "CACHE_TYPE": "FileSystemCache", - "CACHE_DIR": cache_dir, -} - -cache = Cache(config=cache_config) -cache_timeout = 86400 - -#SETUP BASE URL -HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0] - -DIR_PATH = os.path.dirname(os.path.realpath(__file__)) - -DEBUG = True # False - -HOSTNAMES = json.load(open(os.path.join(DIR_PATH, 'conf/hostnames.json'))) -VARS = json.load(open(os.path.join(DIR_PATH, 'conf/vars.json'))) -MODELS = json.load(open(os.path.join(DIR_PATH, 'conf/models.json'))) -OBS = json.load(open(os.path.join(DIR_PATH, 'conf/obs.json'))) -WAS = json.load(open(os.path.join(DIR_PATH, 'conf/was.json'))) -PROB = json.load(open(os.path.join(DIR_PATH, 'conf/prob.json'))) -DATES = json.load(open(os.path.join(DIR_PATH, 'conf/dates.json'))) -ALIASES = json.load(open(os.path.join(DIR_PATH, 'conf/aliases.json'))) -STATS_CONF = json.load(open(os.path.join(DIR_PATH, 'conf/stats.json'))) -STYLES = json.load(open(os.path.join(DIR_PATH, 'conf/styles.json'))) -ALL_COLORS = json.load(open(os.path.join(DIR_PATH, 'conf/colors.json'))) -MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json'))) - -MODEBAR_CONFIG = MODEBARS['config'] -MODEBAR_CONFIG_TS = MODEBARS['config_ts'] -MODEBAR_LAYOUT = MODEBARS['layout'] -MODEBAR_LAYOUT_TS = MODEBARS['layout_ts'] -INFO_STYLE = MODEBARS['info_style'] - -COLORS = ALL_COLORS['std'] -COLORS_NORGB = ALL_COLORS['std_norgb'] -COLORS_PROB = ALL_COLORS['prob'] - -COLORMAP = ListedColormap(COLORS_NORGB) -COLORMAP_PROB = ListedColormap(COLORS_PROB) - -START_DATE = DATES['start_date'] -DELAY = DATES['delay']['delayed'] -DELAY_DATE = DATES['delay']['start_date'] -END_DATE = DATES['end_date'] -END_DATE = END_DATE or (DELAY and (dt.now() - - timedelta(days=1)).strftime("%Y%m%d") or dt.now().strftime("%Y%m%d")) - -ROUTE_DEFAULTS = { - 'tab':['forecast'], - 'var': ['OD550_DUST'], - 'model': ['median'], - 'for_option': ['models'], - 'eval_option': ['nrt'], - 'obs_option': ['rgb'], - 'country': ['burkinafaso'], - 'download': [None], - 'date': [END_DATE] - } - -STATS = OrderedDict([(key, STATS_CONF[key]['name']) for key in STATS_CONF]) -STATS.update({ 'totn': 'TOTAL CASES' }) - -GRAPH_HEIGHT = 90 - -# Frequency = 3 Hourly -FREQ = 3 -OPACITY = 0.7 - -DEFAULT_VAR = 'OD550_DUST' -DEFAULT_MODEL = 'median' - -DISCLAIMER_NO_FORECAST = [html.Span(html.P("""Dust data ©2023 WMO Barcelona Dust Regional Center."""), id='forecast-disclaimer')] - -DISCLAIMER_MODELS = [html.Span(html.P("""Dust data ©2023 WMO Barcelona Dust Regional Center."""), id='forecast-disclaimer')] - -DISCLAIMER_OBS = html.P("""Aerosol data ©2023 WMO Barcelona Dust Regional Center, NASA.""") - -GEOJSON_TEMPLATE = "{}/geojson/{}/{:02d}_{}_{}.geojson" -NETCDF_TEMPLATE = "{}/netcdf/{}{}.nc" - -if HOSTNAME in HOSTNAMES: - PATHNAME = HOSTNAMES[HOSTNAME] -else: - PATHNAME = HOSTNAMES['default'] - - -class Observations1dHandler(object): - """ Class which handles 1D obs data """ - - def __init__(self, sdate, edate, obs): - fday = sdate[:-2] + '01' - lday = edate[:-2] + str(calendar.monthrange(int(edate[:4]), int(edate[4:6]))[1]) - date_range = pd.date_range(fday, lday, freq='M') - months = [d.strftime("%Y%m") for d in date_range.to_pydatetime()] - filepaths = ["{}.nc".format(os.path.join(OBS[obs]['path'], 'netcdf', - OBS[obs]['template'].format(OBS[obs]['obs_var'], month))) for month - in months] - input_files = [nc_file(filepath) for filepath in filepaths] - if 'longitude' in input_files[0].variables: - lon_var = 'longitude' - lat_var = 'latitude' - else: - lon_var = 'lon' - lat_var = 'lat' - - lon = input_files[0].variables[lon_var][:] - lat = input_files[0].variables[lat_var][:] - self.varname = [var for var in input_files[0].variables if var == OBS[obs]['obs_var']][0] - if DEBUG: print('VARNAME', self.varname) - - sites = pd.read_csv(os.path.join('./conf/', - OBS[obs]['sites'])) -# sites = pd.read_table(os.path.join('./conf/', -# OBS[obs]['sites']), delimiter=r"\s+", engine='python') - - idxs, self.station_names = np.array([[idx, st_name[~st_name.mask].tobytes().decode('utf-8')] - for idx, st_name in - enumerate(input_files[0].variables['station_name'][:]) - if st_name[~st_name.mask].tobytes().decode('utf-8').upper() in map(str.upper, sites['SITE'])] - ).T - - if DEBUG: print('IDXS', idxs) - if DEBUG: print('ST_NAMES', self.station_names) - - self.clon = lon[idxs.astype(int)] - self.clat = lat[idxs.astype(int)] - - self.values = { - self.varname: np.concatenate([input_file.variables[self.varname][:, idxs.astype(int)] for input_file in input_files]) - } - - - def generate_obs1d_tstep_trace(self, var): - """ Generate trace to be added to data, per variable and timestep """ - varname = self.varname - val = np.ma.masked_where(self.values[varname]<0., self.values[varname]) - notnan = (np.array([i for i in range(val.shape[1]) if not val[:, i].mask.all()]),) - clon = self.clon[notnan] - clat = self.clat[notnan] - cstations = self.station_names[notnan] - df = pd.DataFrame({ - 'lon': clon.round(2), - 'lat': clat.round(2), - 'stations': cstations - }) # .T, columns=['lon', 'lat', 'station']) - dicts = df.to_dict('rows') - geojson = dlx.dicts_to_geojson(dicts, lon="lon") - - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - ns = Namespace("evaluationTab", "evaluationMaps") - point_to_layer = ns("pointToLayer") - # Create geojson. - return df, dl.GeoJSON(data=geojson, - options=dict( - pointToLayer=point_to_layer, - ), - hideout=dict( - circleOptions=dict( - fillOpacity=OPACITY, - stroke=False, - fillColor='#f0b450', - radius=8), - ) - ) - - -class ObsTimeSeriesHandler(object): - """ Class to handle evaluation time series """ - - def __init__(self, obs, start_date, end_date, variable, models=None): - self.obs = obs - if models is None: - models = list(MODELS.keys()) - self.model = models - self.variable = variable - self.dataframe = [] - if DEBUG: print("ObsTimeSeries", start_date, end_date) - self.date_range = pd.date_range(start_date, end_date, freq='D') - - fname_tpl = os.path.join(OBS[obs]['path'], - 'feather', - '{{}}-{dat}-{{}}_interp.ft') - - months = np.unique([d.strftime("%Y%m") for d in self.date_range.to_pydatetime()]) - - self.date_index = pd.date_range('{}01'.format(months[0]), - '{}01'.format((datetime.strptime(months[-1], '%Y%m') + - relativedelta(days=31)).strftime('%Y%m')), freq=f'{FREQ}H') - - if DEBUG: - print('MONTHS', months) - if DEBUG: - print('DATE_INDEX', self.date_index) - - fname_obs = fname_tpl.format(dat=obs) - notnans, obs_df = concat_dataframes(fname_obs, months, variable, - rename_from=OBS[obs]['obs_var']) - self.dataframe.append(obs_df) - - if 'flt_var' in OBS[obs]: - fname_flt = fname_tpl.format(dat='{}_{}'.format(obs, OBS[obs]['flt_var'])) - _, flt_df = concat_dataframes(fname_flt, months, variable, - rename_from=OBS[obs]['flt_var'], notnans=notnans) - self.dataframe.append(flt_df) - - for mod in self.model: - fname_mod = fname_tpl.format(dat=mod) - _, mod_df = concat_dataframes(fname_mod, months, variable, - rename_from=None, notnans=notnans) - self.dataframe.append(mod_df) - - - def retrieve_timeseries(self, idx, st_name, model): - """ Return traces with timeseries of observations, filtering variable and - all models """ - - #FIXME pop filtering variable for now - self.dataframe.pop(1) - old_indexes = self.dataframe[0]['station'].unique() - new_indexes = np.arange(old_indexes.size) - dict_idx = dict(zip(new_indexes, old_indexes)) - if DEBUG: print("RETRIEVE TS", idx, dict_idx[idx], st_name, model) - fig = go.Figure() - for mod, df in zip([self.obs]+self.model, self.dataframe): - if df is None: - continue - if DEBUG: print("MOD", mod, "COLS", df.columns) - if df.columns[-1].upper() == self.variable: - df = df.rename(columns = { df.columns[-1]: self.variable }) - - if DEBUG: print("BUILDING TIME-SERIES") - try: - tmp_df = df[df['station']==dict_idx[idx]].set_index('time') - tmp_df.drop_duplicates(keep='first') - timeseries = \ - tmp_df.reindex(self.date_index) - if timeseries[self.variable].isnull().all(): - continue - except Exception as e: - if DEBUG: print("ERROR timeseries", str(e)) - continue - - if DEBUG: print("SELECTING COORDS") - if 'lat' in df.columns: - lat_col = 'lat' - lon_col = 'lon' - else: - lat_col = 'latitude' - lon_col = 'longitude' - - if mod == self.obs: - if DEBUG: print("OBSERVATION") - sc_mode = 'markers' - marker = {'size': 10, 'symbol': "triangle-up-dot", 'color': '#f0b450'} - line = {} - visible = True - name = mod.upper() - else: - if DEBUG: print("MODEL", mod) - try: - sc_mode = 'lines' - marker = {} - line = { 'color': MODELS[mod]['color'] } - visible = (mod == model) and True or 'legendonly' - name = "{}".format(MODELS[mod]['name']) - cur_lat = round(timeseries[lat_col].dropna()[0], 2) - cur_lon = round(timeseries[lon_col].dropna()[0], 2) - except Exception as e: - if DEBUG: print("ERROR MODEL", mod, str(e), timeseries[lat_col].dropna()) - continue - - if mod == 'median': - line['dash'] = 'dash' - - if DEBUG: print("ADD TRACE") - fig.add_trace(dict( - type='scatter', - name=name, - x=timeseries.index, - y=timeseries[self.variable].round(2), - mode=sc_mode, - marker=marker, - line=line, - visible=visible, - connectgaps=False - ) - ) - - title = "{} @ {} (lat = {:.2f}, lon = {:.2f})".format( - VARS[self.variable]['name'], st_name, cur_lat, cur_lon, - ) - - fig.update_layout( - title=dict(text=title, x=0.45, y=0.99), - # uirevision=True, - autosize=True, - showlegend=True, - plot_bgcolor='#F9F9F9', - font_size=12, - hovermode="x", # highlight closest point on hover - margin={"r": 10, "t": 35, "l": 10, "b": 10}, - ) - fig.update_xaxes( - range=[self.date_range[0], self.date_range[-1]], - rangeslider_visible=True, - rangeselector=dict( - buttons=list([ - dict(step="all", label="all"), - dict(count=14, label="2w", - step="day", stepmode="backward"), - dict(count=7, # label="1w", - step="day", stepmode="backward"), - ]) - ) - ) - - if DEBUG: print('FIG TYPE', type(fig)) - return fig - - -class TimeSeriesHandler(object): - """ Class to handle forecast time series """ - - def __init__(self, model, date, variable): - if isinstance(model, str): - model = [model] - self.model = model - self.variable = variable - self.fpaths = [] - try: - self.month = datetime.strptime(date, "%Y%m%d").strftime("%Y%m") - self.currdate = datetime.strptime(date, "%Y%m%d").strftime("%Y%m%d") - except: - self.month = datetime.strptime(date, "%Y-%m-%d").strftime("%Y%m") - self.currdate = datetime.strptime(date, "%Y-%m-%d").strftime("%Y%m%d") - - def retrieve_single_point(self, tstep, lat, lon, model=None): - - if not model: - model = self.model[0] - - if DEBUG: print("----------", model) - - method = 'netcdf' - if (MODELS[model]['start'] == 12 and not DELAY and DELAY_DATE and (datetime.strptime(self.currdate, "%Y%m%d") >= datetime.strptime(DELAY_DATE, "%Y%m%d"))) or \ - (MODELS[model]['start'] == 12 and not DELAY and not DELAY_DATE): - mod_date = (datetime.strptime(self.currdate, "%Y%m%d") - - timedelta(days=1)).strftime("%Y%m%d") - else: - mod_date = self.currdate - path_template = '{}{}.nc'.format(mod_date, - MODELS[model]['template'], - self.variable) - - fpath = os.path.join(MODELS[model]['path'], method, path_template) - - return retrieve_single_point(fpath, tstep, lat, lon, - self.variable) - - def retrieve_timeseries(self, lat, lon, model=None, method='netcdf', forecast=False): - - if not model: - model = self.model - - # if DEBUG: print("----------", model) - - obs_eval = model[0] not in MODELS and model[0] in OBS - if obs_eval: - all_models = [model[0]] + list(MODELS.keys()) - else: - all_models = list(MODELS.keys()) - - for mod in all_models: - if obs_eval: - filedir = OBS[model[0]]['path'] - path_tpl = '{}-{}-{}_interp.ft' # 202010-median-OD550_DUST_interp.ft - else: # if mod in MODELS: - filedir = MODELS[mod]['path'] - path_tpl = '{}-{}-{}.ft' # 202010-median-OD550_DUST_interp.ft - - if method == 'feather': - path_template = path_tpl.format(self.month, mod, self.variable) - elif method == 'netcdf': - path_template = '{}*{}.nc'.format(self.month, MODELS[mod]['template'], self.variable) - - if forecast: - method = 'netcdf' - if (MODELS[mod]['start'] == 12 and not DELAY and DELAY_DATE and (datetime.strptime(self.currdate, "%Y%m%d") >= datetime.strptime(DELAY_DATE, "%Y%m%d"))) or \ - (MODELS[mod]['start'] == 12 and not DELAY and not DELAY_DATE): - mod_date = (datetime.strptime(self.currdate, "%Y%m%d") - - timedelta(days=1)).strftime("%Y%m%d") - else: - mod_date = self.currdate - path_template = '{}{}.nc'.format(mod_date, MODELS[mod]['template'], self.variable) - - fpath = os.path.join(filedir, - method, - path_template) - self.fpaths.append(fpath) - - title = "{} @ lat = {} and lon = {}".format( - VARS[self.variable]['name'], round(lat, 2), round(lon, 2) - ) - - mul = VARS[self.variable]['mul'] - - fig = go.Figure() - - for mod, fpath in zip(all_models, self.fpaths): - # print(mod, fpath) - if mod not in MODELS and mod in OBS: - variable = OBS[mod]['obs_var'] - else: - variable = self.variable - - if not os.path.exists(fpath): - if DEBUG: print("NOT retrieving", fpath, "File doesn't exist.") - continue - - if DEBUG: print('Retrieving *** FPATH ***', fpath) - try: - ts_lat, ts_lon, ts_index, ts_values = retrieve_timeseries( - fpath, lat, lon, variable, method=method, forecast=forecast) - except Exception as e: - if DEBUG: print("NOT retrieving", fpath, "ERROR:", str(e)) - continue - - if forecast is True or mod in ('cams', 'ema-regcm4'): - ts = pd.Series(index=ts_index, data=ts_values) - else: - date_index = pd.date_range('{}01'.format(self.month), - '{}01'.format((datetime.strptime(self.month, '%Y%m') + - relativedelta(days=31)).strftime('%Y%m')), freq=f'{FREQ}H') - ts = pd.Series(index=ts_index, data=ts_values).reindex(date_index) - - if isinstance(ts_lat, np.ndarray): - ts_lat = float(ts_lat) - ts_lon = float(ts_lon) - if isinstance(ts.values, np.ndarray): - ts_values = (ts.values*mul).round(2) - else: - ts_values = round((ts.values*mul), 2) - - if obs_eval and mod == model[0]: - sc_mode = 'markers' - marker = {'size': 12, 'symbol': "triangle-up-dot", 'color': '#f0b450'} - line = {} - visible = True - name = "{}".format(mod.upper()) - elif obs_eval: - sc_mode = 'lines' - marker = {} - line = { 'color': MODELS[mod]['color'] } - if mod in model: - visible = True - else: - visible = 'legendonly' - name = "{}".format(MODELS[mod]['name']) - else: - sc_mode = 'lines' - marker = {} - line = { 'color': MODELS[mod]['color'] } - if mod in model: - visible = True - else: - visible = 'legendonly' - name = "{} ({}, {})".format( - MODELS[mod]['name'], round(ts_lat, 2), round(ts_lon, 2)) - - if mod == 'median': - line['dash'] = 'dash' - else: - line['dash'] = 'solid' - - fig.add_trace(dict( - type='scatter', - name=name, - x=ts.index, - y=ts_values, - mode=sc_mode, - marker=marker, - line=line, - visible=visible, - ) - ) - - fig.update_layout( - title=dict(text=title, x=0.45, y=.99), - yaxis=dict(exponentformat="none"), - # uirevision=True, - autosize=True, - showlegend=True, - plot_bgcolor='#F9F9F9', - font_size=12, - # hovermode="closest", # highlight closest point on hover - hovermode="x", # highlight closest point on hover - margin={"r": 10, "t": 35, "l": 10, "b": 10}, - ) - - return fig - - -class FigureHandler(object): - """ Class to manage the figure creation """ - - def __init__(self, model=None, selected_date=None): - """ FigureHandler init """ - - self.st_time = time.time() - if isinstance(model, list): - model = model[0] - - self.model = model - - if self.model not in MODELS and self.model in OBS: - self.filedir = OBS[self.model]['path'] - self.filevars = [OBS[self.model]['obs_var']] - self.confvars = [OBS[self.model]['mod_var']] - filetpl = OBS[self.model]['template'].format(OBS[self.model]['obs_var'], selected_date) + '.nc' - filepath = os.path.join(self.filedir, 'netcdf', filetpl) - - elif self.model in MODELS: - if DEBUG: print("MODEL", model) - self.filedir = MODELS[self.model]['path'] - self.filevars = VARS - self.confvars = None - - filepath = NETCDF_TEMPLATE.format( - self.filedir, - selected_date, - MODELS[self.model]['template'] - ) - else: - self.model = None - self.filedir = None - self.filevars = None - self.confvars = None - filepath = None - self.bounds = None - - if filepath is None or not os.path.exists(filepath): - self.filedir = None - self.filevars = None - self.confvars = None - filepath = None - self.bounds = None - self.varlist = None - self.rdatetime = None - self.tim = [0] - elif filepath is not None: - self.input_file = nc_file(filepath) - if 'lon' in self.input_file.variables: - lon = self.input_file.variables['lon'][:] - lat = self.input_file.variables['lat'][:] - else: - lon = self.input_file.variables['longitude'][:] - lat = self.input_file.variables['latitude'][:] - time_obj = self.input_file.variables['time'] - self.tim = time_obj[:] - tim_units = time_obj.units.split() - if len(tim_units) == 3: - self.what, _, rdate = tim_units - rtime = "00:00" - elif len(tim_units) >= 4: - self.what, _, rdate, rtime = tim_units[:4] - if len(rtime) > 5: - rtime = rtime[:5] - self.rdatetime = datetime.strptime("{} {}".format(rdate, rtime), - "%Y-%m-%d %H:%M") - varlist = [var for var in self.input_file.variables if (var.upper() in self.filevars) or (var in self.filevars)] - self.varlist = varlist - if DEBUG: print('VARLIST', varlist) - self.xlon, self.ylat = np.meshgrid(lon, lat) - - if not self.varlist: - self.varlist = VARS.keys() - - if DEBUG: print('VARLIST', self.varlist, 'CONFVAR', self.confvars) - - if self.confvars is not None: - self.bounds = { - varname.upper(): np.array(VARS[confvar]['bounds']).astype('float32') - for varname, confvar in zip(self.filevars, self.confvars) if (varname.upper() in self.varlist) or (varname in self.varlist) - } - else: - self.bounds = { - varname.upper(): np.array(VARS[varname.upper()]['bounds']).astype('float32') - for varname in self.varlist - } - - self.colormaps = { - varname.upper(): get_colorscale(self.bounds[varname.upper()], COLORMAP) - for varname in self.varlist - } - # print(varlist, self.confvars, self.filevars, self.bounds, self.colormaps) - - if selected_date: - self.selected_date_plain = selected_date - - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") - - if not self.rdatetime: - self.rdatetime = datetime.strptime(selected_date, "%Y%m%d") - self.what = 'hours' - - self.fig = None - if DEBUG: print("FILEPATH", filepath) - if DEBUG: print("FILEDIR", self.filedir) - - def get_center(self, center=None): - """ Returns center of map """ - if center is None and hasattr(self, 'ylat'): - center = [ - (self.ylat.max()-self.ylat.min())/2 + self.ylat.min() + (self.ylat.max()-self.ylat.min())/6, - (self.xlon.max()-self.xlon.min())/2 + self.xlon.min(), - ] - elif center is None: - center = [ 30, 15 ] - - return center - - def set_data(self, varname, tstep=0): - """ Set time dependent data """ - if self.model in OBS: - mul = VARS[OBS[self.model]['mod_var']]['mul'] - else: - mul = VARS[varname]['mul'] - - realvar = [var for var in self.varlist if var.upper()==varname.upper()][0] - # if DEBUG: print("***", mul, realvar, "***") - var = self.input_file.variables[realvar][tstep]*mul - idx = np.where((~var.ravel().mask) & (var.ravel() >= self.bounds[varname.upper()][0])) # !=-9.e+33) - xlon = self.xlon.ravel()[idx] - ylat = self.ylat.ravel()[idx] - var = var.ravel()[idx] - # if DEBUG: print("***", xlon.shape, ylat.shape, var.shape, "***") - return xlon.data, ylat.data, var.data - - def retrieve_cdatetime(self, tstep=0): - """ Retrieve data from NetCDF file """ - tstep = int(tstep) - tim = int(self.tim[tstep]) - if self.what == 'days': - cdatetime = self.rdatetime + relativedelta(days=tim) - elif self.what == 'hours': - cdatetime = self.rdatetime + relativedelta(hours=tim) - elif self.what == 'minutes': - cdatetime = self.rdatetime + relativedelta(minutes=tim) - elif self.what == 'seconds': - cdatetime = self.rdatetime + relativedelta(seconds=tim) - - return cdatetime - - def generate_contour_tstep_trace_leaflet(self, varname, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ - from dash_server import app - - if varname not in VARS and self.model in OBS: - name = VARS[OBS[self.model]['mod_var']]['name'] - else: - name = VARS[varname]['name'] - if self.bounds: - bounds = self.bounds[varname.upper()] - else: - bounds = [0, 1] - if self.model in OBS: - data_path = os.path.basename(OBS[self.model]['path'][:-1]) - else: - data_path = os.path.basename(MODELS[self.model]['path']) - - if DEBUG: print(data_path) - colorscale = COLORS - - geojson_url = app.get_asset_url(os.path.join('geojsons', - GEOJSON_TEMPLATE.format(data_path, - self.selected_date_plain, tstep, self.selected_date_plain, - varname))) - if DEBUG: print("MODEL", self.model, "GEOJSON_URL", geojson_url) - - style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) - - # Create colorbar. - ctg = ["{:d}".format(int(cls)) if cls.as_integer_ratio()[1] == 1 else "{:.1f}".format(cls) for i, cls in enumerate(bounds[1:-1])] - if DEBUG: print("BOUNDS", bounds) - if DEBUG: print("CTG", ctg) - indices = list(range(len(ctg) + 2)) - colorbar = dl.Colorbar( - min=0, max=len(ctg)+1, - classes=indices, - colorscale=colorscale, - tickValues=indices[1:-1], - tickText=ctg, - position='topleft', - width=270, - height=15, - style={ 'top': '55px' } - ) - - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - ns = Namespace("forecastTab", "forecastMaps") - style_handle = ns("styleHandle") - - geojson = dl.GeoJSON( - url=geojson_url, - options=dict(style=style_handle), - hideout=dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") - ) # url to geojson file - - return geojson, colorbar - - - def generate_contour_tstep_trace(self, varname, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ - from dash_server import app - - geojson_file = GEOJSON_TEMPLATE.format(self.filedir, - self.selected_date_plain, tstep, self.selected_date_plain, varname) - - if os.path.exists(geojson_file): - geojson = orjson.loads(open(geojson_file).read()) - else: - if DEBUG: print('ERROR', geojson_file, 'not available') - geojson = { - "type": "FeatureCollection", - "features": [] - } - - if varname not in VARS and self.model in OBS: - name = VARS[OBS[self.model]['mod_var']]['name'] - else: - name = VARS[varname]['name'] - # if DEBUG: print(self.bounds) - if self.bounds: - bounds = self.bounds[varname.upper()] - else: - bounds = [0, 1] - loc_val = [ - ( - feature['id'], - np.around(feature['properties']['value'], 2), - ) - for feature in geojson['features'] - if feature['geometry']['coordinates'] - ] - locations, values = np.array(loc_val).T if loc_val else ([], []) - # if DEBUG: print(varname, self.colormaps[varname], values) - return dict( - type='choroplethmapbox', - name=name+'_contours', - geojson=geojson, - z=values, - ids=locations, - locations=locations, - zmin=bounds[0], - zmax=bounds[-1], - colorscale=self.colormaps[varname.upper()], - showscale=False, - showlegend=False, - hoverinfo='none', - marker=dict( - opacity=0.6, - line_width=0, - ), - colorbar=None, - ) - - def generate_var_tstep_trace_leaflet(self, varname=None, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ - colorscale = COLORS - xlon, ylat, val = self.set_data(varname, tstep) - df = pd.DataFrame(np.array([xlon, ylat, val]).T.round(2), columns=['lon', 'lat', 'value']) - dicts = df.to_dict('rows') - for item in dicts: - item["tooltip"] = \ - "Lat {:.2f} Lon {:.2f} Val {:.2f}".format(item['lat'], item['lon'], item['value']) - geojson = dlx.dicts_to_geojson(dicts, lon="lon") - # geobuf = dlx.geojson_to_geobuf(geojson) - - if DEBUG: print("GEOBUF CREATED ***********") - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - ns = Namespace("forecastTab", "forecastMaps") - point_to_layer = ns("pointToLayer") - bind_tooltip = ns("bindTooltip") - if DEBUG: print("BIND", str(bind_tooltip)) - # Create geojson. - # return dl.GeoJSON(data=geobuf, format="geobuf", - return dl.GeoJSON(data=geojson, - options=dict( - pointToLayer=point_to_layer, - # onEachFeature=bind_tooltip, - ), # how to draw points - hideout=dict( - colorProp='value', - circleOptions=dict( - fillOpacity=0, - stroke=False, - radius=0), - min=0, - max=val.max(), - colorscale=colorscale) - ) - - - def generate_var_tstep_trace(self, varname=None, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ - if not varname: - return dict( - type='scattermapbox', - # below='', - lon=[15], - lat=[30], - hoverinfo='none', - opacity=0, - showlegend=False, - marker=dict( - showscale=False, - size=0, - colorbar=None, - ), - ) - xlon, ylat, val = self.set_data(varname, tstep) - if self.model in OBS: - name = OBS[self.model]['name'] - else: - name = MODELS[self.model]['name'] - if DEBUG: print("***", name, "***") - return dict( - type='scattermapbox', - # below='', - lon=xlon, - lat=ylat, - text=val, - name=name, - hovertemplate="lon: %{lon:.2f}
lat: %{lat:.2f}
" + - "value: %{text:.2f}", - opacity=OPACITY, - showlegend=False, - marker=dict( - # autocolorscale=True, - showscale=False, - color=val, - # opacity=0.6, - size=0, - colorscale=self.colormaps[varname.upper()], - cmin=self.bounds[varname.upper()][0], - cmax=self.bounds[varname.upper()][-1], - colorbar=None, - ), - ) - - def get_title(self, varname, tstep=0): - """ return title according to the date """ - if self.model in OBS: - name = OBS[self.model]['name'] - title = OBS[self.model]['title'] - else: - name = MODELS[self.model]['name'] - title = VARS[varname]['title'] - rdatetime = self.retrieve_cdatetime(tstep=0) - cdatetime = self.retrieve_cdatetime(tstep) - return r'{} {}'.format(name, title % { - 'rhour': rdatetime.strftime("%H"), - 'rday': rdatetime.strftime("%d"), - 'rmonth': rdatetime.strftime("%b"), - 'ryear': rdatetime.strftime("%Y"), - 'shour': cdatetime.strftime("%H"), - 'sday': cdatetime.strftime("%d"), - 'smonth': cdatetime.strftime("%b"), - 'syear': cdatetime.strftime("%Y"), - 'step': "{:02d}".format(tstep*FREQ), - }) - - def hour_to_step(self, hour): - """ Convert hour to relative tstep """ - cdatetime = self.rdatetime.date() + relativedelta(hours=hour) - - for step in range(len(self.tim)): - if self.retrieve_cdatetime(step) == cdatetime: - return step - - return 0 - - def retrieve_var_tstep(self, varname=None, tstep=0, hour=None, static=True, aspect=(1,1), center=None, selected_tiles='carto-positron', zoom=None, layer=None, tag='empty'): - """ run plot """ - - if hour is not None: - tstep = int(self.hour_to_step(hour)) - else: - tstep = int(tstep) - - if varname is not None and self.model in OBS: - varname = OBS[self.model]['obs_var'] - - if DEBUG: print('VARNAME', varname) - - if varname and self.filedir: - if DEBUG: print('Adding contours ...') - try: - cont_time = time.time() - geojson_contours, colorbar = self.generate_contour_tstep_trace_leaflet(varname, tstep) - if DEBUG: print("****** CONTOURS EXEC TIME", str(time.time() - cont_time)) - except Exception as err: - if DEBUG: print("----------- ERROR:", str(err)) - self.filedir = None - # if DEBUG: print('ERROR: geojson {}'.format(geojson_url)) - geojson_contours = None - colorbar = None - else: - if DEBUG: print('Adding one point ...') - geojson_contours = None - colorbar = None - - if DEBUG: print("ASPECT", aspect) - if center is None: - center = self.get_center(center) - if zoom is None: - zoom = 3.5 -(aspect[0]-aspect[0]*0.4) - if colorbar is not None: - colorbar.width = 320 - 15 * aspect[0] - colorbar.style = {'overflow':'hidden', 'white-space':'nowrap'} - if aspect == (3,4): - colorbar.width = 320 - 38 * aspect[0] - colorbar.height = 8 - if DEBUG: print("ZOOM", zoom) - if DEBUG: print("CENTER", center) - - - if DEBUG: print('Update layout ...') - if not varname: - fig_title=html.P("") - elif varname and not self.filedir: - if self.model in MODELS: - curr_name = MODELS[self.model]['name'] - elif self.model in OBS: - curr_name = OBS[self.model]['name'] - else: - curr_name = '' - - fig_title = html.P(html.B("{} - DATA NOT AVAILABLE".format(curr_name))) - else: - fig_title = html.P(html.B( - [ - item for sublist in self.get_title(varname, tstep).split('
') for item in [sublist, html.Br()] - ][:-1] - )) - if self.model is not None: - CUR_INFO_STYLE = INFO_STYLE.copy() - if colorbar is not None: - CUR_INFO_STYLE['width'] = str(colorbar.width) + "px" - #add sconc_info class to tilt colorbar values so they won't overlap - if varname == "SCONC_DUST": colorbar.className = 'sconc_info' - if aspect[0] > 2: - CUR_INFO_STYLE['fontSize'] = "{}px".format(int(INFO_STYLE['fontSize'][:-2])-aspect[0]+ 0.3) - info = html.Div( - children=fig_title, - id="{}-info".format(self.model), - className="info", - style=CUR_INFO_STYLE - ) - else: - info = None - - if isinstance(self.model, str): - curr_index = self.model - else: - curr_index = str(self.model) - - tag_template_tile = "{}-tile-layer" - tag_template_map = "{}-map" - - if self.model in MODELS: - curr_tag = 'model' - else: - curr_tag = isinstance(tag, str) and tag or str(tag) - - if not isinstance(layer, list): - layer = [layer] - - if DEBUG: print("********* TAG", tag, "CURR_TAG", curr_tag) - fig = dl.Map(children=[ - dl.TileLayer( - id=dict( - tag=tag_template_tile.format(curr_tag), - index=curr_index - ), - url=STYLES[selected_tiles]['url'], - attribution=STYLES[selected_tiles]['attribution'] - ), - dl.FullscreenControl( - position='topright', - ), - geojson_contours, - colorbar, - info - ] + layer, - zoomSnap=0.1, - zoom=zoom, - wheelPxPerZoomLevel=120, - wheelDebounceTime=80, - center=center, - id=dict( - tag=tag_template_map.format(curr_tag), - index=curr_index - ), - inertia=True, - preferCanvas=True, - animate=False, - minZoom=2, - #className="graph-with-slider", - ) - # if DEBUG: print("---", fig) - if DEBUG: print("*** FIGURE EXECUTION TIME: {} ***".format(str(time.time() - self.st_time))) - return fig - - -class ScoresFigureHandler(object): - """ Class to manage the figure creation """ - - def __init__(self, network, statistic, selection=None): - - if network == 'aeronet': - self.sites = pd.read_csv(os.path.join('./conf/', - OBS[network]['sites'])) -# self.sites = pd.read_table(os.path.join('./conf/', -# OBS[network]['sites']), delimiter=r"\s+", engine='python') - self.size = 15 - else: - self.sites = None - self.size = 7 - - network_name = OBS[network]['name'] - filedir = OBS[network]['path'] - filename = "{}_{}.h5".format(selection, statistic) - tab_name = "{}_{}".format(statistic, selection) - filepath = os.path.join(filedir, "h5", filename) - if DEBUG: print('SCORES filepath', filepath, 'SELECTION', selection, 'TAB', tab_name) - self.dframe = pd.read_hdf(filepath, tab_name) # .replace('_', ' ', regex=True) - - months = ' - '.join([datetime.strptime(sel, '%Y%m').strftime("%B %Y") for sel in selection.split('-')]) - - self.title = """{model} - {network_name} {score}
{selection}""".format( - score=STATS_CONF[statistic]['name'], network_name=network_name, model='{model}', selection=months) - self.xlon = np.array([-27, 60]) - self.ylat = np.array([0, 67]) - self.stat = statistic - - def get_mapbox(self, style='carto-positron', relayout=False, zoom=2.8, center=None): - """ Returns mapbox layout """ - if center is None and hasattr(self, 'ylat'): - center = go.layout.mapbox.Center( - lat=(self.ylat.max()-self.ylat.min())/2 + - self.ylat.min(), - lon=(self.xlon.max()-self.xlon.min())/2 + - self.xlon.min(), - ) - elif center is not None: - center = go.layout.mapbox.Center(center) - else: - center = go.layout.mapbox.Center({'lat': 30, 'lon': 15}) - mapbox_dict = dict( - uirevision=True, - style=style, - bearing=0, - center=center, - pitch=0, - zoom=zoom - ) - - if not relayout: - return mapbox_dict - - return dict( - args=["mapbox", mapbox_dict], - label=STYLES[style].capitalize(), - method="relayout" - ) - - def generate_trace(self, xlon, ylat, stats, vals): - """ Generate trace to be added to data, per variable and timestep """ - if stats is None: - hovertemplate="lon: %{lon:.2f}
lat: %{lat:.2f}
value: %{text}" - else: - hovertemplate="lon: %{lon:.2f}
lat: %{lat:.2f}
value: %{text}
station: %{customdata}" - name = '{} score'.format(STATS_CONF[self.stat]['name']) - return dict( - type='scattermapbox', - lon=xlon, - lat=ylat, - text=vals, - customdata=stats, - name=name, - hovertemplate=hovertemplate, - opacity=0.8, - mode='markers', - showlegend=False, - marker=dict( - showscale=True, - # colorscale=STATS_CONF[self.stat]['cmap'], - colorscale=get_colorscale(np.array(STATS_CONF[self.stat]['bounds']), STATS_CONF[self.stat]['cmap']), - cmax=STATS_CONF[self.stat]['max'], - cmin=STATS_CONF[self.stat]['min'], - cmid=STATS_CONF[self.stat]['mid'], - color=vals, - size=self.size, - colorbar=dict( - x=0.94, - y=0.45, - len=0.9, - tickmode='array', - tickvals=np.array(STATS_CONF[self.stat]['bounds']), - thickness=20, - ) - ), - ) - - def retrieve_scores(self, model, aspect=(1,1), center=None): - """ run plot """ - - data_available='' - if self.sites is not None: - stations = [st for st in self.sites['SITE']] - for site in stations: - self.dframe.loc[self.dframe['station'] == site, 'lon'] = \ - str(self.sites.loc[self.sites['SITE'] == site, 'LONGITUDE'].values[0].round(2)) - self.dframe.loc[self.dframe['station'] == site, 'lat'] = \ - str(self.sites.loc[self.sites['SITE'] == site, 'LATITUDE'].values[0].round(2)) - if DEBUG and self.sites is not None: print('BEFORE', model, '\n', self.dframe[['lon', 'lat', 'station', model]].to_markdown()) - self.dframe = self.dframe.replace('-', np.nan) - if DEBUG and self.sites is not None: print('AFTER', model, '\n', self.dframe[['lon', 'lat', 'station', model]].to_markdown()) - # self.dframe.dropna(inplace=True) - if DEBUG: print('DATAFRAME', self.dframe, model) - self.fig = go.Figure(go.Scattermapbox()) - if self.sites is not None and model in self.dframe.columns: - if DEBUG: print('DATA FOR MODEL', model, '\n', self.dframe[['lon', 'lat', 'station', model]].to_markdown()) - if DEBUG: print('DATA FOR MODEL', model, '\n', self.dframe[['lon', 'lat', 'station', model]].dropna().to_markdown()) - xlon, ylat, stats, vals = self.dframe[['lon', 'lat', 'station', model]].dropna().values.T - if DEBUG: print('Adding SCORES points ...', xlon, ylat, vals) - self.fig.add_trace(self.generate_trace(xlon.astype(float), ylat.astype(float), stats, vals.astype(float))) - if not vals.any(): data_available='
NO DATA AVAILABLE' - - elif model in self.dframe.columns: - xlon, ylat, vals = self.dframe[['lon', 'lat', model]].dropna().values.T - stats = None - if DEBUG: print('Adding SCORES points ...', xlon, ylat, vals) - self.fig.add_trace(self.generate_trace(xlon.astype(float), ylat.astype(float), stats, vals.astype(float))) - if not vals.any(): data_available='
NO DATA AVAILABLE' - - else: - data_available='
NO DATA AVAILABLE' - - if DEBUG: print('Update layout ...', self.title.format(model=MODELS[model]['name'])) - fig_title=dict(text='{} {}'.format(self.title.format(model=MODELS[model]['name']), data_available), - xanchor='left', - yanchor='top', - x=0.01, y=0.95) - - self.fig.update_layout( - title=fig_title, - uirevision=True, - autosize=True, - hovermode="closest", # highlight closest point on hover - mapbox=self.get_mapbox(zoom=2.8-(0.5*aspect[0]), center=center), - font_size=12-(0.5*aspect[0]), - # width="100%", - legend=dict( - x=0.01, - y=0.9, - bgcolor="rgba(0,0,0,0)" - ), - margin={"r": 0, "t": 0, "l": 0, "b": 0}, - ) - - # if DEBUG: print('Returning fig of size {}'.format(sys.getsizeof(self.fig))) - return self.fig - - -class VisFigureHandler(object): - """ Class to manage the figure creation """ - - def __init__(self, selected_date=None): - - self.path_tpl = '/data/daily_dashboard/obs/visibility/{year}/{month}/{year}{month}{day}{tstep0:02d}{tstep1:02d}_visibility.csv' - self.title = ["Visibility reduced by airborne dust", html.Br(), "{date} {tstep0:02d}-{tstep1:02d} UTC"] - self.xlon = np.array([-25, 60]) - self.ylat = np.array([0, 65]) - self.ec = 'none' - self.size = 80 - self.freq = 6 - self.colors = ('#714921', '#da7230', '#fcd775', 'CadetBlue') - self.labels = ("< 1 km", "1 - 2 km", "2 - 5 km", "Haze < 5 km") - self.values = (0, 1, 2, 3) - self.markers = ('o', 'o', 'o', '^') - - if selected_date: - self.selected_date_plain = selected_date - - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") - else: - self.selected_date_plain = None - self.selected_date = None - - def set_data(self, tstep=0): - """ Set time dependent data """ - tstep0 = tstep - tstep1 = tstep + self.freq - - year = datetime.strptime(self.selected_date_plain, '%Y%m%d').strftime('%Y') - month = datetime.strptime(self.selected_date_plain, '%Y%m%d').strftime('%m') - day = datetime.strptime(self.selected_date_plain, '%Y%m%d').strftime('%d') - - filepath = self.path_tpl.format(year=year, month=month, day=day, tstep0=tstep0, tstep1=tstep1) - if DEBUG: print("VIS FILEPATH", filepath) - if not os.path.exists(filepath): - return [], [], [], [], [], () - - data = pd.read_table(filepath, na_filter=False) - - # uncertain - cx = np.where((data['WW'].astype(str) == "HZ") | (data['WW'].astype(str) == "5")| (data['WW'].astype(str) == "05")) - - # vis <= 1km - c0t = np.where((data['VV'] <= 1000))[0] - c0x = np.where([c0t == i for i in cx[0]])[-1] - c0 = (np.delete(c0t, c0x),) - - # vis 1km <= 2km - c1t = np.where((data['VV'] > 1000) & (data['VV'] <= 2000))[0] - c1x = np.where([c1t == i for i in cx[0]])[-1] - c1 = (np.delete(c1t, c1x),) - - # vis 2km <= 5km - c2t = np.where((data['VV'] > 2000) & (data['VV'] <= 5000))[0] - c2x = np.where([c2t == i for i in cx[0]])[-1] - c2 = (np.delete(c2t, c2x),) - - xlon = data['LON'].values - ylat = data['LAT'].values - stats = data['STATION'].values - - visibility = 'VV' in data and data['VV'] - humidity = 'HUMIDITY' in data and data['HUMIDITY'] - - if DEBUG: print("VIS DATA", xlon, ylat, stats, (c0, c1, c2, cx)) - return xlon, ylat, stats, visibility, humidity, (c0, c1, c2, cx) - - def generate_var_tstep_trace(self, xlon, ylat, stats, visibility, humidity, value, color, label, marker, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ - name = 'visibility {}'.format(label) - - legend = [] - res = np.zeros((len(xlon))) - for i, (val, lab) in enumerate(zip(value, label)): - # assign a value from 0 to 3 to the different - res[val] = i - leg = html.Div([ - html.Span( - "", - className="vis-legend-point", - style={ - 'backgroundColor': self.colors[i], - } - ), - html.Span( - lab, - className="vis-legend-label", - ) - ], - style={ 'display': 'block' }) - legend.append(leg) - - legend = html.Div( - legend, - className="vis-legend" - ) - - if DEBUG: - print('VIS ___', xlon, ylat, '\n*********', visibility, '\n*********', humidity) - df = pd.DataFrame({ - 'lon': np.array(xlon).round(2), - 'lat': np.array(ylat).round(2), - 'stat': stats, - 'visibility': visibility, - 'humidity': humidity, - 'value': res - }) - dicts = df.to_dict('rows') - for item in dicts: - if DEBUG: - print("-------------", item, "-----------------") - item["tooltip"] = \ - """ - {station} -
LAT: {lat} LON: {lon} -
VISIBILITY: {visibility:.1f} km - {humidity} -
""".format( - lat=item['lat'], - lon=item['lon'], - station=item['stat'], - visibility=float(item['visibility']/1e3), - humidity=(item['humidity'] not in (False, '') and "
RELATIVE HUMIDITY: {}%".format(int(float(item['humidity']))) or '') - ) - - geojson = dlx.dicts_to_geojson(dicts, lon="lon") - ns = Namespace("observationsTab", "observationsMaps") - point_to_layer = ns("pointToLayer") - geojson = dl.GeoJSON(data=geojson, - options=dict( - pointToLayer=point_to_layer, - ), - hideout=dict( - colorProp='value', - colorscale=self.colors, - circleOptions=dict( - fillOpacity=OPACITY, - stroke=False, - #fillColor='#f0b450', - radius=8), - ) - ) - - if list(visibility): - cur_title = self.get_title(tstep) - else: - cur_title = self.get_title() - - info = html.Div( - children=cur_title, - id="vis-info", - className="info", - style=INFO_STYLE - ) - return df, [geojson, info, legend] - - def get_title(self, tstep=None): - """ return title according to the date """ - if tstep is None: - return ["NO DATA AVAILABLE"] - tstep0 = tstep - tstep1 = tstep + self.freq - fdate = datetime.strptime(self.selected_date_plain, '%Y%m%d').strftime('%d %B %Y') - return [tit.format(date=fdate, tstep0=tstep0, tstep1=tstep1) - if isinstance(tit, str) else tit for tit in self.title] - - def retrieve_var_tstep(self, tstep=0, hour=None, static=True, aspect=(1,1), center=None): - """ run plot """ - - tstep = int(tstep) - - xlon, ylat, stats, visibility, humidity, vals = self.set_data(tstep) - if tstep is not None: - return self.generate_var_tstep_trace(xlon, ylat, stats, visibility, humidity, vals, self.colors, self.labels, self.markers, tstep) - if DEBUG: print('Adding one point ...') - return None - - -class ProbFigureHandler(object): - """ Class to manage the figure creation """ - - def __init__(self, var=None, prob=None, selected_date=None): - """ Initialization with variable, prob and date """ - - if var is None: - var = DEFAULT_VAR - - self.varname = var - - probs = PROB[var]['prob_thresh'] - if prob is None: - prob = probs[0] - - self.bounds = np.arange(0, 110, 10) - - self.prob = prob - - geojson_path = PROB[var]['geojson_path'] - geojson_file = PROB[var]['geojson_template'] - netcdf_path = PROB[var]['netcdf_path'] - netcdf_file = PROB[var]['netcdf_template'] - - self.geojson = os.path.join(geojson_path, geojson_file).format(prob=prob, date=selected_date, var=var) - self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=prob, date=selected_date, var=var) - - if os.path.exists(self.filepath): - self.input_file = nc_file(self.filepath) - - if 'lon' in self.input_file.variables: - lon = self.input_file.variables['lon'][:] - lat = self.input_file.variables['lat'][:] - else: - lon = self.input_file.variables['longitude'][:] - lat = self.input_file.variables['latitude'][:] - time_obj = self.input_file.variables['time'] - self.tim = time_obj[:] - tim_units = time_obj.units.split() - if len(tim_units) == 3: - self.what, _, rdate = tim_units - rtime = "00:00" - elif len(tim_units) >= 4: - self.what, _, rdate, rtime = tim_units[:4] - if len(rtime) > 5: - rtime = rtime[:5] - self.rdatetime = datetime.strptime("{} {}".format(rdate, rtime), - "%Y-%m-%d %H:%M") - varlist = [var for var in self.input_file.variables if var in VARS] - self.xlon, self.ylat = np.meshgrid(lon, lat) - - if selected_date: - self.selected_date_plain = selected_date - - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") - - self.fig = None - - def set_data(self, varname, tstep=0): - """ Set time dependent data """ - mul = 1 # VARS[varname]['mul'] - var = self.input_file.variables[varname][tstep]*mul - idx = np.where(var.ravel() >= VARS[varname]['bounds'][0]) # !=-9.e+33) - xlon = self.xlon.ravel()[idx] - ylat = self.ylat.ravel()[idx] - var = var.ravel()[idx] - - return xlon, ylat, var - - def retrieve_cdatetime(self, tstep=0): - """ Retrieve data from NetCDF file """ - if DEBUG: print("----------", tstep, self.tim) - tim = int(self.tim[tstep]) - if self.what == 'days': - cdatetime = self.rdatetime + relativedelta(days=tim) - elif self.what == 'hours': - cdatetime = self.rdatetime + relativedelta(hours=tim) - elif self.what == 'minutes': - cdatetime = self.rdatetime + relativedelta(minutes=tim) - elif self.what == 'seconds': - cdatetime = self.rdatetime + relativedelta(seconds=tim) - - return cdatetime - - def generate_contour_tstep_trace(self, varname, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ - from dash_server import app - if varname is None: - varname = self.varname - - if DEBUG: print("##############", tstep) - - geojson_file = self.geojson.format(step=tstep) - geojson_url = app.get_asset_url(geojson_file.replace('/data/daily_dashboard', 'geojsons')) - - name = VARS[varname]['name'] - bounds = self.bounds - colorscale = COLORS_PROB - - if DEBUG: print("GEOJSON_URL", geojson_url) - - style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) - - # Create colorbar. - ctg = ["{:.1f}".format(cls) if '.' in str(cls) else "{:d}".format(cls) - for i, cls in enumerate(bounds)] - indices = list(range(len(ctg))) - colorbar = dl.Colorbar( - min=-.1, max=len(ctg)-.7, - classes=indices, - colorscale=colorscale, - tickValues=indices, - tickText=ctg, - position='topleft', - width = 330, - height=8, - style={ 'top': '65px','overflow':'hidden', 'white-space':'nowrap'} - ) - - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - ns = Namespace("forecastTab", "forecastMaps") - style_handle = ns("styleHandle") - - geojson = dl.GeoJSON( - url=geojson_url, - options=dict(style=style_handle), - hideout=dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") - ) # url to geojson file - - return geojson, colorbar - - def get_title(self, varname, tstep=0): - """ return title according to the date """ - # tstep += 1 - rdatetime = self.rdatetime - cdatetime = self.retrieve_cdatetime(tstep) - - mod_avail = 0 - for model in MODELS: - if model == 'median': - continue - path_template = '{}{}.nc'.format(rdatetime.strftime("%Y%m%d"), MODELS[model]['template']) - fpath = os.path.join(MODELS[model]['path'], 'netcdf', path_template) - if os.path.exists(fpath): - mod_avail += 1 - - return PROB[varname]['title'].format( - prob=self.prob, - members=mod_avail, - rday=rdatetime.strftime("%d"), - rmonth=rdatetime.strftime("%b"), - ryear=rdatetime.strftime("%Y"), - sday=cdatetime.strftime("%d"), - smonth=cdatetime.strftime("%b"), - syear=cdatetime.strftime("%Y"), - ) - - def retrieve_var_tstep(self, varname=None, day=0, static=True, aspect=(1,1)): - """ run plot """ - tstep = int(day) - if varname is None: - varname = self.varname - if DEBUG: print("***", varname, day, static, self.geojson, self.filepath) - - geojson = colorbar = None - - if varname and os.path.exists(self.geojson.format(step=day)): - if DEBUG: print('Adding contours ...') - geojson, colorbar = self.generate_contour_tstep_trace(varname, tstep) - if varname and os.path.exists(self.filepath) and static: - if DEBUG: print('Adding points ...') - elif varname is None or not os.path.exists(self.filepath): - if DEBUG: print('Adding one point ...') - - if DEBUG: print('Update layout ...') - if not varname: - if DEBUG: print('ONE') - fig_title = '' - elif varname and not os.path.exists(self.filepath): - if DEBUG: print('TWO') - fig_title = html.P(html.B("DATA NOT AVAILABLE")) - else: - if DEBUG: print('THREE') - fig_title = html.P(html.B( - [ - item for sublist in self.get_title(varname, tstep).split('
') for item in [sublist, html.Br()] - ][:-1] - )) - - info = html.Div( - children=fig_title, - id="prob-info", - className="info", - style=INFO_STYLE - ) - return geojson, colorbar, info - - -class WasFigureHandler(object): - """ Class to manage the figure creation """ - - def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=None): - """ Initialize WasFigureHandler with shapefile and netCDF data """ - self.model = model - self.was = was - self.variable = variable - - if self.model and selected_date: - # read nc file - if DEBUG: print("MODEL", model) - filepath = NETCDF_TEMPLATE.format( - MODELS[self.model]['path'], - selected_date, - MODELS[self.model]['template'] - ) - - if os.path.exists(filepath): - self.input_file = nc_file(filepath) - time_obj = self.input_file.variables['time'] - self.tim = time_obj[:] - tim_units = time_obj.units.split() - if len(tim_units) == 3: - self.what, _, rdate = tim_units - rtime = "00:00" - elif len(tim_units) >= 4: - self.what, _, rdate, rtime = tim_units[:4] - if len(rtime) > 5: - rtime = rtime[:5] - self.rdatetime = datetime.strptime("{} {}".format(rdate, rtime), - "%Y-%m-%d %H:%M") - else: - self.input_file = None - - if selected_date: - self.selected_date_plain = selected_date - - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") - - self.fig = None - - def get_regions_data(self, day=0): - input_dir = WAS[self.was]['path'].format(was=self.was, format='h5', date=self.selected_date_plain) - input_file = WAS[self.was]['template'].format(date=self.selected_date_plain, var=self.variable, format='h5') - - input_path = os.path.join(input_dir, input_file) - - if DEBUG: print("INPUT PATH", input_path, self.selected_date_plain) - if not os.path.exists(input_path): - return [], [], [] - - df = pd.read_hdf(input_path, 'was_{}'.format(self.selected_date_plain)).set_index('day') - - names, colors, definitions = df.loc['Day{}'.format(day)].values.T - return names, colors, definitions - - def get_geojson_url(self, day=0): - from dash_server import app - geojsons_dir = WAS[self.was]['path'].format(was=self.was, format='geojson', date=self.selected_date_plain) - geojson_file = WAS[self.was]['template'].format(date=self.selected_date_plain, var=self.variable, format='geojson').replace('.geojson', '_{}.geojson'.format(day)) - geojson_filepath = os.path.join(geojsons_dir, geojson_file) - if not os.path.exists(geojson_filepath): - if DEBUG: print("ERROR: WAS GEOJSON URL", geojson_filepath) - return None - - pathlist = os.path.normpath(geojsons_dir).split(os.sep) - geojsons_dir_cleaned = os.sep.join(pathlist[pathlist.index('was'):]) - - geojson_path = os.path.join(geojsons_dir_cleaned, geojson_file) - geojson_url = app.get_asset_url(os.path.join('geojsons', geojson_path)) - - if DEBUG: print("WAS GEOJSON URL", geojson_url) - - return geojson_url - - def retrieve_cdatetime(self, tstep=0): - tim = int(self.tim[tstep]) - """ Retrieve data from NetCDF file """ - if self.what == 'days': - cdatetime = self.rdatetime + relativedelta(days=tim) - elif self.what == 'hours': - cdatetime = self.rdatetime + relativedelta(hours=tim) - elif self.what == 'minutes': - cdatetime = self.rdatetime + relativedelta(minutes=tim) - elif self.what == 'seconds': - cdatetime = self.rdatetime + relativedelta(seconds=tim) - - return cdatetime - - def generate_contour_tstep_trace(self, day=0): - """ Generate trace to be added to data, per variable and timestep """ - - if not hasattr(self, 'was'): - return None, None - - names, colors, definitions = self.get_regions_data(day=day) - colors = np.array(colors) - if DEBUG: print("::::::::::", names, colors, definitions) - style = dict(weight=1, opacity=1, color='white', dashArray='3', fillOpacity=OPACITY) - - # Create colorbar. - colormap = WAS[self.was]['colors'] - - legend = [] - # res = np.zeros(len(colormap)) - # if DEBUG: print("^^^^^", colormap) - for color_idx, (color, definition) in enumerate(colormap.items()): - # assign a value from 0 to 3 to the different - leg = html.Div([ - html.Span( - "", - className="was-legend-point", - style={ - 'backgroundColor': color, - } - ), - html.Span( - definition, - className="was-legend-label", - ) - ], - style={ 'display': 'block' }) - legend.append(leg) - - legend = html.Div( - legend, - className="was-legend" - ) - - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - ns = Namespace("forecastTab", "wasMaps") - style_handle = ns("styleHandle") - - geojson = dl.GeoJSON( - url=self.get_geojson_url(day=day), - # data=geojson_data, - # zoomToBounds=True, - hoverStyle=arrow_function(dict(weight=2, color='white', dashArray='', fillOpacity=OPACITY)), - options=dict(style=style_handle), - hideout=dict(colorscale=list(colormap.keys()), - bounds=[i for i in range(len(colormap.keys()))], - style=style, - colorProp="value") - ) # url to geojson file - - return geojson, legend - - def get_title(self, day=0): - """ return title according to the date """ - rdatetime = self.retrieve_cdatetime(tstep=0) - cdatetime = rdatetime + relativedelta(days=day) - return r'{}'.format(WAS[self.was]['title'] % { - 'rhour': rdatetime.strftime("%H"), - 'rday': rdatetime.strftime("%d"), - 'rmonth': rdatetime.strftime("%b"), - 'ryear': rdatetime.strftime("%Y"), - 'shour': cdatetime.strftime("%H"), - 'sday': cdatetime.strftime("%d"), - 'smonth': cdatetime.strftime("%b"), - 'syear': cdatetime.strftime("%Y"), - #'step': "{:02d}".format(tstep*FREQ), - }) - - def retrieve_var_tstep(self, day=0, static=True, aspect=(1,1)): - """ run plot """ - self.fig = go.Figure() - day = int(day) - if DEBUG: print('Adding contours ...') - geojson, legend = self.generate_contour_tstep_trace(day) - if DEBUG: print('Update layout ...') - if self.input_file is not None: - fig_title = html.P(html.B( - [ - item - for sublist in self.get_title(day).split('
') - for item in [sublist, html.Br()] - ][:-1] - )) - else: - fig_title = html.P(html.B("DATA NOT AVAILABLE")) - - info = html.Div( - children=fig_title, - id="was-info", - className="info", - style=INFO_STYLE - ) - return geojson, legend, info diff --git a/ines_core_map_handler.py b/ines_core_map_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..40f78d06ffa42ca329a787f1900ab3ae7df4d44a --- /dev/null +++ b/ines_core_map_handler.py @@ -0,0 +1,540 @@ +# -*- coding: utf-8 -*- +""" Core """ + +import os +import time +import json +import copy +from dash import html +import dash_leaflet as dl +import numpy as np +from dateutil.relativedelta import relativedelta +from netCDF4 import Dataset as nc_file +from datetime import datetime as dt +import logging + +DIR_PATH = os.path.dirname(os.path.realpath(__file__)) +COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) +INFO_STYLE = json.load(open(os.path.join(DIR_PATH, 'conf/info_style.json'))) +MAP_LAYERS = json.load(open(os.path.join(DIR_PATH, 'conf_ines_core/map_layers.json'))) +DEBUG = True + +class MapHandler: + """ Class than handles the creation of maps""" + + def __init__(self): + """ Initialize MapHandler class """ + + self.info_style = INFO_STYLE + # TODO: Add common variables from all figure handlers here + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements + + Returns + ------- + title : str + Map title + """ + + logging.debug('================= Retrieving figure title...') + + # Get selected and current datetime details + rhour = self.rdatetime.strftime("%H") + rday = self.rdatetime.strftime("%d") + rmonth = self.rdatetime.strftime("%b") + ryear = self.rdatetime.strftime("%Y") + if hasattr(self, 'cdatetime'): + shour = self.cdatetime.strftime("%H") + sday = self.cdatetime.strftime("%d") + smonth = self.cdatetime.strftime("%b") + syear = self.cdatetime.strftime("%Y") + + # Get title elements that we want to show from base title + # e.g. a dict with shour, sday, smonth, syear as keys + title_elements = {} + base_title_split = self.base_title.split("%(") + for element in base_title_split[1:]: + element_split = element.split(")") + if len(element_split) > 1: + title_element = element_split[0] + if hasattr(self, title_element): + title_elements.update({title_element: getattr(self, title_element)}) + else: + title_elements.update({title_element: locals()[title_element]}) + + # Create title from base and elements + title = r'{}'.format(self.base_title % title_elements) + + return title + + def retrieve_cdatetime(self, tstep=0): + """ Retrieve data from NetCDF file + + Parameters + ---------- + tstep : int, optional + Current timestep of running plot, by default 0 + + Returns + ------- + cdatetime : datetime.datetime + Current datetime + """ + + tstep = int(tstep) + time_attr = int(self.timesteps[tstep]) + + # Calculate current datetime given the selected initial datetime and current timestep + if self.what == 'days': + cdatetime = self.rdatetime + relativedelta(days=time_attr) + elif self.what == 'hours': + cdatetime = self.rdatetime + relativedelta(hours=time_attr) + elif self.what == 'minutes': + cdatetime = self.rdatetime + relativedelta(minutes=time_attr) + elif self.what == 'seconds': + cdatetime = self.rdatetime + relativedelta(seconds=time_attr) + + return cdatetime + + def create_legend(self, colormap): + """ Create map legend given the colors for each label + + Parameters + ---------- + colormap : dict + Dictionary with labels and keys and colors as values + + Returns + ------- + legend : html.Div + Map legend + """ + + logging.debug('================= Retrieving legend...') + + # Create categorical legend given the labels and its colors + legend_items = [] + labels = colormap.keys() + colors = colormap.values() + + for i, (label, color) in enumerate(zip(labels, colors)): + legend_item = html.Div([html.Span( + "", + className="{}-legend-point".format(self.name), + style={ + 'backgroundColor': color, + } + ), + html.Span( + label, + className="{}-legend-label".format(self.name), + ) + ], + style={ 'display': 'block' }) + legend_items.append(legend_item) + + legend = html.Div(legend_items, className="{}-legend".format(self.name)) + + return legend + + def retrieve_info(self, name): + """ Get information box with title + + Parameters + ---------- + name : str + Map name + + Returns + ------- + info : html.Div + Map title + """ + + logging.debug('================= Retrieving figure title box...') + + # Box width to match colorbar width + if hasattr(self, 'colorbar'): + if self.colorbar is not None: + self.info_style['width'] = str(self.colorbar.width) + "px" + + # Create figure title box + info = html.Div( + children=self.fig_title, + id="{}-info".format(name), + className="info", + style=self.info_style + ) + + return info + + def retrieve_colorbar(self, aspect=(1,1)): + """ Create map colorbar + + Parameters + ---------- + aspect : tuple, optional + Map aspect, by default (1,1) + + Returns + ------- + colorbar : dash_leaflet.Colorbar + Map colorbar + """ + + logging.debug('================= Retrieving colorbar...') + + # Create colorbar + colorbar = dl.Colorbar(**self.colorbar_info) + + # Resize and place on top left corner + colorbar.width = 320 - 15 * aspect[0] + colorbar.style = {'overflow':'hidden', 'white-space':'nowrap'} + if aspect == (3,4): + colorbar.width = 320 - 38 * aspect[0] + colorbar.height = 8 + + return colorbar + + def get_time_details(self): + """ Get time details + + Returns + ------- + timesteps : list + Steps corresponding to each datetime, e.g. [0, 3, 6, 9, 12] + what : str + Detects if time units are in hours, months, etc. + rdatetime : datetime.datetime + Selected datetime + """ + + input_file = nc_file(self.filepath) + time_obj = input_file.variables['time'] + timesteps = time_obj[:] + tim_units = time_obj.units.split() + if len(tim_units) == 3: + what, _, rdate = tim_units + rtime = "00:00" + elif len(tim_units) >= 4: + what, _, rdate, rtime = tim_units[:4] + if len(rtime) > 5: + rtime = rtime[:5] + rdatetime = dt.strptime("{} {}".format(rdate, rtime), "%Y-%m-%d %H:%M") + + return timesteps, what, rdatetime + + def hour_to_tstep(self, hour): + """ Convert hour to relative tstep + + Parameters + ---------- + hour : int + Hour + + Returns + ------- + tstep : int + Timestep + """ + + cdatetime = self.rdatetime.date() + relativedelta(hours=hour) + for tstep in range(len(self.timesteps)): + if self.retrieve_cdatetime(tstep) == cdatetime: + return tstep + return 0 + + def get_center(self, center=None): + """ Returns center of map + + Parameters + ---------- + center : list, optional + Map center, by default None + + Returns + ------- + center : list + Map center + """ + + if center is None: + if hasattr(self, 'ylat'): + center = [(self.ylat.max()-self.ylat.min())/2 + self.ylat.min() + (self.ylat.max()-self.ylat.min())/6, + (self.xlon.max()-self.xlon.min())/2 + self.xlon.min(),] + else: + center = [30, 15] + + return center + + def retrieve_fig(self, aspect=(1,1), center=None, selected_tiles='carto-positron', + zoom=None, tag='empty', index=None, layers=None): + """ Get figure with all the elements (legend, layers, colorbar, etc.) + + Parameters + ---------- + aspect : tuple, optional + Map aspect, by default (1,1) + center : list, optional + Map center, by default None + selected_tiles : str, optional + Map tiles, by default 'carto-positron' + zoom : float, optional + Map zoom, by default None + tag : str, optional + Map tag, by default 'empty' + index : str, optional + Map index, by default None + layers : list, optional + Map layers, by default None + + Returns + ------- + dash_leaflet.Map + Figure + """ + + self.st_time = time.time() + + logging.debug('================= Retrieving figure...') + + # Get zoom and center if it hasn't been specified + if center is None: + center = self.get_center(center) + if zoom is None: + zoom = 3.5 -(aspect[0]-aspect[0]*0.4) + logging.debug("ASPECT %s", aspect) + logging.debug("ZOOM %s", zoom) + logging.debug("CENTER %s", center) + + # Get index and template names + index = str(index) + tag_template_tile = "{}-tile-layer" + tag_template_map = "{}-map" + + if not isinstance(layers, list): + layers = [layers] + + logging.debug("TAG %s", tag) + logging.debug("INDEX %s", index) + + fig = dl.Map(children=[ + dl.TileLayer( + id=dict( + tag=tag_template_tile.format(tag), + index=index + ), + url=MAP_LAYERS[selected_tiles]['url'], + attribution=MAP_LAYERS[selected_tiles]['attribution'] + ), + dl.FullscreenControl( + position='topright', + )] + layers, + zoomSnap=0.1, + zoom=zoom, + wheelPxPerZoomLevel=120, + wheelDebounceTime=80, + center=center, + id=dict( + tag=tag_template_map.format(tag), + index=index + ), + inertia=True, + preferCanvas=True, + animate=False, + minZoom=2, + #className="graph-with-slider", + ) + + logging.debug("*** FIGURE EXECUTION TIME: {} ***".format(str(time.time() - self.st_time))) + + return fig + +class PointsFigureHandler(MapHandler): + """ Class that handles the generation of maps with points """ + + def __init__(self): + """ Initialize PointsFigureHandler class """ + + super(PointsFigureHandler, self).__init__() + + if self.name in COLORBARS: + self.colorbar_info = COLORBARS[self.name] + + # TODO: Add common variables from: + # VisFigureHandler, EvaluationGroundFigureHandler and EvaluationStatisticsFigureHandler + + return None + + def retrieve_geojson(self, geojson_data, namespace): + """ Get points layer as GeoJSON + + Parameters + ---------- + geojson_data : dict + Data to generate GeoJSON + namespace : dash_extensions.javascript.Namespace + Namespace + + Returns + ------- + geojson : dash_leaflet.GeoJSON + GeoJSON layer + """ + + logging.debug('================= Retrieving geojson...') + + # Get points properties + hideout = {"circleOptions": self.circle_options} + if 'fillColor' not in hideout["circleOptions"].keys(): + hideout.update({'colorProp': "value", + 'colorscale': self.colors}) + + # Get geojson + point_to_layer = namespace("pointToLayer") + geojson = dl.GeoJSON(data=geojson_data, + options=dict(pointToLayer=point_to_layer,), + hideout=hideout) + + return geojson + + def get_tooltip(self, df, var_list): + """ Get tooltip on hover + + Parameters + ---------- + df : dict + Data to generate tooltip + var_list : list + Variables to be shown in the tooltip if available in df + + Returns + ------- + data_dict : dict + Data dictionary with tooltips + """ + + logging.debug('================= Retrieving tooltip...') + + data_dict = df.to_dict('records') + for item in data_dict: + logging.debug("------------- %s -----------------", item) + tooltip = """""" + tooltip_keys = [key for key in var_list if key in item.keys()] + for key in tooltip_keys: + # Check for nans, empty and false numbers + if (item[key] in [False, '']) or (item[key] != item[key]): + continue + if key == 'station': + tooltip += """{0}
""".format(item[key]) + else: + tooltip += """{0}: {1}
""".format(key.capitalize(), item[key]) + tooltip += """
""" + item["tooltip"] = tooltip + + return data_dict + + +class ContourFigureHandler(MapHandler): + """ Class that handles the generation of maps with countours """ + + def __init__(self): + """ Initialize ContourFigureHandler class """ + + super(ContourFigureHandler, self).__init__() + + # TODO: Add common variables from old FigureHandler and ProbFigureHandler + + if self.name in COLORBARS: + self.colorbar_info = COLORBARS[self.name] + else: + self.colorbar_info = COLORBARS['model'] + + return None + + def retrieve_geojson(self, geojson_path, namespace): + """ Get contour layer as GeoJSON + + Parameters + ---------- + geojson_path : str + Directory with data to generate GeoJSON + namespace : dash_extensions.javascript.Namespace + Namespace + + Returns + ------- + geojson : dash_leaflet.GeoJSON + GeoJSON layer + """ + + from dash_server import app + + logging.debug('================= Retrieving geojson...') + + # Get contour properties + hideout = dict(colorscale=self.colorscale, bounds=self.bounds, style=self.style, + colorProp="value") + + # Get geojson + geojson_url = app.get_asset_url(geojson_path) + style_handle = namespace("styleHandle") + geojson = dl.GeoJSON(url=geojson_url, + options=dict(style=style_handle), + hideout=hideout) + + logging.info("GEOJSON URL %s", geojson_url) + + return geojson + + +class ShapefileFigureHandler(MapHandler): + """ Class that handles the generation of maps with shapefiles """ + + def __init__(self): + """ Initialize ShapefileFigureHandler class """ + + super(ShapefileFigureHandler, self).__init__() + + # TODO: Add variables from ForecastWasFigureHandler that could be reused in other figures + + return None + + def retrieve_geojson(self, geojson_path, namespace, hideout, hover_style): + """ Get shapefile layer as GeoJSON + + Parameters + ---------- + geojson_path : str + Directory with data to generate GeoJSON + namespace : dash_extensions.javascript.Namespace + Namespace + hideout : dict + Shapefile style + hover_style : dict + Shapefile hover style + + Returns + ------- + geojson : dash_leaflet.GeoJSON + GeoJSON layer + """ + + from dash_server import app + + logging.debug('================= Retrieving geojson...') + + # Get geojson + geojson_url = app.get_asset_url(geojson_path) + style_handle = namespace("styleHandle") + geojson = dl.GeoJSON(url=geojson_url, + options=dict(style=style_handle), + hoverStyle=hover_style, + hideout=hideout) + + logging.info("GEOJSON URL %s", geojson_url) + + return geojson diff --git a/ines_core_utils.py b/ines_core_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ac9cdc3a13c02155cdd44633b8e816daaf51e6ca --- /dev/null +++ b/ines_core_utils.py @@ -0,0 +1,377 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" Utils module with utility functions """ + +import math +import os.path +import xarray as xr +import numpy as np +import pandas as pd +import feather +import logging +from dash import dcc +from dash import html +from map_handler import PATHNAME +from map_handler import RENDER404 +from map_handler import HOSTNAMES + +def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None): + """ Concatenate monthly dataframes + + Parameters + ---------- + fname_tpl : str + Feather file path + months : numpy.ndarray + Months, e.g. ['202304', '202305'] + variable : str + Variable name + rename_from : str, optional + Variable name after renaming it, by default None + notnans : list, optional + Stations that have only NaN values, by default None + + Returns + ------- + list + Stations that have only NaN values + pandas.core.frame.DataFrame + Dataframe with data from all months + """ + + # Build feather files paths + opaths = [fname_tpl.format(month, variable) + for month in months if os.path.exists(fname_tpl.format(month, variable))] + + logging.debug("__________ %s __________", opaths) + if not opaths: + return None, None + + # Read monthly dataframes and concatenate into one + if rename_from: + mon_dfs = pd.concat([feather.read_dataframe(opath) + .rename(columns={rename_from: variable}) + for opath in opaths]) + # In case of models we don't rename the variable column + else: + mon_dfs = pd.concat([feather.read_dataframe(opath) + for opath in opaths]) + + # 1d observations + if 'station' in mon_dfs.columns: + if notnans is None: + notnans = [st for st in mon_dfs['station'].unique() + if not mon_dfs[mon_dfs['station']==st][variable].isnull().all()] + mon_dfs_filter = mon_dfs['station'] + final_df = mon_dfs[mon_dfs_filter.isin(notnans)] + # 2d observations + else: + final_df = mon_dfs + + return notnans, final_df + + +def retrieve_single_point(fname, tstep, lat, lon, variable): + """ Retrive data on single point + + Parameters + ---------- + fname : str + File path + tstep : int + Timestep + lat : float + Latitude + lon : float + Longitude + variable : str + Variable name + + Returns + ------- + float + Variable value at closest available point from (lat, lon) + """ + + from map_handler import DEBUG + + logging.debug(" %s %s %s %s %s", fname, tstep, lat, lon, variable) + ds = xr.open_dataset(fname) + if variable not in ds.variables: + variable = variable.lower() + + # logging.debug('TIMESERIES %s %s %s %s', fname, variable, lon, lat) + + if 'lat' in ds.variables: + da = ds[variable].sel(lon=lon, lat=lat, method='nearest') + else: + da = ds[variable].sel(longitude=lon, latitude=lat, method='nearest') + logging.debug('%s', da) + + return da.values[tstep] + + +def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=False): + """ Retrieve timeseries data + + Parameters + ---------- + fname : str + File path + lat : float + Latitude + lon : float + Longitude + variable : str + Variable name + method : str, optional + Method name, by default 'netcdf' + forecast : bool, optional + Indicates if we are using forecast data, by default False + + Returns + ------- + numpy.ndarray + Latitudes + numpy.ndarray + Longitudes + pandas.core.indexes.datetimes.DatetimeIndex + Times + xarray.core.dataarray.DataArray + Data + """ + + from map_handler import DEBUG + + if method == 'feather' and not forecast: + df = feather.read_dataframe(fname) + if 'lat' in df.columns: + lat_col = 'lat' + lon_col = 'lon' + else: + lat_col = 'latitude' + lon_col = 'longitude' + + if variable not in df.columns: + variable = variable.lower() + + if variable not in df.columns: + return None, None, None, None + + n_lon = find_nearest(df[lon_col].values, lon) + n_lat = find_nearest(df[lat_col].values, lat) + ts = df[(df[lat_col] == n_lat) & (df[lon_col] == n_lon)][['time', + variable]].set_index('time') + + return n_lat, n_lon, ts.index, ts[variable] + + def preprocess(ds, n=8): + return ds.isel(time=range(n)) + + if forecast: + ds = xr.open_dataset(fname) + else: + ds = xr.open_mfdataset(fname, concat_dim='time', combine='nested', + preprocess=preprocess) + if variable not in ds.variables: + variable = variable.lower() + + # logging.debug('TIMESERIES %s %s %s %s', fname, variable, lon, lat) + + if 'lat' in ds.variables: + da = ds[variable].sel(lon=lon, lat=lat, method='nearest') + clat = 'lat' + clon = 'lon' + else: + da = ds[variable].sel(longitude=lon, latitude=lat, method='nearest') + clat = 'latitude' + clon = 'longitude' + + return da[clat].values, da[clon].values, da.indexes['time'], da + + +def find_nearest(array, value): + """ Find the nearest value of a couple of coordinates + + Parameters + ---------- + array : numpy.ndarray + Array from which we extract the closest value (e.g. latitudes array) + value : float + Value for which we will search its closest value + + Returns + ------- + float + Nearest value in array for value + """ + + return array[np.abs(array-value).argmin()] + + +def find_nearest2(array, value): + """ Find the nearest value of a couple of coordinates + + Parameters + ---------- + array : numpy.ndarray + Array from which we extract the closest value (e.g. latitudes array) + value : float + Value for which we will search its closest value + + Returns + ------- + float + Nearest value in array for value + """ + + idx = np.searchsorted(array, value, side="left") + if idx > 0 and (idx == len(array) or math.fabs(value - array[idx-1]) < + math.fabs(value - array[idx])): + return array[idx-1] + + return array[idx] + + +def calc_matrix(n): + """ Calculate the mosaic optimum matrix shape + + Parameters + ---------- + n : int + Number of mosaic cells + + Returns + ------- + int + Number of columns + int + Number of rows + """ + + sqrt_n = math.sqrt(n) + ncols = sqrt_n == int(sqrt_n) and int(sqrt_n) or int(sqrt_n) + 1 + nrows = n%ncols > 0 and int(n/ncols)+1 or int(n/ncols) + + return ncols, nrows + + +def magnitude(num): + """ Calculate magnitude + + Parameters + ---------- + num : float + Number + + Returns + ------- + int + Magnitude of num + """ + + if num == 0: + num = 1 + + return int(math.floor(math.log10(num))) + + +def normalize_vals(vals, valsmin, valsmax, rnd=2): + """ Normalize values to 0-1 scale + + Parameters + ---------- + vals : numpy.ndarray + Values to normalize + valsmin : float + Minimum + valsmax : float + Maximum + rnd : int, optional + Rounding decimals, by default 2 + + Returns + ------- + numpy.ndarray + Normalized values + """ + + if len(vals) == 1: + return np.array([0]) + vals = np.array(vals) + if rnd < 2: + rnd = 2 + + return np.around((vals-valsmin)/(valsmax-valsmin), rnd) + +def get_404_link(tab): + """ + Create the links for the 404 page. The links will redirect to the + fullscreen version of the app app if the app is in the + development environment at /dashboard/. Otherwise the links will + point to the running instance on the AEMET server. + """ + if PATHNAME == HOSTNAMES['default']: + return PATHNAME+RENDER404[tab]['path_dev'], + return RENDER404[tab]['path_prod'] + +def render404(): + """ Create a 404 page """ + + # Define error message + children = [html.H2('404 Error', id='error_title'), + html.P("Sorry we can't find the page you were looking for."), + html.P("Here are some links that might help:"),] + + # Define links + for tab_i, tab in enumerate(RENDER404.keys()): + # Setup the href for the dev/prod environment + link = get_404_link(tab) + + # Add spacing if between links, not above + if tab_i > 0: + children.extend([html.Br(), + html.Br(),]) + # Add links to tabs + children.extend([dcc.Link(RENDER404[tab]['name'], + id=RENDER404[tab]['id'], + href=link, + className='error_links',target='_parent', + refresh=True),]) + + # Create page + page = [html.Div( + className='background', + children=[ + html.Div( + id='error_div', + children=children, + ) + ] + ) + ] + + return page + + +def extend_list(l): + """ Extend list of lists to a flat list + + Parameters + ---------- + l : list + List + + Returns + ------- + list + Flat list + """ + + res = [] + for x in l: + res.extend(x) + if not isinstance(res, list): + res = [res] + + return res diff --git a/js/create_model_loop_zoom_fit.js b/js/create_model_loop_zoom_fit.js index a46585044ea3db213467b97471a7f3c8c87a3e58..69f3c438bfb7e7d203005aca4221fcc88e9e07aa 100644 --- a/js/create_model_loop_zoom_fit.js +++ b/js/create_model_loop_zoom_fit.js @@ -1,31 +1,40 @@ -// CREATE A GIF FOCUSED GIF OR PNG OVER SELECTED fit -// anim variable can be true(for gif) or a number for timestep, for png -// call from interactive-forecast-viewer folder -// date currently does not work, and defaults to current day, or most recent data -// node create_model_loop_zoom_fit true monarch 20220808 od550_dust spain +// CREATE A GIF FOCUSED GIF OR PNG OVER SELECTED FIT AREA +// ANIM VARIABLE CAN BE TRUE(FOR GIF) OR A NUMBER FOR TIMESTEP, FOR PNG +// CALL FROM INTERACTIVE-FORECAST-VIEWER FOLDER +// DATE CURRENTLY DOES NOT WORK, AND DEFAULTS TO CURRENT DAY, OR MOST RECENT DATA +// NODE CREATE_MODEL_LOOP_ZOOM_FIT TRUE MONARCH 20220808 OD550_DUST SPAIN +// SETUP PUPPETEER const { Cluster } = require('puppeteer-cluster'); const util = require('util'); const path = require('path'); -const url = 'http://127.0.0.1:9000/dashboard/' -// const url = 'http://0.0.0.0:7778/dashboard/' -const modelDict = {'od550_dust':'AOD', - 'sconc_dust':'Concentration', - 'dust_depd':'Dry deposition', - 'dust_depw':'Wet deposition', - 'dust_load':'Load', - 'dust_ext_sfc':'Extinction'}; - -//READ COORDINATES AND CENTER FROM CONF FILE -const relpath = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/coords.json'); -const coords = require(relpath); +// USE THE CREATE_LOOP_DEV CONF DURING DEVELOPMENT +const output_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/create_loop_prod.json') +// const output_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/create_loop_dev.json'); +const setup_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/create_loop_setup.json'); +const selectors_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/create_loop_selectors.json'); +const coords_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/coords.json'); +const vars_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-viewer/conf/vars.json'); + +//REQUIRE CONFS +const conf = require(output_conf); +const vars = require(vars_conf); +const coords = require(coords_conf); +const setup = require(setup_conf); +const selectors = require(selectors_conf); + +// ASSIGN THE URL THAT PUPPETEER SHOULD VISIT TO TAKE SCREENSHOTS +const url = conf.url; + +// FUNCTION TO SET UP DELAYS WHEN NEEDED function delay(time) { return new Promise(function(resolve) { setTimeout(resolve, time) }); } +// START PUPPETEER CLUSTER (EXTERIOR VARIABLES MUST BE PASSED IN) const RunCluster = async (anim, curmodel, seldate, variable, fit) => { const cluster = await Cluster.launch({ concurrency: Cluster.CONCURRENCY_CONTEXT, @@ -39,6 +48,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { maxConcurrency: 4, }); + // SET SCREEN DIMENSIONS AND GOTO URL await cluster.task(async ({ page, data: args }) => { process.stdout.write("ARGS: " + args + "\n"); var tstep = args[0]; @@ -53,111 +63,99 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { await page.goto(url, { waitUntil: 'networkidle0', }); - await page.waitForSelector("#graph-collection"); - // select variable - try { - const sel = await page.$('#variable-dropdown-forecast'); + await page.waitForSelector(selectors.graphs); + + // SELECT VARIABLE + try { + const sel = await page.$(selectors.varDropdown); await sel.click(); - await page.waitForSelector(".Select-menu-outer"); - const dropdown_variable = modelDict[variable]; - process.stdout.write("Selected dropdown variable: " + dropdown_variable + '\n'); - await page.evaluate((dropdown_variable) => { - const options = [...document.querySelectorAll('.VirtualizedSelectOption')]; + await page.waitForSelector(selectors.selectMenu); + const dropdown_variable = vars[variable.toUpperCase()].name_sidebar; + await page.evaluate(({dropdown_variable, selectedVar}) => { + const options = [...document.querySelectorAll(selectedVar)]; selected_variable = options.find(element => element.textContent === dropdown_variable); selected_variable.click(); - }, dropdown_variable); + }, {dropdown_variable: dropdown_variable, selectedVar: selectors.selectedVar}); } catch (err) { process.stdout.write("ERR0: " + err + "\n"); } - // select all models - if (curmodel === "all") { - try { - process.stdout.write("SELECT ALL MODELS\n"); - for (const model of await page.$$('.custom-control-input')) { - const checked = await model.evaluate(elem => elem.checked); - process.stdout.write("CHECKED BEFORE: " + checked + "\n"); - if (!checked) { - await model.click(); - } + + // SELECT MODEL + try { + process.stdout.write("SELECT MODEL: " + curmodel + "\n"); + for (const model of await page.$$(selectors.selectedModels)) { + const checked = await model.evaluate(elem => elem.checked); + process.stdout.write("CHECKED BEFORE: " + checked + "\n"); + const value = await model.evaluate(elem => elem.value); + process.stdout.write("VALUE: " + value + "\n"); + if (!checked && value === curmodel) { + await model.click(); } - } catch (err) { - process.stdout.write("ERR1: " + err + "\n"); - } - } - // select only one model - else { - try { - process.stdout.write("SELECT MODEL: " + curmodel + "\n"); - for (const model of await page.$$('.custom-control-input')) { - const checked = await model.evaluate(elem => elem.checked); - process.stdout.write("CHECKED BEFORE: " + checked + "\n"); - const value = await model.evaluate(elem => elem.value); - process.stdout.write("VALUE: " + value + "\n"); - if (!checked && value === curmodel) { - await model.click(); - } - if (checked && value !== curmodel) { - await model.click(); - await delay(500); - } + if (checked && value !== curmodel) { + await model.click(); + await delay(500); } - } catch (err) { - process.stdout.write("ERR2: " + err + "\n"); } + } catch (err) { + process.stdout.write("ERR1: " + err + "\n"); } - // apply button + + // CLICK APPLY BUTTON try { - for (const model of await page.$$('.custom-control-input')) { + for (const model of await page.$$(selectors.selectedModels)) { const checked = await model.evaluate(elem => elem.checked); process.stdout.write("CHECKED AFTER: " + checked + "\n"); } process.stdout.write("CLICK APPLY BUTTON\n"); - const btn = await page.$('#models-apply'); + const btn = await page.$(selectors.modelsApply); await btn.click(); // "button#models-apply"); await delay(500); } catch (err) { - process.stdout.write("ERR3: " + err + "\n"); + process.stdout.write("ERR2: " + err + "\n"); } - // apply none fit button + + // APPLY ZOOM AND COORDINATES try { var zoom = coords[fit].zoom; var lat = coords[fit].lat; var lon = coords[fit].lon; process.stdout.write("Zoom: " + zoom + " Lat " + lat + " Lon " + lon +"\n"); - // Change hidden inputs and button to visible - let zoomInput = await page.$('#country-zoom'); - await zoomInput.evaluate((el) => el.style.display = 'block'); - let latInput = await page.$('#country-lat'); - await latInput.evaluate((el) => el.style.display = 'block'); - let lonInput = await page.$('#country-lon'); - await lonInput.evaluate((el) => el.style.display = 'block'); - let zoomButton = await page.$('#country-focus'); - await zoomButton.evaluate((el) => el.style.display = 'block'); - - // Add data and click - await page.type('#country-zoom', zoom); - await page.type('#country-lat', lat); - await page.type('#country-lon', lon); - process.stdout.write("CLICK HIDDEN country BUTTON\n"); - await page.click('#country-focus'); - - // Make elements invisible again - await zoomInput.evaluate((el) => el.style.display = 'none'); - await latInput.evaluate((el) => el.style.display = 'none'); - await lonInput.evaluate((el) => el.style.display = 'none'); - await zoomButton.evaluate((el) => el.style.display = 'none'); + // CHANGE HIDDEN INPUTS AND BUTTON TO VISIBLE + let zoomInput = await page.$(selectors.zoomLevel); + await zoomInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); + let latInput = await page.$(selectors.lat); + await latInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); + let lonInput = await page.$(selectors.lon); + await lonInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); + let zoomButton = await page.$(selectors.focus); + await zoomButton.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); + + // ADD DATA AND CLICK + await page.type(selectors.zoomLevel, zoom); + await page.type(selectors.lat, lat); + await page.type(selectors.lon, lon); + process.stdout.write("CLICK HIDDEN COUNTRY BUTTON\n"); + await page.click(selectors.focus); + + // MAKE ELEMENTS INVISIBLE AGAIN + await zoomInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeInvisible); + await latInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeInvisible); + await lonInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeInvisible); + await zoomButton.evaluate((el, visible) => {el.style.display = visible}, setup.makeInvisible); } catch (err) { - process.stdout.write("ERR4: " + err + "\n"); + process.stdout.write("ERR3: " + err + "\n"); } + + // GO THROUGH TIMESTEPS var num = "00"; if (tstep === "false") { process.stdout.write("CURRENT SELECTION: " + tstep + "\n"); num = "_curr"; } else { process.stdout.write("CURRENT TSTEP: " + tstep + "\n"); - const steps = await page.$$("span.rc-slider-dot"); + const steps = await page.$$(selectors.sliderDot); await steps[tstep].click(); if (tstep<10) { num = "0"+tstep; @@ -165,38 +163,41 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { num = tstep; } } - // wait for alert div to disappear to start grabbing elements - await page.waitForSelector("#alert-forecast", {hidden: true}); - await page.waitForSelector("#graph-collection"); - const graph = await page.$('#graph-collection'); - // change graph height to fill output gif - let graphHeight = await page.$('.leaflet-container'); - await graphHeight.evaluate((el) => el.style.height = '93vh'); - // remove timeslider - await page.waitForSelector(".navbar-timebar"); - process.stdout.write("REMOVING NAVBAR" + "\n"); - await page.evaluate((sel) => { - let toRemove = document.querySelector(sel); - toRemove.parentNode.removeChild(toRemove); - }, '.navbar-timebar'); - // remove zoom panel - process.stdout.write("REMOVING ZOOM PANEL(S)" + "\n"); - await page.waitForSelector(".leaflet-bar"); - await page.evaluate((sel) => { - let toRemove = document.querySelectorAll(sel); - for (let j=0; j el.style.height = styledHeight, + setup.graphHeight); + // REMOVE TIMESLIDER + await page.waitForSelector(selectors.navTimebar); + process.stdout.write("REMOVING NAVBAR" + "\n"); + await page.evaluate((sel) => { + let toRemove = document.querySelector(sel); + toRemove.parentNode.removeChild(toRemove); + }, selectors.navTimebar); + // REMOVE ZOOM PANEL + process.stdout.write("REMOVING ZOOM PANEL(S)" + "\n"); + await page.waitForSelector(selectors.leafletBar); + await page.evaluate((sel) => { + let toRemove = document.querySelectorAll(sel); + for (let j=0; j el.style.display = 'inline'); + let logos = await page.$(selectors.logos); + await logos.evaluate((el, logoDisplay) => {el.style.display = logoDisplay}, setup.logoDisplay); await logos.evaluate((el, margin) => {el.style.marginTop = margin}, coords[fit].marginTop); await logos.evaluate((el, padding) => {el.style.paddingRight = padding}, coords[fit].paddingRight); } catch (err) { @@ -204,35 +205,44 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { process.stdout.write("ERR5:" + err + "\n"); } }; - // Move disclaimer into the image for the monarch_fit - let disclaimer = await page.$('.disclaimer'); - await disclaimer.evaluate((el) => el.style.right = '1px'); - // Handle output and take screenshot - //Filename shouldn't include fit unless it is a specified country - if (fit == 'monarch_fit' || fit == 'default') { - // Change the below to './js/tmp' ... during developement, run script from interactiv... - var outputFile = './tmp/' + variable.toLowerCase() + '/' + seldate + '_' + curmodel + '_' + num + '.png'; + + // MOVE DISCLAIMER INTO THE IMAGE FOR THE MONARCH_FIT + let disclaimer = await page.$(selectors.disclaimer); + await disclaimer.evaluate((el, disclaimerOffset) => el.style.right = disclaimerOffset, + setup.disclaimerRight); + + // HANDLE OUTPUT AND TAKE SCREENSHOT + // FILENAME SHOULDN'T INCLUDE FIT UNLESS IT IS A SPECIFIED COUNTRY + var targetFolder = conf.targetFolder; + if (fit == setup.specialFit || fit == setup.defaultFit) { + var outputFile = `${targetFolder}/${variable.toLowerCase()}/${seldate}_${curmodel}_${num}.png`; } else { - // Create a filename that includes the specified country fit - // Change the below to './js/tmp' ... during developement, run script from interactiv... - var outputFile = './tmp/' + variable.toLowerCase() + '/' + fit + '/' + seldate + '_' + curmodel + '_' + fit + '_' + num + '.png'; + // CREATE A FILENAME THAT INCLUDES THE SPECIFIED COUNTRY FIT + var outputFile = `${targetFolder}/${variable.toLowerCase()}/${fit}/${seldate}_${curmodel}_${fit}_${num}.png`; } process.stdout.write("TAKE SCREENSHOT => " + outputFile + "\n"); - await graph.screenshot({ path: outputFile }); + try { + await graph.screenshot({ path: outputFile }); + } catch(err) { + console.log(err); + process.stdout.write("ERR6:" + err + "\n"); + } }); + // TAKE SCREENSHOTS FOR ALL TSTEPS TO CREATE GIF if (anim === "true") { - for (let i=0; i<25; i++) { + for (let i = 0; i < setup.tstepCount; i++) { try { process.stdout.write("QUEUEING step:" + i + "\n"); await cluster.queue([i, curmodel, seldate, variable, fit]); } catch (err) { console.log(err); - process.stdout.write("ERR6:" + err + "\n"); + process.stdout.write("ERR7:" + err + "\n"); } } + // ELSE TAKE SCREENSHOT OF SPECIFIED TSTEP } else { process.stdout.write("QUEUEING current:" + anim + "\n"); cluster.queue([anim, curmodel, seldate, variable, fit]); @@ -246,7 +256,8 @@ var anim = process.argv[2]; // default: false; var curmodel = process.argv[3]; // default: "none"; var seldate = process.argv[4]; // default: "none"; var variable = process.argv[5]; // default: OD550_DUST -var fit = process.argv[6]; +var fit = process.argv[6]; // default: default + process.stdout.write("START -> ANIM: " + anim + " CURMODEL: " + curmodel + " DATE: " + seldate + " VAR: " + variable + " fit: " + fit + "\n"); RunCluster(anim, curmodel, seldate, variable, fit); diff --git a/map_handler.py b/map_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..9b19dd0b0e574f400a64d884c3e238036ab38715 --- /dev/null +++ b/map_handler.py @@ -0,0 +1,1365 @@ +# -*- coding: utf-8 -*- +""" Data Handler """ + +from dash import html +import dash_leaflet as dl +import dash_leaflet.express as dlx +from dash_extensions.javascript import arrow_function +from dash_extensions.javascript import Namespace +import matplotlib +from matplotlib import cm +from matplotlib.colors import ListedColormap +import numpy as np +from netCDF4 import Dataset as nc_file +import pandas as pd +import json +import orjson +from collections import OrderedDict +import os +import time +import calendar +from datetime import timedelta +from datetime import datetime as dt +from dateutil.relativedelta import relativedelta + +from ines_core_map_handler import MapHandler +from ines_core_map_handler import PointsFigureHandler +from ines_core_map_handler import ContourFigureHandler +from ines_core_map_handler import ShapefileFigureHandler + +from pathlib import Path +from flask_caching import Cache +import uuid +import socket +import logging + +DIR_PATH = os.path.dirname(os.path.realpath(__file__)) +DEBUG = False +DASH_LOG_LEVEL = logging.INFO # DEBUG INFO WARNING ERROR + +# Set up cache +CACHE = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) +cache_config = CACHE['config'] +cache_config.update({'CACHE_DIR': "/dev/shm/{}".format(str(uuid.uuid1()))}) +Path(cache_config['CACHE_DIR']).mkdir(parents=True, exist_ok=True) +cache = Cache(config=cache_config) +cache_timeout = CACHE['timeout'] + +# Set up base url +HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0] +HOSTNAMES = json.load(open(os.path.join(DIR_PATH, 'conf/hostnames.json'))) + +# Get configurations +INIT = json.load(open(os.path.join(DIR_PATH, 'conf/init.json'))) +DASH_STYLE = json.load(open(os.path.join(DIR_PATH, 'conf/dash_style.json'))) +VARS = json.load(open(os.path.join(DIR_PATH, 'conf/vars.json'))) +MODELS = json.load(open(os.path.join(DIR_PATH, 'conf/models.json'))) +OBS = json.load(open(os.path.join(DIR_PATH, 'conf/obs.json'))) +WAS = json.load(open(os.path.join(DIR_PATH, 'conf/was.json'))) +PROB = json.load(open(os.path.join(DIR_PATH, 'conf/prob.json'))) +VIS = json.load(open(os.path.join(DIR_PATH, 'conf/vis.json'))) +SCORES = json.load(open(os.path.join(DIR_PATH, 'conf/scores.json'))) +DATES = json.load(open(os.path.join(DIR_PATH, 'conf/dates.json'))) +ALIASES = json.load(open(os.path.join(DIR_PATH, 'conf/aliases.json'))) +STATS_CONF = json.load(open(os.path.join(DIR_PATH, 'conf/stats.json'))) +ALL_COLORS = json.load(open(os.path.join(DIR_PATH, 'conf/colors.json'))) +MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json'))) +DISCLAIMERS = json.load(open(os.path.join(DIR_PATH, 'conf/disclaimers.json'))) +SATELLITE_IMAGE_SRC = json.load(open(os.path.join(DIR_PATH, 'conf/satellite_image_src.json'))) +PATHS = json.load(open(os.path.join(DIR_PATH, 'conf/paths.json'))) +RENDER404 = json.load(open(os.path.join(DIR_PATH, 'conf/render404.json'))) + +# Set up initial conditions +FREQ = INIT['frequency'] +FORECAST_MAX = INIT['forecast_max'] +FORECAST_FINAL_DAY = INIT['forecast_final_day'] +DEFAULT_VAR = INIT['default_variable'] +DEFAULT_MODEL = INIT['default_model'] +START_DATE = DATES['start_date'] +DELAY = DATES['delay']['delayed'] +DELAY_DATE = DATES['delay']['start_date'] +END_DATE = DATES['end_date'] +END_DATE = END_DATE or (DELAY and (dt.now() - + timedelta(days=1)).strftime("%Y%m%d") or dt.now().strftime("%Y%m%d")) + +# Set up dashboard basic properties +GRAPH_HEIGHT = DASH_STYLE['graph_height'] +OPACITY = DASH_STYLE['opacity'] + +# Set up color scheme +COLORS = ALL_COLORS['std'] +COLORS_NORGB = ALL_COLORS['std_norgb'] +COLORS_PROB = ALL_COLORS['prob'] +COLORMAP = ListedColormap(COLORS_NORGB) +COLORMAP_PROB = ListedColormap(COLORS_PROB) + +# Set up route defaults +ROUTE_DEFAULTS = json.load(open(os.path.join(DIR_PATH, 'conf/route.json'))) +ROUTE_DEFAULTS.update({'date': [END_DATE]}) + +# Set up statistics +STATS = OrderedDict([(key, STATS_CONF[key]['name']) for key in STATS_CONF]) +STATS.update({'totn': 'TOTAL CASES'}) + +# Set up timeseries configuration +MODEBAR_CONFIG = MODEBARS['config'] +MODEBAR_CONFIG_TS = MODEBARS['config_ts'] +MODEBAR_LAYOUT = MODEBARS['layout'] +MODEBAR_LAYOUT_TS = MODEBARS['layout_ts'] + +# Set up disclaimers +DISCLAIMER_MODELS = [html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')] +DISCLAIMER_OBS = html.P(DISCLAIMERS['observations']) + +# Get templates +GEOJSON_TEMPLATE = "{}/geojson/{}/{:02d}_{}_{}.geojson" +NETCDF_TEMPLATE = "{}/netcdf/{}{}.nc" + +# Get path names +if HOSTNAME in HOSTNAMES: + PATHNAME = HOSTNAMES[HOSTNAME] +else: + PATHNAME = HOSTNAMES['default'] + + +class ForecastModelsFigureHandler(ContourFigureHandler): + """ Class which handles forecast model data """ + + def __init__(self, var=None, model=None, tstep=0, hour=None, selected_date=None): + """ Initialize ForecastModelsFigureHandler class + + Parameters + ---------- + var : str, optional + Variable name, by default None + model : str, optional + Model name, by default None + tstep : int, optional + Timestep, by default 0 + hour : int, optional + Hour, by default None + selected_date : str, optional + Selected date, by default None + """ + + if isinstance(model, list): + self.model = model[0] + else: + self.model = model + self.name = self.model + super(ForecastModelsFigureHandler, self).__init__() + + self.var = var + self.selected_date = selected_date + + self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') + self.style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) + self.colorscale = COLORS + + if self.selected_date: + # Get NetCDF file path + self.filepath = NETCDF_TEMPLATE.format(MODELS[self.model]['path'], selected_date, + MODELS[self.model]['template']) + + # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime + if os.path.exists(self.filepath): + self.timesteps, self.what, self.rdatetime = self.get_time_details() + + return None + + def generate_var_tstep_trace(self, tstep=0): + """ Generate GeoJSON trace to be added per timestep + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + """ + + data_path = os.path.basename(MODELS[self.model]['path']) + geojson_path = os.path.join('geojsons', GEOJSON_TEMPLATE.format(data_path, + self.selected_date, tstep, self.selected_date, self.var)) + namespace = Namespace("forecastTab", "forecastMaps") + self.geojson = self.retrieve_geojson(geojson_path, namespace) + + return None + + def generate_colorbar(self): + """ Generate colorbar """ + + ctg = ["{:d}".format(int(cls)) if cls.as_integer_ratio()[1] == 1 else "{:.1f}".format(cls) + for i, cls in enumerate(self.bounds[1:-1])] + indices = list(range(len(ctg) + 2)) + self.colorbar_info.update({'min': 0, + 'max': len(ctg)+1, + 'classes': indices, + 'colorscale': self.colorscale, + 'tickValues': indices[1:-1], + 'tickText': ctg}) + self.colorbar = self.retrieve_colorbar() + + logging.debug("BOUNDS %s", self.bounds) + logging.debug("CTG %s", ctg) + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + varname = kwargs['varname'] + tstep = kwargs['tstep'] + + self.base_title = " ".join([MODELS[self.model]['name'], VARS[varname]['title']]) + self.cdatetime = self.retrieve_cdatetime(tstep) + self.step = "{:02d}".format(tstep*FREQ) + title = super(ForecastModelsFigureHandler, self).get_title() + + return title + + def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): + """ Get figure layers (GeoJSON trace, colorbar and information box with title) + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + hour : int, optional + Hour, by default None + aspect : tuple, optional + Map aspect, by default (1,1) + + Returns + ------- + list + Figure layers + """ + + logging.debug('Updating layout ...') + + if os.path.exists(self.filepath) and self.var: + + # Get timestep + if hour is not None: + tstep = int(self.hour_to_tstep(hour)) + else: + tstep = int(tstep) + + # Get trace and colorbar + self.generate_var_tstep_trace(tstep) + self.generate_colorbar() + + # Get figure title + self.fig_title = html.P(html.B( + [ + item for sublist in self.get_title(varname=self.var, tstep=tstep).split('
') + for item in [sublist, html.Br()] + ][:-1] + )) + + else: + self.geojson = None + self.colorbar = None + + # Get figure title + if not self.var: + self.fig_title = "" + elif self.var and not os.path.exists(self.filepath): + if self.model in MODELS: + title = "{} - DATA NOT AVAILABLE".format(MODELS[self.model]['name']) + else: + title = "DATA NOT AVAILABLE" + self.fig_title = html.P(html.B(title)) + + if self.model is not None: + # Tilt colorbar values so they won't overlap for SCONC_DUST + if self.var == "SCONC_DUST": + self.colorbar.className = 'sconc_info' + if aspect[0] > 2: + self.info_style['fontSize'] = "{}px".format(int(self.info_style['fontSize'][:-2])-aspect[0]+ 0.3) + + # Get title information element + self.info = self.retrieve_info(name=self.model) + + return [self.geojson, self.colorbar, self.info] + + +class ForecastProbFigureHandler(ContourFigureHandler): + """ Class which handles forecast probability data """ + + def __init__(self, var=None, prob=None, selected_date=None): + """ Initialize ForecastProbFigureHandler class + + Parameters + ---------- + var : str, optional + Variable name, by default None + prob : float, optional + Probability threshold, by default None + selected_date : str, optional + Selected date, by default None + """ + + self.name = 'prob' + super(ForecastProbFigureHandler, self).__init__() + + if var is None: + self.var = DEFAULT_VAR + else: + self.var = var + + probs = PROB[self.var]['prob_thresh'] + if prob is None: + prob = probs[0] + + self.prob = prob + self.selected_date = selected_date + + self.bounds = np.arange(0, 110, 10) + self.colorscale = COLORS_PROB + self.style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) + + # Get NetCDF file path + netcdf_path = PROB[self.var]['netcdf_path'] + netcdf_file = PROB[self.var]['netcdf_template'] + self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=self.prob, + date=self.selected_date, + var=self.var) + + # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime + if os.path.exists(self.filepath): + self.timesteps, self.what, self.rdatetime = self.get_time_details() + + return None + + def generate_var_tstep_trace(self, tstep=0): + """ Generate GeoJSON trace to be added per timestep + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + """ + + geojson_path = os.path.join(PROB[self.var]['geojson_path'], + PROB[self.var]['geojson_template']).format(prob=self.prob, + date=self.selected_date, var=self.var) + geojson_path = geojson_path.format(step=tstep).replace('/data/daily_dashboard', + 'geojsons') + namespace = Namespace("forecastTab", "forecastMaps") + self.geojson = self.retrieve_geojson(geojson_path, namespace) + + return None + + def generate_colorbar(self): + """ Generate colorbar """ + + ctg = ["{:.1f}".format(cls) if '.' in str(cls) else "{:d}".format(cls) + for i, cls in enumerate(self.bounds)] + indices = list(range(len(ctg))) + self.colorbar_info.update({'min': -0.1, + 'max': len(ctg)-.7, + 'classes': indices, + 'colorscale': self.colorscale, + 'tickValues': indices, + 'tickText': ctg}) + self.colorbar = self.retrieve_colorbar() + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + tstep = kwargs['tstep'] + + self.members = 0 + for model in MODELS: + if model == 'median': + continue + path_template = '{}{}.nc'.format(self.rdatetime.strftime("%Y%m%d"), MODELS[model]['template']) + fpath = os.path.join(MODELS[model]['path'], 'netcdf', path_template) + if os.path.exists(fpath): + self.members += 1 + + self.base_title = PROB[self.var]['title'] + self.cdatetime = self.rdatetime + relativedelta(days=tstep) + + title = super(ForecastProbFigureHandler, self).get_title() + + return title + + def get_figure_layers(self, day=0): + """ Get figure layers (GeoJSON trace, colorbar and information box with title) + + Parameters + ---------- + day : int, optional + Day, by default 0 + + Returns + ------- + list + Figure layers + """ + + logging.debug('Updating layout ...') + + if self.var and os.path.exists(self.filepath): + + # Get timestep + tstep = int(day) + + # Get trace and colorbar + self.generate_colorbar() + self.generate_var_tstep_trace(tstep) + + # Get figure title + self.fig_title = html.P(html.B( + [ + item for sublist in self.get_title(varname=self.var, tstep=tstep).split('
') + for item in [sublist, html.Br()] + ][:-1] + )) + else: + self.geojson = None + self.colorbar = None + + # Get figure title + if not self.var: + self.fig_title = "" + else: + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.colorbar, self.info] + + +class ForecastWasFigureHandler(ShapefileFigureHandler): + """ Class which handles forecast WAS data """ + + def __init__(self, was='burkinafaso', model='median', var='SCONC_DUST', selected_date=None): + """ Initialize ForecastWasFigureHandler class + + Parameters + ---------- + was : str, optional + Region name, by default 'burkinafaso' + model : str, optional + Model name, by default 'median' + var : str, optional + Variable name, by default 'SCONC_DUST' + selected_date : str, optional + Selected date, by default None + """ + + self.name = 'was' + super(ForecastWasFigureHandler, self).__init__() + + self.model = model + self.was = was + self.var = var + self.selected_date = selected_date + + self.colormap = WAS[self.was]['colormap'] + self.hideout_style = WAS[self.was]['hideout_style'] + self.hideout_style.update({'fillOpacity': OPACITY}) + self.hover_style = WAS[self.was]['hover_style'] + self.hover_style.update({'fillOpacity': OPACITY}) + + if (self.model in MODELS) and self.selected_date: + + # Get NetCDF file path + self.filepath = NETCDF_TEMPLATE.format( + MODELS[self.model]['path'], + selected_date, + MODELS[self.model]['template'] + ) + + if os.path.exists(self.filepath): + # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime + self.timesteps, self.what, self.rdatetime = self.get_time_details() + + # Correct timesteps to show first timestep of each day ([0, 24, 48, ...]) + self.timesteps = self.timesteps[::8] + + return None + + def get_regions_data(self, day=0): + """ Get regions data + + Parameters + ---------- + day : int, optional + Day, by default 0 + + Returns + ------- + numpy.ndarray + Region names + numpy.ndarray + Region colors + numpy.ndarray + Region definitions + """ + + input_dir = WAS[self.was]['path'].format(was=self.was, format='h5', date=self.selected_date) + input_file = WAS[self.was]['template'].format(date=self.selected_date, var=self.var, + format='h5') + + input_path = os.path.join(input_dir, input_file) + + logging.debug("INPUT PATH %s %s", input_path, self.selected_date) + if not os.path.exists(input_path): + return [], [], [] + + df = pd.read_hdf(input_path, 'was_{}'.format(self.selected_date)).set_index('day') + + names, colors, definitions = df.loc['Day{}'.format(day)].values.T + + return names, colors, definitions + + def get_geojson_url(self, day=0): + """ Get GeoJSON url + + Parameters + ---------- + day : int, optional + Day, by default 0 + + Returns + ------- + str + Path to GeoJSON data + """ + + geojsons_dir = WAS[self.was]['path'].format(was=self.was, format='geojson', + date=self.selected_date) + geojson_file = WAS[self.was]['template'].format(date=self.selected_date, var=self.var, + format='geojson').replace('.geojson', + '_{}.geojson'.format(day)) + geojson_filepath = os.path.join(geojsons_dir, geojson_file) + if not os.path.exists(geojson_filepath): + logging.debug("ERROR: WAS GEOJSON URL %s", geojson_filepath) + return None + + pathlist = os.path.normpath(geojsons_dir).split(os.sep) + geojsons_dir_cleaned = os.sep.join(pathlist[pathlist.index('was'):]) + + geojson_path = os.path.join(geojsons_dir_cleaned, geojson_file) + geojson_url = os.path.join('geojsons', geojson_path) + + logging.debug("WAS GEOJSON URL %s", geojson_url) + + return geojson_url + + def generate_var_tstep_trace(self, day=0): + """ Generate GeoJSON trace to be added per day + + Parameters + ---------- + day : int, optional + Day, by default 0 + """ + + logging.debug('Adding contours ...') + + day = int(day) + names, colors, definitions = self.get_regions_data(day=day) + colors = np.array(colors) + logging.debug(":::::::::: %s %s %s", names, colors, definitions) + + # Create legend + self.legend = self.create_legend(self.colormap) + + # Geojson rendering logic, must be JavaScript as it is executed in clientside. + namespace = Namespace("forecastTab", "wasMaps") + geojson_path = self.get_geojson_url(day=day) + hideout = dict(colorscale=list(self.colormap.values()), + bounds=[i for i in range(len(self.colormap.values()))], + style=self.hideout_style, + colorProp="value") + hover_style = arrow_function(self.hover_style) + self.geojson = self.retrieve_geojson(geojson_path, namespace, hideout, hover_style) + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + tstep = kwargs['day'] + self.base_title = WAS[self.was]['title'] + self.cdatetime = self.retrieve_cdatetime(tstep) + title = super(ForecastWasFigureHandler, self).get_title() + + return title + + def get_figure_layers(self, day=0): + """ Get figure layers (GeoJSON trace, legend and information box with title) + + Parameters + ---------- + day : int, optional + Day, by default 0 + + Returns + ------- + list + Figure layers + """ + + logging.debug('Updating layout ...') + + if os.path.exists(self.filepath): + # Get trace + self.generate_var_tstep_trace(day) + + # Get figure title + self.fig_title = html.P(html.B( + [ + item + for sublist in self.get_title(day=day).split('
') + for item in [sublist, html.Br()] + ][:-1] + )) + else: + self.geojson = None + self.legend = None + + # Get figure title + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.legend, self.info] + + +class EvaluationGroundFigureHandler(PointsFigureHandler): + """ Class which handles AERONET observations data """ + + def __init__(self, start_date=None, end_date=None, obs=None, var=None): + """_summary_ + + Parameters + ---------- + start_date : str, optional + Start date, by default None + end_date : str, optional + End date, by default None + obs : str, optional + Observations name, by default None + var : str, optional + Variable name, by default None + """ + + self.name = obs + super(EvaluationGroundFigureHandler, self).__init__() + self.circle_options = OBS[obs]['circle_options'] + + fday = start_date[:-2] + '01' + lday = end_date[:-2] + str(calendar.monthrange(int(end_date[:4]), int(end_date[4:6]))[1]) + date_range = pd.date_range(fday, lday, freq='M') + months = [d.strftime("%Y%m") for d in date_range.to_pydatetime()] + filepaths = ["{}.nc".format(os.path.join(OBS[obs]['path'], 'netcdf', + OBS[obs]['template'].format(OBS[obs]['obs_var'], month))) for month + in months] + self.input_files = [nc_file(filepath) for filepath in filepaths if os.path.exists(filepath)] + + if self.input_files: + + if 'longitude' in self.input_files[0].variables: + lon_var = 'longitude' + lat_var = 'latitude' + else: + lon_var = 'lon' + lat_var = 'lat' + + lon = self.input_files[0].variables[lon_var][:] + lat = self.input_files[0].variables[lat_var][:] + self.var = [var for var in self.input_files[0].variables if var == OBS[obs]['obs_var']][0] + logging.debug('VARNAME %s', self.var) + + sites_path = os.path.join(DIR_PATH, 'conf/', OBS[obs]['sites']) + if os.path.exists(sites_path): + sites = pd.read_csv(sites_path) + idxs, self.station_names = np.array([[idx, st_name[~st_name.mask].tobytes().decode('utf-8')] + for idx, st_name in + enumerate(self.input_files[0].variables['station_name'][:]) + if st_name[~st_name.mask].tobytes().decode('utf-8').upper() in map(str.upper, sites['SITE'])] + ).T + + logging.debug('IDXS %s', idxs) + logging.debug('ST_NAMES %s', self.station_names) + + self.clon = lon[idxs.astype(int)] + self.clat = lat[idxs.astype(int)] + + self.values = { + self.var: np.concatenate([input_file.variables[self.var][:, idxs.astype(int)] + for input_file in self.input_files]) + } + + return None + + def generate_var_tstep_trace(self): + """ Generate GeoJSON trace to be added """ + + val = np.ma.masked_where(self.values[self.var]<0., self.values[self.var]) + notnan = (np.array([i for i in range(val.shape[1]) if not val[:, i].mask.all()]),) + clon = self.clon[notnan] + clat = self.clat[notnan] + cstations = self.station_names[notnan] + + # Create dataframe + self.df = pd.DataFrame({ + 'lon': clon.round(2), + 'lat': clat.round(2), + 'stations': cstations + }) # .T, columns=['lon', 'lat', 'station']) + dicts = self.df.to_dict('records') + + # Create geojson + # Note: It must be JavaScript as it is executed in clientside + geojson_data = dlx.dicts_to_geojson(dicts, lon="lon") + namespace = Namespace("evaluationTab", "evaluationMaps") + self.geojson = self.retrieve_geojson(geojson_data, namespace) + + return None + + def get_figure_layers(self): + """ Get dataframe and figure layers (GeoJSON trace) + + Returns + ------- + pandas.core.frame.DataFrame + Dataframe with stations information + list + Figure layers + """ + + logging.debug('Updating layout ...') + + if self.input_files: + # Get trace + self.generate_var_tstep_trace() + + else: + self.geojson = None + self.df = pd.DataFrame([]) + + return self.df, [self.geojson] + + +class EvaluationSatelliteFigureHandler(ContourFigureHandler): + """ Class which handles MODIS sensor data """ + + def __init__(self, var, obs, tstep, selected_date): + """ Initialize EvaluationSatelliteFigureHandler class + + Parameters + ---------- + var : str + Variable name + obs : str + Observations name + tstep : int + Timestep + selected_date : str + Selected date + """ + + self.obs = obs + self.name = self.obs + super(EvaluationSatelliteFigureHandler, self).__init__() + + self.var = var + self.selected_date = selected_date + + self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') + self.style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) + self.colorscale = COLORS + + if self.selected_date: + # Get NetCDF file path + filedir = OBS[self.obs]['path'] + filetpl = OBS[self.obs]['template'].format(OBS[self.obs]['obs_var'], + self.selected_date) + '.nc' + self.filepath = os.path.join(filedir, 'netcdf', filetpl) + + if os.path.exists(self.filepath): + # Read NetCDF file and retrieve timesteps, what (hours, months, etc.) and rdatetime + self.timesteps, self.what, self.rdatetime = self.get_time_details() + + return None + + def generate_var_tstep_trace(self, tstep): + """ Generate GeoJSON trace to be added per timestep + + Parameters + ---------- + tstep : int + Timestep + """ + + namespace = Namespace("forecastTab", "forecastMaps") + data_path = os.path.basename(OBS[self.obs]['path'][:-1]) + geojson_path = os.path.join('geojsons', GEOJSON_TEMPLATE.format(data_path, + self.selected_date, tstep, self.selected_date, OBS[self.obs]['obs_var'])) + self.geojson = self.retrieve_geojson(geojson_path, namespace) + + return None + + def generate_colorbar(self): + """ Generate colorbar """ + + ctg = ["{:d}".format(int(cls)) if cls.as_integer_ratio()[1] == 1 else "{:.1f}".format(cls) + for i, cls in enumerate(self.bounds[1:-1])] + indices = list(range(len(ctg) + 2)) + self.colorbar_info.update({'min': 0, + 'max': len(ctg)+1, + 'classes': indices, + 'colorscale': COLORS, + 'tickValues': indices[1:-1], + 'tickText': ctg}) + self.colorbar = self.retrieve_colorbar() + + logging.debug("BOUNDS %s", self.bounds) + logging.debug("CTG %s", ctg) + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + varname = kwargs['varname'] + tstep = kwargs['tstep'] + + self.base_title = " ".join([OBS[self.obs]['name'], OBS[self.obs]['title']]) + self.cdatetime = self.retrieve_cdatetime(tstep) + self.step = "{:02d}".format(tstep*FREQ) + title = super(EvaluationSatelliteFigureHandler, self).get_title() + + return title + + def get_figure_layers(self, tstep=0): + """ Get figure layers (GeoJSON trace, colorbar and information box with title) + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + + Returns + ------- + list + Figure layers + """ + + logging.debug('Updating layout ...') + + if self.var and os.path.exists(self.filepath): + # Get trace and colorbar + self.generate_colorbar() + self.generate_var_tstep_trace(tstep) + + # Get figure title + self.fig_title = html.P(html.B( + [ + item for sublist in self.get_title(varname=self.var, tstep=tstep).split('
') + for item in [sublist, html.Br()] + ][:-1] + )) + else: + self.geojson = None + self.colorbar = None + + # Get figure title + if self.obs in OBS[self.obs]: + title = "{} - DATA NOT AVAILABLE".format(OBS[self.obs]['name']) + else: + title = "DATA NOT AVAILABLE" + self.fig_title = html.P(html.B(title)) + + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.colorbar, self.info] + + +class EvaluationStatisticsFigureHandler(PointsFigureHandler): + """ Class which handles evaluation statistics data """ + + def __init__(self, network=None, statistic=None, model=None, selection=None): + """ Initialize EvaluationStatisticsFigureHandler class + + Parameters + ---------- + network : str, optional + Network name, by default None + statistic : str, optional + Statistic name, by default None + model : str, optional + Model name, by default None + selection : str, optional + Period selection, by default None + """ + + self.name = 'scores' + super(EvaluationStatisticsFigureHandler, self).__init__() + + # self.extent = SCORES['extent'] + self.model = model + self.network = network + self.statistic = statistic + self.selection = selection + + if self.network and self.model and self.statistic and self.selection: + + # Define markers style + self.bounds = np.array(STATS_CONF[self.statistic]['bounds']) + self.cmap = cm.get_cmap(STATS_CONF[self.statistic]['cmap'], len(self.bounds)) + self.colormap = {} + for i in range(self.cmap.N): + self.colormap[i] = matplotlib.colors.rgb2hex(self.cmap(i)) + self.labels = list(self.colormap.keys()) + self.colors = list(self.colormap.values()) + self.circle_options = SCORES['circle_options'] + + # Get stations data + if network == 'aeronet': + self.sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[network]['sites'])) + self.circle_options.update({'radius': 6}) + else: + self.sites = None + self.circle_options.update({'radius': 2}) + + # Get selected month / year + # Set day to first date of the month to transform into datetime and avoid errors creating title + year = int(self.selection[0:4]) + month = int(self.selection[4:6]) + self.rdatetime = dt(year, month, 1) + + # Read data + self.read_data() + + return None + + def read_data(self): + """ Read data """ + + # Get HDF5 file + filedir = OBS[self.network]['path'] + filename = "{}_{}.h5".format(self.selection, self.statistic) + tab_name = "{}_{}".format(self.statistic, self.selection) + self.filepath = os.path.join(filedir, "h5", filename) + + # Read data + if os.path.exists(self.filepath): + self.data = pd.read_hdf(self.filepath, tab_name) + logging.debug('SCORES FILEPATH %s SELECTION %s TAB %s', self.filepath, self.selection, tab_name) + + return None + + def select_data(self): + """ Select data + + Returns + ------- + numpy.ndarray + Longitudes + numpy.ndarray + Latitudes + numpy.ndarray + Stations + numpy.ndarray + Scores + numpy.ndarray + Correspondence on legend scale + """ + + # Get coordinates for each site + if self.sites is not None: + for station in self.sites['SITE']: + self.data.loc[self.data['station'] == station, 'lon'] = \ + self.sites.loc[self.sites['SITE'] == station, 'LONGITUDE'].values[0] + self.data.loc[self.data['station'] == station, 'lat'] = \ + self.sites.loc[self.sites['SITE'] == station, 'LATITUDE'].values[0] + + # Transform empty values into np.nan + data = self.data.replace('-', np.nan) + + # Remove continents and stations without coordinates / model score + data = data.dropna(subset=['lat', 'lon', self.model]) + + # Transform into numeric (if needed) + for column in ['lat', 'lon', self.model]: + data[column] = pd.to_numeric(data[column]) + + # Get coordinates / scores / stations for selected model + lon = np.array(data['lon']) + lat = np.array(data['lat']) + scores = np.array(data[self.model]) + if self.sites is not None: + stations = np.array(data['station']) + else: + stations = None + + # Assign color index in colormap to scores + res = np.zeros((len(lon))) + for point_n, score in enumerate(scores): + for label_n, label in enumerate(self.labels): + # Skip last position + if label_n < self.labels[-1]: + # Below minimum limit, get first color in cmap + if float(score) < self.bounds[0]: + res[point_n] = self.labels[0] + break + # Above maximum limit, get last color in cmap + elif float(score) >= self.bounds[-1]: + res[point_n] = self.labels[-1] + break + # In between, find cmap color in range + elif (float(score) >= self.bounds[label_n]) and (float(score) < self.bounds[label_n+1]): + res[point_n] = self.labels[label_n] + break + + return lon, lat, stations, scores, res + + def generate_var_tstep_trace(self, lon, lat, stations, scores, res): + """ Generate GeoJSON trace to be added + + Parameters + ---------- + lon : numpy.ndarray + Longitudes + lat : numpy.ndarray + Latitudes + stations : numpy.ndarray + Stations + scores : numpy.ndarray + Scores + res : numpy.ndarray + Correspondence on legend scale + """ + + # Create dataframe + df = pd.DataFrame({ + 'lon': np.array(lon).round(2), + 'lat': np.array(lat).round(2), + 'score': scores.round(2), + 'value': res + }) + if stations is not None: + df['station'] = stations + var_list=['station', 'lon', 'lat', 'score'] + else: + var_list=['lon', 'lat', 'score'] + + # Add tooltips (hover information) to map + data_dict = self.get_tooltip(df, var_list=var_list) + + # Create geojson + geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") + namespace = Namespace("evaluationTab", "evaluationMaps") + self.geojson = self.retrieve_geojson(geojson_data, namespace) + + def generate_colorbar(self): + """ Generate colorbar """ + + bounds = STATS_CONF[self.statistic]['bounds'] + ctg = ["{}".format(cls) if '.' in str(cls) else "{:d}".format(cls) + for i, cls in enumerate(bounds)] + indices = list(range(len(bounds))) + self.colorbar_info.update({'min': bounds[0], + 'max': bounds[-1], + 'classes': len(bounds)-1, + 'colorscale': self.colors, + 'tickValues': self.bounds, + 'tickText': ctg}) + self.colorbar = self.retrieve_colorbar() + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + self.model_name = MODELS[self.model]['name'] + self.network_name = OBS[self.network]['name'] + self.statistic_name = STATS_CONF[self.statistic]['name'] + self.base_title = SCORES['title'] + title = super(EvaluationStatisticsFigureHandler, self).get_title() + + return title + + def get_figure_layers(self): + """ Get figure layers (GeoJSON trace, colorbar and information box with title) + + Returns + ------- + list + Figure layers + """ + + logging.debug('Updating layout ...') + + if (os.path.exists(self.filepath)) and (self.model in self.data.columns): + # Get data + lon, lat, stations, scores, res = self.select_data() + + # Get trace and colorbar + self.generate_colorbar() + self.generate_var_tstep_trace(lon, lat, stations, scores, res) + + # Get figure title + self.fig_title = html.P(html.B( + [ + item for sublist in self.get_title().split('
') + for item in [sublist, html.Br()] + ][:-1] + )) + else: + self.geojson = None + self.colorbar = None + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.colorbar, self.info] + + +class VisFigureHandler(PointsFigureHandler): + """ Class which handles visibility data """ + + def __init__(self, selected_date=None): + """ Initialize VisFigureHandler class + + Parameters + ---------- + selected_date : str, optional + Selected date, by default None + """ + + self.name = 'vis' + super(VisFigureHandler, self).__init__() + + self.selected_date = selected_date + + self.path_tpl = VIS['path'] + self.xlon = np.array(VIS['xlon']) + self.ylat = np.array(VIS['ylat']) + self.ec = VIS['ec'] + self.size = VIS['size'] + self.freq = VIS['freq'] + self.colormap = VIS['colormap'] + self.colors = list(VIS['colormap'].values()) + self.labels = list(VIS['colormap'].keys()) + self.values = VIS['values'] + self.circle_options = VIS['circle_options'] + + if self.selected_date: + self.rdatetime = dt.strptime(self.selected_date, '%Y%m%d') + + return None + + def read_data(self, tstep): + """ Read data + + Parameters + ---------- + tstep : int + Timestep + """ + + # Get CSV file + tstep0 = tstep + tstep1 = tstep + self.freq + logging.info("%s %s %s", self.selected_date, tstep0, tstep1) + year = dt.strptime(self.selected_date, '%Y%m%d').strftime('%Y') + month = dt.strptime(self.selected_date, '%Y%m%d').strftime('%m') + day = dt.strptime(self.selected_date, '%Y%m%d').strftime('%d') + self.filepath = self.path_tpl.format(year=year, month=month, day=day, tstep0=tstep0, + tstep1=tstep1) + + # Read data + if os.path.exists(self.filepath): + self.data = pd.read_table(self.filepath, na_filter=False) + logging.debug("VIS FILEPATH %s", self.filepath) + + return None + + def select_data(self, tstep=0): + """ Select data + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + + Returns + ------- + numpy.ndarray + Longitudes + numpy.ndarray + Latitudes + numpy.ndarray + Stations + numpy.ndarray + Visibilities + numpy.ndarray + Relative humidities + numpy.ndarray + Correspondence on legend scale + """ + + # Uncertain + cx = np.where((self.data['WW'].astype(str) == "HZ") | (self.data['WW'].astype(str) == "5")| (self.data['WW'].astype(str) == "05")) + + # Visibility <= 1km + c0t = np.where((self.data['VV'] <= 1000))[0] + c0x = np.where([c0t == i for i in cx[0]])[-1] + c0 = (np.delete(c0t, c0x),) + + # Visibility 1km <= 2km + c1t = np.where((self.data['VV'] > 1000) & (self.data['VV'] <= 2000))[0] + c1x = np.where([c1t == i for i in cx[0]])[-1] + c1 = (np.delete(c1t, c1x),) + + # Visibility 2km <= 5km + c2t = np.where((self.data['VV'] > 2000) & (self.data['VV'] <= 5000))[0] + c2x = np.where([c2t == i for i in cx[0]])[-1] + c2 = (np.delete(c2t, c2x),) + + lon = self.data['LON'].values + lat = self.data['LAT'].values + stations = self.data['STATION'].values + visibility = self.data['VV'] if 'VV' in self.data.columns else [] + humidity = self.data['HUMIDITY'] if 'HUMIDITY' in self.data.columns else [] + + # Make sure that the dtype is not object, this appears in files with missing values + humidity = pd.to_numeric(humidity, errors='coerce') + + # Replace np.nan by -999 + # In the case of humidity, we want to show the values as int and cannot do so with nan + humidity = humidity.replace(float('nan'), -999) + + # Assign colors to values + n_points = len(lon) + res = np.zeros((n_points)) + values = (c0, c1, c2, cx) + for i, (value, label) in enumerate(zip(values, self.labels)): + res[value] = i + + return lon, lat, stations, np.array(visibility), np.array(humidity), res + + def generate_var_tstep_trace(self, lon, lat, stations, visibility, humidity, res, tstep=0): + """ Generate GeoJSON trace to be added per timestep + + Parameters + ---------- + lon : numpy.ndarray + Longitudes + lat : numpy.ndarray + Latitudes + stations : numpy.ndarray + Stations + visibility : numpy.ndarray + Visibilities + humidity : numpy.ndarray + Humidities + res : tuple + Correspondence on legend scale + tstep : int, optional + Timestep, by default 0 + """ + + if list(lon) and list(lat) and list(stations) and list(visibility) and list(humidity): + + # Create legend + self.legend = self.create_legend(self.colormap) + + # Create dataframe + df = pd.DataFrame({ + 'station': stations, + 'lon': lon.round(2), + 'lat': lat.round(2), + 'visibility': (visibility/1e3).round(2), + 'humidity': humidity.astype(int), + 'value': res + }) + + # Replace -999 by np.nan to revert previous change + df['humidity'] = df['humidity'].replace(-999, float('nan')) + + # Rename humidity to relative humidity + df = df.rename(columns = {'humidity':'relative humidity'}) + + # Add tooltips (hover information) to map + data_dict = self.get_tooltip(df, + var_list=['station', 'lon', 'lat', 'visibility', 'relative humidity']) + + # Create geojson + geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") + namespace = Namespace("observationsTab", "observationsMaps") + self.geojson = self.retrieve_geojson(geojson_data, namespace) + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + tstep = None if 'tstep' not in kwargs else kwargs['tstep'] + if tstep is None: + return "NO DATA AVAILABLE" + else: + self.base_title = VIS['title'] + + self.tstep0 = "{:02d}".format(tstep) + self.tstep1 = "{:02d}".format(tstep + self.freq) + title = super(VisFigureHandler, self).get_title() + + return title + + def get_figure_layers(self, tstep=0): + """ Get figure layers (GeoJSON trace, legend and information box with title) + + Parameters + ---------- + tstep : int, optional + Timestep, by default 0 + + Returns + ------- + list + Figure layers + """ + + logging.debug('Updating layout ...') + + # Read data + if self.selected_date: + self.read_data(tstep) + + if os.path.exists(self.filepath): + # Get trace + tstep = int(tstep) + xlon, ylat, stations, visibility, humidity, res = self.select_data(tstep) + self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, res, tstep) + + # Get figure title + self.fig_title = html.P(html.B( + [ + item for sublist in self.get_title(tstep=tstep).split('
') + for item in [sublist, html.Br()] + ][:-1] + )) + else: + self.geojson = None + self.legend = None + + # Get figure title + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.legend, self.info] diff --git a/preproc/interp.py b/preproc/interp.py deleted file mode 100644 index 7a211faab3fabdc7256b1182b93c692c4a6579c5..0000000000000000000000000000000000000000 --- a/preproc/interp.py +++ /dev/null @@ -1,94 +0,0 @@ -#import numpy as np -import xarray as xr -import dask -#from dask.diagnostics import ProgressBar -#import matplotlib.pyplot as plt - -dask.config.set(scheduler='processes') - -EXP = { - 'cams': '../data/models/CAMS/netcdf/20200*.nc4', - 'monarch': '../data/models/MONARCH/netcdf/20200*.nc4', - 'dream8-macc': '../data/models/DREAM8-MACC/netcdf/20200*.nc4', - 'nasa-geos': '../data/models/NASA-GEOS/netcdf/20200*.nc4', - 'median': '../data/models/median/netcdf/20200*.nc4', -} -OBS = '../data/obs/aeronet/netcdf/od550aero_2020*.nc' -FIL = '../data/obs/aeronet/netcdf/ae440-870aero_20200*.nc' - -DEST = '../data/obs/aeronet/feather' - - -def preprocess(ds, n=8): - '''keep only the first N timestep for each file''' - return ds.isel(time=range(n)) - - -def plot_station(i=0): - - obs_ds = xr.open_mfdataset(OBS, parallel=True) - filter_ds = xr.open_mfdataset(FIL, parallel=True) - - obs_df = obs_ds['od550aero'].to_dataframe() - obs_df.reset_index(inplace=True) - obs_df.to_feather( - '{}/{}.ft'.format(DEST, 'od550aero'), - ) - filter_df = filter_ds['ae440-870aero'].to_dataframe() - filter_df.reset_index(inplace=True) - filter_df.to_feather( - '{}/{}.ft'.format(DEST, 'ae440-870aero'), - ) - - obs_lon = obs_ds['longitude'][0].data - obs_lat = obs_ds['latitude'][0].data - -# olon = obs_lon.compute() -# olat = obs_lat.compute() - -# print("Station with LON: {} and LAT: {}".format(olon[i], olat[i])) - # interpolated = [] - print(obs_ds['od550aero'].shape) - print(filter_ds['ae440-870aero'].shape) - for exp in EXP: - print(exp) - exp_ds = xr.open_mfdataset(EXP[exp], concat_dim='time', - preprocess=preprocess, parallel=True) - - da = exp_ds['od550_dust'] - da - print(exp_ds['od550_dust'].shape) - int_data = exp_ds['od550_dust'].interp(lon=obs_lon, lat=obs_lat) - print(int_data.shape) - int_df = int_data.to_dataframe() - int_df.reset_index(inplace=True) - int_df.to_feather( - '{}/{}.ft'.format(DEST, exp), - ) -# interpolated.append(int_data[:, i, i]) - -# for ar in interpolated: -# print(ar) -# print(obs_ds['od550aero']) -# print(filter_ds['ae440-870aero']) -# merged = xr.merge(interpolated + [obs_ds['od550aero'][:, i], filter_ds['ae440-870aero'][:, i]], -# compat='no_conflicts',) -# -# fig, axes = plt.subplots() -# -# print("Station with LON: {} and LAT: {}".format(olon[i], olat[i])) -# with ProgressBar(): -# data = merged.compute().astype('float32') -# print((data == np.nan).all()) -# print(data) -# data.plot.line() -# plt.savefig('test.png') -# print('done') - - -if __name__ == "__main__": - plot_station() -# import sys -# i = int(sys.argv[1]) -# j = int(sys.argv[2]) -# for s in range(i, j): diff --git a/preproc/nc2scores_aeronet.py b/preproc/nc2scores_aeronet.py index c616294b7858656cc3a95ae8bdd60a9c2556cdad..60d194139874897ec319114d1a6b56504228ef29 100755 --- a/preproc/nc2scores_aeronet.py +++ b/preproc/nc2scores_aeronet.py @@ -12,7 +12,7 @@ import os import sys from datetime import datetime from dateutil.relativedelta import relativedelta -from data_handler import ObsTimeSeriesHandler +from timeseries_handler import EvaluationGroundTimeSeriesHandler CURRENT_PATH = os.path.abspath(os.path.dirname(__file__)) VARS = json.load(open(os.path.join(CURRENT_PATH, '../conf/vars.json'))) @@ -110,7 +110,7 @@ def convert2timeseries(model, obs=None, months=[]): last_day = (datetime.strptime(last_month_fd, "%Y%m%d") + relativedelta(months=1) - relativedelta(days=1)).strftime("%Y%m%d") print("**********", months, first_day, last_day, "************") - timeseries_obj = ObsTimeSeriesHandler(obs, first_day, last_day, variable, models=model) + timeseries_obj = EvaluationGroundTimeSeriesHandler(obs, first_day, last_day, variable, models=model) dataframes = timeseries_obj.dataframe print(dataframes) diff --git a/preproc/nc2scores_modis.py b/preproc/nc2scores_modis.py index be0d6bb18d3798b033d83f15318d8f61c8edbe5c..d2588e6cae4b912a57490b2a2e902b9c0e73447b 100755 --- a/preproc/nc2scores_modis.py +++ b/preproc/nc2scores_modis.py @@ -13,7 +13,7 @@ import os import sys from datetime import datetime from dateutil.relativedelta import relativedelta -from data_handler import ObsTimeSeriesHandler +from timeseries_handler import EvaluationGroundTimeSeriesHandler from data_handler import STATS @@ -149,7 +149,7 @@ def convert2timeseries(model, obs=None, months=None): last_day = (datetime.strptime(last_month_fd, "%Y%m%d") + relativedelta(months=1) - relativedelta(days=1)).strftime("%Y%m%d") print("**********", months, first_day, last_day, "************") - timeseries_obj = ObsTimeSeriesHandler(obs, first_day, last_day, variable, models=model) + timeseries_obj = EvaluationGroundTimeSeriesHandler(obs, first_day, last_day, variable, models=model) dataframes = timeseries_obj.dataframe print(dataframes) total_columns = ['model'] + list(STATS.keys()) diff --git a/requirements.txt b/requirements.txt index 3c34c70e86dba250ac7de2eda22aaddbf84f62b6..d9c0abaea69760fd2dd7b4130a18cb99ca24fbc6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,149 +1,102 @@ -anyio==3.6.2 -argon2-cffi==21.3.0 -argon2-cffi-bindings==21.2.0 -attrs==19.3.0 -Babel==2.11.0 -backcall==0.2.0 -bleach==3.1.5 -bokeh==2.2.3 -Brotli==1.0.7 -certifi==2020.12.5 -cffi==1.15.1 -cftime==1.1.3 -chardet==4.0.0 -click==7.1.2 +attrs==23.1.0 +backcall==0.1.0 +blosc2==2.0.0 +Bottleneck==1.3.7 +cachelib==0.9.0 +certifi==2023.5.7 +cftime==1.6.2 +chardet==3.0.4 +click==8.1.3 click-plugins==1.1.1 -cligj==0.7.1 -cloudpickle==1.6.0 -contextvars==2.4 +cligj==0.7.2 +cloudpickle==2.2.1 +configobj==5.0.6 +contourpy==1.0.7 cycler==0.10.0 -dash==2.6.2 -dash-bootstrap-components==0.13.1 +Cython==0.29.35 +dash==2.10.2 +dash-bootstrap-components==1.4.1 dash-core-components==2.0.0 dash-daq==0.5.0 -dash-extensions==0.0.71 +dash-extensions==1.0.1 dash-html-components==2.0.0 dash-leaflet==0.1.23 dash-renderer==1.9.1 dash-table==5.0.0 -dask==2.30.0 -dataclasses==0.8 -decorator==4.4.2 -defusedxml==0.6.0 -distributed==2.30.1 -dnspython==1.16.0 +dask==2023.6.0 +decorator==4.2.1 EditorConfig==0.12.3 -entrypoints==0.3 -eventlet==0.30.2 feather-format==0.4.1 -Fiona==1.8.18 -Flask==2.0.0 -Flask-Caching==1.10.1 -Flask-Compress==1.5.0 -fsspec==0.8.4 -future==0.18.2 +Fiona==1.9.4.post1 +Flask==2.2.5 +Flask-Caching==2.0.2 +fonttools==4.39.4 +fsspec==2023.6.0 geobuf==1.1.1 -geopandas==0.9.0 -gevent==21.1.2 -gif==3.0.0 -greenlet==1.1.2 +geopandas==0.13.2 +gevent==22.10.2 +greenlet==2.0.2 gunicorn==20.1.0 -HeapDict==1.0.1 -idna==2.10 -immutables==0.14 -importlib-metadata==4.8.3 -ipykernel==5.3.0 +idna==2.5 +importlib-metadata==6.6.0 +iniconfig==2.0.0 ipython==7.16.1 -ipython-genutils==0.2.0 -ipywidgets==7.5.1 -itsdangerous==2.0.0 -jedi==0.17.1 -Jinja2==3.0.0 -joblib==1.0.1 -jsbeautifier==1.14.0 -json5==0.9.10 -jsonschema==3.2.0 -jupyter==1.0.0 -jupyter-client==6.1.3 -jupyter-console==6.1.0 -jupyter-core==4.6.3 -jupyter-server==1.13.1 -jupyterlab==3.2.9 -jupyterlab-server==2.10.3 -kaleido==0.2.1 -kiwisolver==1.2.0 -locket==0.2.0 -MarkupSafe==2.0.0 -matplotlib==3.2.2 -mistune==0.8.4 -more-itertools==8.7.0 -msgpack==1.0.0 -munch==2.5.0 -nbclassic==0.3.5 -nbconvert==5.6.1 -nbformat==5.0.7 -netCDF4==1.5.3 -notebook==6.0.3 -numexpr==2.7.3 -numpy==1.19.5 -orca==1.6 -orjson==3.6.1 -packaging==20.4 -pandas==1.0.5 -pandocfilters==1.4.2 -parso==0.7.0 -partd==1.1.0 -pexpect==4.8.0 +ipython-genutils==0.1.0 +itsdangerous==2.1.2 +jedi==0.15.1 +Jinja2==3.1.2 +joblib==1.2.0 +jsbeautifier==1.14.8 +kiwisolver==1.1.0 +locket==1.0.0 +MarkupSafe==2.1.3 +matplotlib==3.7.2 +more-itertools==9.1.0 +msgpack==1.0.5 +netCDF4==1.6.4 +numexpr==2.8.4 +numpy==1.24.3 +orjson==3.9.0 +packaging==23.1 +pandas==2.0.2 +parso==0.5.1 +partd==1.4.0 +pexpect==4.3.1 pickleshare==0.7.5 -Pillow==8.0.1 -plotly==5.10.0 -prometheus-client==0.8.0 -prompt-toolkit==3.0.5 +Pillow==9.5.0 +plotly==5.14.1 +pluggy==1.0.0 +ply==3.9 +prompt-toolkit==2.0.10 protobuf==3.19.4 -psutil==5.7.3 -ptyprocess==0.6.0 -pyarrow==0.17.1 -pycparser==2.21 -Pygments==2.6.1 -pyparsing==2.4.7 -pyproj==3.0.1 -pyrsistent==0.16.0 -python-dateutil==2.8.1 -pytz==2020.1 -PyYAML==5.3.1 -pyzmq==19.0.1 -qtconsole==4.7.5 -QtPy==1.9.0 -requests==2.25.1 -retrying==1.3.3 -scikit-learn==0.24.1 -scipy==1.5.4 -Send2Trash==1.5.0 -Shapely==1.7.1 -six==1.15.0 -sklearn==0.0 -sniffio==1.2.0 -sortedcontainers==2.3.0 -tables==3.6.1 -tabulate==0.8.10 -tblib==1.7.0 -tenacity==8.0.1 -terminado==0.8.3 -testpath==0.4.4 -threadpoolctl==2.1.0 -toolz==0.11.1 -tornado==6.1 -traitlets==4.3.3 -typing-extensions==3.7.4.3 -urllib3==1.26.5 -Wand==0.6.6 -wcwidth==0.2.5 -webencodings==0.5.1 -websocket-client==1.3.1 -Werkzeug==2.0.0 -widgetsnbextension==3.5.1 -xarray==0.16.2 -zict==2.0.0 -zipp==3.1.0 -zope.event==4.5.0 -zope.interface==5.4.0 +ptyprocess==0.5.2 +py-cpuinfo==9.0.0 +pyarrow==12.0.0 +Pygments==2.2.0 +pyparsing==3.0.9 +pyproj==3.5.0 +PySocks==1.6.8 +pytest==7.3.2 +python-dateutil==2.8.2 +pytz==2023.3 +pyudev==0.21.0 +PyYAML==6.0 +requests==2.20.0 +scikit-learn==1.2.2 +scipy==1.10.1 +shapely==2.0.1 +six==1.16.0 +tables==3.8.0 +tabulate==0.9.0 +tenacity==8.2.2 +threadpoolctl==3.1.0 +toolz==0.12.0 +traitlets==5.9.0 +tzdata==2023.3 +urllib3==1.24.2 +wcwidth==0.2.6 +Werkzeug==2.2.3 +xarray==2023.5.0 +zipp==3.15.0 +zope.event==4.6 +zope.interface==6.0 \ No newline at end of file diff --git a/router.py b/router.py index 013c57a0baac7ad349332fa22d82fea5a858fd01..80eeb62755be439fb8d29932d9a1117a021cd765 100644 --- a/router.py +++ b/router.py @@ -7,12 +7,12 @@ from dash import dcc from dash import html from dash.dependencies import Output from dash.dependencies import Input -from data_handler import VARS -from data_handler import MODELS -from data_handler import DEBUG -from data_handler import PATHNAME -from data_handler import ALIASES -from data_handler import ROUTE_DEFAULTS +from map_handler import VARS +from map_handler import MODELS +from map_handler import DEBUG +from map_handler import ALIASES +from map_handler import ROUTE_DEFAULTS +from ines_core_utils import render404 from tabs.forecast import tab_forecast from tabs.forecast import sidebar_forecast @@ -22,6 +22,7 @@ from tabs.observations import tab_observations from tabs.observations import sidebar_observations from tabs.fullscreen import go_fullscreen +import logging #-------------------- ADD ROUTING FUNCTIONS ---------------------- def get_input_aliases(route_selections): @@ -36,13 +37,15 @@ def get_input_aliases(route_selections): def eval_section_query(queries): """pull SECTION var from url query and assign value to appropriate option""" + if 'section' in queries: - if queries['tab']==['forecast-tab']: + if queries['tab'] == ['forecast-tab']: queries['for_option'] = queries['section'] - elif queries['tab']==['evaluation-tab']: + elif queries['tab'] == ['evaluation-tab']: queries['eval_option'] = queries['section'] - elif queries['tab']==['observations-tab']: + elif queries['tab'] == ['observations-tab']: queries['obs_option'] = queries['section'] + return queries def get_url_queries(url, route_defaults=ROUTE_DEFAULTS): @@ -73,74 +76,36 @@ def render_sidebar(tab='forecast-tab', route_selections=ROUTE_DEFAULTS): sidebar_evaluation(route_selections['eval_option'][0]), 'observations-tab' : sidebar_observations(route_selections['obs_option'][0]), - #'fullscreen-tab' : 'Full' } - if tabs[tab] is 'Full': - return return tabs[tab] -def render404(): - """Create a 404 page""" - #setup routes for links - forecast_link = PATHNAME + "/?tab=forecast" - eval_link = PATHNAME + "/?tab=evaluation" - obs_link = PATHNAME + "/?tab=observations" - - page = [html.Div( - className='background', - children=[ - html.Div( - id='error_div', - children=[html.H2('404 Error', id='error_title'), - html.P("Sorry we can't find the page you were looking for."), - html.P("Here are some helpful links that might help:"), - dcc.Link("Forecast", id='forecast_link', - href=forecast_link, - className='error_links',target='_parent', - refresh=True), - html.Br(), - html.Br(), - dcc.Link("Evaluation", id='evaluation_link', - href=eval_link, - className='error_links',target='_parent', - refresh=True), - html.Br(), - html.Br(), - dcc.Link("Observations", id='observations_link', - href=obs_link, - className='error_links',target='_parent', - refresh=True) - ], - ) - ] - ) - ] - return page - @dash.callback( - Output("content", "children"), - Input("url", "href"), -) + Output("content", "children"), + Input("url", "href"), + ) def router(url): """ Get url search queries and build layout for app""" + route_selections = get_url_queries(url) - if DEBUG: print('===== route_selections', route_selections) - # try: - children = [ - html.Div( - id='app-sidebar', - children=render_sidebar(route_selections['tab'][0], - route_selections), - className='sidebar' - ), - dcc.Tabs(id='app-tabs', value=route_selections['tab'][0] , children=[ - tab_forecast(window=route_selections['for_option'][0], end_date=route_selections['date'][0]), - tab_evaluation(route_selections['eval_option'][0]), #nrt or scores - tab_observations(route_selections['obs_option'][0]),#rgb or visibility - go_fullscreen(), - ]), - ] -# except Exception as err: #This handles when user inputs incorrect URL params -# if DEBUG: print("ERROR 404", str(err)) -# children = render404() + logging.debug('===== route_selections %s', route_selections) + + try: + children = [ + html.Div( + id='app-sidebar', + children=render_sidebar(route_selections['tab'][0], + route_selections), + className='sidebar' + ), + dcc.Tabs(id='app-tabs', value=route_selections['tab'][0] , children=[ + tab_forecast(window=route_selections['for_option'][0], end_date=route_selections['date'][0]), + tab_evaluation(route_selections['eval_option'][0]), #nrt or scores + tab_observations(route_selections['obs_option'][0]),#rgb or visibility + go_fullscreen(), + ]), + ] + except Exception as err: # This handles when user inputs incorrect URL params + logging.error("ERROR 404 %s", str(err)) + children = render404() + return children diff --git a/tabs/evaluation.py b/tabs/evaluation.py index 602b0bf2608258c7c1a25f84a85f2782366f41eb..3fcf96ddbb423b7b96cf1982977cdd5a187cdee0 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -2,17 +2,15 @@ from dash import dcc import dash_bootstrap_components as dbc from dash import html from dash import dash_table -from data_handler import DEFAULT_VAR -from data_handler import DEFAULT_MODEL -from data_handler import FREQ -from data_handler import VARS -from data_handler import MODELS -from data_handler import OBS -from data_handler import STYLES -from data_handler import DATES -from data_handler import STATS -from data_handler import MODEBAR_CONFIG -from data_handler import DISCLAIMER_NO_FORECAST +from map_handler import DEFAULT_VAR +from map_handler import DEFAULT_MODEL +from map_handler import FREQ +from map_handler import VARS +from map_handler import MODELS +from map_handler import OBS +from map_handler import DATES +from map_handler import STATS +from map_handler import DISCLAIMER_MODELS from datetime import datetime as dt from datetime import timedelta @@ -36,7 +34,7 @@ scores_maps = dbc.Spinner( id='obs-models-dropdown-modal', options=[{'label': MODELS[model]['name'], 'value': model} for model in MODELS], - value=DEFAULT_MODEL, + # value=DEFAULT_MODEL, clearable=False, searchable=False, # className="sidebar-dropdown" @@ -52,7 +50,7 @@ scores_maps = dbc.Spinner( options=[ {'label': v, 'value': l} for l, v in STATS.items() if l != 'totn' ], - value='bias', + # value='bias', clearable=False, searchable=False, # className="sidebar-dropdown" @@ -61,13 +59,10 @@ scores_maps = dbc.Spinner( style={ 'width': '10rem' }, className="linetool", ), - dcc.Graph( - id='scores-map-modalbody', - figure={}, - config=MODEBAR_CONFIG, # {"displayModeBar": False} - ), - html.Div(DISCLAIMER_NO_FORECAST, - className='disclaimer') + html.Div(children={}, + id="scores-map-modalbody" + ), + html.Div(DISCLAIMER_MODELS, className='disclaimer') ] )], id='scores-map-modal', @@ -235,7 +230,7 @@ def tab_evaluation(window='nrt'): id='obs-models-dropdown', options=[{'label': MODELS[model]['name'], 'value': model} for model in MODELS], - #value=[default_model,], + # value=[DEFAULT_MODEL,], className="sidebar-dropdown" ) ], @@ -254,7 +249,7 @@ def tab_evaluation(window='nrt'): options=[ {'label': v, 'value': l} for l, v in STATS.items() ], - #value=[default_model,], + # value=[DEFAULT_MODEL,], className="sidebar-dropdown" ) ], @@ -272,7 +267,7 @@ def tab_evaluation(window='nrt'): {'label': 'Annual', 'value': 'annual'}, ], placeholder='Select timescale', - value='montly', + # value='monthly', clearable=False, searchable=False )], @@ -286,7 +281,7 @@ def tab_evaluation(window='nrt'): options=[ ], placeholder='Select month', - # value='montly', + # value='monthly', clearable=False, searchable=False )], @@ -431,24 +426,24 @@ def sidebar_evaluation(window='nrt'): searchable=False, optionHeight=70, disabled=True, - )], - id='evaluation-variable', - className="sidebar-first-item", - ), - html.Div([ - dbc.Button("Visual comparison", - color="link", - id='nrt-evaluation', - style = nrt_style - )], - className="sidebar-item", - ), - html.Div([ - dbc.Button("Statistics", - color="link", - id='scores-evaluation', - style = scores_style - )], - className="sidebar-item", - ), -] + )], + id='evaluation-variable', + className="sidebar-first-item", + ), + html.Div([ + dbc.Button("Visual comparison", + color="link", + id='nrt-evaluation', + style = nrt_style + )], + className="sidebar-item", + ), + html.Div([ + dbc.Button("Statistics", + color="link", + id='scores-evaluation', + style = scores_style + )], + className="sidebar-item", + ), + ] diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 7a5b1162309a10049476c4784d98da5c5527c6c0..d5e18f191064c22975b3c966a1b9afc451847005 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -8,20 +8,23 @@ from dash.dependencies import Input from dash.dependencies import State from dash.exceptions import PreventUpdate import dash_leaflet as dl -from data_handler import DEFAULT_VAR -from data_handler import VARS -from data_handler import MODELS -from data_handler import OBS -from data_handler import DEBUG -from data_handler import END_DATE -from data_handler import MODEBAR_CONFIG_TS -from data_handler import MODEBAR_LAYOUT_TS -from data_handler import DISCLAIMER_NO_FORECAST -from data_handler import DISCLAIMER_OBS -from data_handler import cache, cache_timeout +from map_handler import DEFAULT_VAR +from map_handler import VARS +from map_handler import MODELS +from map_handler import OBS +from map_handler import DEBUG +from map_handler import END_DATE +from map_handler import MODEBAR_CONFIG_TS +from map_handler import MODEBAR_LAYOUT_TS +from map_handler import DISCLAIMER_MODELS +from map_handler import DISCLAIMER_OBS +from map_handler import cache, cache_timeout from tabs.evaluation import tab_evaluation from tabs.evaluation import STATS +from ines_core_utils import extend_list +from utils import format_floats +from utils import alphabetize_stations from datetime import datetime as dt from datetime import timedelta @@ -30,22 +33,13 @@ import pandas as pd import orjson import os.path from random import random +import logging SCORES = list(STATS.keys()) -def extend_l(l): - """ Estend list of lists to a flat list """ - res = [] - for x in l: - res.extend(x) - if not isinstance(res, list): - res = [res] - return res - - -#def register_callbacks(app, cache, cache_timeout): +# def register_callbacks(app, cache, cache_timeout): # """ Registering callbacks """ @dash.callback( @@ -57,14 +51,31 @@ def extend_l(l): prevent_initial_call=True ) def render_evaluation_tab(nrtbutton, scoresbutton): - """ Function rendering requested tab """ + """ Render evaluation tab + + Parameters + ---------- + nrtbutton : int + Number of clicks on Visual Comparison button + scoresbutton : int + umber of clicks on Statistics button + + Returns + ------- + dash.dcc.Tab.Tab + Tab + dict + Font weight for Visual Comparison button + dict + Font weight for Statistics button + """ + bold = { 'fontWeight': 'bold' } norm = { 'fontWeight': 'normal' } ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if button_id == "nrt-evaluation" and nrtbutton: return tab_evaluation('nrt'), bold, norm elif button_id == "scores-evaluation" and scoresbutton: @@ -81,7 +92,23 @@ def render_evaluation_tab(nrtbutton, scoresbutton): prevent_initial_call=True ) def update_time_selection(timescale, network): - """ Update time selection among different networks """ + """ Update time selection among different networks + + Parameters + ---------- + timescale : str + Selected timescale (Monthly, Seasonal or Annual) + network : str + Network name + + Returns + ------- + list + List of dicts containing reference to dates (e.g. {'label': 'May 2022', 'value': '202205'}) + str + Selection label + """ + if timescale is None: raise PreventUpdate @@ -121,7 +148,7 @@ def update_time_selection(timescale, network): return ret, placeholder def _no_modis_data(): - """ Return the data needed to make MODIS table output NO DATA""" + """ Return the data needed to make MODIS table output NO DATA """ return [{'name': 'NO DATA', 'id': ''}], [], { 'display': 'block'} @dash.callback( @@ -136,21 +163,44 @@ def _no_modis_data(): State('obs-selection-dropdown', 'value')], prevent_initial_call=True ) -def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection): - """ Read scores tables and show data """ - - if not n or network != 'modis': +def modis_scores_tables_retrieve(n_clicks, models, score, network, timescale, selection): + """ Read scores tables and show data for MODIS + + Parameters + ---------- + n_clicks : int + Number of clicks on APPLY button + models : str, list + Model names + score : str + Statistic + network : str + Network name + timescale : str + Timescale + selection : str + Period selection + + Returns + ------- + list + Column names + pandas.core.frame.DataFrame + Dataframe with statistics information + """ + + if not n_clicks or network != 'modis': return dash.no_update, dash.no_update, { 'display': 'none' } if isinstance(models, str): models = [models] - if isinstance(stat, str): - stat = [stat] + if isinstance(score, str): + score = [score] - stat = ['model'] + stat + score = ['model'] + score - if DEBUG: print("###########", models, stat, network, timescale, selection, n) + logging.debug("########### %s %s %s %s %s %s", models, score, network, timescale, selection, n_clicks) filedir = OBS[network]['path'] filename = f"{selection}_scores.h5" tab_name = f"total_{selection}" @@ -158,22 +208,21 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) if not os.path.exists(filepath): return _no_modis_data() df = pd.read_hdf(filepath, tab_name) - ret = df.loc[df['model'].isin(models), stat] + ret = df.loc[df['model'].isin(models), score] ret['model'] = ret['model'].map({k:MODELS[k]['name'] for k in MODELS}) - if DEBUG: - print('---', ret.columns) - print('---', ret.to_dict('records')) + logging.debug('--- %s', ret.columns) + logging.debug('--- %s', ret.to_dict('records')) # Check if there is any data in returned table, return No Data if not if len(ret.to_dict('records')) < 1: return _no_modis_data() columns = [{'name': i in SCORES and STATS[i] or '', 'id': i} for - i in stat] + i in score] return columns, ret.replace('_', ' ', regex=True).to_dict('records'), { 'display': 'block' } @dash.callback( - [Output('scores-map-modalbody', 'figure'), + [Output('scores-map-modalbody', 'children'), Output('scores-map-modal', 'is_open'), Output('obs-models-dropdown-modal', 'value'), Output('obs-statistics-dropdown-modal', 'value')], @@ -187,87 +236,76 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) ) @cache.memoize(timeout=cache_timeout) def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, orig_stats): - """ Read scores tables and plot maps """ - from tools import get_scores_figure + """ Read scores tables and plot maps + + Parameters + ---------- + n_clicks : int + Number of clicks on APPLY button + model : str + Model name + score : str + Statistic + network : str + Network name + selection : str + Period selection + orig_model : list + Selected model + orig_stats : list + Selected statistics + + Returns + ------- + dash_leaflet.Map + Figure + bool + Indicates whether the modal window is shown or not + str + Model name + str + Score name + """ + + from callback_tools import get_evaluation_statistics_figure + from callback_tools import get_figure + mb = MODEBAR_LAYOUT_TS ctx = dash.callback_context - if DEBUG: - print(':::', n_clicks, model, score, network, selection) + logging.debug('::: %s %s %s %s %s', n_clicks, model, score, network, selection) if ctx.triggered: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id != "scores-map-apply": if model is not None and score is not None: - if DEBUG: - print('::: 1 :::') - figure = get_scores_figure(network, model, score, selection) - figure.update_layout(mb) - return figure, True, model, score + logging.debug('::: 1 :::') + layers = get_evaluation_statistics_figure(network, model, score, selection) + fig = get_figure(layers=layers) + return fig, True, model, score raise PreventUpdate if orig_model and orig_stats: - if DEBUG: - print(':::', orig_model, orig_stats, ':::') + logging.debug('::: %s %s :::', orig_model, orig_stats) curr_model = [mod for mod in MODELS if mod in orig_model][0] - curr_stat = [sc for sc in SCORES if sc in orig_stats][0] - figure = get_scores_figure(network, curr_model, curr_stat, selection) - figure.update_layout(mb) - return figure, True, curr_model, curr_stat + curr_score = [sc for sc in SCORES if sc in orig_stats][0] + layers = get_evaluation_statistics_figure(network, curr_model, curr_score, selection) + fig = get_figure(layers=layers) + + return fig, True, curr_model, curr_score else: - print('::: 2.5 :::') - figure = get_scores_figure(network, model, score, selection) - figure.update_layout(mb) - return figure, True, model, score + logging.debug('::: 2.5 :::') + layers = get_evaluation_statistics_figure(network, model, score, selection) + fig = get_figure(layers=layers) + return fig, True, model, score return dash.no_update, False, dash.no_update, dash.no_update # PreventUpdate -def format_floats(dataframe): - """This function takes a dataframe and changes all columns except for 'station' - so that floats will be formatted to 2 digits after the decimal place""" - for col in dataframe.columns: - # check if the column is not 'station' - if col != 'station': - # convert the column to a string to allow for string formatting - dataframe[col] = dataframe[col].astype(str) - # iterate over the values in the column - for i, val in enumerate(dataframe[col]): - # check if the value is a float - if '.' in val: - # if so, format it to have 2 decimal places - dataframe.at[i, col] = '{:.2f}'.format(float(val)) - return dataframe - -def alphabetize_stations(dataframe): - """ This will alphabetize the stations for each region in the aeronet stats table""" - # Define a list of regions to sort between (in the desired order) - regions = ['Europe', 'Mediterranean', 'MiddleEast', 'NAfrica', 'Total'] - region_dfs = [] - # Iterate over each pair of consecutive regions and sort the rows between them - for i in range(len(regions)-1): - # Get the indices of the current and next regions - current_region_idx = dataframe[dataframe['station'] == regions[i]].index[0] - next_region_idx = dataframe[dataframe['station'] == regions[i+1]].index[0] - # Slice the dataframe to select the rows between the current and next regions - subset_df = dataframe.loc[current_region_idx+1:next_region_idx-1] - # Sort the rows alphabetically based on the 'station' column - subset_df = subset_df.sort_values(by='station') - # Create a new dataframe containing only the current region row - current_region_row = dataframe.loc[current_region_idx].to_frame().T - # Append the current region row and sorted subset dataframe to the list of region DataFrames - region_dfs.append(current_region_row) - region_dfs.append(subset_df) - # Add the 'Total' row back to the end of the sorted dataframe - total_row_idx = dataframe[dataframe['station'] == 'Total'].index[0] - total_row = dataframe.loc[total_row_idx].to_frame().T - region_dfs.append(total_row) - # Concatenate all of the region DataFrames into a single sorted DataFrame - sorted_df = pd.concat(region_dfs) - return sorted_df @dash.callback( - extend_l([ + extend_list([ [Output(f'aeronet-scores-table-{score}', 'columns'), Output(f'aeronet-scores-table-{score}', 'data'), Output(f'aeronet-scores-table-{score}', 'style_table'), @@ -282,33 +320,44 @@ def alphabetize_stations(dataframe): State('obs-network-dropdown', 'value'), State('obs-timescale-dropdown', 'value'), State('obs-selection-dropdown', 'value')] + - extend_l([[ + extend_list([[ State(f'aeronet-scores-table-{score}', 'columns'), State(f'aeronet-scores-table-{score}', 'data'), State(f'aeronet-scores-table-{score}', 'style_table')] for score in SCORES]), prevent_initial_call=True ) -def aeronet_scores_tables_retrieve(n, *args): - """ Read scores tables and show data """ +def aeronet_scores_tables_retrieve(n_clicks, *args): + """ Read scores tables and show data for AERONET + + Parameters + ---------- + n_clicks : int + Number of clicks on APPLY button + + Returns + ------- + list + Dataframe columns, data, tables style, selected cells and active cells + """ ctx = dash.callback_context active_cells = list(args[:len(SCORES)]) tables = list(args[-len(SCORES)*3:]) - if DEBUG: print("ACTIVES", active_cells) + logging.debug("ACTIVES %s", active_cells) - if DEBUG: print("###########", args[len(SCORES):-len(SCORES)*3]) + logging.debug("########### %s", args[len(SCORES):-len(SCORES)*3]) models, stat, network, timescale, selection = args[len(SCORES):-len(SCORES)*3] if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("BUTTON", button_id) + logging.debug("BUTTON %s", button_id) if button_id not in ['scores-apply'] + [f'aeronet-scores-table-{score}' for score in SCORES]: raise PreventUpdate - if not n or network != 'aeronet': - return extend_l([[dash.no_update, dash.no_update, { 'display': 'none' }, + if not n_clicks or network != 'aeronet': + return extend_list([[dash.no_update, dash.no_update, { 'display': 'none' }, dash.no_update, dash.no_update] for _ in SCORES]) # ORDER is IMPORTANT @@ -322,7 +371,7 @@ def aeronet_scores_tables_retrieve(n, *args): models = ['station'] + models - if DEBUG: print("@@@@@@@@@@@", models, stat, network, timescale, selection, n, len(tables)) + logging.debug("@@@@@@@@@@@ %s %s %s %s %s %s %s", models, stat, network, timescale, selection, n_clicks, len(tables)) filedir = OBS[network]['path'] stat_idxs = [SCORES.index(st) for st in stat] @@ -348,15 +397,15 @@ def aeronet_scores_tables_retrieve(n, *args): tab_name = "{}_{}".format(SCORES[table_idx], selection) filepath = os.path.join(filedir, "h5", filename) if not os.path.exists(filepath): - if DEBUG: print ("TABLES 0", tables) + logging.debug ("TABLES 0 %s", tables) #Build a no_data list of values to return no_data = [] for _ in range(len(SCORES)): no_data += [[], [], {'display': 'block'}, dash.no_update, None] # Add no data update for UI output only for 1 output no_data[0].append({'name':['NO DATA'], 'id':'station'}) - if DEBUG: print ("TABLES 1", tables) - if DEBUG: print ("No Data Table ", no_data) + logging.debug ("TABLES 1 %s", tables) + logging.debug ("No Data Table %s", no_data) return no_data df = pd.read_hdf(filepath, tab_name) @@ -369,15 +418,14 @@ def aeronet_scores_tables_retrieve(n, *args): i in models] # replace "tables" data if curr_active_cell is not None: - if DEBUG: print("ACTIVE", curr_active_cell) + logging.debug("ACTIVE %s", curr_active_cell) curr_data = tables[obj_idx+1] if not curr_data: continue row_number = curr_active_cell['row'] # 1st case: - if DEBUG: - print('CURRDATA', curr_data) - print('ROWNUMBER', row_number) + logging.debug('CURRDATA %s', curr_data) + logging.debug('ROWNUMBER %s', row_number) value = curr_data[row_number]['station'] if value not in areas[:-1]: raise PreventUpdate @@ -390,14 +438,13 @@ def aeronet_scores_tables_retrieve(n, *args): tables[obj_idx+1] = [table_row for table_row in curr_data if curr_data.index(table_row) < row_number] + \ - df.iloc[val_idx:foll_idx-1][models].to_dict('rows') + \ + df.iloc[val_idx:foll_idx-1][models].to_dict('records') + \ [table_row for table_row in curr_data if curr_data.index(table_row) > row_number] else: foll_area = areas[areas.index(value)+1] - if DEBUG: - print("'''", curr_data) - print("---", foll_area) + logging.debug("''' %s", curr_data) + logging.debug("--- %s", foll_area) foll_idx = curr_data.index([row for row in curr_data if row['station'] == foll_area][0]) tables[obj_idx+1] = [table_row @@ -426,8 +473,9 @@ def aeronet_scores_tables_retrieve(n, *args): ret_tables[ret_idx+4] = dash.no_update - if DEBUG: print('LEN', len(ret_tables)) - if DEBUG: print ("TABLES RET", ret_tables) + logging.debug('LEN %s', len(ret_tables)) + logging.debug ("TABLES RET %s", ret_tables) + return ret_tables @@ -442,25 +490,53 @@ def aeronet_scores_tables_retrieve(n, *args): prevent_initial_call=True ) @cache.memoize(timeout=cache_timeout) -def show_eval_modis_timeseries(nclicks, coords, date, obs, model): - """ Retrieve MODIS evaluation timeseries according to station selected """ - from tools import get_timeseries - if coords is None or nclicks == 0: +def show_eval_modis_timeseries(n_clicks, coords, date, obs, model): + """ Retrieve MODIS evaluation timeseries according to station selected + + Parameters + ---------- + n_clicks : int + Number of clicks on EXPLORE TIMESERIES button + coords : list + Latitude, longitude and value on coordinates + date : str + Selected date + obs : str + Observations name + model : str + Model name + + Returns + ------- + dash_bootstrap_components._components.ModalBody.ModalBody + Timeseries modal window + bool + Indicates whether the modal window is shown or not + """ + + from callback_tools import get_timeseries + if coords is None or n_clicks == 0: raise PreventUpdate ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) - if not ctxt or ctxt is None or ctxt != 'ts-eval-modis-button': # or nclicks == 0:P: + logging.debug("CTXT %s %s", ctxt, type(ctxt)) + if not ctxt or ctxt is None or ctxt != 'ts-eval-modis-button': # or n_clicks == 0:P: raise PreventUpdate - if DEBUG: print('TRIGGER', ctxt, type(ctxt)) + logging.debug('TRIGGER %s %s', ctxt, type(ctxt)) lat, lon, val = coords - print(coords, date) + logging.debug(" %s %s ", coords, date) models = [obs, model] # [model for model in MODELS] - if DEBUG: print('SHOW MODIS EVAL TS"""""', coords) + logging.debug('SHOW MODIS EVAL TS""""" %s', coords) figure = get_timeseries(models, date, DEFAULT_VAR, lat, lon) mb = MODEBAR_LAYOUT_TS figure.update_layout(mb) + print(type(dbc.ModalBody( + dcc.Graph( + id='timeseries-eval-modal', + figure=figure, + config=MODEBAR_CONFIG_TS + )))) return dbc.ModalBody( dcc.Graph( id='timeseries-eval-modal', @@ -478,26 +554,47 @@ def show_eval_modis_timeseries(nclicks, coords, date, obs, model): State('eval-date-picker', 'date'), State('obs-mod-dropdown', 'value')], ) -def modis_popup(click_data, mapid, date, model): - """ Manages popup info for modis """ - from tools import get_single_point - if DEBUG: print("CLICK:", str(click_data)) +def modis_popup(click_data, figure, date, model): + """ Manage popup info for MODIS + + Parameters + ---------- + click_data : list + Coordinates + figure : list + Figure elements + date : str + Selected date + model : str + Model name + + Returns + ------- + list + Latitude, longitude and value on coordinates + list + Figure elements with map marker + """ + + from callback_tools import get_single_point + + logging.debug("CLICK: %s", str(click_data)) if not click_data: raise PreventUpdate ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] ctxt = orjson.loads(dash.callback_context.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("CTXT", ctxt, type(ctxt)) + logging.debug("CTXT %s %s", ctxt, type(ctxt)) if not ctxt or ctxt is None or ctxt != {'index': 'modis', 'tag': 'modis-map'}: raise PreventUpdate lat, lon = click_data value = get_single_point(model, date, 0, DEFAULT_VAR, lat, lon) - if DEBUG: print("VALUE", value) + logging.debug("VALUE %s", value) if not value: raise PreventUpdate - if DEBUG: print("VALUE", value) + logging.debug("VALUE %s", value) try: valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=12) except: @@ -534,7 +631,7 @@ def modis_popup(click_data, mapid, date, model): className='popup-map-point' ) - return [lat, lon, value], mapid + [marker] + return [lat, lon, value], figure + [marker] @dash.callback( @@ -547,30 +644,51 @@ def modis_popup(click_data, mapid, date, model): prevent_initial_call=True ) @cache.memoize(timeout=cache_timeout) -def stations_popup(click_data, mapid, stations): - """ Manages popup info for aeronet """ - if not click_data: +def stations_popup(click_data, figure, stations): + """ Manage popup info for AERONET + + Parameters + ---------- + click_data : list + Coordinates + figure : list + Figure elements + stations : str + Stations + + Returns + ------- + dict + Dataframe with stations information + list + Figure elements with map marker + None + Cleared coordinates + """ + + if not click_data or not stations: raise PreventUpdate - - if DEBUG: print("CLICK:", str(click_data)) + + logging.debug("CLICK: %s", str(click_data)) ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) + logging.debug("CTXT %s %s", ctxt, type(ctxt)) if not ctxt or ctxt is None: raise PreventUpdate trigger = orjson.loads(ctxt) - if DEBUG: print('TRIGGER', trigger, type(trigger)) + logging.debug('TRIGGER %s %s', trigger, type(trigger)) if trigger != {'index': 'None', 'tag': 'empty-map'}: raise PreventUpdate df_stations = pd.DataFrame(stations) + lat, lon = click_data curr_station = df_stations[(df_stations['lon'].round(2) == round(lon, 2)) & \ (df_stations['lat'].round(2) == round(lat, 2))]['stations'].values - if DEBUG: print("CURR_STATION", curr_station) + logging.debug("CURR_STATION %s", curr_station) if not curr_station: raise PreventUpdate @@ -608,22 +726,22 @@ def stations_popup(click_data, mapid, stations): curr_data = df_stations[(df_stations['lon'].round(2) == round(lon, 2)) & \ (df_stations['lat'].round(2) == round(lat, 2))].to_dict() - print("MARKER", marker.to_plotly_json()) - last = mapid[-1] + logging.debug("MARKER %s", marker.to_plotly_json()) + last = figure[-1] if last['type'] == 'Popup': - mapid[-1] = marker.to_plotly_json() + figure[-1] = marker.to_plotly_json() else: - mapid.append(marker.to_plotly_json()) - print("LAST", type(mapid), type(last), last) - for _, log in enumerate(mapid): + figure.append(marker.to_plotly_json()) + logging.debug("LAST %s %s %s", type(figure), type(last), last) + for _, log in enumerate(figure): if log is not None: - # mapid[pos]['id']['random'] = + # figure[pos]['id']['random'] = if DEBUG: - print("********", type(log), log.keys()) - # print(log['type']) - #Clear the click_data so that repeat clicks continue to call the callback + logging.debug("******** %s %s", type(log), log.keys()) + # logging.debug(log['type']) + # Clear the click_data so that repeat clicks continue to call the callback click_data = None - return curr_data, mapid, click_data + return curr_data, figure, click_data @dash.callback( @@ -638,32 +756,56 @@ def stations_popup(click_data, mapid, stations): prevent_initial_call=True ) @cache.memoize(timeout=cache_timeout) -def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, model): - """ Retrieve AERONET evaluation timeseries according to station selected """ - from tools import get_eval_timeseries +def show_eval_aeronet_timeseries(n_clicks, cdata, start_date, end_date, obs, model): + """ Retrieve AERONET evaluation timeseries according to selected station + + Parameters + ---------- + n_clicks : int + Number of clicks on EXPLORE TIMESERIES button + cdata : str + Current station data + start_date : str + Start date + end_date : str + End date + obs : str + Observations name + model : str + Model name + + Returns + ------- + dash_bootstrap_components._components.ModalBody.ModalBody + Timeseries modal window + bool + Indicates whether the modal window is shown or not + """ + + from callback_tools import get_eval_timeseries ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) + logging.debug("CTXT %s %s", ctxt, type(ctxt)) if not ctxt or ctxt is None: raise PreventUpdate - if ctxt != 'ts-eval-button' or nclicks == 0: + if ctxt != 'ts-eval-button' or n_clicks == 0: raise PreventUpdate if not cdata: raise PreventUpdate - - print(start_date, end_date, obs, model, cdata) - if DEBUG: print('EVAL AERONET CLICKDATA', cdata) + + logging.debug(" %s %s %s %s %s", start_date, end_date, obs, model, cdata) + logging.debug('EVAL AERONET CLICKDATA %s', cdata) cdata = pd.DataFrame(cdata) idx = int(cdata.index.values[0]) # lon = cdata.lon.round(2).values[0] # lat = cdata.lat.round(2).values[0] - stat = cdata.stations.values[0] + station_name = cdata.stations.values[0] if idx != 0: - figure = get_eval_timeseries(obs, start_date, end_date, DEFAULT_VAR, idx, stat, model) + figure = get_eval_timeseries(obs, start_date, end_date, DEFAULT_VAR, idx, station_name, model) mb = MODEBAR_LAYOUT_TS figure.update_layout(mb) - if DEBUG: print('SHOW AERONET EVAL TS"""""', obs, idx, stat, start_date, end_date) + logging.debug('SHOW AERONET EVAL TS""""" %s %s %s %s %s', obs, idx, station_name, start_date, end_date) return dbc.ModalBody( dcc.Graph( id='timeseries-eval-modal', @@ -684,49 +826,71 @@ def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, mode State('obs-dropdown', 'value')], prevent_initial_call=True) @cache.memoize(timeout=cache_timeout) -def update_eval_aeronet(n_clicks, sdate, edate, obs): - """ Update AERONET evaluation figure according to all parameters """ +def update_eval_aeronet(n_clicks, start_date, end_date, obs): + """ Update AERONET evaluation figure according to all parameters + + Parameters + ---------- + n_clicks : int + Number of clicks on APPLY button + start_date : str + Start date + end_date : str + End date + obs : str + Observations name + + Returns + ------- + dict + Dict with stations positions and names + dash_leaflet.Map + Figure + """ + ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("BUTTON", button_id) + logging.debug("BUTTON %s", button_id) if button_id != 'eval-apply': raise PreventUpdate else: raise PreventUpdate - if sdate is None or edate is None or obs != 'aeronet': + if start_date is None or end_date is None or obs != 'aeronet': raise PreventUpdate - from tools import get_models_figure - from tools import get_obs1d - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - if DEBUG: print('SERVER: SDATE', str(sdate)) + from callback_tools import get_figure + from callback_tools import get_evaluation_comparison_aeronet_figure - sdate = sdate.split()[0] + logging.debug('SERVER: calling figure from EVAL picker callback') + logging.debug('SERVER: start date %s', str(start_date)) + + start_date = start_date.split()[0] try: - sdate = dt.strptime( - sdate, "%Y-%m-%d").strftime("%Y%m%d") + start_date = dt.strptime( + start_date, "%Y-%m-%d").strftime("%Y%m%d") except: - sdate = END_DATE + start_date = END_DATE pass if DEBUG: - print(f'SERVER: callback start_date {sdate}') + logging.debug('SERVER: callback start_date %s', start_date) - if edate is not None: - edate = edate.split()[0] + if end_date is not None: + end_date = end_date.split()[0] try: - edate = dt.strptime( - edate, "%Y-%m-%d").strftime("%Y%m%d") + end_date = dt.strptime( + end_date, "%Y-%m-%d").strftime("%Y%m%d") except: pass if DEBUG: - print(f'SERVER: callback end_date {edate}') + logging.debug('SERVER: callback end date %s', end_date) else: - edate = END_DATE + end_date = END_DATE - stations, points_layer = get_obs1d(sdate, edate, obs, DEFAULT_VAR) - fig = get_models_figure(model=None, var=DEFAULT_VAR, layer=points_layer) + stations, points_layer = get_evaluation_comparison_aeronet_figure(start_date, end_date, obs, DEFAULT_VAR) + fig = get_figure(layers=points_layer) + return stations.to_dict(), fig @@ -740,25 +904,51 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): State('graph-eval-modis-mod', 'children')], prevent_initial_call=True) @cache.memoize(timeout=cache_timeout) -def update_eval_modis(n_clicks, date, mod, obs, mod_div): - """ Update MODIS evaluation figure according to all parameters """ +def update_eval_modis(n_clicks, date, mod, obs, model_figure): + """ Update MODIS evaluation figure according to all parameters + + Parameters + ---------- + n_clicks : int + Number of clicks on APPLY button + date : str + Selected date + mod : str + Model name + obs : str + Observations name + model_figure : dict + Model figure properties + + Returns + ------- + dash_leaflet.Map + Figure with MODIS sensor data + dash_leaflet.Map + Figure with model data + """ + ctx = dash.callback_context if not ctx.triggered: raise PreventUpdate button_id = ctx.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("BUTTON", button_id) + logging.debug("BUTTON %s", button_id) if button_id != 'eval-apply': raise PreventUpdate if date is None or mod is None or obs != 'modis': raise PreventUpdate - from tools import get_models_figure - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - if DEBUG: print(mod_div) - mod_center = mod_div['props']['center'] - mod_zoom = mod_div['props']['zoom'] + from callback_tools import get_model_figure + from callback_tools import get_evaluation_comparison_modis_figure + from callback_tools import get_figure + + logging.debug('SERVER: calling figure from EVAL picker callback') + logging.debug(" %s ", model_figure) + + mod_center = model_figure['props']['center'] + mod_zoom = model_figure['props']['zoom'] if date is not None: date = date.split()[0] @@ -768,22 +958,28 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): except: pass if DEBUG: - print(f'SERVER: callback date {date}') + logging.debug('SERVER: callback date %s', date) else: date = END_DATE if DEBUG: - print("ZOOM", mod_zoom, "CENTER", mod_center) + logging.debug("ZOOM %s %s", mod_zoom, mod_center) if MODELS[mod]['start'] == 12: tstep = 4 else: tstep = 0 - fig_mod = get_models_figure(model=mod, var=DEFAULT_VAR, selected_date=date, tstep=tstep, - hour=12, center=mod_center, zoom=mod_zoom) - fig_obs = get_models_figure(model=obs, var=DEFAULT_VAR, selected_date=date, tstep=0, - center=mod_center, zoom=mod_zoom, tag='modis') - if DEBUG: print("MODIS", fig_obs) + logging.debug("================================ %s %s", date, tstep) + # Get model figure + layers = get_model_figure(var=DEFAULT_VAR, model=mod, tstep=tstep, selected_date=date, + hour=12) + fig_mod = get_figure(center=mod_center, zoom=mod_zoom, layers=layers) + + # Get sensor figure + layers = get_evaluation_comparison_modis_figure(var=DEFAULT_VAR, obs='modis', tstep=0, + selected_date=date) + fig_obs = get_figure(center=mod_center, zoom=mod_zoom, layers=layers, tag='modis', index='modis') + return fig_obs, fig_mod @@ -796,11 +992,28 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): prevent_initial_call=True) @cache.memoize(timeout=cache_timeout) def update_eval(obs): - """ Update evaluation figure according to all parameters """ - from tools import get_models_figure - # from tools import get_obs1d - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - # if DEBUG: print('SERVER: interval ' + str(n)) + """ Update evaluation figure according to all parameters + + Parameters + ---------- + obs : str + Observations name + + Returns + ------- + list + Date picker properties + list + Figure with one map for AERONET and two maps for MODIS (MODIS and model) + str + Observations name + dict + Figure style + """ + + from callback_tools import get_figure + + logging.debug('SERVER: calling figure from EVAL picker callback') start_date = OBS[obs]['start_date'] @@ -820,7 +1033,7 @@ def update_eval(obs): eval_graph = html.Div([ html.Div( - get_models_figure(), + get_figure(), id='graph-eval-aeronet' ), html.Div( @@ -846,8 +1059,8 @@ def update_eval(obs): # with_portal=True, )] - fig_mod = get_models_figure() - fig_obs = get_models_figure(tag='modis') + fig_mod = get_figure() + fig_obs = get_figure(tag='modis') graph_obs = html.Div([ html.Div( @@ -863,7 +1076,7 @@ def update_eval(obs): fig_mod, id='graph-eval-modis-mod', ), - html.Div(DISCLAIMER_NO_FORECAST, + html.Div(DISCLAIMER_MODELS, className='disclaimer', id='eval-vis-modis-disclaimer' ) @@ -874,7 +1087,9 @@ def update_eval(obs): dbc.Col(graph_mod, width=6) ], align='start', - no_gutters=True + # no_gutters is replaced with the className because of bootstrap update + # no_gutters=True, + className='g-0', )] style = { 'display': 'table-cell' } diff --git a/tabs/forecast.py b/tabs/forecast.py index 2b8e60a569d20d5e206a8204c1f25d703ef17aee..a67012dc0a9091019a7d1108961a70a171d87ca6 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -8,38 +8,23 @@ import dash_bootstrap_components as dbc from dash import dcc from dash import html -from data_handler import FREQ -from data_handler import DEBUG -from data_handler import STYLES -from data_handler import START_DATE, END_DATE, DELAY, DELAY_DATE -from data_handler import WAS -from data_handler import DISCLAIMER_MODELS - - -def get_forecast_days(curdate=END_DATE): - """ Return forecast days according to configuration file """ - delay = DELAY - st_date = DELAY_DATE - if (delay and st_date \ - and (dt.strptime(curdate, "%Y%m%d") >= dt.strptime(st_date, "%Y%m%d"))) or \ - (not delay and st_date \ - and (dt.strptime(curdate, "%Y%m%d") < dt.strptime(st_date, "%Y%m%d"))) or \ - (delay and not st_date): - st_day = 1 - else: - st_day = 0 - - return dict([ - ( - idx, (dt.strptime(curdate, "%Y%m%d") + \ - timedelta(days=idx)).strftime("%a %d").upper() - ) for idx in range(st_day, 3) - ]) +from map_handler import FORECAST_MAX +from map_handler import FREQ +from map_handler import DEBUG +from map_handler import START_DATE, END_DATE, DELAY, DELAY_DATE +from map_handler import WAS +from map_handler import DISCLAIMER_MODELS +from map_handler import PATHS +from tabs.generic import get_forecast_days, layout_view, time_series +import logging + +# MOVED TO GENERIC +# def get_forecast_days(curdate=END_DATE): def gen_ts_marks(ts_type, curdate=END_DATE): """ Generate time slider marks """ - if DEBUG: print("TS MARKS", ts_type, curdate) + logging.debug("TS MARKS %s %s", ts_type, curdate) if ts_type == 'was': forecast_dict = get_forecast_days(curdate) forecast_values = sorted(forecast_dict.keys()) @@ -61,7 +46,7 @@ def gen_ts_marks(ts_type, curdate=END_DATE): freq = 1 else: fcst_min = 0 - fcst_max = 72 + fcst_max = FORECAST_MAX forecast_values = range(fcst_min, fcst_max+FREQ, FREQ) forecast_dict = dict([ (fcst_val, f'{fcst_val}') for fcst_val in forecast_values @@ -94,7 +79,7 @@ def gen_time_slider(ts_type='prob', end_date=END_DATE): for tstep in ts_marks } - if DEBUG: print("FCST MARKS", marks[list(marks.keys())[-1]]) + logging.debug("FCST MARKS %s", marks[list(marks.keys())[-1]]) if ts_type in ('prob', 'was'): marks[list(marks.keys())[-1]]['style'] = {} marks[list(marks.keys())[-1]]['style']['left'] = '' @@ -143,7 +128,7 @@ def gen_time_bar(ts_type='prob', start_date=START_DATE, end_date=END_DATE): className="timesliderline", ) - if DEBUG: print("TIME SLIDER", time_slider) + logging.debug("TIME SLIDER %s", time_slider) if ts_play: return html.Div([ @@ -161,69 +146,14 @@ def gen_time_bar(ts_type='prob', start_date=START_DATE, end_date=END_DATE): className="timeslider" ) -def layout_view(): - """ Return the menu for the various mapview types""" +### MOVED TO GENERIC +# def layout_view(): - return html.Div([ - html.Span( - dbc.DropdownMenu( - id='map-view-dropdown', - label='VIEW', - children=[ - dbc.DropdownMenuItem( - STYLES[style]['name'], - id=dict( - tag='view-style', - index=style - ), - active=active - ) - for style, active in zip(list(STYLES.keys()), - [True if i == 'carto-positron' - else False for i in STYLES]) - ], - direction="up", - in_navbar=True, - ), - ) - ], - id='map-view-dropdown-div') - -def time_series(): - """ Return the timeseries element""" - return html.Div( - id='open-timeseries', - children=[ - dbc.Spinner( - id='loading-ts-modal', - fullscreen=True, - fullscreen_style={'opacity': '0.5', 'zIndex' : '200000'}, - show_initially=False, - # debounce=200, - children=[ - dbc.Modal([], - id='ts-modal', - size='xl', - centered=True, - is_open=False, - ), - ], - )], - #style={'display': 'none'}, - ) +# MOVED TO GENERIC +# def time_series(): -def layout_layers(): - return html.Div([ - html.Span( - dbc.DropdownMenu( - id='map-layers-dropdown', - label='LAYERS', - children=[ - dbc.DropdownMenuItem('AIRPORTS', id='airports') - ], - direction="up", - ), - )]) +# # MOVED TO GENERIC +# def layout_layers(**args): def models_children(start_date=START_DATE, end_date=END_DATE): """ Return the html for models maps """ @@ -247,7 +177,6 @@ def models_children(start_date=START_DATE, end_date=END_DATE): id='div-collection', # children=[dbc.Spinner( # id='loading-graph-collection', - # #debounce=10, # show_initially=False, children=[ dbc.Container( @@ -433,12 +362,12 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode dbc.Button(children=["Models", html.Span( html.I(className='fa fa-solid fa-angle-up'), - id='caret1', className="caret-span") + id={'type':'caret', 'index':1}, className="caret-span") ], color="link", id='group-1-toggle', className='dropdown'), )), dbc.Collapse( - id='collapse-1', + id={'type':'collapse', 'index':1}, is_open=dropdown['models'], children=[ dbc.CardBody([ @@ -448,7 +377,7 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode 'value': model} for model in models], #value=[default_model,], value=default_model, - className="sidebar-dropdown", + class_name="sidebar-dropdown", ), html.Span([ html.Button('APPLY', id='models-apply', n_clicks=0), @@ -470,12 +399,12 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode dbc.Button(children=["Probability of exceedance", html.Span( html.I(className='fa fa-solid fa-angle-up'), - id='caret2', className="caret-span-closed") + id={'type':'caret', 'index':2}, className="caret-span-closed") ], color="link", id='group-2-toggle', className='dropdown'), )), dbc.Collapse( - id='collapse-2', + id={'type':'collapse', 'index':2}, is_open=dropdown['prob'], children=[ dbc.CardBody([ @@ -498,12 +427,14 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode dbc.Button(children=["Warning Advisory System", html.Span( html.I(className='fa fa-solid fa-angle-up'), - id='caret3', className="caret-span-closed")], + id={'type':'caret', 'index':3}, + className="caret-span-closed")], color="link", id='group-3-toggle', className='dropdown', disabled=False) )), dbc.Collapse( - id='collapse-3', + id={'type':'collapse', 'index': 3}, + # id='collapse-3', is_open=dropdown['was'], children=[ dbc.CardBody([ @@ -539,7 +470,9 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode width=9, ), ], - no_gutters=True, + # no_gutters is replaced with the className because of bootstrap update + # no_gutters=True, + className='g-0', ), dbc.Row([ dbc.Col([ @@ -549,7 +482,7 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode dbc.Button('USER GUIDE', id='btn-userguide-download', n_clicks=0, - href="/products/overview/user-guide/@@download", + href=PATHS['user_guide'], className='download-section', ), html.P("""Please check out the User Guide for more information."""), @@ -587,7 +520,7 @@ def sidebar_forecast(variables, default_var, models, default_model, window='mode dbc.Button('NETCDF', id='btn-netcdf-download', n_clicks=0, - href="/products/data-download", + href= PATHS['netcdf'], external_link=True, target="_blank", className='download-section', diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 78e5c1268516eea5e1899898811240161e4472f6..e54ed71bf93c9bf080992f11cf22f77164cc6664 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -7,6 +7,7 @@ import time import math from random import random import orjson +import logging import dash import dash_bootstrap_components as dbc @@ -19,28 +20,28 @@ from dash.dependencies import ALL from dash.exceptions import PreventUpdate import dash_leaflet as dl -from data_handler import DEFAULT_VAR -from data_handler import DEFAULT_MODEL -from data_handler import VARS -from data_handler import WAS -from data_handler import MODELS -from data_handler import STYLES -from data_handler import FREQ -from data_handler import DEBUG -from data_handler import END_DATE -from data_handler import PROB -from data_handler import GRAPH_HEIGHT -from data_handler import MODEBAR_CONFIG_TS -from data_handler import MODEBAR_LAYOUT_TS -from data_handler import cache, cache_timeout +from ines_core_map_handler import MAP_LAYERS +from map_handler import DEFAULT_VAR +from map_handler import DEFAULT_MODEL +from map_handler import VARS +from map_handler import WAS +from map_handler import MODELS +from map_handler import FREQ +from map_handler import DEBUG +from map_handler import END_DATE +from map_handler import PROB +from map_handler import GRAPH_HEIGHT +from map_handler import MODEBAR_CONFIG_TS +from map_handler import MODEBAR_LAYOUT_TS +from map_handler import cache, cache_timeout from tabs.forecast import tab_forecast -from utils import calc_matrix +from ines_core_utils import calc_matrix @dash.callback( - [Output('collapse-1', 'is_open'), - Output('collapse-2', 'is_open'), - Output('collapse-3', 'is_open'), + [Output({'type':'collapse', 'index': 1}, 'is_open'), + Output({'type':'collapse', 'index': 2}, 'is_open'), + Output({'type':'collapse', 'index': 3}, 'is_open'), Output('group-2-toggle', 'disabled'), Output('group-3-toggle', 'disabled'), Output('variable-dropdown-forecast','value')], @@ -48,9 +49,9 @@ from utils import calc_matrix Input('group-2-toggle', 'n_clicks'), Input('group-3-toggle', 'n_clicks'), Input('variable-dropdown-forecast', 'value')], - [State('collapse-1', 'is_open'), - State('collapse-2', 'is_open'), - State('collapse-3', 'is_open'),], + [State({'type':'collapse', 'index': 1}, 'is_open'), + State({'type':'collapse', 'index': 2}, 'is_open'), + State({'type':'collapse', 'index': 3}, 'is_open'),], prevent_initial_call=True ) def render_forecast_tab(modbutton, probbutton, wasbutton, var, modopen, @@ -134,52 +135,14 @@ def update_models_dropdown(variable, checked): checked = [c for c in models if c in checked or len(models)==1] if len(checked) > 1: btn_style = { 'display' : 'none' } - if DEBUG: print('MODELS', models, 'OPTS', type(options), options) + logging.debug('MODELS %s OPTS %s %s', models, type(options), options) return options, checked, btn_style -@dash.callback( - Output('caret1', 'style'), - [Input('collapse-1', 'is_open')], -) -def rotate_models_caret(collapse_open): - """ Rotates models menu caret """ - rotate_caret = { - 'top':'.05rem', - 'transform': 'rotate(180deg)', - '-ms-transform': 'rotate(180deg)', - '-webkit-transform': 'rotate(180deg)' - } - if not collapse_open: - return rotate_caret - -@dash.callback( - Output('caret2', 'style'), - [Input('collapse-2', 'is_open')], -) -def rotate_prob_caret(collapse_open): - """ Rotates probability menu caret """ - rotate_caret = { - 'transform': 'rotate(0deg)', - '-ms-transform': 'rotate(0deg)', - '-webkit-transform': 'rotate(0deg)' - } - if collapse_open: - return rotate_caret - -@dash.callback( - Output('caret3', 'style'), - [Input('collapse-3', 'is_open')], -) -def rotate_was_caret(collapse_open): - """ Rotates was menu caret """ - rotate_caret = { - 'transform': 'rotate(0deg)', - '-ms-transform': 'rotate(0deg)', - '-webkit-transform': 'rotate(0deg)' - } - if collapse_open: - return rotate_caret +#MOVED TO GENERIC CALLBACKS +# def rotate_models_caret(collapse_open): +# def rotate_prob_caret(collapse_open): +# def rotate_was_caret(collapse_open): @dash.callback( [Output('info-collapse', 'is_open'), @@ -197,13 +160,13 @@ def sidebar_bottom(n_info, n_download, open_info, open_download): button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id == 'info-button': - if DEBUG: print('clicked INFO', not open_info, False) + logging.debug('clicked INFO %s, %s', not open_info, False) return not open_info, False elif button_id == 'download-button': - if DEBUG: print('clicked DOWN', False, not open_download) + logging.debug('clicked DOWN %s %s', False, not open_download) return False, not open_download - if DEBUG: print('clicked NONE', False, False) + logging.debug('clicked NONE %s %s', False, False) raise PreventUpdate @dash.callback( @@ -219,16 +182,16 @@ def download_anim_link(models, variable, date, tstep): if variable is None or date is None or tstep is None: raise PreventUpdate - # from tools import get_models_figure - from tools import download_image_link + # from callback_tools import get_models_figure + from callback_tools import download_image_link - if DEBUG: print('GIF', models, variable, date) + logging.debug('GIF %s %s %s', models, variable, date) try: curdate = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') except: curdate = date anim = download_image_link(models, variable, curdate, anim=True) - if DEBUG: print('DOWNLOAD LINK', anim) + logging.debug('DOWNLOAD LINK %s', anim) return anim @dash.callback( @@ -259,16 +222,16 @@ def update_was_timeslider(date): State({'tag': 'was-map', 'index': ALL}, 'center')], prevent_initial_call=False ) -def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, center): +def update_was_figure(n_clicks, date, day, was, var, previous, selected_tiles, zoom, center): """ Update Warning Advisory Systems maps """ - if DEBUG: print('WAS', n_clicks, date, day, was, previous, view, zoom, center) + logging.debug('WAS %s %s %s %s %s %s %s %s', n_clicks, date, day, was, previous, selected_tiles, zoom, center) ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id != 'was-apply' and date is None and day is None: raise PreventUpdate - from tools import get_was_figure - from tools import get_models_figure + from callback_tools import get_was_figure + from callback_tools import get_figure if not zoom or previous != was: zoom = WAS[was]['zoom'] else: @@ -280,7 +243,7 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente previous = was - print('WAS', was) + logging.debug('WAS %s', was) if date is not None: date = date.split(' ')[0] try: @@ -288,17 +251,15 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente date, "%Y-%m-%d").strftime("%Y%m%d") except: pass - if DEBUG: - print(f'SERVER: callback date {date}') + logging.debug('SERVER: callback date %s', date) else: date = END_DATE - if DEBUG: - print("WAS figure " + date, was, day) + logging.debug("WAS figure %s %s %s", date, was, day) if was: - view = list(STYLES.keys())[view.index(True)] - geojson, legend, info = get_was_figure(was, day, selected_date=date) - fig = get_models_figure(model=None, var=var, layer=[geojson, legend, info], view=view, zoom=zoom, center=center, tag='was') + selected_tiles = list(MAP_LAYERS.keys())[selected_tiles.index(True)] + layers = get_was_figure(was, day, selected_date=date) + fig = get_figure(selected_tiles=selected_tiles, zoom=zoom, center=center, tag='was', layers=layers) return fig, previous raise PreventUpdate @@ -330,11 +291,8 @@ def update_prob_timeslider(date): prevent_initial_call=False ) @cache.memoize(timeout=cache_timeout) -def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): +def update_prob_figure(n_clicks, date, day, prob, var, selected_tiles, zoom, center): """ Update Probability maps """ - from tools import get_prob_figure - from tools import get_models_figure - # if not prob in case user navigates to section via URL if not prob: prob = 'prob_0.1' @@ -346,13 +304,16 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): center = None else: center = center[0] - if DEBUG: print('PROB', date, day, var, prob, var, view, zoom, center) + logging.debug('PROB %s %s %s %s %s %s %s %s', date, day, var, prob, var, selected_tiles, zoom, center) ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id != 'prob-apply' and var is None and day is None: raise PreventUpdate + from callback_tools import get_prob_figure + from callback_tools import get_figure + if date is not None: date = date.split(' ')[0] try: @@ -360,122 +321,79 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): date, "%Y-%m-%d").strftime("%Y%m%d") except: pass - if DEBUG: print('SERVER: callback date {}'.format(date)) + logging.debug('SERVER: callback date %s', date) else: date = END_DATE if prob: prob = prob.replace('prob_', '') - view = list(STYLES.keys())[view.index(True)] - geojson, colorbar, info = get_prob_figure(var, prob, day, selected_date=date) - fig = get_models_figure(model=None, var=var, layer=[geojson, colorbar, info], view=view, zoom=zoom, center=center, tag='prob') - if DEBUG: print("FIG", fig) - return fig - raise PreventUpdate - -@dash.callback( - [Output({'tag': 'was-tile-layer', 'index': ALL}, 'url'), - Output({'tag': 'was-tile-layer', 'index': ALL}, 'attribution')], - [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], - [State({'tag': 'was-tile-layer', 'index': ALL}, 'url'), - State({'tag': 'view-style', 'index': ALL}, 'active')], - prevent_initial_call=True -) -@cache.memoize(timeout=cache_timeout) -def update_was_styles_button(*args): - """ Function updating styles button """ - ctx = dash.callback_context - graphs = args[-2] - num_graphs = len(graphs) - if ctx.triggered and num_graphs > 0: - button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) - if button_id['index'] in STYLES: - active = args[-1] - # if DEBUG: print("CURRENT ARGS", str(args)) - # if DEBUG: print("NUM GRAPHS", num_graphs) - - url = [STYLES[button_id['index']]['url'] for _ in range(num_graphs)] - attr = [STYLES[button_id['index']]['attribution'] for _ in range(num_graphs)] - res = [False for _ in active] - st_idx = list(STYLES.keys()).index(button_id['index']) - if active[st_idx] is False: - res[st_idx] = True - if DEBUG: - print("*****", url, attr, res) - return url, attr #, res + selected_tiles = list(MAP_LAYERS.keys())[selected_tiles.index(True)] + layers = get_prob_figure(var, prob, day, selected_date=date) + fig = get_figure(selected_tiles=selected_tiles, zoom=zoom, center=center, tag='prob', layers=layers) - if DEBUG: print('NOTHING TO DO') + logging.debug("FIG %s", fig) + return fig raise PreventUpdate -@dash.callback( - [Output({'tag': 'prob-tile-layer', 'index': ALL}, 'url'), - Output({'tag': 'prob-tile-layer', 'index': ALL}, 'attribution')], - [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], - [State({'tag': 'prob-tile-layer', 'index': ALL}, 'url'), - State({'tag': 'view-style', 'index': ALL}, 'active')], - prevent_initial_call=True -) -@cache.memoize(timeout=cache_timeout) -def update_prob_styles_button(*args): - """ Function updating styles button """ - ctx = dash.callback_context - graphs = args[-2] - num_graphs = len(graphs) - if ctx.triggered and num_graphs > 0: - button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) - if button_id['index'] in STYLES: - active = args[-1] - # if DEBUG: print("CURRENT ARGS", str(args)) - # if DEBUG: print("NUM GRAPHS", num_graphs) - - url = [STYLES[button_id['index']]['url'] for _ in range(num_graphs)] - attr = [STYLES[button_id['index']]['attribution'] for _ in range(num_graphs)] - res = [False for _ in active] - st_idx = list(STYLES.keys()).index(button_id['index']) - if active[st_idx] is False: - res[st_idx] = True - if DEBUG: - print("*****", url, attr, res) - return url, attr #, res - - if DEBUG: print('NOTHING TO DO') - raise PreventUpdate +def get_view(index, url, attr): + """ Helper function to assign selected map with selected view """ + mod_url = mod_attr = [] + prob_url = prob_attr = [] + was_url = was_attr = [] + if index == 1: + mod_url = url + mod_attr = attr + if index == 2: + prob_url = url + prob_attr = attr + if index == 3: + was_url = url + was_attr = attr + return mod_url, mod_attr, prob_url, prob_attr, was_url, was_attr @dash.callback( [Output({'tag': 'model-tile-layer', 'index': ALL}, 'url'), Output({'tag': 'model-tile-layer', 'index': ALL}, 'attribution'), + Output({'tag': 'prob-tile-layer', 'index': ALL}, 'url'), + Output({'tag': 'prob-tile-layer', 'index': ALL}, 'attribution'), + Output({'tag': 'was-tile-layer', 'index': ALL}, 'url'), + Output({'tag': 'was-tile-layer', 'index': ALL}, 'attribution'), Output({'tag': 'view-style', 'index': ALL}, 'active')], [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], [State({'tag': 'model-tile-layer', 'index': ALL}, 'url'), + State({'tag': 'prob-tile-layer', 'index': ALL}, 'url'), + State({'tag': 'was-tile-layer', 'index': ALL}, 'url'), State({'tag': 'view-style', 'index': ALL}, 'active')], prevent_initial_call=True ) @cache.memoize(timeout=cache_timeout) -def update_models_styles_button(*args): +def update_styles_button(*args): """ Function updating styles button """ - ctx = dash.callback_context - graphs = args[-2] + #get the index of the map which view was triggered + for i in range(1, 4): + if len(args[i]) >= 1: + index = i + graphs = args[index] num_graphs = len(graphs) + ctx = dash.callback_context if ctx.triggered and num_graphs > 0: button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) - if button_id['index'] in STYLES: - if DEBUG: print("CURRENT ARGS", str(args)) + logging.debug("BUTTON ID %s %s", str(button_id), type(button_id)) + if button_id['index'] in MAP_LAYERS: + logging.debug("CURRENT ARGS %s", str(args)) active = args[-1] - if DEBUG: print("NUM GRAPHS", num_graphs) - url = [STYLES[button_id['index']]['url'] for _ in range(num_graphs)] - attr = [STYLES[button_id['index']]['attribution'] for _ in range(num_graphs)] + logging.debug("NUM GRAPHS %s", num_graphs) + url = [MAP_LAYERS[button_id['index']]['url'] for _ in range(num_graphs)] + attr = [MAP_LAYERS[button_id['index']]['attribution'] for _ in range(num_graphs)] res = [False for _ in active] - st_idx = list(STYLES.keys()).index(button_id['index']) + st_idx = list(MAP_LAYERS.keys()).index(button_id['index']) if active[st_idx] is False: res[st_idx] = True - if DEBUG: - print('*****', url, attr, res) - return url, attr, res + logging.debug('***** %s %s %s', url, attr, res) + mod_url, mod_attr, prob_url, prob_attr, was_url, was_attr = get_view(index, url, attr) + return mod_url, mod_attr, prob_url, prob_attr, was_url, was_attr, res - if DEBUG: print('NOTHING TO DO') + logging.debug('NOTHING TO DO') raise PreventUpdate @dash.callback( @@ -494,16 +412,16 @@ def update_models_styles_button(*args): ) def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups): """ Manages models popups for timeseries """ - from tools import get_single_point - if DEBUG: print("CLICK:", str(click_data)) + from callback_tools import get_single_point + logging.debug("CLICK: %s", str(click_data)) if click_data.count(None) == len(click_data): raise PreventUpdate - if DEBUG: print("MAPID:", str(map_ids), type(map_ids)) - # if DEBUG: print("RESLIST:", str(res_list), type(res_list)) + logging.debug("MAPID: %s %s", str(map_ids), type(map_ids)) + # logging.debug("RESLIST: %s %s", str(res_list), type(res_list)) ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] - if DEBUG: print("CTXT", ctxt, type(ctxt)) + logging.debug("CTXT %s %s", ctxt, type(ctxt)) if not ctxt or ctxt is None: raise PreventUpdate @@ -511,7 +429,7 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups popups = {} trigger = orjson.loads(ctxt) - if DEBUG: print('TRIGGER', trigger, type(trigger)) + logging.debug('TRIGGER %s %s', trigger, type(trigger)) # curr_models = [m['index'] for m in map_ids] res = res_list @@ -524,7 +442,7 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups else: tstep = int(tstep/FREQ) - if DEBUG: print("MODEL", model, "CLICK", click, "DATE", date, "STEP", tstep, "MODIDX", mod_idx) + logging.debug("MODEL %s CLICK %s DATE %s TSTEP %s MODIDX %s", model, click, date, tstep, mod_idx) if click is not None and model is not None: lat, lon = click @@ -545,9 +463,9 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups date = selected_date.strftime("%Y%m%d") valid_dt = dt.strptime(date, '%Y%m%d') + timedelta(hours=tstep*FREQ) - if DEBUG: print("MODEL", model, "CLICK", click, "DATE", date, "STEP", tstep) + logging.debug("MODEL %s CLICK %s DATE %s TSTEP %s", model, click, date, tstep, mod_idx) value = get_single_point(model, date, int(tstep), var, lat, lon) - if DEBUG: print("VALUE:", str(value)) + logging.debug("VALUE: %s", str(value)) marker = dl.Popup( children=[ @@ -586,16 +504,15 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups className='popup-map-point', ) - # if DEBUG: print("||||", res, "\n", res[mod_idx], type(res[mod_idx])) - if DEBUG: print("||||", res[mod_idx][-1], type(res[mod_idx][-1])) + logging.debug("|||| %s %s", res[mod_idx][-1], type(res[mod_idx][-1])) if not res[mod_idx] or res[mod_idx] is None: - if DEBUG: print("*** 1 ***") + logging.debug("*** 1 ***") res[mod_idx] = marker elif res[mod_idx][-1] is None: - if DEBUG: print("*** 2 ***") + logging.debug("*** 2 ***") res[mod_idx][-1] = marker else: - if DEBUG: print("*** 3 ***") + logging.debug("*** 3 ***") res[mod_idx].append(marker) if not coords or coords is None: coords = { model: [lat, lon] } @@ -606,8 +523,8 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups for i in range(len(res[mod_idx])) if 'type' in res[mod_idx][i]]) + 1 - if DEBUG: print("COORDS:", str(coords)) - if DEBUG: print("POPUPS:", str(popups)) + logging.debug("COORDS: %s", str(coords)) + logging.debug("POPUPS: %s", str(popups)) return coords, popups, res return {}, {}, dash.no_update @@ -629,16 +546,16 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups def show_timeseries(ts_button, mod, date, variable, coords, popups): """ Renders model comparison timeseries, retrieve timeseries according to coordinates selected. """ - from tools import get_timeseries + from callback_tools import get_timeseries ctx = dash.callback_context if ctx.triggered: button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) - if DEBUG: print("BUTTONID:", mod, str(button_id), str(ts_button)) - if DEBUG: print('SHOW TS COORDS:', coords, type(coords), mod) + logging.debug("BUTTONID: %s %s %s", mod, str(button_id), str(ts_button)) + logging.debug('SHOW TS COORDS: %s %s %s', coords, type(coords), mod) ts_popups = 0 for m in popups: - if DEBUG: print("___", m, "___") + logging.debug("___%s___", m) ts_popups += popups[m] if m == button_id['index']: break @@ -652,7 +569,7 @@ def show_timeseries(ts_button, mod, date, variable, coords, popups): if lat is None or lon is None: raise PreventUpdate - if DEBUG: print('SHOW TS """""', mod, lat, lon) + logging.debug('SHOW TS """"" %s %s %s', mod, lat, lon) figure = get_timeseries(mod, date, variable, lat, lon, forecast=True) figure.update_layout(MODEBAR_LAYOUT_TS) ts_body = dbc.ModalBody( @@ -714,7 +631,6 @@ def zooms(viewport, models): Output('slider-interval', 'n_intervals'), Output('open-timeseries', 'style'), Output('btn-play', 'className'), - #Output('div-collection', 'children'), ], Input('btn-play', 'n_clicks'), [State('slider-interval', 'disabled'), @@ -724,7 +640,7 @@ def zooms(viewport, models): def start_stop_autoslider(n_play, disabled, value): """ Play/Pause map animation """ ctx = dash.callback_context - if DEBUG: print("VALUE", value) + logging.debug("VALUE %s", value) if not value: value = 0 @@ -747,14 +663,14 @@ def start_stop_autoslider(n_play, disabled, value): @cache.memoize(timeout=cache_timeout) def update_slider(n): """ Update slider value according to the number of intervals """ - if DEBUG: print('SERVER: updating model-slider-graph ' + str(n)) + logging.debug('SERVER: updating model-slider-graph %s' + str(n)) if not n: return if n >= 24: tstep = int(round(24*math.modf(n/24)[0], 0)) else: tstep = int(n) - if DEBUG: print('SERVER: updating model-slider-graph ' + str(tstep*FREQ)) + logging.debug('SERVER: updating model-slider-graph %s' + str(tstep*FREQ)) return tstep*FREQ @dash.callback( @@ -775,11 +691,11 @@ def update_tab_content(models_clicks, prob_clicks, was_clicks, curtab): if button_id not in ('models-apply', 'prob-apply', 'was-apply'): raise PreventUpdate - if DEBUG: print("::::::::::::", len(curtab), curtab[0]['index']) + logging.debug(":::::::::::: %s %s", len(curtab), curtab[0]['index']) curtab_name = curtab[0]['index'] nexttab_name = button_id.replace('-apply', '') - if DEBUG: print("::::::::::::", curtab_name, nexttab_name) + logging.debug(":::::::::::: %s %s", curtab_name, nexttab_name) if curtab_name != nexttab_name: return tab_forecast(nexttab_name) @@ -787,15 +703,6 @@ def update_tab_content(models_clicks, prob_clicks, was_clicks, curtab): raise PreventUpdate -# @dash.callback( -# Output('model-date-picker', 'value'), -# Input('clear_button', 'n_clicks'), -# State('model-date-picker', 'value'), -# ) -# def clear_date(click, value): -# print('hello', value) -# return '08 AUG 2022' -# @dash.callback( Output('graph-collection', 'children'), [Input('models-apply', 'n_clicks'), @@ -803,7 +710,6 @@ def update_tab_content(models_clicks, prob_clicks, was_clicks, curtab): Input('model-date-picker', 'date')], [State('model-dropdown', 'value'), State('variable-dropdown-forecast', 'value'), - State('slider-interval', 'disabled'), State({'tag': 'view-style', 'index': ALL}, 'active'), State({'tag': 'model-map', 'index': ALL, "n_clicks": ALL}, 'zoom'), State({'tag': 'model-map', 'index': ALL, "n_clicks": ALL}, 'center'), @@ -811,7 +717,7 @@ def update_tab_content(models_clicks, prob_clicks, was_clicks, curtab): prevent_initial_call=False ) @cache.memoize(timeout=cache_timeout) -def update_models_figure(n_clicks, tstep, date, model, variable, static, view, zoom, center): +def update_models_figure(n_clicks, tstep, date, model, variable, selected_tiles, zoom, center): """ Update mosaic of maps figures according to all parameters """ ctx = dash.callback_context @@ -822,13 +728,14 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z elif tstep is None and date is None: raise PreventUpdate - from tools import get_models_figure - if DEBUG: print('SERVER: calling figure from picker callback') + from callback_tools import get_model_figure + from callback_tools import get_figure + + logging.debug('SERVER: calling figure from picker callback') st_time = time.time() - # if DEBUG: print('SERVER: interval ' + str(n)) - if DEBUG: print('SERVER: tstep ' + str(tstep)) + logging.debug('SERVER: tstep %s', str(tstep)) if date is not None: date = date.split(' ')[0] @@ -837,7 +744,7 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z date, "%Y-%m-%d").strftime("%Y%m%d") except: pass - if DEBUG: print('SERVER: callback date {}'.format(date)) + logging.debug('SERVER: callback date %s', date) else: date = END_DATE @@ -852,8 +759,8 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z else: tstep = 0 - if DEBUG: print('SERVER: tstep calc ' + str(tstep)) - # if DEBUG: print('#### IDS, ZOOM, CENTER:', ids, zoom, center) + logging.debug('SERVER: tstep calc %s', tstep) + # logging.debug('#### IDS, ZOOM, CENTER: %s %s %s', ids, zoom, center) # curr_mods = { i['index']: { 'zoom': z, 'center': c } # for i, z, c in zip(ids, zoom, center) } @@ -868,21 +775,34 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z center = [None] if len(model) != len(zoom): - if DEBUG: print("##############", len(model), len(zoom), "**********") + logging.debug("############## %s %s **********", len(model), len(zoom)) zoom = [None for _ in model] center = [None for _ in model] - if DEBUG: print('#### ZOOM, CENTER:', zoom, center, model, ncols, nrows) - view = list(STYLES.keys())[view.index(True)] -# for mod in model: -# mod_zoom = mod in curr_mods and curr_mods[mod]['zoom'] or None -# mod_center = mod in curr_mods and curr_mods[mod]['center'] or None + logging.debug('#### ZOOM, CENTER: %s %s %s %s %s', zoom, center, model, ncols, nrows) + selected_tiles = list(MAP_LAYERS.keys())[selected_tiles.index(True)] + # for mod in model: + # mod_zoom = mod in curr_mods and curr_mods[mod]['zoom'] or None + # mod_center = mod in curr_mods and curr_mods[mod]['center'] or None for mod, mod_zoom, mod_center in zip(model, zoom, center): - if DEBUG: print("MOD", mod, "ZOOM", mod_zoom, "CENTER", mod_center, 'VIEW', view) + logging.debug("MOD %s ZOOM %s CENTER %s VIEW %s", mod, mod_zoom, mod_center, selected_tiles) - figure = get_models_figure(mod, variable, date, tstep, - static=static, aspect=(nrows, ncols), view=view, - center=mod_center, zoom=mod_zoom) + # Get current tag + if mod in MODELS: + tag = 'model' + else: + tag = isinstance(tag, str) and tag or str(tag) + + # Get current index + if isinstance(mod, str): + index = mod + else: + index = str(mod) + + layers = get_model_figure(var=variable, model=mod, tstep=tstep, selected_date=date, + aspect=(nrows, ncols)) + figure = get_figure(aspect=(nrows, ncols), selected_tiles=selected_tiles, center=mod_center, + zoom=mod_zoom, tag=tag, index=index, layers=layers) figure.style = { 'height': '{}vh'.format(int(GRAPH_HEIGHT/nrows)), @@ -890,13 +810,12 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z # add n_clicks to id dict to reset the state of map, for centering figure.id['n_clicks'] = n_clicks - # if DEBUG: print('FIGURE KEYS 2', figure.id) - if DEBUG: print('STATIC', static, int(GRAPH_HEIGHT)/nrows) + # logging.debug('FIGURE KEYS 2 %s', figure.id) figures.append(figure) if DEBUG: for fig in figures: - print("************", fig.id, fig.style, "**************") + logging.debug("************ %s %s**************", fig.id, fig.style, ) res = [ dbc.Row( @@ -909,8 +828,10 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z if len(figures) > row+col+(row*(ncols-1)) ], align="start", - no_gutters=True, + # no_gutters is replaced with the className because of bootstrap update + # no_gutters=True, + className='g-0', ) for row in range(nrows) ] - if DEBUG: print("**** REQUEST TIME", str(time.time() - st_time)) + logging.debug("**** REQUEST TIME %s", str(time.time() - st_time)) return res diff --git a/tabs/generic.py b/tabs/generic.py new file mode 100644 index 0000000000000000000000000000000000000000..a417cfefa9c0871f64080e9995b1188e5dc176db --- /dev/null +++ b/tabs/generic.py @@ -0,0 +1,94 @@ +import dash_bootstrap_components as dbc +from dash import html +from ines_core_map_handler import MAP_LAYERS +from map_handler import DELAY, DELAY_DATE, END_DATE, FORECAST_FINAL_DAY +from datetime import datetime +from datetime import timedelta + +def get_forecast_days(curdate=END_DATE, lastday=FORECAST_FINAL_DAY): + """ Return forecast days according to configuration file """ + delay = DELAY + st_date = DELAY_DATE + if (delay and st_date \ + and (datetime.strptime(curdate, "%Y%m%d") >= datetime.strptime(st_date, "%Y%m%d"))) or \ + (not delay and st_date \ + and (datetime.strptime(curdate, "%Y%m%d") < datetime.strptime(st_date, "%Y%m%d"))) or \ + (delay and not st_date): + st_day = 1 + else: + st_day = 0 + + return dict([ + ( + idx, (datetime.strptime(curdate, "%Y%m%d") + \ + timedelta(days=idx)).strftime("%a %d").upper() + ) for idx in range(st_day, lastday) + ]) + +def layout_view(): + """ Return the menu for the various mapview types""" + + return html.Div([ + html.Span( + dbc.DropdownMenu( + id='map-view-dropdown', + label='VIEW', + children=[ + dbc.DropdownMenuItem( + MAP_LAYERS[style]['name'], + id=dict( + tag='view-style', + index=style + ), + active=active + ) + for style, active in zip(list(MAP_LAYERS.keys()), + [True if i == 'carto-positron' + else False for i in MAP_LAYERS]) + ], + direction="up", + in_navbar=True, + ), + ) + ], + id='map-view-dropdown-div') + + +def time_series(): + """ Return the timeseries element""" + return html.Div( + id='open-timeseries', + children=[ + dbc.Spinner( + id='loading-ts-modal', + fullscreen=True, + fullscreen_style={'opacity': '0.5', 'zIndex' : '200000'}, + show_initially=False, + # debounce=200, + children=[ + dbc.Modal([], + id='ts-modal', + size='xl', + centered=True, + is_open=False, + ), + ], + )], + #style={'display': 'none'}, + ) + +def layout_layers(*args): + """ Build the layers dropdown. User inputs strings for the item ids. + Returns the dropdown element with the ids shown capitalized as items. """ + dropdown = [] + for layer in args: + dropdown.append(dbc.DropdownMenuItem(layer.upper(), id=layer)) + return html.Div([ + html.Span( + dbc.DropdownMenu( + id='map-layers-dropdown', + label='LAYERS', + children=dropdown, + direction="up", + ), + )]) diff --git a/tabs/generic_callbacks.py b/tabs/generic_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..df0b28003d97c11859643dba781f3eaa390e34a9 --- /dev/null +++ b/tabs/generic_callbacks.py @@ -0,0 +1,20 @@ +import dash +from dash.dependencies import ALL, MATCH +from dash.exceptions import PreventUpdate +from dash.dependencies import Input, State, Output +from map_handler import DEBUG, FREQ +from map_handler import cache, cache_timeout + +@dash.callback( + Output({'type':'caret', 'index': MATCH}, 'style'), + [Input({'type':'collapse', 'index': MATCH}, 'is_open')], +) +def rotate_section_caret(collapse_open): + """ Rotates section 3 menu caret """ + rotation = 0 if collapse_open else 180 + rotate_caret = { + 'transform': 'rotate({}deg)'.format(rotation), + '-ms-transform': 'rotate({}deg)'.format(rotation), + '-webkit-transform': 'rotate({}deg)'.format(rotation) + } + return rotate_caret diff --git a/tabs/observations.py b/tabs/observations.py index dc2d69ab95cf88d8a3329c5836f962216ec946d9..831d1862cefba9cae6dad520535d1055bca61d57 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -1,20 +1,21 @@ from dash import dcc import dash_bootstrap_components as dbc from dash import html -from data_handler import DEBUG -from data_handler import DEFAULT_VAR -from data_handler import DEFAULT_MODEL -from data_handler import FREQ -from data_handler import VARS -from data_handler import MODELS -from data_handler import START_DATE, END_DATE -from data_handler import STYLES -from data_handler import DISCLAIMER_NO_FORECAST +from ines_core_map_handler import MAP_LAYERS +from map_handler import DEBUG +from map_handler import DEFAULT_VAR +from map_handler import DEFAULT_MODEL +from map_handler import FREQ +from map_handler import VARS +from map_handler import MODELS +from map_handler import START_DATE, END_DATE +from map_handler import DISCLAIMER_MODELS +from map_handler import SATELLITE_IMAGE_SRC # from tabs.forecast import layout_view -from utils import get_vis_edate from datetime import datetime as dt from datetime import timedelta +import logging aod_end_date = '20210318' @@ -26,23 +27,74 @@ layout_view = html.Div([ label='VIEW', children=[ dbc.DropdownMenuItem( - STYLES[style]['name'], + MAP_LAYERS[style]['name'], id=dict( tag='vis-view-style', index=style ), active=active ) - for style, active in zip(STYLES, [True if i == 'carto-positron' - else False for i in STYLES]) + for style, active in zip(MAP_LAYERS, [True if i == 'carto-positron' + else False for i in MAP_LAYERS]) ], direction="up", ), )]) +def get_vis_edate(end_date, hour=None): + """ Return default date and timestep for visibility + + Parameters + ---------- + end_date : str + End date + hour : int, optional + Hour, by default None + + Returns + ------- + str + Default date + int + Default hour + """ + + from map_handler import DEBUG + + delay = timedelta(hours=8) + half_day = timedelta(hours=12) + fmt_full = "%Y%m%d %H:%M" + fmt_date = "%Y%m%d" + hours = (0, 6, 12, 18) + + now = dt.now() + now_hour = now.hour + if hour is not None: + now_hour = hour + edate = dt.strptime(end_date, fmt_date) + timedelta(hours=now_hour) + cdate = edate.strftime(fmt_date) + + curr = None + for idx, h in enumerate(hours[:-1]): + curr = dt.strptime("{} {:02d}:00".format(cdate, h), fmt_full) + curr1 = dt.strptime("{} {:02d}:00".format(cdate, hours[idx+1]), fmt_full) + if edate < curr + delay: + curr = curr - half_day + curr1 = curr1 - half_day + + if (edate >= curr + delay) and (edate < curr1 + delay): + if DEBUG: logging.debug("NOW %s CURR %s H %s", edate, curr, curr.hour) + break + + if curr is not None: + return curr.strftime(fmt_date), curr.hour + + return cdate, edate.hour + + def obs_time_slider(div='obs', start=0, end=23, step=1, start_date=START_DATE, end_date=END_DATE): - # if DEBUG: print("------------\n", start, type(start), step, type(step), "------------\n") + # logging.debug("----------- %s %s %s %s -------------\n", start, type(start), step, type(step)) default_tstep = 0 @@ -53,7 +105,7 @@ def obs_time_slider(div='obs', start=0, end=23, step=1, start_date=START_DATE, e else: edate = end_date - # if DEBUG: print("------------\n", edate, default_tstep, "\n------------\n") + # logging.debug("------------\n %s, %s \n------------\n", edate, default_tstep) date_picker = html.Span([ dcc.DatePickerSingle( id='{}-date-picker'.format(div), @@ -88,7 +140,7 @@ def obs_time_slider(div='obs', start=0, end=23, step=1, start_date=START_DATE, e # if tstep%2 == 0 else '' for tstep in range(start, end+1, step) } - if DEBUG: print("VIS MARKS", marks[list(marks.keys())[-1]]) + logging.debug("VIS MARKS %s", marks[list(marks.keys())[-1]]) marks[list(marks.keys())[-1]]['style'] = {} marks[list(marks.keys())[-1]]['style']['left'] = '' marks[list(marks.keys())[-1]]['style']['right'] = '-32px' @@ -163,7 +215,7 @@ def tab_observations(window='rgb', start_date=START_DATE, end_date=END_DATE): html.Div([ html.Img( id='rgb-image', - src='./assets/eumetsat/FullDiscHD/archive/{date}/FRAME_OIS_RGB-dust-all_{date}{tstep:02d}00.gif'.format(date=end_date, tstep=0), + src=SATELLITE_IMAGE_SRC['fulldisc_obs'].format(date=end_date, tstep=0), alt='EUMETSAT RGB - NOT AVAILABLE', ), html.Div( @@ -197,7 +249,7 @@ def tab_observations(window='rgb', start_date=START_DATE, end_date=END_DATE): html.Div([ html.Img( id='aod-image', - src='./assets/metoffice/{date}/MSG_{date}{tstep:02}00_AOD_444x278.gif'.format(date=aod_end_date, tstep=0), + src=SATELLITE_IMAGE_SRC['aod_image'].format(date=aod_end_date, tstep=0), alt='MetOffice AOD - NOT AVAILABLE', ), html.Div( @@ -246,7 +298,7 @@ def tab_observations(window='rgb', start_date=START_DATE, end_date=END_DATE): #layout_view, html.Br(), html.Br(), - html.Div(DISCLAIMER_NO_FORECAST, + html.Div(DISCLAIMER_MODELS, className='disclaimer'), ], className="layout-dropdown rgb-layout-dropdown", diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index a540e32de68810f6cf323502d0991ea78c88e014..44bb1ef52f37c3ab8edf755c5ec58ea6ce80b623 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -5,18 +5,17 @@ from dash.dependencies import Input from dash.dependencies import State from dash.dependencies import ALL from dash.exceptions import PreventUpdate -from data_handler import DEBUG -from data_handler import START_DATE, END_DATE +from map_handler import DEBUG +from map_handler import START_DATE, END_DATE +from map_handler import SATELLITE_IMAGE_SRC from tabs.observations import tab_observations from datetime import datetime as dt import math +import logging -from data_handler import cache, cache_timeout - -#def register_callbacks(app, cache, cache_timeout): -# """ Registering callbacks """ +from map_handler import cache, cache_timeout @dash.callback( [Output('observations-tab', 'children'), @@ -34,12 +33,10 @@ def render_observations_tab(rgb_button, vis_button): bold = { 'font-weight': 'bold' } norm = { 'font-weight': 'normal' } - if DEBUG: - print(rgb_button, vis_button) + logging.debug('rgb and vis buttons %s %s', rgb_button, vis_button) if ctx.triggered: - if DEBUG: - print(ctx.triggered[0]["prop_id"].split(".")) + logging.debug('render_obs_tab ctx %s', ctx.triggered[0]["prop_id"].split(".")) button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id == "rgb" and rgb_button: @@ -52,68 +49,6 @@ def render_observations_tab(rgb_button, vis_button): return dash.no_update, bold, norm, 'rgb' raise PreventUpdate -# @dash.callback( -# Output('aod-image', 'src'), -# [Input('obs-aod-date-picker', 'date'), -# Input('obs-aod-slider-graph', 'value')], -# prevent_initial_call=True -# ) -# def update_aod_image_src(date, tstep): -# -# path_tpl = 'metoffice/{date}/MSG_{date}{tstep:02}00_AOD_444x278.gif' -# -# try: -# date = dt.strptime(date, '%Y-%m-%d').strftime('%Y%m%d') -# except: -# pass -# path = path_tpl.format(date=date, tstep=tstep) -# # print('......', path) -# return app.get_asset_url(path) -# -# # start/stop animation -# @dash.callback( -# [Output('obs-aod-slider-interval', 'disabled'), -# Output('obs-aod-slider-interval', 'n_intervals')], -# [Input('btn-obs-aod-play', 'n_clicks'), -# Input('btn-obs-aod-stop', 'n_clicks')], -# [State('obs-aod-slider-interval', 'disabled'), -# State('obs-aod-slider-graph', 'value')], -# prevent_initial_call=True -# ) -# def start_stop_obs_aod_autoslider(n_play, n_stop, disabled, value): -# """ Play/Pause map animation """ -# ctx = dash.callback_context -# if DEBUG: print("VALUE", value) -# if not value: -# value = 0 -# if ctx.triggered: -# button_id = ctx.triggered[0]["prop_id"].split(".")[0] -# if button_id == 'btn-obs-aod-play' and disabled: -# return not disabled, int(value) -# elif button_id == 'btn-obs-aod-stop' and not disabled: -# return not disabled, int(value) -# -# raise PreventUpdate -# -# -# @dash.callback( -# Output('obs-aod-slider-graph', 'value'), -# [Input('obs-aod-slider-interval', 'n_intervals')], -# prevent_initial_call=True -# ) -# def update_obs_aod_slider(n): -# """ Update slider value according to the number of intervals """ -# if DEBUG: print('SERVER: updating slider-graph ' + str(n)) -# if not n: -# return 0 -# if n >= 24: -# tstep = int(round(24*math.modf(n/24)[0], 0)) -# else: -# tstep = int(n) -# if DEBUG: print('SERVER: updating slider-graph ' + str(tstep)) -# return tstep - - @dash.callback( [Output('rgb-image', 'src'), Output('btn-fulldisc', 'active'), @@ -129,8 +64,7 @@ def render_observations_tab(rgb_button, vis_button): @cache.memoize(timeout=cache_timeout) def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_active, btn_middleeast_active): - if DEBUG: - print('BUTTONS', date, tstep, btn_fulldisc_active, btn_middleeast_active) + logging.debug('BUTTONS %s %s %s %s', date, tstep, btn_fulldisc_active, btn_middleeast_active) ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] @@ -142,14 +76,14 @@ def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_act elif btn_fulldisc_active: button_id = 'btn-fulldisc' - if DEBUG: print('BUTTONS', button_id) + logging.debug('BUTTONS %s', button_id) if button_id == 'btn-middleeast': - path_tpl = 'eumetsat/MiddleEast/archive/{date}/MET8_RGBDust_MiddleEast_{date}{tstep:02d}00.gif' + path_tpl = SATELLITE_IMAGE_SRC['middle_east'] btn_fulldisc_active = False btn_middleeast_active = True elif button_id == 'btn-fulldisc': - path_tpl = 'eumetsat/FullDiscHD/archive/{date}/FRAME_OIS_RGB-dust-all_{date}{tstep:02d}00.gif' + path_tpl = SATELLITE_IMAGE_SRC['fulldisc'] btn_fulldisc_active = True btn_middleeast_active = False @@ -158,7 +92,7 @@ def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_act except: pass path = path_tpl.format(date=date, tstep=tstep) - if DEBUG: print('......', path) + logging.debug('...... %s', path) return dash.get_asset_url(path), btn_fulldisc_active, btn_middleeast_active @@ -176,7 +110,7 @@ def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_act def start_stop_obs_autoslider(n_play, disabled, value): """ Play/Pause map animation """ ctx = dash.callback_context - if DEBUG: print("VALUE", value) + logging.debug("VALUE %s", value) if not value: value = 0 if ctx.triggered: @@ -196,53 +130,18 @@ def start_stop_obs_autoslider(n_play, disabled, value): @cache.memoize(timeout=cache_timeout) def update_obs_slider(n): """ Update slider value according to the number of intervals """ - if DEBUG: print('SERVER: updating slider-graph ' + str(n)) + logging.debug('SERVER: updating slider-graph %s', str(n)) if not n: return 0 if n >= 24: tstep = int(round(24*math.modf(n/24)[0], 0)) else: tstep = int(n) - if DEBUG: print('SERVER: updating slider-graph ' + str(tstep)) + logging.debug('SERVER: updating slider-graph %s', str(tstep)) return tstep -# @dash.callback( -# [Output({'tag': 'model-tile-layer', 'index': ALL}, 'url'), -# Output({'tag': 'model-tile-layer', 'index': ALL}, 'attribution'), -# Output({'tag': 'view-style', 'index': ALL}, 'active')], -# [Input({'tag': 'view-style', 'index': ALL}, 'n_clicks')], -# [State({'tag': 'view-style', 'index': ALL}, 'active'), -# State({'tag': 'model-tile-layer', 'index': ALL}, 'url')], -# prevent_initial_call=True -# ) -# # @cache.memoize(timeout=cache_timeout) +# MOVED TO GENERIC CALLBACKS # def update_styles_button(*args): -# """ Function updating styles button """ -# ctx = dash.callback_context -# if ctx.triggered: -# button_id = orjson.loads(ctx.triggered[0]["prop_id"].split(".")[0]) -# if DEBUG: print("BUTTON ID", str(button_id), type(button_id)) -# if button_id['index'] in STYLES: -# active = args[-2] -# graphs = args[-1] -# num_graphs = len(graphs) -# # if DEBUG: print("CURRENT ARGS", str(args)) -# # if DEBUG: print("NUM GRAPHS", num_graphs) -# -# res = [False for i in active] -# st_idx = list(STYLES.keys()).index(button_id['index']) -# if active[st_idx] is False: -# res[st_idx] = True -# url = [STYLES[button_id['index']]['url'] for x in range(num_graphs)] -# attr = [STYLES[button_id['index']]['attribution'] for x in range(num_graphs)] -# if DEBUG: -# print(res, url, attr) -# return url, attr, res -# # return [True if i == button_id['index'] else False for i in active] -# -# if DEBUG: print('NOTHING TO DO') -# raise PreventUpdate - @dash.callback( Output('obs-vis-graph', 'children'), @@ -254,9 +153,9 @@ def update_obs_slider(n): ) @cache.memoize(timeout=cache_timeout) def update_vis_figure(date, tstep, zoom, center): - from tools import get_vis_figure - from tools import get_models_figure - if DEBUG: print("*************", date, tstep, zoom, center) + from callback_tools import get_vis_figure + from callback_tools import get_figure + logging.debug("************* %s %s %s %s", date, tstep, zoom, center) if date is not None: date = date.split(' ')[0] try: @@ -278,8 +177,7 @@ def update_vis_figure(date, tstep, zoom, center): center = None # view = list(STYLES.keys())[view.index(True)] - if DEBUG: print('SERVER: VIS callback date {}, tstep {}'.format(date, tstep)) - df, points_layer = get_vis_figure(tstep=tstep, selected_date=date) - if DEBUG: print("POINTS LAYER", points_layer) - fig = get_models_figure(model=None, var=None, layer=points_layer, zoom=zoom, center=center, tag='obs-vis') + logging.debug('SERVER: VIS callback date %s tstep %s', date, tstep) + layers = get_vis_figure(tstep=tstep, selected_date=date) + fig = get_figure(layers=layers, zoom=zoom, center=center, tag='obs-vis') return fig diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py deleted file mode 100644 index 2e20ba378c624be4d3c617735b612f73bbe30555..0000000000000000000000000000000000000000 --- a/tests/test_data_handler.py +++ /dev/null @@ -1,285 +0,0 @@ -import pytest -from datetime import datetime -from datetime import timedelta -import importlib -code = importlib.import_module('data_handler') -from data_handler import END_DATE -from data_handler import FREQ - -FMT_ISO = "%Y%m%d" -FMT_DASH = "%Y-%m-%d" -FMT_MON = "%d %b %Y" -FMT_MON_HR = f"%Hh {FMT_MON}" -FMT_FULLMON = "%d %B %Y" -FMT_FULLMON_HR = f"%Hh {FMT_FULLMON}" -FMT_HR = "%H:%M:%S" -FMT_DASH_HR = f"{FMT_DASH} {FMT_HR}" -EDATE_OBJ = datetime.strptime(END_DATE, FMT_ISO) -EDATE_PREV = (datetime.strptime(END_DATE, FMT_ISO) - timedelta(days=7)).strftime(FMT_ISO) - -#.strftime("%d %B %Y")) -@pytest.fixture -def aeronet_instance(): - return code.Observations1dHandler(EDATE_PREV, END_DATE , 'aeronet') - -def test_generate_obs1d_tstep_trace1(aeronet_instance): - run = aeronet_instance.generate_obs1d_tstep_trace('OD550_DUST') - assert str(type(run[1])) == "" - -# @pytest.fixture -# def obsTSHandler_instance(): -# return code.ObsTimeSeriesHandler('aeronet','20220404','20220405', 'OD550_DUST', ['median', 'monarch']) -# -# def test_retrieve_timeseries(): -# assert obsTSHandler_instance.retrieve_timeseries(idx, st_name, model) == 'hello' - - -# =================== TIME SERIES HANDLER ============================ -@pytest.fixture -def TSHandler(): - return code.TimeSeriesHandler('median', END_DATE, 'OD550_DUST') - -# def test_TimeSeriesHandler(TSHandler): -# run = TSHandler.retrieve_single_point(1, 45, 45, model='median') -# assert float(run) == 0.10633552074432373 -# -# def test_TimeSeriesHandler_1(TSHandler): -# run = TSHandler.retrieve_single_point(3, 5, 5, model=None) -# assert float(run) == 0.00830854382365942 - -def test_retrieve_timeseries(TSHandler): - run = TSHandler.retrieve_timeseries(5, 5, model=None, method='netcdf', forecast=False) - assert run.layout.title.text =='Dust Optical Depth @ lat = 5 and lon = 5' - -def test_retrieve_timeseries_1(TSHandler): - run = TSHandler.retrieve_timeseries(35, 15, model='monarch', method='netcdf', forecast=True) - assert run.layout.title.text =='Dust Optical Depth @ lat = 35 and lon = 15' - -def test_retrieve_timeseries_2(TSHandler): - run = TSHandler.retrieve_timeseries(35, 15, model='monarch', method='netcdf', forecast=True) - assert run.data[0].name == 'MULTI-MODEL (35.25, 15.25)' - -# =================== FIGURE HANDLER ============================ -@pytest.fixture -def FigureHandler(): - return code.FigureHandler('median', END_DATE) - -def test_FH_get_center(FigureHandler): - assert FigureHandler.get_center([35,45]) == [35, 45] - assert FigureHandler.get_center([-35,-45]) == [-35, -45] - -def test_set_data_AOD(FigureHandler): - assert FigureHandler.set_data('OD550_DUST', tstep=0)[0][0] == -26.75 - -def test_set_data_SCONC(FigureHandler): - assert FigureHandler.set_data('SCONC_DUST', tstep=0)[0][0] == -26.75 - -def test_retrieve_cdatetime(FigureHandler): - assert str(FigureHandler.retrieve_cdatetime(tstep=0)) == EDATE_OBJ.strftime(FMT_DASH_HR) # '2022-06-06 12:00:00' - assert str(FigureHandler.retrieve_cdatetime(tstep=6)) == (EDATE_OBJ + timedelta(hours=6*FREQ)).strftime(FMT_DASH_HR) # '2022-06-07 06:00:00' - assert str(FigureHandler.retrieve_cdatetime(tstep=12)) == (EDATE_OBJ + timedelta(hours=12*FREQ)).strftime(FMT_DASH_HR) # '2022-06-08 00:00:00' - -def test_generate_contour_tstep_trace_leaflet(FigureHandler): - result = "GeoJSON(hideout={'colorscale': ['rgba(255,255,255,0.4)', '#a1ede3', '#5ce3ba', '#fcd775', '#da7230', '#9e6226', '#714921', '#392511', '#1d1309'], 'bounds': array([ 0. , 0.1, 0.2, 0.4, 0.8, 1.2, 1.6, 3.2, 6.4, 10. ]" - assert result in str(FigureHandler.generate_contour_tstep_trace_leaflet('OD550_DUST', tstep=0)[0]) - result2 = "Colorbar(classes=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9], colorscale=['rgba(255,255,255,0.4)', '#a1ede3', '#5ce3ba', '#fcd775', '#da7230', '#9e6226', '#714921', '#392511', '#1d1309']" - assert result2 in str(FigureHandler.generate_contour_tstep_trace_leaflet('OD550_DUST', tstep=0)[1]) - -def test_generate_contour_tstep_trace_AOD(FigureHandler): - assert FigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=0)['name'] == 'Dust Optical Depth_contours' - assert FigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=0)['showlegend'] == False - -def test_generate_contour_tstep_trace_SCONC(FigureHandler): - assert FigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=0)['name'] == 'Dust Surface Conc. (µg/m³)_contours' - assert FigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=0)['showlegend'] == False - -def test_generate_var_tstep_trace_leaflet_AOD(FigureHandler): - assert str(FigureHandler.generate_var_tstep_trace_leaflet(varname='OD550_DUST', tstep=0))[0:162] == "GeoJSON(data={'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [-26.75, 0.25]}, 'properties': {'value': " - -def test_generate_var_tstep_trace_leaflet_SCONC(FigureHandler): - assert str(FigureHandler.generate_var_tstep_trace_leaflet(varname='SCONC_DUST', tstep=0))[0:162] == "GeoJSON(data={'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'geometry': {'type': 'Point', 'coordinates': [-26.75, 0.25]}, 'properties': {'value': " - -def test_generate_var_tstep_trace_AOD(FigureHandler): - assert FigureHandler.generate_var_tstep_trace(varname='OD550_DUST', tstep=0)['name'] == "MULTI-MODEL" - -def test_generate_var_tstep_trace_SCONC(FigureHandler): - assert FigureHandler.generate_var_tstep_trace(varname='SCONC_DUST', tstep=6)['name'] == "MULTI-MODEL" - -def test_get_title(FigureHandler): - assert FigureHandler.get_title('OD550_DUST', tstep=0) == 'MULTI-MODEL Dust Optical Depth (550nm)
Valid: {edate} (H+00)'.format(edate=EDATE_OBJ.strftime(FMT_MON_HR)) - assert FigureHandler.get_title('SCONC_DUST', tstep=9) == 'MULTI-MODEL Dust Surface Conc. (µg/m³)
Valid: {edate} (H+27)'.format(edate=(EDATE_OBJ + timedelta(hours=9*FREQ)).strftime(FMT_MON_HR)) - -def test_hour_to_step(FigureHandler): - assert FigureHandler.hour_to_step(0) == 0 - assert FigureHandler.hour_to_step(3) == int(3/FREQ) - assert FigureHandler.hour_to_step(6) == int(6/FREQ) - assert FigureHandler.hour_to_step(36) == int(36/FREQ) - -def test_retrieve_var_tstep(FigureHandler): - assert FigureHandler.retrieve_var_tstep(varname='OD550_DUST', tstep=0, hour=None, static=True, aspect=(1,1), center=None, selected_tiles='carto-positron', zoom=None, layer=None, tag='empty').children[0].id == {'tag': 'model-tile-layer', 'index': 'median'} - assert FigureHandler.retrieve_var_tstep(varname='OD550_DUST', tstep=0, hour=None, static=True, aspect=(1,1), center=None, selected_tiles='carto-positron', zoom=None, layer=None, tag='empty').children[3].classes == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] - assert str(FigureHandler.retrieve_var_tstep(varname='OD550_DUST', tstep=0, hour=None, static=True, aspect=(1,1), center=None, selected_tiles='carto-positron', zoom=None, layer=None, tag='empty').children[4].children) == "P(B(['MULTI-MODEL Dust Optical Depth (550nm)', Br(None), 'Valid: {edate} (H+00)']))".format(edate=EDATE_OBJ.strftime(FMT_MON_HR)) - -# =================== Scores Figure Handler ============================ -@pytest.fixture -def ScoresFigureHandler(): - return code.ScoresFigureHandler('aeronet', 'bias', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) - -def test_get_mapbox(ScoresFigureHandler): - assert ScoresFigureHandler.get_mapbox('carto-positron', False, 2.8, None)['bearing'] == 0 - assert ScoresFigureHandler.get_mapbox('carto-positron', False, 2.8, None)['pitch'] == 0 - assert ScoresFigureHandler.get_mapbox('carto-positron', False, 2.8, None)['uirevision'] == True - assert ScoresFigureHandler.get_mapbox('carto-positron', False, 2.8, None)['zoom'] == 2.8 - assert str(ScoresFigureHandler.get_mapbox('carto-positron', False, 2.8, None)['center']) == "layout.mapbox.Center({\n 'lat': 33.5, 'lon': 16.5\n})" - -def test_generate_trace(ScoresFigureHandler): - stats = ['Lille', 'Mainz', 'Palaiseau', 'Paris', 'Kyiv'] - vals = [-0.11, -0.12, -0.07, -0.09, -0.14] - assert ScoresFigureHandler.generate_trace(35, 35, stats, vals)['name'] == 'MBE score' - assert ScoresFigureHandler.generate_trace(35, 35, stats, vals)['marker']['cmin'] == -0.1 - assert ScoresFigureHandler.generate_trace(35, 35, stats, vals)['marker']['cmax'] == 0.1 - assert ScoresFigureHandler.generate_trace(35, 35, stats, vals)['marker']['colorbar']['x'] == 0.94 - -def test_retrieve_scores(ScoresFigureHandler): - assert ScoresFigureHandler.retrieve_scores('median', aspect=(1,1), center=None).data[1].name == 'MBE score' - assert ScoresFigureHandler.retrieve_scores('median', aspect=(1,1), center=None).data[1].type == 'scattermapbox' - -# =================== Vis Handler ============================ -@pytest.fixture -def VisFigureHandler(): - return code.VisFigureHandler(END_DATE) - -# def test_set_data(VisFigureHandler): -# assert VisFigureHandler.set_data(0)[5][1][0] == [11] -# assert VisFigureHandler.set_data(0)[5][3][0][1] == 1 -# assert VisFigureHandler.set_data(24) == ([], [], [], [], [], ()) -# assert VisFigureHandler.set_data(36) == ([], [], [], [], [], ()) - -def test_generate_var_tstep_trace(VisFigureHandler): - returned = VisFigureHandler.generate_var_tstep_trace([], [], [], [], [], (), ('#714921', '#da7230', '#fcd775', 'CadetBlue'), ('<1 km', '1 - 2 km', '2 - 5 km', 'Haze'), ('o', 'o', 'o', '^'), 6) - assert len(returned) == 2 - assert str(type(returned[0])) == "" - assert returned[1][1].id == 'vis-info' - assert returned[1][1].children ==['NO DATA AVAILABLE'] - assert returned[1][0].data['type'] == 'FeatureCollection' - -def test_vis_get_title(VisFigureHandler): - assert str(VisFigureHandler.get_title(tstep=0)) =="['Visibility reduced by airborne dust', Br(None), '{edate} 00-06 UTC']".format(edate=EDATE_OBJ.strftime(FMT_FULLMON)) - assert str(VisFigureHandler.get_title(tstep=9)) =="['Visibility reduced by airborne dust', Br(None), '{edate} 09-15 UTC']".format(edate=EDATE_OBJ.strftime(FMT_FULLMON)) - assert str(VisFigureHandler.get_title(tstep=39)) =="['Visibility reduced by airborne dust', Br(None), '{edate} 39-45 UTC']".format(edate=EDATE_OBJ.strftime(FMT_FULLMON)) - assert str(VisFigureHandler.get_title(tstep=72)) =="['Visibility reduced by airborne dust', Br(None), '{edate} 72-78 UTC']".format(edate=EDATE_OBJ.strftime(FMT_FULLMON)) - -def test_vis_retrieve_var_tstep(VisFigureHandler): - result = VisFigureHandler.retrieve_var_tstep(tstep=0, hour=None, static=True, aspect=(1,1), center=None) - assert str(type(result[0])) == "" - assert result[1][0].data['type'] =='FeatureCollection' - - result = VisFigureHandler.retrieve_var_tstep(tstep=1, hour=1, static=True, aspect=(1,1), center=None) - assert str(type(result[0])) == "" - assert result[1][0].data['type'] =='FeatureCollection' - -# =================== Prob Handler ============================ -@pytest.fixture -def ProbFigureHandler(): - return code.ProbFigureHandler('OD550_DUST', 0.1, END_DATE) - -# def test_prob_set_data(ProbFigureHandler): -# assert ProbFigureHandler.set_data('OD550_DUST', tstep=0)[0].data[0] == -24.75 -# assert ProbFigureHandler.set_data('OD550_DUST', tstep=0)[0].data[9] == -20.25 -# assert ProbFigureHandler.set_data('OD550_DUST', tstep=0)[1].data[9] == 5.25 - -def test_prob_retrieve_cdatetime(ProbFigureHandler): - assert str(ProbFigureHandler.retrieve_cdatetime(tstep=0)) == '{edate} 00:00:00'.format(edate=EDATE_OBJ.strftime(FMT_DASH)) - assert str(ProbFigureHandler.retrieve_cdatetime(tstep=1)) == '{edate} 00:00:00'.format(edate=(EDATE_OBJ + timedelta(days=1)).strftime(FMT_DASH)) - -def test_prob_generate_contour_tstep_trace_AOD(ProbFigureHandler): - #test day 0 - assert ProbFigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=0)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/00_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ProbFigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=0)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ProbFigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=0)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - #test day 1 - assert ProbFigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=1)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/01_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ProbFigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=1)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ProbFigureHandler.generate_contour_tstep_trace('OD550_DUST', tstep=1)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - -def test_prob_generate_contour_tstep_trace_SCONC(ProbFigureHandler): - #test day 0 - assert ProbFigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=0)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/00_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ProbFigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=0)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ProbFigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=0)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - #test day 1 - assert ProbFigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=1)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/01_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ProbFigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=1)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ProbFigureHandler.generate_contour_tstep_trace('SCONC_DUST', tstep=1)[1].classes == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - -def test_prob_get_title(ProbFigureHandler): - assert ProbFigureHandler.get_title('OD550_DUST', tstep=0)[-30:] == '{edate} Valid: {edate}'.format(edate=EDATE_OBJ.strftime(FMT_MON)) - assert ProbFigureHandler.get_title('SCONC_DUST', tstep=1)[-30:] == '{edate} Valid: {edate_next}'.format(edate=EDATE_OBJ.strftime(FMT_MON), edate_next=(EDATE_OBJ + timedelta(days=1)).strftime(FMT_MON)) - -def test_prob_retrieve_var_tstep(ProbFigureHandler): - assert ProbFigureHandler.retrieve_var_tstep('OD550_DUST', 0, True, (1,1))[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/00_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ProbFigureHandler.retrieve_var_tstep('OD550_DUST', 1, True, (1,1))[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - - assert ProbFigureHandler.retrieve_var_tstep('SCONC_DUST', 1, True, (1,1))[0].url =='/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/01_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ProbFigureHandler.retrieve_var_tstep('SCONC_DUST', 0, True, (1,1))[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - - -# =================== Was Handler ============================ -@pytest.fixture -def WasFigureHandler(): - return code.WasFigureHandler(was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=END_DATE) - -def test_was_get_regions_data(WasFigureHandler): - #day 1 - assert WasFigureHandler.get_regions_data(day=1)[0][0] == 'Boucle du Mouhoun' - assert WasFigureHandler.get_regions_data(day=1)[0][1] == 'Cascades' - assert WasFigureHandler.get_regions_data(day=1)[0][-1] == 'Sud-Ouest' - - assert WasFigureHandler.get_regions_data(day=1)[1][0] == 'green' - assert WasFigureHandler.get_regions_data(day=1)[1][1] == 'green' - assert WasFigureHandler.get_regions_data(day=1)[1][-1] == 'green' - - assert WasFigureHandler.get_regions_data(day=1)[2][0] == 'Normal' - assert WasFigureHandler.get_regions_data(day=1)[2][1] == 'Normal' - assert WasFigureHandler.get_regions_data(day=1)[2][-1] =='Normal' - - #day 2 - assert WasFigureHandler.get_regions_data(day=2)[0][0] == 'Boucle du Mouhoun' - assert WasFigureHandler.get_regions_data(day=2)[0][1] == 'Cascades' - assert WasFigureHandler.get_regions_data(day=2)[0][-1] == 'Sud-Ouest' - - assert WasFigureHandler.get_regions_data(day=2)[1][0] == 'green' - assert WasFigureHandler.get_regions_data(day=2)[1][1] == 'green' - assert WasFigureHandler.get_regions_data(day=2)[1][-1] == 'green' - - assert WasFigureHandler.get_regions_data(day=2)[2][0] == 'Normal' - assert WasFigureHandler.get_regions_data(day=2)[2][1] == 'Normal' - assert WasFigureHandler.get_regions_data(day=2)[2][-1] =='Normal' - -def test_was_get_geojson_url(WasFigureHandler): - assert WasFigureHandler.get_geojson_url(day=1) == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) - assert WasFigureHandler.get_geojson_url(day=2) == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_2.geojson'.format(edate=END_DATE) - -def test_was_retrieve_cdatetime(WasFigureHandler): - assert str(WasFigureHandler.retrieve_cdatetime(tstep=0)) == '{edate} 00:00:00'.format(edate=EDATE_OBJ.strftime(FMT_DASH)) - -def test_was_generate_contour_tstep_trace(WasFigureHandler): - assert WasFigureHandler.generate_contour_tstep_trace(1)[0].options == {'style': {'variable': 'forecastTab.wasMaps.styleHandle'}} - assert WasFigureHandler.generate_contour_tstep_trace(1)[0].url == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) - assert WasFigureHandler.generate_contour_tstep_trace(1)[1].className == 'was-legend' - - assert WasFigureHandler.generate_contour_tstep_trace(2)[0].options == {'style': {'variable': 'forecastTab.wasMaps.styleHandle'}} - assert WasFigureHandler.generate_contour_tstep_trace(2)[0].url == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_2.geojson'.format(edate=END_DATE) - assert WasFigureHandler.generate_contour_tstep_trace(2)[1].className == 'was-legend' - -def test_was_get_title(WasFigureHandler): - assert str(WasFigureHandler.get_title(0)) =="Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: {edate}. Valid: {edate}".format(edate=EDATE_OBJ.strftime(FMT_MON)) - assert str(WasFigureHandler.get_title(1)) =="Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: {edate}. Valid: {edate_next}".format(edate=EDATE_OBJ.strftime(FMT_MON), edate_next=(EDATE_OBJ + timedelta(days=1)).strftime(FMT_MON)) - -def test_was_retrieve_var_tstep(WasFigureHandler): - assert WasFigureHandler.retrieve_var_tstep(1, True, (1,1))[0].url =='/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) - assert WasFigureHandler.retrieve_var_tstep(1, True, (1,1))[0].options =={'style': {'variable': 'forecastTab.wasMaps.styleHandle'}} - assert WasFigureHandler.retrieve_var_tstep(2, True, (1,1))[0].url =='/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_2.geojson'.format(edate=END_DATE) - assert WasFigureHandler.retrieve_var_tstep(2, True, (1,1))[0].options =={'style': {'variable': 'forecastTab.wasMaps.styleHandle'}} diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py index 2d0353b1cb4d68116350964f3deb899a04f71bcb..e78bbc4ae96ddf99d8bac8eac6e1c5540cabef91 100644 --- a/tests/test_evaluation_callbacks.py +++ b/tests/test_evaluation_callbacks.py @@ -7,6 +7,7 @@ import dash from dash._callback_context import context_value from dash._utils import AttributeDict code = importlib.import_module('tabs.evaluation_callbacks') +from map_handler import START_DATE, END_DATE #add equality checker for objects def __eq__(self, other): @@ -14,9 +15,6 @@ def __eq__(self, other): return self.a == other.a and self.b == other.b return False -#============ TEST extend_l============================================ -def test_extend_l(): - assert code.extend_l([[1],[2],[3]]) == [1,2,3] #============ TEST render_evaluation_tab================================= def test_render_evaluation_tab(): @@ -35,7 +33,7 @@ def test_no_data_modis(): assert code._no_modis_data() == ([{'name': 'NO DATA', 'id': ''}], [], {'display': 'block'}) #============ TEST update_time_selection=================================== -def test_update_time_selection(): +def test_update_time_selection_seasonal(): def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "obs-timescale-dropdown.value"},{"prop_id": "obs-network-dropdown.value"}]})) return code.update_time_selection('seasonal', 'modis') @@ -44,7 +42,24 @@ def test_update_time_selection(): output = ctx.run(run_callback) assert "'Spring 2018', 'value': '201803-201805'}], 'Select season')" in str(output) +def test_update_time_selection_annual(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "obs-timescale-dropdown.value"},{"prop_id": "obs-network-dropdown.value"}]})) + return code.update_time_selection('annual', 'modis') + + ctx = copy_context() + output = ctx.run(run_callback) + assert "{'label': '2018', 'value': '201801-201812'}], 'Select year')" in str(output) + +def test_update_time_selection_monthly(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "obs-timescale-dropdown.value"},{"prop_id": "obs-network-dropdown.value"}]})) + return code.update_time_selection('monthly', 'modis') + + ctx = copy_context() + output = ctx.run(run_callback) + assert "{'label': 'April 2023', 'value': '202304'}, {'label': 'March 2023', 'value': '202303'}, {'label': 'February 2023', 'value': '202302'}, {'label': 'January 2023', 'value': '202301'}" in str(output) #============ TEST modis_scores_tables_retrieve======================== def test_modis_scores_tables_retrieve_no_n_click(): def run_callback(): @@ -141,3 +156,100 @@ def test_aeronet_scores_tables_retrieve_no_data(): ctx = copy_context() output = ctx.run(run_callback) assert output[0] == [{'id': 'station', 'name': ['NO DATA']},{'id': 'station', 'name': ['NO DATA']}, {'id': 'station', 'name': ['NO DATA']}, {'id': 'station', 'name': ['NO DATA']}, {'id': 'station', 'name': ['NO DATA']}] + +#=====================test show eval modis timeseries=============================== +# def test_show_eval_modis_timeseries(): +# def run_callback(): +# #df_input is not the correct format and needs to be changed +# df_input = {'lon': {'10': 10.949999809265137}, 'lat': {'10': 44.630001068115234}, 'stations': {'10': 'Modena'}} +# context_value.set(AttributeDict(**{"triggered_inputs": +# [{"prop_id": "ts-eval-modis-button.n_clicks"}, +# {"prop_id": "modis-clicked-coords.data"}, +# {"prop_id": "eval-date-picker.date"}, +# {"prop_id": "obs-dropdown.value"}, +# {"prop_id": "obs-mod-dropdown.value"}]})) +# return code.show_eval_modis_timeseries.uncached(1, df_input, START_DATE, 'aeronet', 'median') +# +# ctx = copy_context() +# output = ctx.run(run_callback) +# assert output == 0 + +#=====================test show aeronet modis timeseries=============================== +def test_show_eval_aeronet_timeseries(): + def run_callback(): + df_input = {'lon': {'10': 10.949999809265137}, 'lat': {'10': 44.630001068115234}, 'stations': {'10': 'Modena'}} + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "ts-eval-button.n_clicks"}, + {"prop_id": "stations-clicked-coords.data"}, + {"prop_id": "eval-date-picker.start_date"}, + {"prop_id": "eval-date-picker.end_date"}, + {"prop_id": "obs-dropdown.value"}, + {"prop_id": "obs-mod-dropdown.value"}]})) + return code.show_eval_aeronet_timeseries.uncached(1, df_input, START_DATE, END_DATE, 'aeronet', 'monarch') + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0].children.id == 'timeseries-eval-modal' + assert output[0].children.config == {'displayModeBar': True, 'displaylogo': False, 'modeBarButtonsToRemove': ['zoom2d', 'pan2d', 'select2d', 'lasso2d', 'autoScale2d']} + assert output[1] == True + +#===================== test update_eval_aeronet =============================== +def test_update_eval_aeronet(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "eval-apply.n_clicks"}, + {"prop_id": "obs-models-dropdown.start_date"}, + {"prop_id": "obs-models-dropdown._date"}, + {"prop_id": "obs-selection-dropdown.value"}]})) + return code.update_eval_aeronet.uncached(1, START_DATE, END_DATE, 'aeronet') + + ctx = copy_context() + output = ctx.run(run_callback) + assert list(output[0].keys()) == ['lon', 'lat', 'stations'] + assert output[1].id == {'index': 'None', 'tag': 'empty-map'} + assert output[1].children[0].id == {'tag': 'empty-tile-layer', 'index': 'None'} + assert output[0]['stations'][88] == 'REIMS_GSMA' + assert output[0]['lat'][88] == 49.2400016784668 + assert output[0]['lon'][88] == 4.070000171661377 + assert "OpenStreetMap contributors © CARTO" in str(output[1]) + +#===================== test update_eval_modis =============================== +def test_update_eval_modis(): + def run_callback(): + mod_div = {'props': {'children': [{'props': {'children': None, 'id': {'tag': 'empty-tile-layer', 'index': 'None'}, 'attribution': "© OpenStreetMap contributors © CARTO", 'url': 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'}, 'type': 'TileLayer', 'namespace': 'dash_leaflet'}, {'props': {'position': 'topright'}, 'type': 'FullscreenControl', 'namespace': 'dash_leaflet'}, None, None, None, None], 'id': {'tag': 'empty-map', 'index': 'None'}, 'animate': False, 'center': [30, 15], 'inertia': True, 'minZoom': 2, 'preferCanvas': True, 'wheelDebounceTime': 80, 'wheelPxPerZoomLevel': 120, 'zoom': 2.9, 'zoomSnap': 0.1, 'bounds': [[-24.686952411999144, -31.640625000000004], [65.87472467098549, 61.52343750000001]]}, 'type': 'Map', 'namespace': 'dash_leaflet'} + + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "eval-apply.n_clicks"}, + {"prop_id": "eval-date-picker.date"}, + {"prop_id": "obs-models-dropdown.date"}, + {"prop_id": "obs-dropdown.value"}, + {"prop_id": "graph-eval-modis-mod.children"}]})) + return code.update_eval_modis.uncached(1, START_DATE, 'median', 'modis', mod_div) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0].id == {'index': 'modis', 'tag': 'modis-map'} + assert output[0].children[0].id == {'tag': 'modis-tile-layer', 'index': 'modis'} + assert output[0].children[0].attribution == "© OpenStreetMap contributors © CARTO" + assert output[1].children[0].url == 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' + assert output[1].id == {'index': 'None', 'tag': 'empty-map'} + +#===================== test update_eval('modis') =============================== +def test_update_eval_test_modis(): + assert str(code.update_eval.uncached('modis')[0][0]) == "Label('Date')" + assert code.update_eval.uncached('modis')[0][1].id == "eval-date-picker" + assert code.update_eval.uncached('modis')[0][1].display_format == 'DD MMM YYYY' + + assert " zoomSnap=0.1), id='graph-eval-modis-obs'), Div(children=P('Aerosol data ©2023 WMO Barcelona Dust Regional Center, NASA.')" in str(code.update_eval.uncached('modis')[1][0]) + assert str(code.update_eval.uncached('modis')[2]) == "modis" + assert str(code.update_eval.uncached('modis')[3]) == "{'display': 'table-cell'}" + +#===================== test update_eval('aeronet') =============================== +def test_update_eval_test_aeronet(): + assert str(code.update_eval.uncached('aeronet')[0][0]) == "Label('Date range')" + assert code.update_eval.uncached('aeronet')[0][1].id == "eval-date-picker" + assert code.update_eval.uncached('aeronet')[0][1].display_format == 'DD MMM YYYY' + + assert "id='graph-eval-aeronet'), Div(children=P('Aerosol data ©2023 WMO Barcelona Dust Regional Center, NASA.'), id='eval-aeronet-disclaimer', className='disclaimer" in str(code.update_eval.uncached('aeronet')[1]) + assert str(code.update_eval.uncached('aeronet')[2]) == "aeronet" + assert str(code.update_eval.uncached('aeronet')[3]) == "{'display': 'none'}" diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 3c7e26afe5e03c9ce0483d7d6f3a731b02a939f7..cade99fe2784e12b5bbdfdc8e3ad9e5570ad967d 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -1,17 +1,8 @@ import importlib -from data_handler import VARS -from data_handler import MODELS +from map_handler import VARS +from map_handler import MODELS code = importlib.import_module('tabs.forecast') -def test_layout_view(): - assert "Div(children=[Span(DropdownMenu(children=[DropdownMenuItem(children='Light', id={'tag': 'view-style', 'index': 'carto-positron'}, active=True), DropdownMenuItem(children='Open street map', id={'tag': 'view-style', 'index': 'open-street-map'}, active=False), DropdownMenuItem(children='Terrain', id={'tag': 'view-style', 'index': 'stamen-terrain'}, active=False), DropdownMenuItem(children='ESRI', id={'tag': 'view-style', 'index': 'esri-world'}, active=False)], id='map-view-dropdown', direction='up', in_navbar=True, label='VIEW'))], id='map-view-dropdown-div')" in str(code.layout_view()) - -def test_time_series(): - assert "Div(children=[Spinner(children=[Modal(children=[], id='ts-modal', centered=True, is_open=False, size='xl')], id='loading-ts-modal', fullscreen=True, fullscreen_style={'opacity': '0.5', 'zIndex': '200000'}, show_initially=False)], id='open-timeseries')" in str(code.time_series()) - -def test_layout_layers(): - assert "Div([Span(DropdownMenu(children=[DropdownMenuItem(children='AIRPORTS', id='airports')], id='map-layers-dropdown', direction='up', label='LAYERS'))])" in str(code.layout_layers()) - def test_model_time_bar(): assert str(code.gen_time_bar('model', '20120120', '20220808').children[0]) == "Span(children=[DatePickerSingle(date='20220808', min_date_allowed=datetime.datetime(2012, 1, 20, 0, 0), max_date_allowed=datetime.datetime(2022, 8, 8, 0, 0), placeholder='DD MON YYYY', initial_visible_month=datetime.datetime(2022, 8, 8, 0, 0), display_format='DD MMM YYYY', id='model-date-picker'), Button(id='clear_button')], className='timesliderline')" assert str(code.gen_time_bar('model', '20120120', '20220808').children[1]) == "Span(children=[Button(id='btn-play', className='fa fa-play text-center', n_clicks=0, title='Play')], className='timesliderline anim-buttons')" @@ -23,7 +14,7 @@ def test_was_time_bar(): assert "Div(children=[Span(children=[DatePickerSingle(date='20220808', min_date_allowed=datetime.datetime(2012, 1, 20, 0, 0), max_date_allowed=datetime.datetime(2022, 8, 8, 0, 0), placeholder='DD MON YYYY', initial_visible_month=datetime.datetime(2022, 8, 8, 0, 0), display_format='DD MMM YYYY', id='was-date-picker'), Button(id='clear_button')], className='timesliderline'), Span(children=Slider(min=1, max=2, step=1, marks={1: {'label': 'TUE 09'}, 2: {'label': 'WED 10', 'style': {'left': '', 'right': '-40px'}}}, value=1, id='was-slider-graph'), id='was-slider-container', className='timesliderline')], className='timeslider')" in str(code.gen_time_bar('was', '20120120', '20220808')) def test_models_children(): - assert "[Div(id={'tag': 'tab-name', 'index': 'models'}), Alert(children='To explore the forecast, please select a variable and click on APPLY.', id='alert-forecast', color='primary', duration=6000, fade=True, is_open=True, style={'overflow': 'auto', 'marginBottom': 0}), Div(children=[Container(children=[], id='graph-collection', fluid=True), Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='forecast-disclaimer')], className='disclaimer')], id='div-collection'), Div([Store(id='model-clicked-coords'), Store(id='current-popups-stored')]), Div(Interval(id='slider-interval', disabled=True, interval=1000, n_intervals=0))" in str(code.models_children('20120120', '20220808')) + assert "[Div(id={'tag': 'tab-name', 'index': 'models'}), Alert(children='To explore the forecast, please select a variable and click on APPLY.', id='alert-forecast', color='primary', duration=6000, fade=True, is_open=True, style={'overflow': 'auto', 'marginBottom': 0}), Div(children=[Container(children=[], id='graph-collection', fluid=True), Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='models-disclaimer')], className='disclaimer')], id='div-collection'), Div([Store(id='model-clicked-coords'), Store(id='current-popups-stored')]), Div(Interval(id='slider-interval', disabled=True, interval=1000, n_intervals=0))" in str(code.models_children('20120120', '20220808')) def test_alert_3day_update(): assert str(code.alert_3day_update()) == "Toast(children=[P(children='3 day forecast available starting Apr 01 2023', className='mb1'), I(className='fa fa-solid fa-caret-down')], id='toast', dismissable=True, duration=10000, icon='warning')" @@ -33,7 +24,7 @@ def test_prob_children(): assert code.prob_children('20120120', '20220808')[0].id == {'tag': 'tab-name', 'index': 'prob'} assert str(code.prob_children('20120120', '20220808')[1]) == "Div(id='prob-graph', className='graph-with-slider')" assert str(code.prob_children('20120120', '20220808')[2]) == "Toast(children=[P(children='3 day forecast available starting Apr 01 2023', className='mb1'), I(className='fa fa-solid fa-caret-down')], id='toast', dismissable=True, duration=10000, icon='warning')" - assert str(code.prob_children('20120120', '20220808')[3]) == "Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='forecast-disclaimer')], className='disclaimer')" + assert str(code.prob_children('20120120', '20220808')[3]) == "Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='models-disclaimer')], className='disclaimer')" def test_was_children(): assert "Div(children=[Span(DropdownMenu(children=[DropdownMenuItem(children='Light', id={'tag': 'view-style', 'index': 'carto-positron'}, active=True), DropdownMenuItem(children='Open street map', id={'tag': 'view-style', 'index': 'open-street-map'}, active=False), DropdownMenuItem(children='Terrain', id={'tag': 'view-style', 'index': 'stamen-terrain'}, active=False), DropdownMenuItem(children='ESRI', id={'tag': 'view-style', 'index': 'esri-world'}, active=False)], id='map-view-dropdown', direction='up', in_navbar=True, label='VIEW'))], id='map-view-dropdown-div')], id='layout-dropdown', className='layout-dropdown')], className='fixed-bottom navbar-timebar', dark=True, expand='lg', fixed='bottom', fluid=True)]" in str(code.was_children('20120120', '20220808')) @@ -65,10 +56,10 @@ def test_expand_dropdown(): def test_sidebar_forecast(): #TEST MODELS - assert "[Div(children=[Label('Variable'), Dropdown(options=[{'label': 'AOD', 'value': 'OD550_DUST'}, {'label': 'Concentration', 'value': 'SCONC_DUST'}, {'label': 'Dry deposition', 'value': 'DUST_DEPD'}, {'label': 'Wet deposition', 'value': 'DUST_DEPW'}, {'label': 'Load', 'value': 'DUST_LOAD'}, {'label': 'Extinction', 'value': 'DUST_EXT_SFC'}], value=['OD550_DUST'], clearable=False, searchable=False, optionHeight=50, maxHeight=400, id='variable-dropdown-forecast')], className='sidebar-first-item'), Div(children=[Card([CardHeader(H2(Button(children=['Models', Span(children=I(className='fa fa-solid fa-angle-up'), id='caret1', className='caret-span')], id='group-1-toggle', className='dropdown', color='link'))), Collapse(children=[CardBody([Checklist(id='model-dropdown', className='sidebar-dropdown', options=[{'label': 'MULTI-MODEL', 'value': 'median'}, {'label': 'MONARCH', 'value': 'monarch'}, {'label': 'CAMS-IFS', 'value': 'cams'}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc'}, {'label': 'NASA-GEOS', 'value': 'nasa-geos'}, {'label': 'MetOffice-UM', 'value': 'metoffice'}, {'label': 'NCEP-GEFS', 'value': 'ncep-gefs'}, {'label': 'EMA-RegCM4', 'value': 'ema-regcm4'}" in str(code.sidebar_forecast(VARS, ['OD550_DUST'], MODELS, ['median'], window='models', country='burkinafaso')) + assert "[Div(children=[Label('Variable'), Dropdown(options=[{'label': 'AOD', 'value': 'OD550_DUST'}, {'label': 'Concentration', 'value': 'SCONC_DUST'}, {'label': 'Dry deposition', 'value': 'DUST_DEPD'}, {'label': 'Wet deposition', 'value': 'DUST_DEPW'}, {'label': 'Load', 'value': 'DUST_LOAD'}, {'label': 'Extinction', 'value': 'DUST_EXT_SFC'}], value=['OD550_DUST'], clearable=False, searchable=False, optionHeight=50, maxHeight=400, id='variable-dropdown-forecast')], className='sidebar-first-item'), Div(children=[Card([CardHeader(H2(Button(children=['Models', Span(children=I(className='fa fa-solid fa-angle-up'), id={'type': 'caret', 'index': 1}, className='caret-span')], id='group-1-toggle', className='dropdown', color='link'))), Collapse(children=[CardBody([Checklist(id='model-dropdown', class_name='sidebar-dropdown', options=[{'label': 'MULTI-MODEL', 'value': 'median'}" in str(code.sidebar_forecast(VARS, ['OD550_DUST'], MODELS, ['median'], window='models', country='burkinafaso')) #TEST PROB - assert "[Div(children=[Label('Variable'), Dropdown(options=[{'label': 'AOD', 'value': 'OD550_DUST'}, {'label': 'Concentration', 'value': 'SCONC_DUST'}, {'label': 'Dry deposition', 'value': 'DUST_DEPD'}, {'label': 'Wet deposition', 'value': 'DUST_DEPW'}, {'label': 'Load', 'value': 'DUST_LOAD'}, {'label': 'Extinction', 'value': 'DUST_EXT_SFC'}], value=['OD550_DUST'], clearable=False, searchable=False, optionHeight=50, maxHeight=400, id='variable-dropdown-forecast')], className='sidebar-first-item'), Div(children=[Card([CardHeader(H2(Button(children=['Models', Span(children=I(className='fa fa-solid fa-angle-up'), id='caret1', className='caret-span')], id='group-1-toggle', className='dropdown', color='link'))), Collapse(children=[CardBody([Checklist(id='model-dropdown', className='sidebar-dropdown', options=[{'label': 'MULTI-MODEL', 'value': 'median'}, {'label': 'MONARCH', 'value': 'monarch'}, {'label': 'CAMS-IFS', 'value': 'cams'}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc'}, {'label': 'NASA-GEOS', 'value': 'nasa-geos'}, {'label': 'MetOffice-UM', " in str(code.sidebar_forecast(VARS, ['OD550_DUST'], MODELS, ['median'], window='prob', country='burkinafaso')) + assert "[Div(children=[Label('Variable'), Dropdown(options=[{'label': 'AOD', 'value': 'OD550_DUST'}, {'label': 'Concentration', 'value': 'SCONC_DUST'}, {'label': 'Dry deposition', 'value': 'DUST_DEPD'}, {'label': 'Wet deposition', 'value': 'DUST_DEPW'}, {'label': 'Load', 'value': 'DUST_LOAD'}, {'label': 'Extinction', 'value': 'DUST_EXT_SFC'}], value=['OD550_DUST'], clearable=False, searchable=False, optionHeight=50, maxHeight=400, id='variable-dropdown-forecast')], className='sidebar-first-item'), Div(children=[Card([CardHeader(H2(Button(children=['Models', Span(children=I(className='fa fa-solid fa-angle-up'), id={'type': 'caret', 'index': 1}, className='caret-span')], id='group-1-toggle', className='dropdown', color='link'))), Collapse(children=[CardBody([Checklist(id='model-dropdown', class_name='sidebar-dropdown', options=[{'label': 'MULTI-MODEL', 'value': 'median'}" in str(code.sidebar_forecast(VARS, ['OD550_DUST'], MODELS, ['median'], window='prob', country='burkinafaso')) #TEST WAS - assert "[Div(children=[Label('Variable'), Dropdown(options=[{'label': 'AOD', 'value': 'OD550_DUST'}, {'label': 'Concentration', 'value': 'SCONC_DUST'}, {'label': 'Dry deposition', 'value': 'DUST_DEPD'}, {'label': 'Wet deposition', 'value': 'DUST_DEPW'}, {'label': 'Load', 'value': 'DUST_LOAD'}, {'label': 'Extinction', 'value': 'DUST_EXT_SFC'}], value=['OD550_DUST'], clearable=False, searchable=False, optionHeight=50, maxHeight=400, id='variable-dropdown-forecast')], className='sidebar-first-item'), Div(children=[Card([CardHeader(H2(Button(children=['Models', Span(children=I(className='fa fa-solid fa-angle-up'), id='caret1', className='caret-span')], id='group-1-toggle', className='dropdown', color='link'))), Collapse(children=[CardBody([Checklist(id='model-dropdown', className='sidebar-dropdown', options=[{'label': 'MULTI-MODEL', 'value': 'median'}, {'label': 'MONARCH', 'value': 'monarch'}, {'label': 'CAMS-IFS', 'value': 'cams'}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc'}, {'label': 'NASA-GEOS', 'value': 'nasa-geos'}, {'label': 'MetOffice-UM', 'value': 'metoffice'}, {'label': 'NCEP-GEFS', 'value': 'ncep-gefs'}, {'label': 'EMA-RegCM4', 'value': 'ema-regcm4'}, {'label': 'SILAM'," in str(code.sidebar_forecast(VARS, ['OD550_DUST'], MODELS, ['median'], window='was', country='chad')) + assert "[Div(children=[Label('Variable'), Dropdown(options=[{'label': 'AOD', 'value': 'OD550_DUST'}, {'label': 'Concentration', 'value': 'SCONC_DUST'}, {'label': 'Dry deposition', 'value': 'DUST_DEPD'}, {'label': 'Wet deposition', 'value': 'DUST_DEPW'}, {'label': 'Load', 'value': 'DUST_LOAD'}, {'label': 'Extinction', 'value': 'DUST_EXT_SFC'}], value=['OD550_DUST'], clearable=False, searchable=False, optionHeight=50, maxHeight=400, id='variable-dropdown-forecast')], className='sidebar-first-item'), Div(children=[Card([CardHeader(H2(Button(children=['Models', Span(children=I(className='fa fa-solid fa-angle-up'), id={'type': 'caret', 'index': 1}, className='caret-span')], id='group-1-toggle', className='dropdown', color='link'))), Collapse(children=[CardBody([Checklist(id='model-dropdown', class_name='sidebar-dropdown', options=[{'label': 'MULTI-MODEL', 'value': 'median'}" in str(code.sidebar_forecast(VARS, ['OD550_DUST'], MODELS, ['median'], window='was', country='chad')) diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index bd60063332ac7fac713730de3d94179be68fcaf3..c56e9bf9da3f027b500848e2f13dc6a2c21c8044 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -5,7 +5,7 @@ import dash from dash._callback_context import context_value from dash._utils import AttributeDict code = importlib.import_module('tabs.forecast_callbacks') - +from map_handler import START_DATE # =======================START render forecast test =========================== @@ -23,34 +23,76 @@ def test_render_forecast_tab_group_1(): assert output[4] == dash.no_update assert output[5] == dash.no_update +def test_render_forecast_tab_group_1_modopen_false(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "group-1-toggle.n_clicks"},{"prop_id": "group-2-toggle.n_clicks"},{"prop_id": "group-3-toggle.n_clicks"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "collapse-1.is_open"},{"prop_id": "collapse-2.is_open"},{"prop_id": "collapse-3.is_open"}]})) + return code.render_forecast_tab(1, 0, 0, 'SCONC_DUST', False, False, False ) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == True + assert output[1] == False + assert output[2] == False + assert output[3] == dash.no_update + assert output[4] == dash.no_update + assert output[5] == dash.no_update + def test_render_forecast_tab_group_2(): def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "group-1-toggle.n_clicks"},{"prop_id": "group-2-toggle.n_clicks"},{"prop_id": "group-3-toggle.n_clicks"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "collapse-1.is_open"},{"prop_id": "collapse-2.is_open"},{"prop_id": "collapse-3.is_open"}]})) - return code.render_forecast_tab(0, 1, 0, 'SCONC_DUST', False, True, False ) + return code.render_forecast_tab(0, 1, 0, 'SCONC_DUST', True, True, False ) ctx = copy_context() output = ctx.run(run_callback) - assert output[0] == False + assert output[0] == True assert output[1] == True assert output[2] == False assert output[3] == False assert output[4] == False assert output[5] == dash.no_update -def test_render_forecast_tab_group_3(): +def test_render_forecast_tab_group_2_probopen_false(): def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "group-1-toggle.n_clicks"},{"prop_id": "group-2-toggle.n_clicks"},{"prop_id": "group-3-toggle.n_clicks"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "collapse-1.is_open"},{"prop_id": "collapse-2.is_open"},{"prop_id": "collapse-3.is_open"}]})) - return code.render_forecast_tab(0,0,1, 'SCONC_DUST', False, False, True ) + return code.render_forecast_tab(0, 1, 0, 'SCONC_DUST', False, False, False ) ctx = copy_context() output = ctx.run(run_callback) assert output[0] == False assert output[1] == False + assert output[2] == False + assert output[3] == False + assert output[4] == False + assert output[5] == dash.no_update + +def test_render_forecast_tab_group_3(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "group-1-toggle.n_clicks"},{"prop_id": "group-2-toggle.n_clicks"},{"prop_id": "group-3-toggle.n_clicks"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "collapse-1.is_open"},{"prop_id": "collapse-2.is_open"},{"prop_id": "collapse-3.is_open"}]})) + return code.render_forecast_tab(0,0,1, 'SCONC_DUST', True, False, True ) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == True + assert output[1] == False assert output[2] == True assert output[3] == False assert output[4] == False assert output[5] == dash.no_update +def test_render_forecast_tab_group_3_wasopen_false(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "group-1-toggle.n_clicks"},{"prop_id": "group-2-toggle.n_clicks"},{"prop_id": "group-3-toggle.n_clicks"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "collapse-1.is_open"},{"prop_id": "collapse-2.is_open"},{"prop_id": "collapse-3.is_open"}]})) + return code.render_forecast_tab(0,0,1, 'SCONC_DUST', False, False, False ) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == False + assert output[1] == False + assert output[2] == False + assert output[3] == False + assert output[4] == False + assert output[5] == dash.no_update + def test_render_forecast_tab_SCONC_DUST(): def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "group-1-toggle.n_clicks"},{"prop_id": "group-2-toggle.n_clicks"},{"prop_id": "group-3-toggle.n_clicks"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "collapse-1.is_open"},{"prop_id": "collapse-2.is_open"},{"prop_id": "collapse-3.is_open"}]})) @@ -94,22 +136,6 @@ def test_update_models_dropdown(): assert str(code.update_models_dropdown('OD550_DUST', ['median', 'monarch'])) == "([{'label': 'MULTI-MODEL', 'value': 'median', 'disabled': False}, {'label': 'MONARCH', 'value': 'monarch', 'disabled': False}, {'label': 'CAMS-IFS', 'value': 'cams', 'disabled': False}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc', 'disabled': False}, {'label': 'NASA-GEOS', 'value': 'nasa-geos', 'disabled': False}, {'label': 'MetOffice-UM', 'value': 'metoffice', 'disabled': False}, {'label': 'NCEP-GEFS', 'value': 'ncep-gefs', 'disabled': False}, {'label': 'EMA-RegCM4', 'value': 'ema-regcm4', 'disabled': False}, {'label': 'SILAM', 'value': 'silam', 'disabled': False}, {'label': 'LOTOS-EUROS', 'value': 'lotos-euros', 'disabled': False}, {'label': 'ICON-ART', 'value': 'icon-art', 'disabled': False}, {'label': 'NOA-WRF-CHEM', 'value': 'noa', 'disabled': False}, {'label': 'WRF-NEMO', 'value': 'wrf-nemo', 'disabled': False}, {'label': 'ALADIN', 'value': 'aladin', 'disabled': False}, {'label': 'ZAMG-WRF-CHEM', 'value': 'zamg', 'disabled': False}, {'label': 'MOCAGE', 'value': 'mocage', 'disabled': False}], ['median', 'monarch'], {'display': 'none'})" assert str(code.update_models_dropdown('SCONC_DUST', ['cams', 'silam'])) == "([{'label': 'MULTI-MODEL', 'value': 'median', 'disabled': False}, {'label': 'MONARCH', 'value': 'monarch', 'disabled': False}, {'label': 'CAMS-IFS', 'value': 'cams', 'disabled': False}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc', 'disabled': False}, {'label': 'NASA-GEOS', 'value': 'nasa-geos', 'disabled': False}, {'label': 'MetOffice-UM', 'value': 'metoffice', 'disabled': False}, {'label': 'NCEP-GEFS', 'value': 'ncep-gefs', 'disabled': False}, {'label': 'EMA-RegCM4', 'value': 'ema-regcm4', 'disabled': False}, {'label': 'SILAM', 'value': 'silam', 'disabled': False}, {'label': 'LOTOS-EUROS', 'value': 'lotos-euros', 'disabled': False}, {'label': 'ICON-ART', 'value': 'icon-art', 'disabled': False}, {'label': 'NOA-WRF-CHEM', 'value': 'noa', 'disabled': False}, {'label': 'WRF-NEMO', 'value': 'wrf-nemo', 'disabled': False}, {'label': 'ALADIN', 'value': 'aladin', 'disabled': False}, {'label': 'ZAMG-WRF-CHEM', 'value': 'zamg', 'disabled': False}, {'label': 'MOCAGE', 'value': 'mocage', 'disabled': False}], ['cams', 'silam'], {'display': 'none'})" -# =======================END update_models_dropdown test =========================== - - -# =======================START CARET TESTS =========================== -def test_rotate_models_caret(): - assert code.rotate_models_caret(True) == None - assert code.rotate_models_caret(False) == {'top': '.05rem', 'transform': 'rotate(180deg)', '-ms-transform': 'rotate(180deg)', '-webkit-transform': 'rotate(180deg)'} - -def test_rotate_prob_caret(): - assert code.rotate_prob_caret(False) == None - assert code.rotate_prob_caret(True) == {'transform': 'rotate(0deg)', '-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)'} - -def test_rotate_was_caret(): - assert code.rotate_was_caret(False) == None - assert code.rotate_was_caret(True) == {'transform': 'rotate(0deg)', '-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)'} -# =======================END CARET TESTS =========================== # =======================START SIDEBAR_BOTTOM TESTS =========================== def test_sidebar_bottom_info(): @@ -142,7 +168,7 @@ def test_sidebar_bottom_download_clicked(): def test_sidebar_bottom_download_clicked2(): def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "info-button.n_clicks"},{"prop_id": "download-button.n_clicks"},{"prop_id": "info-collapse.is_open"},{"prop_id": "download_collapse.is_open"}]})) - return code.sidebar_bottom(0,1,False,False) + return code.sidebar_bottom(0,1,False,True) ctx = copy_context() output = ctx.run(run_callback) @@ -156,41 +182,73 @@ def test_download_anim_link(): assert code.download_anim_link(['median','monarch'], 'SCONC_DUST', '20220831', 3) == 'assets/comparison/all/sconc_dust/2022/08/20220831_all_loop.gif' assert code.download_anim_link(['monarch'], 'OD550_DUST', '20220831', 3) == 'assets/comparison/monarch/od550_dust/2022/08/20220831_monarch_loop.gif' assert code.download_anim_link(['median','monarch'], 'OD550_DUST', '20220831', 3) == 'assets/comparison/all/od550_dust/2022/08/20220831_all_loop.gif' -# =======================END DOWNLOAD_ANIM_LINK TESTS=========================== +# =======================TEST ZOOM COUNTRY=========================== def test_zoom_country(): assert code.zoom_country(1, ['monarch'], 2, 45, 35) == ([2.0], [[45.0, 35.0]]) assert code.zoom_country(1, ['median', 'monarch'], 2, 45, 35) == ([2.0, 2.0], [[45.0, 35.0], [45.0, 35.0]]) -# def test_zooms(): +# =======================TEST ZOOMS=========================== +def test_zooms(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{'prop_id': '{"index":"median","n_clicks":1,"tag":"model-map"}.viewport', 'value': {'center': [31.89971219631015, 19.511718750000004], 'zoom': 5}}]})) + return code.zooms([{'center': [31.89971219631015, 19.511718750000004], 'zoom': 5}],[{'tag': 'model-map', 'index': 'median', 'n_clicks': 1}]) + ctx = copy_context() + output = ctx.run(run_callback) + assert output == (1, 5, 31.89971219631015, 19.511718750000004) + +def test_zooms_multi(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{'prop_id': '{"index":"median","n_clicks":1,"tag":"model-map"}.viewport', 'value': {'center': [43.25, 19], 'zoom': 4}}]})) + return code.zooms([{'center': [43.25, 19], 'zoom': 4}, None],[{'tag': 'model-map', 'index': 'median', 'n_clicks': 1}, {'tag': 'model-map', 'index': 'monarch', 'n_clicks': 1}]) + ctx = copy_context() + output = ctx.run(run_callback) + assert output == (1, 4, 43.25, 19) + +# =======================TEST UPDATE STYLES BUTTON=========================== +def test_update_styles_button(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{'prop_id': '{"index":"open-street-map","tag":"view-style"}.n_clicks', 'value': 1}]})) + return code.update_styles_button.uncached([None, 1, None, None], ['https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'], [], [], [True, False, False, False]) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output == (['https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'], ["© OpenStreetMap contributors"], [], [], [], [], [False, True, False, False]) + + +def test_models_popup(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": '{"tag":"model-map", "index":"median","n_clicks":1}.click_lat_lng'}]})) + return code.models_popup([[56.739260373724775, 91.93359375]],[], [], '20230404', 0, 'OD550_DUST', None, {}) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output[0] == {} + assert output[1] == {} + # ASSERT OUTPUT[2] == RETURNS DASH.NO_UPDATE + +# def test_update_styles_button_from_open_streets(): +# ORJSON ERROR and INPUT IS EXTREMELY LONG +# def test_models_popup(): # def run_callback(): -# context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "{'tag': 'model-map', 'index': 'median', 'n_clicks': 0}"},{"prop_id": "{'tag': 'model-map', 'index': 'median', 'n_clicks': 0}"}]})) +# context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "{'tag': 'model-map', 'index': 'median'}.n_clicks"},{"prop_id": "{'tag': 'model-map', 'index': 'median'}.n_clicks"}]})) # return code.zooms([None, None],[{'tag': 'model-map', 'index': 'median', 'n_clicks': 0}, {'tag': 'model-map', 'index': 'monarch', 'n_clicks': 1}]) # ctx = copy_context() # output = ctx.run(run_callback) # assert output == (True, False) # # -# ORJSON ERROR -# def test_update_was_styles_button(): -# def run_callback(): -# context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "view-style.n_clicks"},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) -# return code.update_was_styles_button.uncached([None, None, 1, None], ['https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'], [True, False, False, False]) -# -# ctx = copy_context() -# output = ctx.run(run_callback) -# assert output == (True, False) -# ORJSON ERROR -# def test_models_popup(): +# =======================Start show_timeseries=========================== +#SHOULD RETURN 3 ITEMS, CURRENTLY ONLY RETURNING NONE +# def test_show_timeseries(): # def run_callback(): -# context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "model-map.click_lat_lng"},{"prop_id": "model-map.id"},{"prop_id": "model-map.children"},{"prop_id": "model-date-picker.date"},{"prop_id": "slider-graph.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "model-clicked-coords.data"},{"prop_id": "current-popups-stored.data"}]})) -# return code.models_popup([[56.739260373724775, 91.93359375]], [], [], '20230404', 0, 'OD550_DUST', None, None) +# context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"median","random":66,"tag":"ts-button"}.n_clicks', 'value': 1}]})) +# assert code.show_timeseries.uncached([1], ['median'], '20230430', 'OD550_DUST', {'median': [28.272939391283685, 23.027343750000004]}, {'median': 1}) # # ctx = copy_context() # output = ctx.run(run_callback) -# assert output == (True, False) - +# assert output == None # =======================Start start_stop_autoslider tests=========================== def test_start_stop_autoslider_pause(): @@ -223,47 +281,74 @@ def test_update_slider(): assert code.update_slider.uncached(72) == 0 # =======================END update_slider tests=========================== -# =======================START UPDATE WAS FIGURE TESTS=========================== -def test_update_was_figure(): +# =======================START UPDATE MODEL FIGURE TESTS=========================== +def test_update_model_figure(): def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "was-apply.n_clicks"},{"prop_id": "was-date-picker.date"},{"prop_id": "was-slider-graph.value"},{"prop_id": "was-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "was-previous.data"},{"prop_id": "view-style.active"},{"prop_id": "was-map.zoom"},{"prop_id": "was-map.center"}]})) - return code.update_was_figure(1, '20230404', 1, 'burkinafaso', None, [True, False, False, False], [True], [], []) + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "models-apply.n_clicks"},{"prop_id": "model-date-picker.date"},{"prop_id": "model-slider-graph.value"},{"prop_id": "model-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "view-style.active"},{"prop_id": "model-map.zoom"},{"prop_id": "model-map.center"}]})) + return code.update_models_figure.uncached(1, 1, '20230404', ['monarch'], None, [True, False, False, False], [], []) ctx = copy_context() output = ctx.run(run_callback) - assert "url='https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'), FullscreenControl(position='topright'), None, None, None, GeoJSON(hideout={'colorscale': ['green', 'gold', 'darkorange', 'red'], 'bounds': [0, 1, 2, 3], 'style': {'weight': 1, 'opacity': 1, 'color': 'white', 'dashArray': '3', 'fillOpacity': 0.7}, 'colorProp': 'value'}, hoverStyle={'arrow': {'weight': 2, 'color': 'white', 'dashArray': '', 'fillOpacity': 0.7}}, options={'style': {'variable': 'forecastTab.wasMaps.styleHandle'}}, url='/dashboard/assets/geojsons/was/burkinafaso/geojson/20230404/20230404_SCONC_DUST_1.geojson'), Div(children=[Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'green'}), Span(children='Normal', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'gold'}), Span(children='High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'darkorange'}), Span(children='Very High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'red'}), Span(children='Extremely High', className='was-legend-label')], style={'display': 'block'})], className='was-legend'), Div(children=P(B(['Barcelona Dust Regional Center - Burkina Faso WAS.', Br(None), 'Expected concentration of airborne dust.', Br(None), 'Issued: 04 Apr 2023. Valid: 05 Apr 2023'])), id='was-info', className='info', style={'position': 'absolute', 'top': '10px', 'left': '10px', 'zIndex': '1000', 'fontFamily': " in str(output) + assert "Div(children=P(B(['MONARCH Dust Optical Depth (550nm)', Br(None), 'Valid: 00h 04 Apr 2023 (H+12)'])), id='monarch-info', className='info', style={'position': 'absolute', 'top': '10px', 'left': '10px', 'zIndex': '1000', 'fontFamily':" in str(output) + assert "(position='topright'), GeoJSON(hideout={'colorscale': ['rgba(255,255,255,0.4)', '#a1ede3', '#5ce3ba', '#fcd775', '#da7230', '#9e6226', '#714921', '#392511', '#1d1309'], 'bounds': array([ 0. , 0.1, 0.2, 0.4, 0.8, 1.2, 1.6, 3.2, 6.4, 10. ],\n dtype=float32), 'style': {'weight': 0, 'opacity': 0, 'color': 'white', 'dashArray': '', 'fillOpacity': 0.7}, 'colorProp': 'value'}, options={'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}}, url='/dashboard/assets/geojsons/NMMB-BSC/geojson/20230403/04_20230403_OD550_DUST.geojson')" in str(output) -def test_update_was_figure_zooms(): +# =======================START UPDATE PROB FIGURE TESTS=========================== +def test_update_prob_figure(): def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "was-apply.n_clicks"},{"prop_id": "was-date-picker.date"},{"prop_id": "was-slider-graph.value"},{"prop_id": "was-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "was-previous.data"},{"prop_id": "view-style.active"},{"prop_id": "was-map.zoom"},{"prop_id": "was-map.center"}]})) - return code.update_was_figure(2, '20230404', 1, 'cabo_verde', 'burkinafaso', [True, False, False, False],[True], [8], [[11.982397942974229, -2.6312255859375004]]) + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "prob-apply.n_clicks"},{"prop_id": "prob-date-picker.date"},{"prop_id": "prob-slider-graph.value"},{"prop_id": "prob-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "view-style.active"},{"prop_id": "prob-map.zoom"},{"prop_id": "prob-map.center"}]})) + return code.update_prob_figure.uncached(1, '20230404', 1, 'prob_0.2', None, [True], [], []) ctx = copy_context() output = ctx.run(run_callback) - assert "url='https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'), FullscreenControl(position='topright'), None, None, None, GeoJSON(hideout={'colorscale': ['green', 'gold', 'darkorange', 'red'], 'bounds': [0, 1, 2, 3], 'style': {'weight': 1, 'opacity': 1, 'color': 'white', 'dashArray': '3', 'fillOpacity': 0.7}, 'colorProp': 'value'}, hoverStyle={'arrow': {'weight': 2, 'color': 'white', 'dashArray': '', 'fillOpacity': 0.7}}, options={'style': {'variable': 'forecastTab.wasMaps.styleHandle'}}, url='/dashboard/assets/geojsons/was/cabo_verde/geojson/20230404/20230404_SCONC_DUST_1.geojson'), Div(children=[Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'green'}), Span(children='Normal', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'gold'}), Span(children='High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'darkorange'}), Span(children='Very High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'red'}), Span(children='Extremely High', className='was-legend-label')], style={'display': 'block'})], className='was-legend'), Div(children=P(B(['Barcelona Dust Regional Center - Cape Verde WAS.', Br(None), 'Expected concentration of airborne dust.', Br(None), 'Issued: 04 Apr 2023. Valid: 05 Apr 2023'])), id='was-info', className='info', style={'position': 'absolute', 'top': '10px', 'left': '10px', 'zIndex': '1000', 'fontFamily':" in str(output) -# =======================END UPDATE WAS FIGURE TESTS=========================== - - - - - - - - - - - - - - + assert output.children[0].id == {'tag': 'prob-tile-layer', 'index': 'None'} + assert output.children[0].attribution == "© OpenStreetMap contributors © CARTO" + assert output.children[0].url == 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png' + assert output.children[1].position == 'topright' + assert output.children[2].url == '/dashboard/assets/geojsons/prob/od550_dust/0.2/geojson/20230404/01_20230404_OD550_DUST.geojson' +def test_update_prob_figure_1(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "prob-apply.n_clicks"},{"prop_id": "prob-date-picker.date"},{"prop_id": "prob-slider-graph.value"},{"prop_id": "prob-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "view-style.active"},{"prop_id": "prob-map.zoom"},{"prop_id": "prob-map.center"}]})) + return code.update_prob_figure.uncached(1, '20230404', 1, None, None, [True], [], []) + ctx = copy_context() + output = ctx.run(run_callback) + assert output.children[2].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/20230404/01_20230404_OD550_DUST.geojson' +def test_update_prob_figure_zoom(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "prob-apply.n_clicks"},{"prop_id": "prob-date-picker.date"},{"prop_id": "prob-slider-graph.value"},{"prop_id": "prob-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "view-style.active"},{"prop_id": "prob-map.zoom"},{"prop_id": "prob-map.center"}]})) + return code.update_prob_figure.uncached(1, '20230404', 1, None, None, [True], [5], []) + ctx = copy_context() + output = ctx.run(run_callback) + assert 'zoom=5' in str(output) +def test_update_prob_figure_center(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "prob-apply.n_clicks"},{"prop_id": "prob-date-picker.date"},{"prop_id": "prob-slider-graph.value"},{"prop_id": "prob-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "view-style.active"},{"prop_id": "prob-map.zoom"},{"prop_id": "prob-map.center"}]})) + return code.update_prob_figure.uncached(1, '20230404', 1, None, None, [True], [], [34, 45]) + ctx = copy_context() + output = ctx.run(run_callback) + assert 'center=34' in str(output) +# =======================START UPDATE WAS FIGURE TESTS=========================== +def test_update_was_figure(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "was-apply.n_clicks"},{"prop_id": "was-date-picker.date"},{"prop_id": "was-slider-graph.value"},{"prop_id": "was-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "was-previous.data"},{"prop_id": "view-style.active"},{"prop_id": "was-map.zoom"},{"prop_id": "was-map.center"}]})) + return code.update_was_figure(1, '20230404', 1, 'burkinafaso', None, [True, False, False, False], [True], [], []) + ctx = copy_context() + output = ctx.run(run_callback) + assert "url='https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'), FullscreenControl(position='topright'), GeoJSON(hideout={'colorscale': ['green', 'gold', 'darkorange', 'red'], 'bounds': [0, 1, 2, 3], 'style': {'weight': 1, 'opacity': 1, 'color': 'white', 'dashArray': '3', 'fillOpacity': 0.7}, 'colorProp': 'value'}, hoverStyle={'arrow': {'weight': 2, 'color': 'white', 'dashArray': '', 'fillOpacity': 0.7}}, options={'style': {'variable': 'forecastTab.wasMaps.styleHandle'}}, url='/dashboard/assets/geojsons/was/burkinafaso/geojson/20230404/20230404_SCONC_DUST_1.geojson'), Div(children=[Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'green'}), Span(children='Normal', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'gold'}), Span(children='High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'darkorange'}), Span(children='Very High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'red'}), Span(children='Extremely High', className='was-legend-label')], style={'display': 'block'})], className='was-legend'), Div(children=P(B(['Barcelona Dust Regional Center - Burkina Faso WAS.', Br(None), 'Expected concentration of airborne dust.', Br(None), 'Issued: 04 Apr 2023. Valid: 05 Apr 2023']" in str(output) +def test_update_was_figure_zooms(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "was-apply.n_clicks"},{"prop_id": "was-date-picker.date"},{"prop_id": "was-slider-graph.value"},{"prop_id": "was-dropdown.value"},{"prop_id": "variable-dropdown-forecast.value"},{"prop_id": "was-previous.data"},{"prop_id": "view-style.active"},{"prop_id": "was-map.zoom"},{"prop_id": "was-map.center"}]})) + return code.update_was_figure(2, '20230404', 1, 'cabo_verde', 'burkinafaso', [True, False, False, False],[True], [8], [[11.982397942974229, -2.6312255859375004]]) -#some comment + ctx = copy_context() + output = ctx.run(run_callback) + assert "url='https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'), FullscreenControl(position='topright'), GeoJSON(hideout={'colorscale': ['green', 'gold', 'darkorange', 'red'], 'bounds': [0, 1, 2, 3], 'style': {'weight': 1, 'opacity': 1, 'color': 'white', 'dashArray': '3', 'fillOpacity': 0.7}, 'colorProp': 'value'}, hoverStyle={'arrow': {'weight': 2, 'color': 'white', 'dashArray': '', 'fillOpacity': 0.7}}, options={'style': {'variable': 'forecastTab.wasMaps.styleHandle'}}, url='/dashboard/assets/geojsons/was/cabo_verde/geojson/20230404/20230404_SCONC_DUST_1.geojson'), Div(children=[Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'green'}), Span(children='Normal', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'gold'}), Span(children='High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'darkorange'}), Span(children='Very High', className='was-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='was-legend-point', style={'backgroundColor': 'red'}), Span(children='Extremely High', className='was-legend-label')], style={'display': 'block'})], className='was-legend'), Div(children=P(B(['Barcelona Dust Regional Center - Cape Verde WAS.', Br(None), 'Expected concentration of airborne dust.', Br(None), 'Issued: 04 Apr 2023. Valid: 05 Apr 2023'])), id='was-info', className='info', style={'position': 'absolute', 'top': '10px', 'left': '10px', 'zIndex': '1000', 'fontFamily':" in str(output) +# =======================END UPDATE WAS FIGURE TESTS=========================== diff --git a/tests/test_generic.py b/tests/test_generic.py new file mode 100644 index 0000000000000000000000000000000000000000..c61db66e3d44a7a914c4a3b1ba5e5ba48d8fc975 --- /dev/null +++ b/tests/test_generic.py @@ -0,0 +1,20 @@ +import pytest +import importlib +code = importlib.import_module('tabs.generic') + +#=============================== START LAYOUT LAYERS ======================== +def test_layout_layers_single_input(): + assert "Div([Span(DropdownMenu(children=[DropdownMenuItem(children='AIRPORTS', id='airports')], id='map-layers-dropdown', direction='up', label='LAYERS'))])" in str(code.layout_layers('airports')) + +def test_layout_layers_double_input(): + assert "Div([Span(DropdownMenu(children=[DropdownMenuItem(children='AIRPORTS', id='airports'), DropdownMenuItem(children='SECOND_LAYER', id='second_layer')], id='map-layers-dropdown', direction='up', label='LAYERS'))])" in str(code.layout_layers('airports', 'second_layer')) + +#=============================== START TIME SERIES ======================== +def test_time_series(): + assert "Div(children=[Spinner(children=[Modal(children=[], id='ts-modal', centered=True, is_open=False, size='xl')], id='loading-ts-modal', fullscreen=True, fullscreen_style={'opacity': '0.5', 'zIndex': '200000'}, show_initially=False)], id='open-timeseries')" in str(code.time_series()) + +#=============================== START LAYOUT VIEW ======================== +def test_layout_view(): + assert "Div(children=[Span(DropdownMenu(children=[DropdownMenuItem(children='Light', id={'tag': 'view-style', 'index': 'carto-positron'}, active=True), DropdownMenuItem(children='Open street map', id={'tag': 'view-style', 'index': 'open-street-map'}, active=False), DropdownMenuItem(children='Terrain', id={'tag': 'view-style', 'index': 'stamen-terrain'}, active=False), DropdownMenuItem(children='ESRI', id={'tag': 'view-style', 'index': 'esri-world'}, active=False)], id='map-view-dropdown', direction='up', in_navbar=True, label='VIEW'))], id='map-view-dropdown-div')" in str(code.layout_view()) + + diff --git a/tests/test_generic_callbacks.py b/tests/test_generic_callbacks.py new file mode 100644 index 0000000000000000000000000000000000000000..9522f45d610ce048e0410a92e984b61de22d4e21 --- /dev/null +++ b/tests/test_generic_callbacks.py @@ -0,0 +1,10 @@ +import pytest +import importlib +code = importlib.import_module('tabs.generic_callbacks') + +# =======================START CARET TESTS =========================== +def test_rotate_section_caret(): + assert code.rotate_section_caret(True) == {'-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)', 'transform': 'rotate(0deg)'} + assert code.rotate_section_caret(False) == {'transform': 'rotate(180deg)', '-ms-transform': 'rotate(180deg)', '-webkit-transform': 'rotate(180deg)'} + +# =======================END CARET TESTS =========================== diff --git a/tests/test_ines_core_map_handler.py b/tests/test_ines_core_map_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..8989da9415c02e69144cc9a3f125e324b4738c93 --- /dev/null +++ b/tests/test_ines_core_map_handler.py @@ -0,0 +1,56 @@ +import pytest +from datetime import datetime +from datetime import timedelta +import importlib +code = importlib.import_module('ines_core_map_handler') +from map_handler import END_DATE +from map_handler import FREQ + +FMT_ISO = "%Y%m%d" +FMT_DASH = "%Y-%m-%d" +FMT_MON = "%d %b %Y" +FMT_MON_HR = f"%Hh {FMT_MON}" +FMT_HR = "%H:%M:%S" +FMT_DASH_HR = f"{FMT_DASH} {FMT_HR}" +EDATE_OBJ = datetime.strptime(END_DATE, FMT_ISO) + +# =================== FIGURE HANDLER ============================ +@pytest.fixture +def MapHandler(): + MapHandler = code.MapHandler() + MapHandler.rdatetime = datetime.strptime("2023-04-30 00:00:00", "%Y-%m-%d %H:%M:%S") + MapHandler.what = 'hours' + MapHandler.timesteps = [0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, + 57, 60, 63, 66, 69, 72] + return MapHandler + +def test_get_center(MapHandler): + assert MapHandler.get_center([35,45]) == [35, 45] + assert MapHandler.get_center([-35,-45]) == [-35, -45] + +def test_retrieve_cdatetime(MapHandler): + assert str(MapHandler.retrieve_cdatetime(tstep=0)) == EDATE_OBJ.strftime(FMT_DASH_HR) # '2023-04-30 12:00:00' + assert str(MapHandler.retrieve_cdatetime(tstep=6)) == (EDATE_OBJ + timedelta(hours=6*FREQ)).strftime(FMT_DASH_HR) # '2023-05-01 06:00:00' + assert str(MapHandler.retrieve_cdatetime(tstep=12)) == (EDATE_OBJ + timedelta(hours=12*FREQ)).strftime(FMT_DASH_HR) # '2022-05-02 00:00:00' + +def test_get_title(MapHandler): + + model_name = "MULTI-MODEL" + + MapHandler.step = "{:02d}".format(0) + MapHandler.cdatetime = MapHandler.retrieve_cdatetime(tstep=0) + var_title = "Dust Optical Depth (550nm)
Valid: %(shour)sh %(sday)s %(smonth)s %(syear)s (H+%(step)s)" + MapHandler.base_title = " ".join([model_name, var_title]) + assert MapHandler.get_title(varname='OD550_DUST', tstep=0) == 'MULTI-MODEL Dust Optical Depth (550nm)
Valid: {edate} (H+00)'.format(edate=EDATE_OBJ.strftime(FMT_MON_HR)) + + MapHandler.step = "{:02d}".format(9*FREQ) + MapHandler.cdatetime = MapHandler.retrieve_cdatetime(tstep=9) + var_title = "Dust Surface Conc. (µg/m³)
Valid: %(shour)sh %(sday)s %(smonth)s %(syear)s (H+%(step)s)" + MapHandler.base_title = " ".join([model_name, var_title]) + assert MapHandler.get_title(varname='SCONC_DUST', tstep=9) == 'MULTI-MODEL Dust Surface Conc. (µg/m³)
Valid: {edate} (H+27)'.format(edate=(EDATE_OBJ + timedelta(hours=9*FREQ)).strftime(FMT_MON_HR)) + +def test_hour_to_tstep(MapHandler): + assert MapHandler.hour_to_tstep(0) == 0 + assert MapHandler.hour_to_tstep(3) == int(3/FREQ) + assert MapHandler.hour_to_tstep(6) == int(6/FREQ) + assert MapHandler.hour_to_tstep(36) == int(36/FREQ) diff --git a/tests/test_utils.py b/tests/test_ines_core_utils.py similarity index 65% rename from tests/test_utils.py rename to tests/test_ines_core_utils.py index 7e430e9dbd84802c902fb06df772bf8371ea0e53..c351cec9a8ceab90bc62545c9d27bfd082db6019 100644 --- a/tests/test_utils.py +++ b/tests/test_ines_core_utils.py @@ -1,11 +1,12 @@ import pytest +from unittest.mock import MagicMock, patch from datetime import datetime from datetime import timedelta import importlib -code = importlib.import_module('utils') +code = importlib.import_module('ines_core_utils') import numpy as np -from data_handler import END_DATE -from data_handler import FREQ +from map_handler import END_DATE +from map_handler import FREQ FMT_ISO = "%Y%m%d" FMT_DASH = "%Y-%m-%d" @@ -101,14 +102,25 @@ def test_normalize_vals(): n_bounds = code.normalize_vals(bounds, bounds[0], bounds[-1], magn) assert np.array_equiv(n_bounds, np.array(result).astype('float32')) -def test_get_colorscale(): - bounds1 = [-0.1, -0.08, -0.06, -0.04, -0.02, 0., 0.02, 0.04, 0.06, 0.08, 0.1] - bounds2 = [0., 0.2, 0.4, 0.6, 0.8, 1., 1.2, 1.4, 1.6, 1.8, 2.] - color1 = 'viridis' - color2 = 'RdBu_r' - assert code.get_colorscale(bounds1, color1, True)[0] == [0.0, 'rgba(68, 1, 84, 255)'] - assert code.get_colorscale(bounds2, color2, True)[0] == [0.0, 'rgba(5, 48, 97, 255)'] - -def test_get_vis_edate(): - assert code.get_vis_edate(END_DATE, hour=9) == (END_DATE, 0) - assert code.get_vis_edate(END_DATE, hour=16) == (END_DATE, 6) +def test_get_404_link_prod(monkeypatch): + """ Test the production url""" + # Mocking PATHNAME for testing + mock_pathname = "/daily_dashboard/" # Set your desired value for testing + # Assign the mocked value to PATHNAME + monkeypatch.setattr("ines_core_utils.PATHNAME", mock_pathname) + assert code.get_404_link('forecast') == 'https://dust.aemet.es/products/daily-dust-products/?tab=forecast' + assert code.get_404_link('evaluation') == 'https://dust.aemet.es/products/daily-dust-products/?tab=evaluation' + assert code.get_404_link('observations') == 'https://dust.aemet.es/products/daily-dust-products/?tab=observations' + +def test_get_404_link_dev(): + """ Test the dev url""" + assert code.get_404_link('forecast') == ('/dashboard//?tab=forecast',) + assert code.get_404_link('evaluation') == ('/dashboard//?tab=evaluation',) + assert code.get_404_link('observations') == ('/dashboard//?tab=observations',) + +def test_render404(): + assert "Div(children=[Div(children=[H2(children='404 Error', id='error_title')" in str(code.render404()[0]) + assert "P('Here are some links that might help:'), Link(children='Forecast', href=('/dashboard//?tab=forecast',), target='_parent', refresh=True, className='error_links', id='forecast_link'), Br(None), Br(None), Link(children='Evaluation', href=('/dashboard//?tab=evaluation',), target='_parent', refresh=True, className='error_links', id='evaluation_link'), Br(None), Br(None), Link(children='Observations', href=('/dashboard//?tab=observations',), target='_parent', refresh=True, className='error_links', id='observations_link')], id='error_div')], className='background')" in str(code.render404()[0]) + +def test_extend_list(): + assert code.extend_list([[1],[2],[3]]) == [1,2,3] diff --git a/tests/test_map_handler.py b/tests/test_map_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..dea4bff74e88bf340a62b678ccca9715e87a599c --- /dev/null +++ b/tests/test_map_handler.py @@ -0,0 +1,173 @@ +import pytest +from datetime import datetime +from datetime import timedelta +import importlib +map_handler = importlib.import_module('map_handler') +timeseries_handler = importlib.import_module('timeseries_handler') +from map_handler import END_DATE +from map_handler import FREQ + +FMT_ISO = "%Y%m%d" +FMT_DASH = "%Y-%m-%d" +FMT_MON = "%d %b %Y" +EDATE_OBJ = datetime.strptime(END_DATE, FMT_ISO) +EDATE_PREV = (datetime.strptime(END_DATE, FMT_ISO) - timedelta(days=7)).strftime(FMT_ISO) + +# =================== AERONET OBSERVATIONS HANDLER ============================ +@pytest.fixture +def EvaluationGroundFigureHandler(): + return map_handler.EvaluationGroundFigureHandler(start_date=EDATE_PREV, end_date=END_DATE, + obs='aeronet', var='OD550_DUST') + +def test_aeronet_get_figure_layers(EvaluationGroundFigureHandler): + + EvaluationGroundFigureHandler.get_figure_layers() + assert str(type(EvaluationGroundFigureHandler.geojson)) == "" + +# =================== TIME SERIES HANDLER ============================ +@pytest.fixture +def TSHandler(): + return timeseries_handler.ForecastModelsTimeSeriesHandler('median', END_DATE, 'OD550_DUST') + +def test_TimeSeriesHandler_retrieve_single_point(TSHandler): + run = TSHandler.retrieve_single_point(1, 45, 45, model='median') + assert float(run) == 0.13055419921875 + +def test_TimeSeriesHandler_retrieve_single_point_1(TSHandler): + run = TSHandler.retrieve_single_point(3, 5, 5, model=None) + assert float(run) == 0.32350343465805054 + +def test_retrieve_timeseries(TSHandler): + run = TSHandler.retrieve_timeseries(5, 5, model=None, method='netcdf', forecast=False) + assert run.layout.title.text =='Dust Optical Depth @ lat = 5 and lon = 5' + +def test_retrieve_timeseries_1(TSHandler): + run = TSHandler.retrieve_timeseries(35, 15, model='monarch', method='netcdf', forecast=True) + assert run.layout.title.text =='Dust Optical Depth @ lat = 35 and lon = 15' + +def test_retrieve_timeseries_2(TSHandler): + run = TSHandler.retrieve_timeseries(35, 15, model='monarch', method='netcdf', forecast=True) + assert run.layout.title.text =='Dust Optical Depth @ lat = 35 and lon = 15' + +# =================== Evaluation Statistics Figure Handler ============================ +@pytest.fixture +def EvaluationStatisticsFigureHandler(): + return map_handler.EvaluationStatisticsFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) + +# =================== Visibility Figure Handler ============================ +@pytest.fixture +def VisFigureHandler(): + return map_handler.VisFigureHandler(selected_date=END_DATE) + +def test_vis_get_figure_layers_empty(VisFigureHandler): + VisFigureHandler.path_tpl = "fakepath" + VisFigureHandler.get_figure_layers(tstep=0) + assert VisFigureHandler.info.id == 'vis-info' + assert str(VisFigureHandler.info.children) == "P(B('DATA NOT AVAILABLE'))" + +def test_vis_get_figure_layers(VisFigureHandler): + VisFigureHandler.get_figure_layers(tstep=0) + assert str(type(VisFigureHandler.geojson)) == "" + +def test_vis_get_title(VisFigureHandler): + assert str(VisFigureHandler.get_title(tstep=0)) == "Visibility reduced by airborne dust
{edate} 00-06 UTC".format(edate=EDATE_OBJ.strftime(FMT_MON)) + assert str(VisFigureHandler.get_title(tstep=9)) == "Visibility reduced by airborne dust
{edate} 09-15 UTC".format(edate=EDATE_OBJ.strftime(FMT_MON)) + assert str(VisFigureHandler.get_title(tstep=39)) == "Visibility reduced by airborne dust
{edate} 39-45 UTC".format(edate=EDATE_OBJ.strftime(FMT_MON)) + assert str(VisFigureHandler.get_title(tstep=72)) == "Visibility reduced by airborne dust
{edate} 72-78 UTC".format(edate=EDATE_OBJ.strftime(FMT_MON)) + +# =================== Prob Handler ============================ +@pytest.fixture +def ForecastProbFigureHandler(): + ForecastProbFigureHandler_OD550_DUST = map_handler.ForecastProbFigureHandler(var='OD550_DUST', prob=0.1, selected_date=END_DATE) + ForecastProbFigureHandler_SCONC_DUST = map_handler.ForecastProbFigureHandler(var='SCONC_DUST', prob=50, selected_date=END_DATE) + return ForecastProbFigureHandler_OD550_DUST, ForecastProbFigureHandler_SCONC_DUST + +# def test_prob_set_data(ForecastProbFigureHandler): +# assert ForecastProbFigureHandler[0].set_data(tstep=0)[0].data[0] == -24.75 +# assert ForecastProbFigureHandler[0].set_data(tstep=0)[0].data[9] == -20.25 +# assert ForecastProbFigureHandler[0].set_data(tstep=0)[1].data[9] == 5.25 + +def test_prob_retrieve_cdatetime(ForecastProbFigureHandler): + assert str(ForecastProbFigureHandler[0].retrieve_cdatetime(tstep=0)) == '{edate} 00:00:00'.format(edate=EDATE_OBJ.strftime(FMT_DASH)) + assert str(ForecastProbFigureHandler[0].retrieve_cdatetime(tstep=1)) == '{edate} 00:00:00'.format(edate=(EDATE_OBJ + timedelta(days=1)).strftime(FMT_DASH)) + +def test_prob_get_title(ForecastProbFigureHandler): + assert ForecastProbFigureHandler[0].get_title(tstep=0)[-30:] == '{edate} Valid: {edate}'.format(edate=EDATE_OBJ.strftime(FMT_MON)) + assert ForecastProbFigureHandler[1].get_title(tstep=1)[-30:] == '{edate} Valid: {edate_next}'.format(edate=EDATE_OBJ.strftime(FMT_MON), edate_next=(EDATE_OBJ + timedelta(days=1)).strftime(FMT_MON)) + +def test_prob_get_figure_layers(ForecastProbFigureHandler): + # day 0 for od550_dust + ForecastProbFigureHandler[0].get_figure_layers(day=0) + assert ForecastProbFigureHandler[0].geojson.url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/00_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[0].geojson.options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + + # day 1 for od550_dust + ForecastProbFigureHandler[0].get_figure_layers(day=1) + assert ForecastProbFigureHandler[0].geojson.url =='/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/01_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[0].geojson.options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + + # day 0 for sconc_dust + ForecastProbFigureHandler[1].get_figure_layers(day=0) + assert ForecastProbFigureHandler[1].geojson.url == '/dashboard/assets/geojsons/prob/sconc_dust/50/geojson/{edate}/00_{edate}_SCONC_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[1].geojson.options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + + # day 1 for sconc_dust + ForecastProbFigureHandler[1].get_figure_layers(day=1) + assert ForecastProbFigureHandler[1].geojson.url =='/dashboard/assets/geojsons/prob/sconc_dust/50/geojson/{edate}/01_{edate}_SCONC_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[1].geojson.options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + +# =================== Was Handler ============================ +@pytest.fixture +def ForecastWasFigureHandler(): + return map_handler.ForecastWasFigureHandler(was='burkinafaso', model='median', var='SCONC_DUST', + selected_date=END_DATE) + +def test_was_get_regions_data(ForecastWasFigureHandler): + # day 1 + assert ForecastWasFigureHandler.get_regions_data(day=1)[0][0] == 'Boucle du Mouhoun' + assert ForecastWasFigureHandler.get_regions_data(day=1)[0][1] == 'Cascades' + assert ForecastWasFigureHandler.get_regions_data(day=1)[0][-1] == 'Sud-Ouest' + + assert ForecastWasFigureHandler.get_regions_data(day=1)[1][0] == 'green' + assert ForecastWasFigureHandler.get_regions_data(day=1)[1][1] == 'green' + assert ForecastWasFigureHandler.get_regions_data(day=1)[1][-1] == 'green' + + assert ForecastWasFigureHandler.get_regions_data(day=1)[2][0] == 'Normal' + assert ForecastWasFigureHandler.get_regions_data(day=1)[2][1] == 'Normal' + assert ForecastWasFigureHandler.get_regions_data(day=1)[2][-1] =='Normal' + + # day 2 + assert ForecastWasFigureHandler.get_regions_data(day=2)[0][0] == 'Boucle du Mouhoun' + assert ForecastWasFigureHandler.get_regions_data(day=2)[0][1] == 'Cascades' + assert ForecastWasFigureHandler.get_regions_data(day=2)[0][-1] == 'Sud-Ouest' + + assert ForecastWasFigureHandler.get_regions_data(day=2)[1][0] == 'green' + assert ForecastWasFigureHandler.get_regions_data(day=2)[1][1] == 'green' + assert ForecastWasFigureHandler.get_regions_data(day=2)[1][-1] == 'green' + + assert ForecastWasFigureHandler.get_regions_data(day=2)[2][0] == 'Normal' + assert ForecastWasFigureHandler.get_regions_data(day=2)[2][1] == 'Normal' + assert ForecastWasFigureHandler.get_regions_data(day=2)[2][-1] =='Normal' + +def test_was_get_geojson_url(ForecastWasFigureHandler): + assert ForecastWasFigureHandler.get_geojson_url(day=1) == 'geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) + assert ForecastWasFigureHandler.get_geojson_url(day=2) == 'geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_2.geojson'.format(edate=END_DATE) + +def test_was_retrieve_cdatetime(ForecastWasFigureHandler): + assert str(ForecastWasFigureHandler.retrieve_cdatetime(tstep=0)) == '{edate} 00:00:00'.format(edate=EDATE_OBJ.strftime(FMT_DASH)) + +def test_was_get_figure_layers(ForecastWasFigureHandler): + + ForecastWasFigureHandler.get_figure_layers(day=1) + assert ForecastWasFigureHandler.geojson.options == {'style': {'variable': 'forecastTab.wasMaps.styleHandle'}} + assert ForecastWasFigureHandler.geojson.url == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) + assert ForecastWasFigureHandler.legend.className == 'was-legend' + + ForecastWasFigureHandler.get_figure_layers(day=2) + assert ForecastWasFigureHandler.geojson.options == {'style': {'variable': 'forecastTab.wasMaps.styleHandle'}} + assert ForecastWasFigureHandler.geojson.url == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_2.geojson'.format(edate=END_DATE) + assert ForecastWasFigureHandler.legend.className == 'was-legend' + +def test_was_get_title(ForecastWasFigureHandler): + assert str(ForecastWasFigureHandler.get_title(day=0)) =="Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: {edate}. Valid: {edate}".format(edate=EDATE_OBJ.strftime(FMT_MON)) + assert str(ForecastWasFigureHandler.get_title(day=1)) =="Barcelona Dust Regional Center - Burkina Faso WAS.
Expected concentration of airborne dust.
Issued: {edate}. Valid: {edate_next}".format(edate=EDATE_OBJ.strftime(FMT_MON), edate_next=(EDATE_OBJ + timedelta(days=1)).strftime(FMT_MON)) diff --git a/tests/test_interp.py b/tests/test_nc2timeseries.py similarity index 68% rename from tests/test_interp.py rename to tests/test_nc2timeseries.py index 15c7ad275684d447b0396908798f938c8918c403..9a51a633b47d81f9b568ec66ee6ce0d55db49e8b 100644 --- a/tests/test_interp.py +++ b/tests/test_nc2timeseries.py @@ -1,14 +1,21 @@ import pytest import importlib -import xarray as xr -import dask import numpy as np -code = importlib.import_module('preproc.interp') +import xarray as xr +code = importlib.import_module('preproc.nc2timeseries_interp') + + +def counter(model): + import os + import fnmatch + dir_path = r'./tmp/{}-feather'.format(model) + count = len(fnmatch.filter(os.listdir(dir_path), '*.*')) + return count + +# def test_convert2timeseries(): +# assert code.convert2timeseries('test', fmt='feather', months=None) == None +# assert counter('test') > 0 -# def test_plot_station(): -# assert code.plot_station(0) == 0 -# assert code.plot_station(0) == 1 -# def test_preprocess(): # Create sample dataset with 10 timesteps data = xr.DataArray( @@ -22,8 +29,8 @@ def test_preprocess(): preprocessed_ds = code.preprocess(ds, n=5) assert preprocessed_ds.dims == {"time": 5, "lat": 3, "lon": 3} assert preprocessed_ds.coords["time"].values.tolist() == [0, 1, 2, 3, 4] - # Test that preprocess function keeps only first 3 timesteps preprocessed_ds = code.preprocess(ds, n=3) assert preprocessed_ds.dims == {"time": 3, "lat": 3, "lon": 3} assert preprocessed_ds.coords["time"].values.tolist() == [0, 1, 2] + diff --git a/tests/test_observations.py b/tests/test_observations.py index 9c6566868838a326a9533cb6dd7c038cecabde4d..0f9ae48506195fdf4ebc3dddd41acf1d7fa35e55 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -10,9 +10,9 @@ from dash._callback_context import context_value from dash._utils import AttributeDict code = importlib.import_module('tabs.observations') -from data_handler import START_DATE -from data_handler import END_DATE -from data_handler import FREQ +from map_handler import START_DATE +from map_handler import END_DATE +from map_handler import FREQ FMT_ISO = "%Y%m%d" FMT_DASH = "%Y-%m-%d" @@ -28,36 +28,35 @@ EDATE_OBJ_STR = str(EDATE_OBJ) SDATE_OBJ = datetime.strptime(START_DATE, FMT_ISO) SDATE_OBJ_STR = str(SDATE_OBJ) -#=================TEST OBS_TIME_SLIDER=========================== + def test_obs_time_slider(): - #TEST DIV=OBS + # TEST DIV=OBS assert f"display_format='DD MMM YYYY', id='obs-date-picker'), Button(id='clear_button', className='clear_button')], className='timesliderline'), Span(children=[Button(id='btn-obs-play', className='fa fa-play', n_clicks=0, title='Play')], className='timesliderline anim-buttons'), Span(children=Slider(min=0, max=23, step=1" in str(code.obs_time_slider(div='obs', start=0, end=23, step=1, start_date=START_DATE, end_date=END_DATE).children[0]) - #TEST DIV=OBS-VIS - from utils import get_vis_edate + # TEST DIV=OBS-VIS from datetime import datetime hour = datetime.now().hour - _, default_tstep = get_vis_edate(END_DATE, hour=hour) + _, default_tstep = code.get_vis_edate(END_DATE, hour=hour) assert "Button(id='clear_button', className='clear_button')], className='timesliderline'), None, Span(children=Slider(min=0, max=18, step=6, marks={0: {'label': '00-06'}, 6: {'label': '06-12'}, 12: {'label': '12-18'}, 18: {'label': '18-24', 'style': {'left': '', 'right': '-32px'}}}, value=%(default_tstep)s, id='obs-vis-slider-graph'), className='timesliderline')], className='timeslider')]" % { 'default_tstep': default_tstep } in str(code.obs_time_slider(div='obs-vis', start=0, end=18, step=6, start_date=START_DATE, end_date=END_DATE)) - #TEST DIV = OBS-AOD + # TEST DIV = OBS-AOD # assert "NavbarSimple(children=[Div(children=[Span(children=[DatePickerSingle(date='20210318', min_date_allowed=datetime.datetime(2023, 3, 30, 0, 0), max_date_allowed=datetime.datetime(2021, 3, 18, 0, 0), placeholder='DD MON YYYY', initial_visible_month=datetime.datetime(2021, 3, 18, 0, 0), display_format='DD MMM YYYY', id='obs-aod-date-picker'), Button(id='clear_button', className='clear_button')]" % { 'default_tstep': default_tstep } in str (code.obs_time_slider(div='obs-aod', start=0, end=23, step=1)) - -#=================TEST SIDEBAR OBSERVATIONS =========================== def test_sidebar_observations(): assert "Button(children='EUMETSAT RGB', id='rgb', color='link', style={'fontWeight': 'bold'})" in str(code.sidebar_observations('rgb')) assert "Button(children='Visibility', id='visibility', color='link', style={'fontWeight': 'bold'})" in str(code.sidebar_observations('visibility')) - -#=================TEST TAB_OBSERVATIONS=========================== def test_tab_observations(): - #TEST RGB + # TEST RGB assert f"All observations are kindly offered by Partners of the WMO Barcelona Dust Regional Center. RGB is a qualitative satellite product that indicates desert dust in the entire atmospheric column (represented by pink colour).']), className='description-body'), Div(children=[Button(children='HEMISPHERIC', id='btn-fulldisc', active=True), Button(children='MIDDLE EAST', id='btn-middleeast', active=False)], id='rgb-buttons'), Div(children=[Img(id='rgb-image', alt='EUMETSAT RGB - NOT AVAILABLE', src='./assets/eumetsat/FullDiscHD/archive/{END_DATE}/FRAME_OIS_RGB-dust-all_{END_DATE}0000.gif')" in str(code.tab_observations('rgb', start_date=START_DATE, end_date=END_DATE)) - #TEST VISIBILITY - from utils import get_vis_edate + # TEST VISIBILITY from datetime import datetime hour = datetime.now().hour - _, default_tstep = get_vis_edate(END_DATE, hour=hour) - assert "Button(id='clear_button', className='clear_button')], className='timesliderline'), None, Span(children=Slider(min=0, max=18, step=6, marks={0: {'label': '00-06'}, 6: {'label': '06-12'}, 12: {'label': '12-18'}, 18: {'label': '18-24', 'style': {'left': '', 'right': '-32px'}}}, value=%(default_tstep)s, id='obs-vis-slider-graph'), className='timesliderline')], className='timeslider')], id='rgb-navbar', className='fixed-bottom navbar-timebar', dark=True, expand='lg', fixed='bottom', fluid=True), Br(None), Br(None), Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='forecast-disclaimer')], className='disclaimer')], className='layout-dropdown rgb-layout-dropdown')], id='observations-tab', className='horizontal-menu', label='Observations', value='observations-tab')" % { 'default_tstep': default_tstep } in str(code.tab_observations('visibility', start_date=START_DATE, end_date=END_DATE)) + _, default_tstep = code.get_vis_edate(END_DATE, hour=hour) + assert "Button(id='clear_button', className='clear_button')], className='timesliderline'), None, Span(children=Slider(min=0, max=18, step=6, marks={0: {'label': '00-06'}, 6: {'label': '06-12'}, 12: {'label': '12-18'}, 18: {'label': '18-24', 'style': {'left': '', 'right': '-32px'}}}, value=%(default_tstep)s, id='obs-vis-slider-graph'), className='timesliderline')], className='timeslider')], id='rgb-navbar', className='fixed-bottom navbar-timebar', dark=True, expand='lg', fixed='bottom', fluid=True), Br(None), Br(None), Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='models-disclaimer')], className='disclaimer')], className='layout-dropdown rgb-layout-dropdown')], id='observations-tab', className='horizontal-menu', label='Observations', value='observations-tab')" % { 'default_tstep': default_tstep } in str(code.tab_observations('visibility', start_date=START_DATE, end_date=END_DATE)) + +def test_get_vis_edate(): + + assert code.get_vis_edate(END_DATE, hour=9) == (END_DATE, 0) + assert code.get_vis_edate(END_DATE, hour=16) == (END_DATE, 6) diff --git a/tests/test_observations_callbacks.py b/tests/test_observations_callbacks.py index 3f7b552e1dc59625e197d48c1fdea8b1e6e28e4d..3597969a8973218a35260a9896835c7bb6ce3a2e 100644 --- a/tests/test_observations_callbacks.py +++ b/tests/test_observations_callbacks.py @@ -5,25 +5,28 @@ from contextvars import copy_context from dash._callback_context import context_value from dash._utils import AttributeDict code = importlib.import_module('tabs.observations_callbacks') - -from data_handler import START_DATE, END_DATE -from data_handler import FREQ +from map_handler import START_DATE +from unittest.mock import MagicMock #============ TEST render_evaluation_tab================================= def test_render_observations_tab(): # CASE 1, RGB 1 CLICK, VISIBILITY NO CLICK def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "rgb.n_clicks"}, {"prop_id": "visibility.n_clicks"}]})) + code.START_DATE = '20120120' + code.END_DATE = '20220831' return code.render_observations_tab(1, 0) ctx = copy_context() output = ctx.run(run_callback) - print("*********************") - print(output[0]) - print("---------------------") - print(output[1:]) - print("*********************") - assert f"Button(children='HEMISPHERIC', id='btn-fulldisc', active=True), Button(children='MIDDLE EAST', id='btn-middleeast', active=False)], id='rgb-buttons'), Div(children=[Img(id='rgb-image', alt='EUMETSAT RGB - NOT AVAILABLE', src='./assets/eumetsat/FullDiscHD/archive/{END_DATE}/FRAME_OIS_RGB-dust-all_{END_DATE}0000.gif'), Div(children=NavbarSimple(children=[Div(children=[Span(children=[DatePickerSingle(date='{END_DATE}'" in str(output[0]) + assert output[0].id == 'observations-tab' + assert output[0].children[0].className == 'description-title' + check_date = output[0].children[3].children[1].children.children[0].children[0].children[0].date + assert check_date == code.END_DATE + check_id = output[0].children[3].children[1].children.children[0].children[0].children[0].id + assert check_id == 'obs-date-picker' + assert "All observations are kindly offered by Partners of the WMO Barcelona Dust Regional Center. RGB is a qualitative satellite product that indicates desert dust in the entire atmospheric column (represented by pink colour).']), className='description-body'), Div(children=[Button(children='HEMISPHERIC', id='btn-fulldisc', active=True), Button(children='MIDDLE EAST', id='btn-middleeast', active=False)], id='rgb-buttons'), " in str(output[0]) + assert ", className='timesliderline')], className='timeslider')], id='rgb-navbar', className='fixed-bottom navbar-timebar', dark=True, expand='lg', fixed='bottom', fluid=True), className='layout-dropdown')], className='centered-image'), Div(Interval(id='obs-slider-interval', disabled=True, interval=1000, n_intervals=0))], id='observations-tab', className='horizontal-menu', label='Observations', value='observations-tab')" in str(output[0]) assert output[1:] == ({ 'font-weight': 'bold' }, { 'font-weight': 'normal' }, 'rgb') @@ -31,23 +34,103 @@ def test_render_observations_tab_visibility(): # CASE 2, RGB NO CLICK, VISIBILITY 1 CLICK def run_callback(): context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id": "visibility.n_clicks"}]})) + code.END_DATE = '20220831' return code.render_observations_tab(0, 1) - from utils import get_vis_edate + from tabs.observations import get_vis_edate from datetime import datetime hour = datetime.now().hour - _, default_tstep = get_vis_edate(END_DATE, hour=hour) + _, default_tstep = get_vis_edate('20220831', hour=hour) ctx = copy_context() output = ctx.run(run_callback) - assert "Slider(min=0, max=18, step=6, marks={0: {'label': '00-06'}, 6: {'label': '06-12'}, 12: {'label': '12-18'}, 18: {'label': '18-24', 'style': {'left': '', 'right': '-32px'}}}, value=%(default_tstep)s, id='obs-vis-slider-graph'), className='timesliderline')], className='timeslider')], id='rgb-navbar', className='fixed-bottom navbar-timebar', dark=True, expand='lg', fixed='bottom', fluid=True), Br(None), Br(None), Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='forecast-disclaimer')], className='disclaimer')], className='layout-dropdown rgb-layout-dropdown')], id='observations-tab', className='horizontal-menu', label='Observations', value='observations-tab')" % { 'default_tstep': default_tstep } in str(output[0]) + assert "Slider(min=0, max=18, step=6, marks={0: {'label': '00-06'}, 6: {'label': '06-12'}, 12: {'label': '12-18'}, 18: {'label': '18-24', 'style': {'left': '', 'right': '-32px'}}}, value=%(default_tstep)s, id='obs-vis-slider-graph'), className='timesliderline')], className='timeslider')], id='rgb-navbar', className='fixed-bottom navbar-timebar', dark=True, expand='lg', fixed='bottom', fluid=True), Br(None), Br(None), Div(children=[Span(children=P('Dust data ©2023 WMO Barcelona Dust Regional Center.'), id='models-disclaimer')], className='disclaimer')], className='layout-dropdown rgb-layout-dropdown')], id='observations-tab', className='horizontal-menu', label='Observations', value='observations-tab')" % { 'default_tstep': default_tstep } in str(output[0]) assert output[1:] == ({ 'font-weight': 'normal' }, { 'font-weight': 'bold' }, 'visibility') -# def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_active, btn_middleeast_active): -# def start_stop_obs_autoslider(n_play, disabled, value): -# def update_obs_slider(n): + +def test_update_image_src_fulldisc(mocker): + # Mocking dash.get_asset_url + mocked_get_asset_url = mocker.patch('dash.get_asset_url') + mocked_get_asset_url.return_value = 'mocked_url' + + def run_callback(): + # Set up the inputs for the callback + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "btn-fulldisc.n_clicks"}, + {"prop_id": "btn-middleeast.n_clicks"}, + {"prop_id": "eval-date-picker.date"}, + {"prop_id": "obs-slider-graph.value"}, + {"prop_id": "btn-fulldisc.active"}, + {"prop_id": "btn-middleeast.active"}]})) + + return code.update_image_src.uncached(1, None, START_DATE, 3, False, True) + + ctx = copy_context() + url, btn_fulldisc_active, btn_middleeast_active = ctx.run(run_callback) + + # Assert the expected behavior + assert url == 'mocked_url' + assert btn_fulldisc_active == True + assert btn_middleeast_active == False + +def test_update_image_src_middleeast(mocker): + # Mocking dash.get_asset_url + mocked_get_asset_url = mocker.patch('dash.get_asset_url') + mocked_get_asset_url.return_value = 'mocked_url' + + def run_callback(): + # Set up the inputs for the callback + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "btn-middleeast.n_clicks"}, + {"prop_id": "eval-date-picker.date"}, + {"prop_id": "obs-slider-graph.value"}, + {"prop_id": "btn-fulldisc.active"}, + {"prop_id": "btn-middleeast.active"}]})) + + return code.update_image_src.uncached(None, 1, START_DATE, 0, True, False) + + ctx = copy_context() + url, btn_fulldisc_active, btn_middleeast_active = ctx.run(run_callback) + + # Assert the expected behavior + assert url == 'mocked_url' + assert btn_fulldisc_active == False + assert btn_middleeast_active == True + +def test_start_stop_obs_autoslider_True(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "btn-obs-play.n_clicks"}, + {"prop_id": "obs-slider-interval.disabled"}, + {"prop_id": "obs-slider-graph.value"}]})) + return code.start_stop_obs_autoslider.uncached(1, True, '3') + + ctx = copy_context() + output = ctx.run(run_callback) + assert output == (False, 3, 'fa fa-pause text-center') + +def test_start_stop_obs_autoslider_False(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": + [{"prop_id": "btn-obs-play.n_clicks"}, + {"prop_id": "obs-slider-interval.disabled"}, + {"prop_id": "obs-slider-graph.value"}]})) + return code.start_stop_obs_autoslider.uncached(1, False, '6') + + ctx = copy_context() + output = ctx.run(run_callback) + assert output == (True, 6, 'fa fa-play text-center') + +def test_update_obs_slider(): + assert code.update_obs_slider.uncached(0) == 0 + assert code.update_obs_slider.uncached(13) == 13 + assert code.update_obs_slider.uncached(23) == 23 + assert code.update_obs_slider.uncached(24) == 0 + assert code.update_obs_slider.uncached(36) == 12 + assert code.update_obs_slider.uncached(39) == 15 + assert code.update_obs_slider.uncached(49) == 1 def test_update_vis_figure(): - run = code.update_vis_figure.uncached(END_DATE, 1, [5], [45,45]) + run = code.update_vis_figure.uncached('20220808', 1, [5], [45,45]) assert "Map(children=[TileLayer(id={'tag': 'obs-vis-tile-layer', 'index': 'None'}" in str(run) assert "id='vis-info', className='info', style={'position': 'absolute', 'top': '10px', 'left': '10px', 'zIndex': '1000', 'fontFamily'" in str(run) diff --git a/tests/test_router.py b/tests/test_router.py index 34bf61160190aad696bd2fc6d230873d7fc52a1b..6bb6cf67441fd1f63c498e2f2555b7c4a05a1812 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -1,7 +1,7 @@ import pytest import importlib code = importlib.import_module('router') -from data_handler import ROUTE_DEFAULTS +from map_handler import ROUTE_DEFAULTS #============ TEST get_input_aliases================================= def test_get_input_aliases(): diff --git a/tests/test_tools.py b/tests/test_tools.py index 6c2a78ebcf3ce933b278deb6021343b39a71ff1e..35e042767aa7e5dc2d35f572f4d56bca6c82d192 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,9 +2,9 @@ import pytest from datetime import datetime from datetime import timedelta import importlib -code = importlib.import_module('tools') -from data_handler import END_DATE -from data_handler import FREQ +code = importlib.import_module('callback_tools') +from map_handler import END_DATE +from map_handler import FREQ FMT_ISO = "%Y%m%d" FMT_DASH = "%Y-%m-%d" @@ -32,15 +32,6 @@ def test_get_timeseries(): # assert float(code.get_single_point('median', '20220808', 3, 'OD550_DUST', 25, 40)) == 0.47600093483924866 # assert float(code.get_single_point('cams', '20220808', 12, 'SCONC_DUST', 15, 4)) == 1.8746418106729834e-07 -def test_get_scores_figure(): - run1 = code.get_scores_figure('aeronet', 'monarch', 'bias', EDATE_OBJ.strftime("%Y%m")) - assert str(run1.data[0]) == 'Scattermapbox()' - assert str(run1.data[1].name) == 'MBE score' - - run2 = code.get_scores_figure('modis', 'median', 'bias', EDATE_OBJ.strftime("%Y%m")) - assert str(run2.data[0]) == 'Scattermapbox()' - assert list(run2.data[1].lat[1:5]) ==[0.25, 0.25, 0.25, 0.25] - def test_get_prob_figure(): assert "Run: {edate} Valid: {edate}".format(edate=EDATE_OBJ.strftime(FMT_MON)) in str(code.get_prob_figure('OD550_DUST', prob=0.5, day=0, selected_date=END_DATE)[2].children) assert code.get_prob_figure('OD550_DUST', prob=0.8, day=1, selected_date=END_DATE)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -53,21 +44,20 @@ def test_get_was_figure(): assert result2 in str(code.get_was_figure(was='chad', day=0, selected_date=END_DATE)) def test_get_vis_figure(): - result = "Div(children=['Visibility reduced by airborne dust', Br(None), '08 August 2022 00-06 UTC'], id='vis-info', className='info', style={'position': 'absolute', 'top': '10px', 'left': '10px', 'zIndex': '1000', 'fontFamily': " - result2 = "Div(children=[Div(children=[Span(children='', className='vis-legend-point', style={'backgroundColor': '#714921'}), Span(children='< 1 km', className='vis-legend-label')], style={'display': 'block'}), Div(children=[Span(children='', className='vis-legend-point', style={'backgroundColor': '#da7230'}), Span(children='1 - 2 km', className='vis-legend-label')]," - run1 = str(code.get_vis_figure(tstep=0, selected_date='20220808')[1][1]) - run2 = str(code.get_vis_figure(tstep=0, selected_date='20220808')[1][2]) - assert result in run1 - assert result2 in run2 + code_run = code.get_vis_figure(tstep=0, selected_date='20230321') + assert code_run[1].children[0].children[0].className == 'vis-legend-point' + assert code_run[2].children.children.children[2] == '21 Mar 2023 00-06 UTC' + assert code_run[2].id == 'vis-info' def test_get_models_figure(): - result = f"/dashboard/assets/geojsons/median/geojson/{END_DATE}/00_{END_DATE}_OD550_DUST.geojson" - run1 = str(code.get_models_figure(model='median', var='OD550_DUST', - selected_date=END_DATE, tstep=0).children[2].url) - assert result in run1 - - result2 = "{'tag': 'model-tile-layer', 'index': 'monarch'}" - run2 = str(code.get_models_figure(model='monarch', var='SCONC_DUST', selected_date=END_DATE, tstep=1, - hour=3, static=True, aspect=(1, 1), center=None, - view='carto-positron', zoom=None, layer=None, tag='empty').children[0].id) + result1 = f"/dashboard/assets/geojsons/median/geojson/{END_DATE}/00_{END_DATE}_OD550_DUST.geojson" + run1 = str(code.get_model_figure(var='OD550_DUST', model='median', tstep=0, + selected_date=END_DATE)[0].url) + assert result1 in run1 + + if END_DATE == '20230430': + monarch_date = '20230429' + result2 = f"/dashboard/assets/geojsons/NMMB-BSC/geojson/{monarch_date}/00_{monarch_date}_SCONC_DUST.geojson" + run2 = str(code.get_model_figure(var='SCONC_DUST', model='monarch', tstep=1, hour=3, + selected_date=END_DATE)[0].url) assert result2 in run2 diff --git a/timeseries_handler.py b/timeseries_handler.py new file mode 100644 index 0000000000000000000000000000000000000000..d98a39e7430152582d08bb074f3707e352fd405a --- /dev/null +++ b/timeseries_handler.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +""" Timeseries Handler """ + +import os +from datetime import datetime as dt +from datetime import timedelta +from dateutil.relativedelta import relativedelta +import plotly.graph_objs as go +import numpy as np +import pandas as pd +import json +import logging + +from ines_core_utils import concat_dataframes +from ines_core_utils import retrieve_timeseries +from ines_core_utils import retrieve_single_point + +from map_handler import DASH_LOG_LEVEL +from map_handler import MODELS +from map_handler import MODEBAR_CONFIG +from map_handler import MODEBAR_CONFIG_TS +from map_handler import MODEBAR_LAYOUT +from map_handler import MODEBAR_LAYOUT_TS +from map_handler import DELAY +from map_handler import DELAY_DATE +from map_handler import FREQ +from map_handler import VARS +from map_handler import OBS + +DIR_PATH = os.path.dirname(os.path.realpath(__file__)) + + +class ForecastModelsTimeSeriesHandler: + """ Class to handle forecast time series """ + + def __init__(self, model, date, var): + """ Initialize ForecastModelsTimeSeriesHandler class + + Parameters + ---------- + model : str + Model name + date : str + Selected date + var : str + Variable name + """ + + if isinstance(model, str): + model = [model] + self.model = model + self.var = var + self.fpaths = [] + try: + self.month = dt.strptime(date, "%Y%m%d").strftime("%Y%m") + self.currdate = dt.strptime(date, "%Y%m%d").strftime("%Y%m%d") + except: + self.month = dt.strptime(date, "%Y-%m-%d").strftime("%Y%m") + self.currdate = dt.strptime(date, "%Y-%m-%d").strftime("%Y%m%d") + + return None + + def retrieve_single_point(self, tstep, lat, lon, model=None): + """ Retrive data on single point + + Parameters + ---------- + tstep : int + Timestep + lat : float + Latitude + lon : float + Longitude + model : str, optional + Model name, by default None + + Returns + ------- + float + Variable value at closest available point from (lat, lon) + """ + + if not model: + model = self.model[0] + + logging.debug("---------- %s", model) + + method = 'netcdf' + if (MODELS[model]['start'] == 12 and not DELAY and DELAY_DATE and (dt.strptime(self.currdate, "%Y%m%d") >= dt.strptime(DELAY_DATE, "%Y%m%d"))) or \ + (MODELS[model]['start'] == 12 and not DELAY and not DELAY_DATE): + mod_date = (dt.strptime(self.currdate, "%Y%m%d") - + timedelta(days=1)).strftime("%Y%m%d") + else: + mod_date = self.currdate + path_template = '{}{}.nc'.format(mod_date, + MODELS[model]['template'], + self.var) + + fpath = os.path.join(MODELS[model]['path'], method, path_template) + + return retrieve_single_point(fpath, tstep, lat, lon, self.var) + + def retrieve_timeseries(self, lat, lon, model=None, method='netcdf', forecast=False): + """ Retrieve timeseries plot for point in modal window + + Parameters + ---------- + lat : float + Latitude + lon : float + Longitude + model : str, optional + Model name, by default None + method : str, optional + Method name, by default 'netcdf' + forecast : bool, optional + Indicates if we are using forecast data, by default False + + Returns + ------- + go.Figure + Modal window with timeseries + """ + + if not model: + model = self.model + + # logging.debug("---------- %s", model) + + obs_eval = model[0] not in MODELS and model[0] in OBS + if obs_eval: + all_models = [model[0]] + list(MODELS.keys()) + else: + all_models = list(MODELS.keys()) + + for mod in all_models: + if obs_eval: + filedir = OBS[model[0]]['path'] + path_tpl = '{}-{}-{}_interp.ft' # 202010-median-OD550_DUST_interp.ft + else: # if mod in MODELS: + filedir = MODELS[mod]['path'] + path_tpl = '{}-{}-{}.ft' # 202010-median-OD550_DUST_interp.ft + + if method == 'feather': + path_template = path_tpl.format(self.month, mod, self.var) + elif method == 'netcdf': + path_template = '{}*{}.nc'.format(self.month, MODELS[mod]['template'], self.var) + + if forecast: + method = 'netcdf' + if (MODELS[mod]['start'] == 12 and not DELAY and DELAY_DATE and (dt.strptime(self.currdate, "%Y%m%d") >= dt.strptime(DELAY_DATE, "%Y%m%d"))) or \ + (MODELS[mod]['start'] == 12 and not DELAY and not DELAY_DATE): + mod_date = (dt.strptime(self.currdate, "%Y%m%d") - + timedelta(days=1)).strftime("%Y%m%d") + else: + mod_date = self.currdate + path_template = '{}{}.nc'.format(mod_date, MODELS[mod]['template'], self.var) + + fpath = os.path.join(filedir, + method, + path_template) + self.fpaths.append(fpath) + + title = "{} @ lat = {} and lon = {}".format( + VARS[self.var]['name'], round(lat, 2), round(lon, 2) + ) + + mul = VARS[self.var]['mul'] + + fig = go.Figure() + + for mod, fpath in zip(all_models, self.fpaths): + # logging.debug(' %s %s', mod, fpath) + if mod not in MODELS and mod in OBS: + var = OBS[mod]['obs_var'] + else: + var = self.var + + if not os.path.exists(fpath): + logging.debug("NOT retrieving %s File doesn't exist.", fpath) + continue + + logging.debug('Retrieving *** FPATH *** %s', fpath) + try: + ts_lat, ts_lon, ts_index, ts_values = retrieve_timeseries( + fpath, lat, lon, var, method=method, forecast=forecast) + except Exception as e: + logging.debug("NOT retrieving %s ERROR: %s", fpath, str(e)) + continue + + if forecast is True or mod in ('cams', 'ema-regcm4'): + ts = pd.Series(index=ts_index, data=ts_values) + else: + date_index = pd.date_range('{}01'.format(self.month), + '{}01'.format((dt.strptime(self.month, '%Y%m') + + relativedelta(days=31)).strftime('%Y%m')), freq=f'{FREQ}H') + ts = pd.Series(index=ts_index, data=ts_values).reindex(date_index) + + if isinstance(ts_lat, np.ndarray): + ts_lat = float(ts_lat) + ts_lon = float(ts_lon) + if isinstance(ts.values, np.ndarray): + ts_values = (ts.values*mul).round(2) + else: + ts_values = round((ts.values*mul), 2) + + if obs_eval and mod == model[0]: + sc_mode = 'markers' + marker = {'size': 12, 'symbol': "triangle-up-dot", 'color': '#f0b450'} + line = {} + visible = True + name = "{}".format(mod.upper()) + elif obs_eval: + sc_mode = 'lines' + marker = {} + line = { 'color': MODELS[mod]['color'] } + if mod in model: + visible = True + else: + visible = 'legendonly' + name = "{}".format(MODELS[mod]['name']) + else: + sc_mode = 'lines' + marker = {} + line = { 'color': MODELS[mod]['color'] } + if mod in model: + visible = True + else: + visible = 'legendonly' + name = "{} ({}, {})".format( + MODELS[mod]['name'], round(ts_lat, 2), round(ts_lon, 2)) + + if mod == 'median': + line['dash'] = 'dash' + else: + line['dash'] = 'solid' + + fig.add_trace(dict( + type='scatter', + name=name, + x=ts.index, + y=ts_values, + mode=sc_mode, + marker=marker, + line=line, + visible=visible, + ) + ) + + fig.update_layout( + title=dict(text=title, x=0.45, y=.99), + yaxis=dict(exponentformat="none"), + # uirevision=True, + autosize=True, + showlegend=True, + plot_bgcolor='#F9F9F9', + font_size=12, + # hovermode="closest", # highlight closest point on hover + hovermode="x", # highlight closest point on hover + margin={"r": 10, "t": 35, "l": 10, "b": 10}, + ) + + return fig + + +class EvaluationGroundTimeSeriesHandler: + + def __init__(self, obs, start_date, end_date, var, models=None): + """ Initialize EvaluationGroundTimeSeriesHandler class + + Parameters + ---------- + obs : str + Observations name + start_date : str + Start date + end_date : str + End date + var : str + Variable name + models : str, optional + Model name, by default None + """ + + self.obs = obs + if models is None: + models = list(MODELS.keys()) + self.model = models + self.var = var + self.dataframe = [] + logging.debug("ObsTimeSeries %s %s", start_date, end_date) + self.date_range = pd.date_range(start_date, end_date, freq='D') + + fname_tpl = os.path.join(OBS[obs]['path'], + 'feather', + '{{}}-{dat}-{{}}_interp.ft') + + months = np.unique([d.strftime("%Y%m") for d in self.date_range.to_pydatetime()]) + + self.date_index = pd.date_range('{}01'.format(months[0]), + '{}01'.format((dt.strptime(months[-1], '%Y%m') + + relativedelta(days=31)).strftime('%Y%m')), freq=f'{FREQ}H') + + logging.debug('MONTHS %s', months) + logging.debug('DATE_INDEX %s', self.date_index) + + fname_obs = fname_tpl.format(dat=obs) + notnans, obs_df = concat_dataframes(fname_obs, months, self.var, + rename_from=OBS[obs]['obs_var']) + self.dataframe.append(obs_df) + + if 'flt_var' in OBS[obs]: + fname_flt = fname_tpl.format(dat='{}_{}'.format(obs, OBS[obs]['flt_var'])) + _, flt_df = concat_dataframes(fname_flt, months, self.var, + rename_from=OBS[obs]['flt_var'], notnans=notnans) + self.dataframe.append(flt_df) + + for mod in self.model: + fname_mod = fname_tpl.format(dat=mod) + _, mod_df = concat_dataframes(fname_mod, months, self.var, + rename_from=None, notnans=notnans) + self.dataframe.append(mod_df) + + return None + + def retrieve_timeseries(self, idx, station_name, model): + """ Retrieve timeseries plot for station in modal window + + Parameters + ---------- + idx : int + Station index + station_name : str + Station name + model : str, optional + Model name, by default None + + Returns + ------- + go.Figure + Modal window with timeseries + """ + + #FIXME pop filtering variable for now + self.dataframe.pop(1) + old_indexes = self.dataframe[0]['station'].unique() + new_indexes = np.arange(old_indexes.size) + dict_idx = dict(zip(new_indexes, old_indexes)) + logging.debug("RETRIEVE TS %s %s %s %s", idx, dict_idx[idx], station_name, model) + fig = go.Figure() + for mod, df in zip([self.obs]+self.model, self.dataframe): + if df is None: + continue + logging.debug("MOD %s COLS %s", mod, df.columns) + if df.columns[-1].upper() == self.var: + df = df.rename(columns = { df.columns[-1]: self.var }) + + logging.debug("BUILDING TIME-SERIES") + try: + tmp_df = df[df['station']==dict_idx[idx]].set_index('time') + tmp_df.drop_duplicates(keep='first') + timeseries = \ + tmp_df.reindex(self.date_index) + if timeseries[self.var].isnull().all(): + continue + except Exception as e: + logging.debug("ERROR timeseries %s", str(e)) + continue + + logging.debug("SELECTING COORDS") + if 'lat' in df.columns: + lat_col = 'lat' + lon_col = 'lon' + else: + lat_col = 'latitude' + lon_col = 'longitude' + + if mod == self.obs: + logging.debug("OBSERVATION") + sc_mode = 'markers' + marker = {'size': 10, 'symbol': "triangle-up-dot", 'color': '#f0b450'} + line = {} + visible = True + name = mod.upper() + else: + logging.debug("MODEL %s", mod) + try: + sc_mode = 'lines' + marker = {} + line = { 'color': MODELS[mod]['color'] } + visible = (mod == model) and True or 'legendonly' + name = "{}".format(MODELS[mod]['name']) + cur_lat = round(timeseries[lat_col].dropna()[0], 2) + cur_lon = round(timeseries[lon_col].dropna()[0], 2) + except Exception as e: + logging.debug("ERROR MODEL %s %s %s", mod, str(e), timeseries[lat_col].dropna()) + continue + + if mod == 'median': + line['dash'] = 'dash' + + logging.debug("ADD TRACE") + fig.add_trace(dict( + type='scatter', + name=name, + x=timeseries.index, + y=timeseries[self.var].round(2), + mode=sc_mode, + marker=marker, + line=line, + visible=visible, + connectgaps=False + ) + ) + + title = "{} @ {} (lat = {:.2f}, lon = {:.2f})".format( + VARS[self.var]['name'], station_name, cur_lat, cur_lon, + ) + + fig.update_layout( + title=dict(text=title, x=0.45, y=0.99), + # uirevision=True, + autosize=True, + showlegend=True, + plot_bgcolor='#F9F9F9', + font_size=12, + hovermode="x", # highlight closest point on hover + margin={"r": 10, "t": 35, "l": 10, "b": 10}, + ) + fig.update_xaxes( + range=[self.date_range[0], self.date_range[-1]], + rangeslider_visible=True, + rangeselector=dict( + buttons=list([ + dict(step="all", label="all"), + dict(count=14, label="2w", + step="day", stepmode="backward"), + dict(count=7, # label="1w", + step="day", stepmode="backward"), + ]) + ) + ) + + logging.debug('FIG TYPE %s', type(fig)) + + return fig diff --git a/tools.py b/tools.py deleted file mode 100644 index 2df73cbdadb2afe116ba3dd0779295fd5f58d2ce..0000000000000000000000000000000000000000 --- a/tools.py +++ /dev/null @@ -1,208 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" Tools module with functions related to plots """ - -from datetime import datetime as dt -from datetime import timedelta -import os - -from data_handler import FigureHandler -from data_handler import WasFigureHandler -from data_handler import ProbFigureHandler -from data_handler import VisFigureHandler -from data_handler import ScoresFigureHandler -from data_handler import TimeSeriesHandler -from data_handler import ObsTimeSeriesHandler -from data_handler import Observations1dHandler -from data_handler import DEBUG -from data_handler import MODELS -from data_handler import END_DATE, DELAY, DELAY_DATE -from utils import get_currdate_tstep - -def download_image_link(models, variable, curdate, tstep=0, anim=False): - """ Generates links to animated gifs """ - if DEBUG: - print('CURRDIR', os.getcwd()) - filepath = "assets/comparison/{model}/{variable}/{year}/{month}/{curdate}_{model}_{tstep}.{ext}" - - if len(models) == 1: - model = models[0] - if DEBUG: - print('DOWNLOAD MODELS', model) - else: - if DEBUG: - print('DOWNLOAD ALL MODELS') - model = "all" - if anim: - tstep = "loop" - ext = "gif" - if DEBUG: - print('DOWNLOAD LOOP') - else: - tstep = "%02d" % tstep - ext = "png" - if DEBUG: - print('DOWNLOAD PNG', tstep) - - filename = filepath.format( - model=model, - variable=variable.lower(), - year=curdate[:4], - month=curdate[4:6], - curdate=curdate, - tstep=tstep, - ext=ext - ) - if DEBUG: - print('DOWNLOAD FILENAME', filename) - return filename - - -def get_eval_timeseries(obs, start_date, end_date, var, idx, name, model): - """ Retrieve timeseries """ - if DEBUG: - print('SERVER: OBS TS init for obs {} ... '.format(str(obs))) - th = ObsTimeSeriesHandler(obs, start_date, end_date, var) - if DEBUG: - print('SERVER: OBS TS generation ... ') - return th.retrieve_timeseries(idx, name, model) - - -def get_timeseries(model, date, var, lat, lon, forecast=False): - """ Retrieve timeseries """ - if DEBUG: - print('SERVER: TS init for models {} ... '.format(str(model))) - th = TimeSeriesHandler(model, date, var) - if DEBUG: - print('SERVER: TS generation ... ') - return th.retrieve_timeseries(lat, lon, method='feather', forecast=forecast) - - -def get_single_point(model, date, tstep, var, lat, lon): - """ Retrieve sigle point """ - if DEBUG: - print('SERVER: SINGLE POINT init for models {} ... '.format(str(model))) - th = TimeSeriesHandler(model, date, var) - if DEBUG: - print('SERVER: SINGLE POINT generation ... ') - return th.retrieve_single_point(tstep, lat, lon) - - -def get_obs1d(sdate, edate, obs, var): - """ Retrieve 1D observation """ - obs_handler = Observations1dHandler(sdate, edate, obs) - return obs_handler.generate_obs1d_tstep_trace(var) - - -def get_scores_figure(network, model, statistic, selection=END_DATE): - """ Retrieve 1D observation """ - fh = ScoresFigureHandler(network, statistic, selection) - return fh.retrieve_scores(model) - - -def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): - """ Retrieve figure """ - if DEBUG: - print(prob, day, selected_date) - try: - selected_date = dt.strptime( - selected_date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: - print(prob, day, selected_date) - if prob: - if DEBUG: - print('SERVER: PROB Figure init ... ') - fh = ProbFigureHandler(var=var, prob=prob, selected_date=selected_date) - if DEBUG: - print('SERVER: PROB Figure generation ... ') - return fh.retrieve_var_tstep(day=day) - if DEBUG: - print('SERVER: NO PROB Figure') - return ProbFigureHandler().retrieve_var_tstep() - - -def get_was_figure(was=None, day=0, selected_date=END_DATE): - """ Retrieve figure """ - if DEBUG: - print(was, day, selected_date) - try: - selected_date = dt.strptime( - selected_date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: - print(was, day, selected_date) - if was: - if DEBUG: - print('SERVER: WAS Figure init ... ') - fh = WasFigureHandler(was=was, selected_date=selected_date) - if DEBUG: - print('SERVER: WAS Figure generation ... ') - return fh.retrieve_var_tstep(day=day) - if DEBUG: - print('SERVER: NO WAS Figure') - return WasFigureHandler().retrieve_var_tstep() - - -def get_vis_figure(tstep=0, selected_date=END_DATE): - """ Retrieve figure """ - if DEBUG: - print(tstep, selected_date) - try: - selected_date = dt.strptime( - selected_date, "%Y-%m-%d").strftime("%Y%m%d") - except: - pass - if DEBUG: - print(tstep, selected_date) - if tstep is not None: - if DEBUG: - print('SERVER: VIS Figure init ... ') - fh = VisFigureHandler(selected_date=selected_date) - if DEBUG: - print('SERVER: VIS Figure generation ... ') - return fh.retrieve_var_tstep(tstep=tstep) - if DEBUG: - print('SERVER: NO VIS Figure') - return VisFigureHandler().retrieve_var_tstep() - - -def get_models_figure(model=None, var=None, selected_date=END_DATE, tstep=0, - hour=None, static=True, aspect=(1, 1), center=None, - view='carto-positron', zoom=None, layer=None, tag='empty'): - """ Retrieve figure """ - if DEBUG: - print("***", model, var, selected_date, tstep, hour, "***") - try: - selected_date = dt.strptime( - selected_date, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d") - except: - pass - - if model in MODELS: - model_start = MODELS[model]['start'] - model_start_before = ('start_before' in MODELS[model]) and MODELS[model]['start_before'] or False - current_time_before = (DELAY_DATE and dt.strptime(selected_date, "%Y%m%d") < dt.strptime(DELAY_DATE, "%Y%m%d")) - if DEBUG: - print(f""" -************************ SERVER: Figure init ********************* -* DELAY: {DELAY}, DELAY_DATE: {DELAY_DATE}, SELECTED DATE: {selected_date}, CURR_BEFORE: {current_time_before} * -* MODEL: {model}, MODEL_START: {model_start}, MODEL_START_DELAYED: {model_start_before} * -****************************************************************** - """) - # If current date is later than the delay date and the model_start is 12 - # or current date is before than the delay date and the model_start - selected_date, tstep, _ = get_currdate_tstep(model_start, model_start_before, current_time_before, DELAY, selected_date, tstep) - - if DEBUG: - print('***** SERVER: Figure generation: CURR_DATE', selected_date, 'TSTEP', tstep, '*****') - # return True - fh = FigureHandler(model, selected_date) - return fh.retrieve_var_tstep(var, tstep, hour, static, aspect, center, - view, zoom, layer, tag) - - if DEBUG: - print('SERVER: No Figure') - return FigureHandler().retrieve_var_tstep(layer=layer, center=center, selected_tiles=view, zoom=zoom, tag=tag) diff --git a/utils.py b/utils.py index cbacd77709386e6d5bb76a0d2c92a539eebb106b..164e433afc699be7122066d9f41e8c365e87456b 100644 --- a/utils.py +++ b/utils.py @@ -2,252 +2,78 @@ # -*- coding: utf-8 -*- """ Utils module with utility functions """ -import math -import os.path -from datetime import datetime -from datetime import timedelta -import matplotlib as mpl -from matplotlib import cm -import xarray as xr -import numpy as np import pandas as pd -import feather - - -def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None): - """ Concatenate monthly dataframes """ - - # build feather files paths - opaths = [fname_tpl.format(month, variable) - for month in months if os.path.exists(fname_tpl.format(month, variable))] - - print("__________", opaths, "__________") - if not opaths: - return None, None - - # read monthly dataframes and concatenate into one - if rename_from: - mon_dfs = pd.concat([feather.read_dataframe(opath) - .rename(columns={rename_from: variable}) - for opath in opaths]) - # in case of models we don't rename the variable column - else: - mon_dfs = pd.concat([feather.read_dataframe(opath) - for opath in opaths]) - - # 1d observations - if 'station' in mon_dfs.columns: - if notnans is None: - notnans = [st for st in mon_dfs['station'].unique() - if not mon_dfs[mon_dfs['station']==st][variable].isnull().all()] - mon_dfs_filter = mon_dfs['station'] - final_df = mon_dfs[mon_dfs_filter.isin(notnans)] - # 2d observations - else: - final_df = mon_dfs - - return notnans, final_df - - -def retrieve_single_point(fname, tstep, lat, lon, variable): - """ """ - from data_handler import DEBUG - if DEBUG: print(fname, tstep, lat, lon, variable) - ds = xr.open_dataset(fname) - if variable not in ds.variables: - variable = variable.lower() - # print('TIMESERIES', fname, variable, lon, lat) - if 'lat' in ds.variables: - da = ds[variable].sel(lon=lon, lat=lat, method='nearest') - else: - da = ds[variable].sel(longitude=lon, latitude=lat, method='nearest') - if DEBUG: print(da) - return da.values[tstep] - - -def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=False): - """ """ - if method == 'feather' and not forecast: - df = feather.read_dataframe(fname) - if 'lat' in df.columns: - lat_col = 'lat' - lon_col = 'lon' - else: - lat_col = 'latitude' - lon_col = 'longitude' - - if variable not in df.columns: - variable = variable.lower() - - if variable not in df.columns: - return None, None, None, None - - n_lon = find_nearest(df[lon_col].values, lon) - n_lat = find_nearest(df[lat_col].values, lat) - ts = df[(df[lat_col] == n_lat) & (df[lon_col] == n_lon)][['time', - variable]].set_index('time') - - return n_lat, n_lon, ts.index, ts[variable] - - def preprocess(ds, n=8): - return ds.isel(time=range(n)) - - if forecast: - ds = xr.open_dataset(fname) - else: - ds = xr.open_mfdataset(fname, concat_dim='time', combine='nested', - preprocess=preprocess) - if variable not in ds.variables: - variable = variable.lower() - # print('TIMESERIES', fname, variable, lon, lat) - if 'lat' in ds.variables: - da = ds[variable].sel(lon=lon, lat=lat, method='nearest') - clat = 'lat' - clon = 'lon' - else: - da = ds[variable].sel(longitude=lon, latitude=lat, method='nearest') - clat = 'latitude' - clon = 'longitude' - return da[clat].values, da[clon].values, da.indexes['time'], da - - -def find_nearest(array, value): - """ Find the nearest value of a couple of coordinates """ - return array[np.abs(array-value).argmin()] - - -def find_nearest2(array, value): - """ Find the nearest value of a couple of coordinates """ - idx = np.searchsorted(array, value, side="left") - if idx > 0 and (idx == len(array) or math.fabs(value - array[idx-1]) < - math.fabs(value - array[idx])): - return array[idx-1] - - return array[idx] - - -def calc_matrix(n): - """ Calculate the mosaic optimum matrix shape """ - sqrt_n = math.sqrt(n) - ncols = sqrt_n == int(sqrt_n) and int(sqrt_n) or int(sqrt_n) + 1 - nrows = n%ncols > 0 and int(n/ncols)+1 or int(n/ncols) - return ncols, nrows - - -def magnitude(num): - """ Calculate magnitude """ - if num == 0: - num = 1 - return int(math.floor(math.log10(num))) - - -def normalize_vals(vals, valsmin, valsmax, rnd=2): - """ Normalize values to 0-1 scale """ - if len(vals) == 1: - return np.array([0]) - vals = np.array(vals) - if rnd < 2: - rnd = 2 - return np.around((vals-valsmin)/(valsmax-valsmin), rnd) - - -def get_colorscale(bounds, colormap, discrete=True): - """ Create colorscale """ - if isinstance(colormap, str): - colormap = cm.get_cmap(colormap) - - bounds = np.array(bounds).astype('float32') - magn = magnitude(bounds[-1]) - n_bounds = normalize_vals(bounds, bounds[0], bounds[-1], magn) - norm = mpl.colors.BoundaryNorm(bounds, colormap.N, clip=True) - s_map = cm.ScalarMappable(norm=norm, cmap=colormap) - - norm_val = not (list(bounds) == list(n_bounds)) - - colorscale = [[idx, - 'rgba' + str(s_map.to_rgba(val, - alpha=True, - bytes=True, - norm=norm_val))] - for idx, val in zip(n_bounds, bounds)] - - if discrete is True: - for item in colorscale.copy(): - if colorscale.index(item) < len(colorscale)-2: - colorscale.insert(colorscale.index(item)+1, - [colorscale[colorscale.index(item)+1][0], - colorscale[colorscale.index(item)][1]]) - elif len(colorscale) == 1: - colorscale.insert(1, [1, colorscale[0][1]]) - - return colorscale - - -def get_vis_edate(end_date, hour=None): - """ Return default date and timestep for visibility. """ - from data_handler import DEBUG - delay = timedelta(hours=8) - half_day = timedelta(hours=12) - fmt_full = "%Y%m%d %H:%M" - fmt_date = "%Y%m%d" - hours = (0, 6, 12, 18) - - now = datetime.now() - now_hour = now.hour - if hour is not None: - now_hour = hour - edate = datetime.strptime(end_date, fmt_date) + timedelta(hours=now_hour) - cdate = edate.strftime(fmt_date) - - curr = None - for idx, h in enumerate(hours[:-1]): - curr = datetime.strptime("{} {:02d}:00".format(cdate, h), fmt_full) - curr1 = datetime.strptime("{} {:02d}:00".format(cdate, hours[idx+1]), fmt_full) - if edate < curr + delay: - curr = curr - half_day - curr1 = curr1 - half_day - - if (edate >= curr + delay) and (edate < curr1 + delay): - if DEBUG: print("NOW", edate, "CURR", curr, "H", curr.hour) - break - - if curr is not None: - return curr.strftime(fmt_date), curr.hour - - return cdate, edate.hour - - -def get_currdate_tstep(model_start, model_start_before, current_time_before, delay, selected_date, tstep=4): - """ Returns date and timestep """ - - # MODELS starting at 00 with 3 days of forecast - print("Starting at 0 and NOT DELAYED: 3 days forecast!") - cdo_tsteps = "1/25" - delayed = (current_time_before and not delay) or (not current_time_before and delay) - - # MODELS not starting at 00 - if (model_start == 12 and not model_start_before) or \ - (current_time_before and model_start_before == 12): - if delayed: - print("Starting at 12 and DELAYED: 2 days forecast!") - # DELAYED (2 days): models that starts at 12h considering end_date = current_date - 1 - if tstep < 4: - selected_date = (datetime.strptime(selected_date, "%Y%m%d") - - timedelta(days=1)).strftime("%Y%m%d") - tstep = int(tstep) + 4 - else: - tstep = int(tstep) - 4 - cdo_tsteps = "1/21" - else: - print("Starting at 12 and NOT DELAYED: 3 days forecast!") - # NOT DELAYED (3 days): models that starts at 12h considering end_date = current_date - selected_date = (datetime.strptime(selected_date, "%Y%m%d") - - timedelta(days=1)).strftime("%Y%m%d") - tstep = int(tstep) + 4 - cdo_tsteps = "5/29" - # MODELS starting at 00 with 2 days of forecast - elif delayed: - print("Starting at 0 and DELAYED: 2 days forecast!") - cdo_tsteps = "5/25" - - return selected_date, tstep, cdo_tsteps + + +def format_floats(dataframe): + """ Change all columns in a dataframe except for 'station', + so that floats will be formatted to 2 digits after the decimal place + + Parameters + ---------- + dataframe : pandas.core.frame.DataFrame + Dataframe + + Returns + ------- + pandas.core.frame.DataFrame + Dataframe with formatted columns + """ + + for col in dataframe.columns: + # check if the column is not 'station' + if col != 'station': + # convert the column to a string to allow for string formatting + dataframe[col] = dataframe[col].astype(str) + # iterate over the values in the column + for i, val in enumerate(dataframe[col]): + # check if the value is a float + if '.' in val: + # if so, format it to have 2 decimal places + dataframe.at[i, col] = '{:.2f}'.format(float(val)) + + return dataframe + + +def alphabetize_stations(dataframe): + """ Alphabetize the stations for each region in the AERONET statistics table + + Parameters + ---------- + dataframe : pandas.core.frame.DataFrame + Dataframe + + Returns + ------- + pandas.core.frame.DataFrame + Dataframe with sorted columns + """ + + # Define a list of regions to sort between (in the desired order) + regions = ['Europe', 'Mediterranean', 'MiddleEast', 'NAfrica', 'Total'] + region_dfs = [] + + # Iterate over each pair of consecutive regions and sort the rows between them + for i in range(len(regions)-1): + # Get the indices of the current and next regions + current_region_idx = dataframe[dataframe['station'] == regions[i]].index[0] + next_region_idx = dataframe[dataframe['station'] == regions[i+1]].index[0] + # Slice the dataframe to select the rows between the current and next regions + subset_df = dataframe.loc[current_region_idx+1:next_region_idx-1] + # Sort the rows alphabetically based on the 'station' column + subset_df = subset_df.sort_values(by='station') + # Create a new dataframe containing only the current region row + current_region_row = dataframe.loc[current_region_idx].to_frame().T + # Append the current region row and sorted subset dataframe to the list of region DataFrames + region_dfs.append(current_region_row) + region_dfs.append(subset_df) + + # Add the 'Total' row back to the end of the sorted dataframe + total_row_idx = dataframe[dataframe['station'] == 'Total'].index[0] + total_row = dataframe.loc[total_row_idx].to_frame().T + region_dfs.append(total_row) + + # Concatenate all of the region DataFrames into a single sorted DataFrame + sorted_df = pd.concat(region_dfs) + + return sorted_df