From 1dfc6b248649621f118cb5f619f3c56057592e1e Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Fri, 2 Jun 2023 16:44:19 +0200 Subject: [PATCH 01/71] Add more testing coverage and remove unnecessary fullscreen tab code from router.py --- router.py | 3 - tests/test_evaluation_callbacks.py | 98 +++++++++++++++++ tests/test_forecast_callbacks.py | 157 ++++++++++++++++++++++++--- tests/test_observations_callbacks.py | 118 +++++++++++++++++--- tests/test_router.py | 7 ++ 5 files changed, 350 insertions(+), 33 deletions(-) diff --git a/router.py b/router.py index 013c57a..0c49a02 100644 --- a/router.py +++ b/router.py @@ -73,10 +73,7 @@ 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(): diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py index 2d0353b..1ffd4eb 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 data_handler import START_DATE, END_DATE #add equality checker for objects def __eq__(self, other): @@ -141,3 +142,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': 'None', 'tag': 'modis-map'} + assert output[0].children[0].id == {'tag': 'modis-tile-layer', 'index': 'None'} + 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': 'median', 'tag': 'model-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_callbacks.py b/tests/test_forecast_callbacks.py index bd60063..0aacc94 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -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,6 +136,8 @@ 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'})" + + assert "([{'label': 'MULTI-MODEL', 'value': 'median', 'disabled': True}, {'label': 'MONARCH', 'value': 'monarch', 'disabled': False}, {'label': 'CAMS-IFS', 'value': 'cams', 'disabled': True}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc', 'disabled': True}, {'label': 'NASA-GEOS', 'value': 'nasa-geos', 'disabled': True}, {'label': 'MetOffice-UM', 'value': 'metoffice', 'disabled': True}, {'label': 'NCEP-GEFS', 'value': 'ncep-gefs', 'disabled': True}, {'label': 'EMA-RegCM4', 'value': 'ema-regcm4', 'disabled': True}, {'label': 'SILAM', 'value': 'silam', 'disabled': True}, {'label': 'LOTOS-EUROS', 'value': 'lotos-euros', 'disabled': True}, {'label': 'ICON-ART', 'value': 'icon-art', 'disabled': True}, {'label': 'NOA-WRF-CHEM', 'value': 'noa', 'disabled': True}, {'label': 'WRF-NEMO', 'value': 'wrf-nemo', 'disabled': True}, {'label': 'ALADIN', 'value': 'aladin', 'disabled': True}, {'label': 'ZAMG-WRF-CHEM', 'value': 'zamg', 'disabled': True}, {'label': 'MOCAGE', 'value': 'mocage', 'disabled': True}], ['monarch'], {'display': 'block'})" in str(code.update_models_dropdown('DUST_DEPW', ['cams', 'silam'])) # =======================END update_models_dropdown test =========================== @@ -142,7 +186,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) @@ -162,24 +206,51 @@ 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]]) +# ORJSON ERROR # def test_zooms(): # 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) +def test_update_was_styles_button_carto(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"carto-positron","tag":"view-style"}.n_clicks'},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) + return code.update_was_styles_button.uncached([1, None, 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}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'], ["© OpenStreetMap " "contributors © CARTO"]) + +def test_update_was_styles_button_open_street(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"open-street-map","tag":"view-style"}.n_clicks'},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) + return code.update_was_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']) + +def test_update_was_styles_button_esri(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"esri-world","tag":"view-style"}.n_clicks'},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) + return code.update_was_styles_button.uncached([None, None, None, 1], ['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://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], ['Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, ' 'Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community']) + +def test_update_was_styles_button_terrain(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"stamen-terrain","tag":"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 == (['https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png'], ["Map tiles by Stamen Design, CC BY 3.0 — Map " 'data © OpenStreetMap " 'contributors']) # ORJSON ERROR # def test_models_popup(): @@ -223,6 +294,62 @@ def test_update_slider(): assert code.update_slider.uncached(72) == 0 # =======================END update_slider tests=========================== +# =======================START UPDATE MODEL FIGURE TESTS=========================== +def test_update_model_figure(): + def run_callback(): + 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 "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 ", 'fontSize': '14px', 'fontWeight': 'bold', 'width': '305px'}), None], id={'tag': 'model-map', 'index': 'monarch', 'n_clicks': 1}, animate=False, center=[43.93333333333334, 19.450000000000003], inertia=True, minZoom=2, preferCanvas=True, style={'height': '90vh'}, wheelDebounceTime=80, wheelPxPerZoomLevel=120, zoom=2.9, zoomSnap=0.1), width=12)], align='start', no_gutters=True)]" in str(output) + +# =======================START UPDATE PROB FIGURE TESTS=========================== +def test_update_prob_figure(): + 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, 'prob_0.2', None, [True], [], []) + + ctx = copy_context() + output = ctx.run(run_callback) + 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] == None + assert output.children[3] == None + assert output.children[4] == None + assert output.children[5].url == '/dashboard/assets/geojsons/prob/od550_dust/0.2/geojson/20230404/01_20230404_OD550_DUST.geojson' + assert output.id == {'index': 'None', 'tag': 'prob-map'} + +def test_update_prob_figure_prob(): + 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[5].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(): diff --git a/tests/test_observations_callbacks.py b/tests/test_observations_callbacks.py index 3f7b552..7137b2a 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 data_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,108 @@ 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 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]) + check_date = output[0].children[3].children[0].children[0].children[0].children[0].date + check_tstep = output[0].children[3].children[0].children[0].children[2].children.value + assert check_date == code.END_DATE + assert check_tstep == default_tstep + assert "Tab(children=[Span(children=P('Visibility'), className='description-title'), Span(children=P([B('You can explore key observations that can be used to track dust events. '), 'All observations are kindly offered by Partners of the WMO Barcelona Dust Regional Center. The reduction of VISIBILITY is an indirect measure of the occurrence of sand and dust storms on the surface.'])" in str(output[0]) + assert "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')" 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 34bf611..50aed5c 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -74,3 +74,10 @@ def test_router(): #verify incorrect url lands on default url ="children=[H2(children='404 Error', id='error_title" assert "id='app-tabs', value='forecast-tab')]" in str(code.router(url)) + + #============ TEST render 404 ================================= +def test_render404(): + #verify output of 404 function + assert "Div(children=[Div(children=[H2(children='404 Error', id='error_title')" in str(code.render404()[0]) + assert "P('Here are some helpful 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]) + -- GitLab From d6d9be02b74803e1740dd7f2380fca1183d3a7e5 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 7 Jun 2023 10:22:16 +0200 Subject: [PATCH 02/71] Added tests for eval and forecast callbacks --- tests/test_evaluation_callbacks.py | 19 +++++++++++++++- tests/test_forecast_callbacks.py | 35 +++++++++++++++++++++++++----- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py index 1ffd4eb..8ca8676 100644 --- a/tests/test_evaluation_callbacks.py +++ b/tests/test_evaluation_callbacks.py @@ -36,7 +36,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') @@ -45,7 +45,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': '2018', 'value': '201801-201812'}], 'Select year')" in str(output) #============ TEST modis_scores_tables_retrieve======================== def test_modis_scores_tables_retrieve_no_n_click(): def run_callback(): diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index 0aacc94..82f9652 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 data_handler import START_DATE # =======================START render forecast test =========================== @@ -252,16 +252,39 @@ def test_update_was_styles_button_terrain(): output = ctx.run(run_callback) assert output == (['https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png'], ["Map tiles by Stamen Design, CC BY 3.0 — Map " 'data © OpenStreetMap " 'contributors']) -# ORJSON ERROR -# def test_models_popup(): +# =======================Start models popup tests=========================== + +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 + +# NEED TO DEAL WITH THE SECOND INPUT, WHICH IS EXTREMELY LONG +# def test_models_popup_full_input(): +# # FIRST TEST INSUFFICIENT INPUTS TO RETURN EMPTY VALUES AND NO_UPDATE # 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": '{"tag":"model-map", "index":"median","n_clicks":1}.click_lat_lng'}]})) +# return code.models_popup([[56.739260373724775, 91.93359375]],[{'tag': 'model-map', 'index': 'median', 'n_clicks': 1}], [], '20230404', 0, 'OD550_DUST', None, {}) # # ctx = copy_context() # output = ctx.run(run_callback) -# assert output == (True, False) +# =======================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":'{"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 == None # =======================Start start_stop_autoslider tests=========================== def test_start_stop_autoslider_pause(): -- GitLab From b6df8838253581f802b3aa7a8fcf8836c349abcb Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 7 Jun 2023 12:30:15 +0200 Subject: [PATCH 03/71] Make updates to bootstrap for new version --- assets/sidebar.css | 16 ++++++++-------- assets/style.css | 3 ++- tabs/evaluation_callbacks.py | 4 +++- tabs/forecast.py | 7 ++++--- tabs/forecast_callbacks.py | 4 +++- 5 files changed, 20 insertions(+), 14 deletions(-) diff --git a/assets/sidebar.css b/assets/sidebar.css index 6b51a62..675966f 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; @@ -113,20 +113,20 @@ } /* Custom control: Classes recently added by Francesco */ -.custom-control { - padding-left: 0; - padding-right: 0; -} +/* .form-check { */ +/* padding-left: 0; */ +/* padding-right: 0; */ +/* } */ -.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; diff --git a/assets/style.css b/assets/style.css index d48b54d..d61f957 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; } @@ -605,7 +606,7 @@ div.SingleDatePickerInput { height: 100%; } -.btn-secondary { +.btn-primary { box-shadow: none !important; border: 0 !important; } diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 7a5b116..09142c2 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -874,7 +874,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 2b8e60a..9e8b816 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -247,7 +247,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( @@ -448,7 +447,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), @@ -539,7 +538,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([ diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 78e5c12..0cc2024 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -909,7 +909,9 @@ 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)) -- GitLab From c1f3c8aedd3439e0a0a644bcee05ef5ca304db6c Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 7 Jun 2023 16:58:19 +0200 Subject: [PATCH 04/71] Changes to sidebar and timebar --- assets/sidebar.css | 37 +++++++++++++++++++++++-------------- assets/style.css | 14 ++++++++++---- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/assets/sidebar.css b/assets/sidebar.css index 675966f..57f30ed 100644 --- a/assets/sidebar.css +++ b/assets/sidebar.css @@ -113,10 +113,13 @@ } /* Custom control: Classes recently added by Francesco */ -/* .form-check { */ -/* padding-left: 0; */ -/* padding-right: 0; */ -/* } */ +.form-check { + display: flex; + flex-direction: row-reverse; + justify-content: space-between; + padding-left: 0; + padding-right: 0.2rem; +} .form-check-label { /* margin-left: 1rem; */ @@ -157,11 +160,16 @@ } .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; } /*.accordion>.card>.card-header:after { @@ -421,7 +429,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 +440,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/style.css b/assets/style.css index d61f957..a19fb77 100644 --- a/assets/style.css +++ b/assets/style.css @@ -505,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; } @@ -523,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 { -- GitLab From 55c224c92892502544ddf99c8ef3b04a47e9a9af Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 7 Jun 2023 17:00:41 +0200 Subject: [PATCH 05/71] Update requirements.txt --- requirements.txt | 208 +++++++++++++++++------------------------------ 1 file changed, 74 insertions(+), 134 deletions(-) diff --git a/requirements.txt b/requirements.txt index 3c34c70..c12b001 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,149 +1,89 @@ -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 +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 +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 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 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 +MarkupSafe==2.1.3 +matplotlib==3.7.1 +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 +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 +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 +python-dateutil==2.8.2 +pytz==2023.3 +pyudev==0.21.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 +tenacity==8.2.2 +threadpoolctl==3.1.0 +traitlets==5.9.0 +tzdata==2023.3 +urllib3==1.24.2 +wcwidth==0.2.6 +Werkzeug==2.2.3 +xarray==2023.5.0 +zope.event==4.6 +zope.interface==6.0 -- GitLab From 98c3306d75556030dbe3271def408e214e5b8178 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 7 Jun 2023 17:41:53 +0200 Subject: [PATCH 06/71] Add tabulate to requirements and fix minor bugs --- data_handler.py | 16 +++++----------- requirements.txt | 1 + tabs/evaluation_callbacks.py | 2 +- 3 files changed, 7 insertions(+), 12 deletions(-) diff --git a/data_handler.py b/data_handler.py index 0586241..02af0b5 100644 --- a/data_handler.py +++ b/data_handler.py @@ -148,10 +148,7 @@ class Observations1dHandler(object): 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') + sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[obs]['sites'])) idxs, self.station_names = np.array([[idx, st_name[~st_name.mask].tobytes().decode('utf-8')] for idx, st_name in @@ -183,7 +180,7 @@ class Observations1dHandler(object): 'lat': clat.round(2), 'stations': cstations }) # .T, columns=['lon', 'lat', 'station']) - dicts = df.to_dict('rows') + dicts = df.to_dict('records') geojson = dlx.dicts_to_geojson(dicts, lon="lon") # Geojson rendering logic, must be JavaScript as it is executed in clientside. @@ -810,7 +807,7 @@ class FigureHandler(object): 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') + dicts = df.to_dict('records') for item in dicts: item["tooltip"] = \ "Lat {:.2f} Lon {:.2f} Val {:.2f}".format(item['lat'], item['lon'], item['value']) @@ -1060,10 +1057,7 @@ class ScoresFigureHandler(object): 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.sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[network]['sites'])) self.size = 15 else: self.sites = None @@ -1325,7 +1319,7 @@ class VisFigureHandler(object): 'humidity': humidity, 'value': res }) - dicts = df.to_dict('rows') + dicts = df.to_dict('records') for item in dicts: if DEBUG: print("-------------", item, "-----------------") diff --git a/requirements.txt b/requirements.txt index c12b001..3ab9cf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -77,6 +77,7 @@ 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 traitlets==5.9.0 diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 09142c2..f92034d 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -390,7 +390,7 @@ 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: -- GitLab From 560044f62c4757192fe4a6dd1c72c0388507ad7c Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 8 Jun 2023 11:59:57 +0200 Subject: [PATCH 07/71] Update sidebar cards with appropriate borders. --- assets/sidebar.css | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/assets/sidebar.css b/assets/sidebar.css index 57f30ed..2097d12 100644 --- a/assets/sidebar.css +++ b/assets/sidebar.css @@ -172,6 +172,11 @@ text-decoration: underline; } +.card { + border-top: none; + border-bottom: none; +} + /*.accordion>.card>.card-header:after { display: inline-block; font-variant: normal; @@ -345,7 +350,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 { -- GitLab From 94846d7057620ca50049410a022e121c23c101a3 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 8 Jun 2023 14:40:30 +0200 Subject: [PATCH 08/71] Fixed PNG issues: colorbar hidden behind info bar, attribution data appearing in corner of screen, and formatting width offset --- assets/download-img.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/assets/download-img.js b/assets/download-img.js index 503e4b1..cca2022 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(); + //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, @@ -82,6 +84,28 @@ function addLogos() { } function removeLogos() { + //This function removes the added logos after screenshot is taken var logos = document.getElementById('logos'); logos.remove(); }; + +function makeChanges() { + //colorbar needs pushed down for picture + 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.remove(); +} + +function removeChanges() { + //put colorbar back in place + const colorbar = document.querySelector('.leaflet-control-colorbar'); + colorbar.style.paddingTop = '0px'; + //remove added logos + removeLogos(); +} + + -- GitLab From d9c64cf804b367a181859f45cb8c5ed28a822b9a Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 8 Jun 2023 15:34:01 +0200 Subject: [PATCH 09/71] Remove line between Variable and options --- assets/sidebar.css | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/sidebar.css b/assets/sidebar.css index 2097d12..706dbe3 100644 --- a/assets/sidebar.css +++ b/assets/sidebar.css @@ -37,6 +37,7 @@ padding-top: 0; border-radius: 0px; border: none; + box-shadow: none !important; } #app-sidebar>label { -- GitLab From 9bc1ba5a2271627d55d97af4830b3812acf2952c Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Fri, 9 Jun 2023 11:01:06 +0200 Subject: [PATCH 10/71] Improve popup content on maps --- data_handler.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/data_handler.py b/data_handler.py index 02af0b5..d87e11b 100644 --- a/data_handler.py +++ b/data_handler.py @@ -869,8 +869,8 @@ class FigureHandler(object): lat=ylat, text=val, name=name, - hovertemplate="lon: %{lon:.2f}
lat: %{lat:.2f}
" + - "value: %{text:.2f}", + hovertemplate="Lon: %{lon:.2f}
Lat: %{lat:.2f}
" + + "Value: %{text:.2f}", opacity=OPACITY, showlegend=False, marker=dict( @@ -1113,10 +1113,10 @@ class ScoresFigureHandler(object): 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}" + hovertemplate="Lon: %{lon:.2f}
Lat: %{lat:.2f}
%{meta}: %{text}" else: - hovertemplate="lon: %{lon:.2f}
lat: %{lat:.2f}
value: %{text}
station: %{customdata}" - name = '{} score'.format(STATS_CONF[self.stat]['name']) + hovertemplate="%{customdata}
Lon: %{lon:.2f}
Lat: %{lat:.2f}
%{meta}: %{text}" + name = '{}'.format(STATS_CONF[self.stat]['name']) return dict( type='scattermapbox', lon=xlon, @@ -1124,8 +1124,10 @@ class ScoresFigureHandler(object): text=vals, customdata=stats, name=name, + meta=[name], hovertemplate=hovertemplate, opacity=0.8, + hoverlabel=dict(bgcolor="#2B383E"), mode='markers', showlegend=False, marker=dict( @@ -1326,15 +1328,16 @@ class VisFigureHandler(object): item["tooltip"] = \ """ {station} -
LAT: {lat} LON: {lon} -
VISIBILITY: {visibility:.1f} km +
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 '') + humidity=(item['humidity'] not in (False, '') and "
Relative humidity: {}%".format(int(float(item['humidity']))) or '') ) geojson = dlx.dicts_to_geojson(dicts, lon="lon") -- GitLab From 3816c925e1f7e5f0987fdd530d323cc8e6e995bd Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Fri, 9 Jun 2023 15:17:05 +0200 Subject: [PATCH 11/71] Fix bug with view selectors resetting to the base case when the date is changed. This also fixes the view selectors not highlighting the selected map style. The three separate functions were combined into one. --- tabs/forecast_callbacks.py | 103 +++++++++++-------------------------- 1 file changed, 30 insertions(+), 73 deletions(-) diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 0cc2024..a5a3145 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -373,91 +373,47 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): 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 - - if DEBUG: print('NOTHING TO DO') - 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)) @@ -473,7 +429,8 @@ def update_models_styles_button(*args): res[st_idx] = True if DEBUG: print('*****', url, attr, res) - return 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') raise PreventUpdate -- GitLab From 36fd79a9fff1fef30d7cbd441465281612627613 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Mon, 12 Jun 2023 12:36:57 +0200 Subject: [PATCH 12/71] Update tests for update_styles_button and zooms --- tabs/forecast_callbacks.py | 1 + tests/test_forecast_callbacks.py | 57 +++++++++++++++++++++----------- 2 files changed, 38 insertions(+), 20 deletions(-) diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index a5a3145..85eaba5 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -467,6 +467,7 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups if popups is None: popups = {} + import pdb; pdb.set_trace() trigger = orjson.loads(ctxt) if DEBUG: print('TRIGGER', trigger, type(trigger)) diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index bd60063..bb209b8 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -156,32 +156,49 @@ 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(): -# 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}"}]})) -# 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) +# =======================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_update_styles_button_from_open_streets(): + def run_callback(): + context_value.set(AttributeDict(**{"triggered_inputs": [{'prop_id': '{"index":"esri-world","tag":"view-style"}.n_clicks', 'value': 1}]})) + return code.update_styles_button.uncached([None, 1, None, 1], ['https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'], [], [], [False, True, False, False]) + + ctx = copy_context() + output = ctx.run(run_callback) + assert output == (['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], ['Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'], [], [], [], [], [False, False, False, True]) -# ORJSON ERROR +# ORJSON ERROR and INPUT IS EXTREMELY LONG # def test_models_popup(): # 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"}]})) -- GitLab From 5b1f0ba44f50a84b375d6eed1f1eee12eef1fb46 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Mon, 12 Jun 2023 13:17:43 +0200 Subject: [PATCH 13/71] Change background color in statistics map --- data_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data_handler.py b/data_handler.py index d87e11b..d489087 100644 --- a/data_handler.py +++ b/data_handler.py @@ -1127,7 +1127,7 @@ class ScoresFigureHandler(object): meta=[name], hovertemplate=hovertemplate, opacity=0.8, - hoverlabel=dict(bgcolor="#2B383E"), + hoverlabel={"bgcolor": "#fff", "bordercolor": "#e2e2e1", "font": {"color": "#000"}}, mode='markers', showlegend=False, marker=dict( -- GitLab From a668992b00aea7b0fe5db2e6040d1c5c241753a8 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Mon, 12 Jun 2023 14:53:00 +0200 Subject: [PATCH 14/71] Add more coverage and delete unused interp.py. Rename test_interp.py to test_nc2timeseries.py --- preproc/interp.py | 94 ------------------- tests/test_data_handler.py | 14 +-- .../{test_interp.py => test_nc2timeseries.py} | 23 +++-- 3 files changed, 22 insertions(+), 109 deletions(-) delete mode 100644 preproc/interp.py rename tests/{test_interp.py => test_nc2timeseries.py} (69%) diff --git a/preproc/interp.py b/preproc/interp.py deleted file mode 100644 index 7a211fa..0000000 --- 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/tests/test_data_handler.py b/tests/test_data_handler.py index 2e20ba3..23a9d20 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -39,13 +39,13 @@ def test_generate_obs1d_tstep_trace1(aeronet_instance): 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_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) diff --git a/tests/test_interp.py b/tests/test_nc2timeseries.py similarity index 69% rename from tests/test_interp.py rename to tests/test_nc2timeseries.py index 15c7ad2..c33af38 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') + + +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] + -- GitLab From b6c541c267645dfa4cd502169bdb167db49cc0d7 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 14 Jun 2023 10:47:52 +0200 Subject: [PATCH 15/71] Refactor router and cache --- conf/cache.json | 4 ++++ conf/route.json | 10 ++++++++++ data_handler.py | 38 +++++++++++--------------------------- 3 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 conf/cache.json create mode 100644 conf/route.json diff --git a/conf/cache.json b/conf/cache.json new file mode 100644 index 0000000..8b37fe6 --- /dev/null +++ b/conf/cache.json @@ -0,0 +1,4 @@ +{ + "DEBUG": true, + "CACHE_TYPE": "FileSystemCache" +} \ No newline at end of file diff --git a/conf/route.json b/conf/route.json new file mode 100644 index 0000000..ca270a9 --- /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/data_handler.py b/data_handler.py index 0586241..cdf0e84 100644 --- a/data_handler.py +++ b/data_handler.py @@ -32,27 +32,20 @@ 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, -} +DIR_PATH = os.path.dirname(os.path.realpath(__file__)) +DEBUG = True # False +# Setup cache +cache_config = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) +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 = 86400 -#SETUP BASE URL +# 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'))) @@ -85,20 +78,11 @@ 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] - } +ROUTE_DEFAULTS = json.load(open(os.path.join(DIR_PATH, 'conf/route.json'))) +ROUTE_DEFAULTS.update({'date': [END_DATE]}) STATS = OrderedDict([(key, STATS_CONF[key]['name']) for key in STATS_CONF]) -STATS.update({ 'totn': 'TOTAL CASES' }) +STATS.update({'totn': 'TOTAL CASES'}) GRAPH_HEIGHT = 90 -- GitLab From c6b08469dc3598504b9a41424a495c0f1af0f02a Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 14 Jun 2023 11:27:40 +0200 Subject: [PATCH 16/71] Refactor disclaimers --- assets/style.css | 2 +- conf/disclaimers.json | 4 ++++ data_handler.py | 11 ++++------- tabs/evaluation.py | 6 +++--- tabs/evaluation_callbacks.py | 9 ++++----- tabs/forecast.py | 8 ++++---- tabs/observations.py | 4 ++-- tests/test_forecast.py | 4 ++-- tests/test_observations.py | 2 +- tests/test_observations_callbacks.py | 2 +- 10 files changed, 26 insertions(+), 26 deletions(-) create mode 100644 conf/disclaimers.json diff --git a/assets/style.css b/assets/style.css index a19fb77..4e5b95a 100644 --- a/assets/style.css +++ b/assets/style.css @@ -155,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; } diff --git a/conf/disclaimers.json b/conf/disclaimers.json new file mode 100644 index 0000000..11fbcf8 --- /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/data_handler.py b/data_handler.py index 42339b5..c116900 100644 --- a/data_handler.py +++ b/data_handler.py @@ -35,14 +35,14 @@ import socket DIR_PATH = os.path.dirname(os.path.realpath(__file__)) DEBUG = True # False -# Setup cache +# Set up cache cache_config = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) 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 = 86400 -# Setup base url +# Set up base url HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0] HOSTNAMES = json.load(open(os.path.join(DIR_PATH, 'conf/hostnames.json'))) @@ -93,11 +93,8 @@ 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.""") +# Set up disclaimers +DISCLAIMERS = json.load(open(os.path.join(DIR_PATH, 'conf/disclaimers.json'))) GEOJSON_TEMPLATE = "{}/geojson/{}/{:02d}_{}_{}.geojson" NETCDF_TEMPLATE = "{}/netcdf/{}{}.nc" diff --git a/tabs/evaluation.py b/tabs/evaluation.py index 602b0bf..b94bbfe 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -12,7 +12,7 @@ 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 data_handler import DISCLAIMERS from datetime import datetime as dt from datetime import timedelta @@ -66,8 +66,8 @@ scores_maps = dbc.Spinner( figure={}, config=MODEBAR_CONFIG, # {"displayModeBar": False} ), - html.Div(DISCLAIMER_NO_FORECAST, - className='disclaimer') + html.Div([html.Span(html.P(DISCLAIMERS['models']), + id='models-disclaimer')], className='disclaimer') ] )], id='scores-map-modal', diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index f92034d..9fec00d 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -16,8 +16,7 @@ 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 DISCLAIMERS from data_handler import cache, cache_timeout from tabs.evaluation import tab_evaluation @@ -824,7 +823,7 @@ def update_eval(obs): id='graph-eval-aeronet' ), html.Div( - DISCLAIMER_OBS, + html.P(DISCLAIMERS['observations']), id='eval-aeronet-disclaimer', className='disclaimer' ), @@ -854,7 +853,7 @@ def update_eval(obs): fig_obs, id='graph-eval-modis-obs', ), - html.Div(DISCLAIMER_OBS, + html.Div(html.P(DISCLAIMERS['observations']), className='disclaimer') ], ) @@ -863,7 +862,7 @@ def update_eval(obs): fig_mod, id='graph-eval-modis-mod', ), - html.Div(DISCLAIMER_NO_FORECAST, + html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], className='disclaimer', id='eval-vis-modis-disclaimer' ) diff --git a/tabs/forecast.py b/tabs/forecast.py index 9e8b816..d112883 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -13,7 +13,7 @@ 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 +from data_handler import DISCLAIMERS def get_forecast_days(curdate=END_DATE): @@ -255,7 +255,7 @@ def models_children(start_date=START_DATE, end_date=END_DATE): ], fluid=True, ), - html.Div(DISCLAIMER_MODELS, + html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], className='disclaimer'), ] # )], @@ -321,7 +321,7 @@ def prob_children(start_date=START_DATE, end_date=END_DATE): id='prob-graph', className='graph-with-slider'), alert_3day_update(), - html.Div(DISCLAIMER_MODELS, + html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], className='disclaimer'), dbc.NavbarSimple([ html.Div([ @@ -359,7 +359,7 @@ def was_children(start_date=START_DATE, end_date=END_DATE): className='graph-with-slider'), ), alert_3day_update(), - html.Div(DISCLAIMER_MODELS, + html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], className='disclaimer'), dbc.NavbarSimple([ html.Div([ diff --git a/tabs/observations.py b/tabs/observations.py index dc2d69a..0a53142 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -9,7 +9,7 @@ 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 data_handler import DISCLAIMERS # from tabs.forecast import layout_view from utils import get_vis_edate @@ -246,7 +246,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([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], className='disclaimer'), ], className="layout-dropdown rgb-layout-dropdown", diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 3c7e26a..1f9645a 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -23,7 +23,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 +33,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')) diff --git a/tests/test_observations.py b/tests/test_observations.py index 9c65668..bb58ac3 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -60,4 +60,4 @@ def test_tab_observations(): 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)) + 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)) diff --git a/tests/test_observations_callbacks.py b/tests/test_observations_callbacks.py index 3f7b552..f45041e 100644 --- a/tests/test_observations_callbacks.py +++ b/tests/test_observations_callbacks.py @@ -39,7 +39,7 @@ def test_render_observations_tab_visibility(): _, default_tstep = get_vis_edate(END_DATE, 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): -- GitLab From 7bbde32d7a4e5c2dd7923c580d8aca3dec920fa9 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 14 Jun 2023 12:14:43 +0200 Subject: [PATCH 17/71] Update disclaimers and refactor init conditions --- conf/dash_style.json | 4 ++ conf/init.json | 6 +++ conf/{styles.json => map_layers.json} | 0 data_handler.py | 72 +++++++++++++++------------ tabs/evaluation.py | 5 +- tabs/evaluation_callbacks.py | 9 ++-- tabs/forecast.py | 11 ++-- tabs/observations.py | 4 +- 8 files changed, 66 insertions(+), 45 deletions(-) create mode 100644 conf/dash_style.json create mode 100644 conf/init.json rename conf/{styles.json => map_layers.json} (100%) diff --git a/conf/dash_style.json b/conf/dash_style.json new file mode 100644 index 0000000..c3bf702 --- /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/init.json b/conf/init.json new file mode 100644 index 0000000..747a638 --- /dev/null +++ b/conf/init.json @@ -0,0 +1,6 @@ +{ + "frequency": 3, + "forecast_max": 72, + "default_variable": "OD550_DUST", + "default_model": "median" +} \ No newline at end of file diff --git a/conf/styles.json b/conf/map_layers.json similarity index 100% rename from conf/styles.json rename to conf/map_layers.json diff --git a/data_handler.py b/data_handler.py index c116900..ce3b3ab 100644 --- a/data_handler.py +++ b/data_handler.py @@ -46,6 +46,9 @@ cache_timeout = 86400 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'))) @@ -54,23 +57,16 @@ 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'))) +STYLES = json.load(open(os.path.join(DIR_PATH, 'conf/map_layers.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'))) -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) - +# Set up initial conditions +FREQ = INIT['frequency'] +FORECAST_MAX = INIT['forecast_max'] +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'] @@ -78,34 +74,48 @@ 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'}) -GRAPH_HEIGHT = 90 - -# Frequency = 3 Hourly -FREQ = 3 -OPACITY = 0.7 - -DEFAULT_VAR = 'OD550_DUST' -DEFAULT_MODEL = 'median' +# Set up timeseries configuration +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'] # Set up disclaimers -DISCLAIMERS = json.load(open(os.path.join(DIR_PATH, 'conf/disclaimers.json'))) +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 Observations1dHandler(object): +class Observations1dHandler: """ Class which handles 1D obs data """ def __init__(self, sdate, edate, obs): @@ -182,7 +192,7 @@ class Observations1dHandler(object): ) -class ObsTimeSeriesHandler(object): +class ObsTimeSeriesHandler: """ Class to handle evaluation time series """ def __init__(self, obs, start_date, end_date, variable, models=None): @@ -336,7 +346,7 @@ class ObsTimeSeriesHandler(object): return fig -class TimeSeriesHandler(object): +class TimeSeriesHandler: """ Class to handle forecast time series """ def __init__(self, model, date, variable): @@ -518,7 +528,7 @@ class TimeSeriesHandler(object): return fig -class FigureHandler(object): +class FigureHandler: """ Class to manage the figure creation """ def __init__(self, model=None, selected_date=None): @@ -1032,7 +1042,7 @@ class FigureHandler(object): return fig -class ScoresFigureHandler(object): +class ScoresFigureHandler: """ Class to manage the figure creation """ def __init__(self, network, statistic, selection=None): @@ -1192,7 +1202,7 @@ class ScoresFigureHandler(object): return self.fig -class VisFigureHandler(object): +class VisFigureHandler: """ Class to manage the figure creation """ def __init__(self, selected_date=None): @@ -1374,7 +1384,7 @@ class VisFigureHandler(object): return None -class ProbFigureHandler(object): +class ProbFigureHandler: """ Class to manage the figure creation """ def __init__(self, var=None, prob=None, selected_date=None): @@ -1573,7 +1583,7 @@ class ProbFigureHandler(object): return geojson, colorbar, info -class WasFigureHandler(object): +class WasFigureHandler: """ Class to manage the figure creation """ def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=None): diff --git a/tabs/evaluation.py b/tabs/evaluation.py index b94bbfe..d9314bc 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -12,7 +12,7 @@ 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 DISCLAIMERS +from data_handler import DISCLAIMER_MODELS from datetime import datetime as dt from datetime import timedelta @@ -66,8 +66,7 @@ scores_maps = dbc.Spinner( figure={}, config=MODEBAR_CONFIG, # {"displayModeBar": False} ), - html.Div([html.Span(html.P(DISCLAIMERS['models']), - id='models-disclaimer')], className='disclaimer') + html.Div(DISCLAIMER_MODELS, className='disclaimer') ] )], id='scores-map-modal', diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 9fec00d..7dfdf5c 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -16,7 +16,8 @@ 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 DISCLAIMERS +from data_handler import DISCLAIMER_MODELS +from data_handler import DISCLAIMER_OBS from data_handler import cache, cache_timeout from tabs.evaluation import tab_evaluation @@ -823,7 +824,7 @@ def update_eval(obs): id='graph-eval-aeronet' ), html.Div( - html.P(DISCLAIMERS['observations']), + DISCLAIMER_OBS, id='eval-aeronet-disclaimer', className='disclaimer' ), @@ -853,7 +854,7 @@ def update_eval(obs): fig_obs, id='graph-eval-modis-obs', ), - html.Div(html.P(DISCLAIMERS['observations']), + html.Div(DISCLAIMER_OBS, className='disclaimer') ], ) @@ -862,7 +863,7 @@ def update_eval(obs): fig_mod, id='graph-eval-modis-mod', ), - html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], + html.Div(DISCLAIMER_MODELS, className='disclaimer', id='eval-vis-modis-disclaimer' ) diff --git a/tabs/forecast.py b/tabs/forecast.py index d112883..38d691d 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -8,12 +8,13 @@ import dash_bootstrap_components as dbc from dash import dcc from dash import html +from data_handler import FORECAST_MAX 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 DISCLAIMERS +from data_handler import DISCLAIMER_MODELS def get_forecast_days(curdate=END_DATE): @@ -61,7 +62,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 @@ -255,7 +256,7 @@ def models_children(start_date=START_DATE, end_date=END_DATE): ], fluid=True, ), - html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], + html.Div(DISCLAIMER_MODELS, className='disclaimer'), ] # )], @@ -321,7 +322,7 @@ def prob_children(start_date=START_DATE, end_date=END_DATE): id='prob-graph', className='graph-with-slider'), alert_3day_update(), - html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], + html.Div(DISCLAIMER_MODELS, className='disclaimer'), dbc.NavbarSimple([ html.Div([ @@ -359,7 +360,7 @@ def was_children(start_date=START_DATE, end_date=END_DATE): className='graph-with-slider'), ), alert_3day_update(), - html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], + html.Div(DISCLAIMER_MODELS, className='disclaimer'), dbc.NavbarSimple([ html.Div([ diff --git a/tabs/observations.py b/tabs/observations.py index 0a53142..c5ac711 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -9,7 +9,7 @@ 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 DISCLAIMERS +from data_handler import DISCLAIMER_MODELS # from tabs.forecast import layout_view from utils import get_vis_edate @@ -246,7 +246,7 @@ def tab_observations(window='rgb', start_date=START_DATE, end_date=END_DATE): #layout_view, html.Br(), html.Br(), - html.Div([html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')], + html.Div(DISCLAIMER_MODELS, className='disclaimer'), ], className="layout-dropdown rgb-layout-dropdown", -- GitLab From 8b21ea7d2cdada33fe539d38235c1cf4d8f33484 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 15 Jun 2023 12:14:34 +0200 Subject: [PATCH 18/71] Refactor forecast.py and move general functions into generic and generic_callbacks. Update tests --- data_handler.py | 4 ++ generic.py | 94 ++++++++++++++++++++++++++++++++ generic_callbacks.py | 46 ++++++++++++++++ tabs/forecast.py | 90 +++--------------------------- tests/test_forecast.py | 9 --- tests/test_forecast_callbacks.py | 16 ------ tests/test_generic.py | 20 +++++++ tests/test_generic_callbacks.py | 17 ++++++ 8 files changed, 190 insertions(+), 106 deletions(-) create mode 100644 generic.py create mode 100644 generic_callbacks.py create mode 100644 tests/test_generic.py create mode 100644 tests/test_generic_callbacks.py diff --git a/data_handler.py b/data_handler.py index d489087..da52db1 100644 --- a/data_handler.py +++ b/data_handler.py @@ -64,6 +64,9 @@ 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'))) +TAB_1 = json.load(open(os.path.join(DIR_PATH, 'conf/tab_1.json'))) +TAB_2 = json.load(open(os.path.join(DIR_PATH, 'conf/tab_2.json'))) +TAB_3 = json.load(open(os.path.join(DIR_PATH, 'conf/tab_3.json'))) MODEBAR_CONFIG = MODEBARS['config'] MODEBAR_CONFIG_TS = MODEBARS['config_ts'] @@ -84,6 +87,7 @@ 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")) +FORECAST_FINAL_DAY = 3 ROUTE_DEFAULTS = { 'tab':['forecast'], diff --git a/generic.py b/generic.py new file mode 100644 index 0000000..5ad6d68 --- /dev/null +++ b/generic.py @@ -0,0 +1,94 @@ +import dash_bootstrap_components as dbc +from dash import html +from data_handler import STYLES +from data_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( + 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'}, + ) + +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/generic_callbacks.py b/generic_callbacks.py new file mode 100644 index 0000000..9580c9e --- /dev/null +++ b/generic_callbacks.py @@ -0,0 +1,46 @@ +import dash +from dash.dependencies import Output +from dash.dependencies import Input + +@dash.callback( + Output('caret1', 'style'), + [Input('collapse-1', 'is_open')], +) +def rotate_section_1_caret(collapse_open): + """ Rotates section 1 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_section_2_caret(collapse_open): + """ Rotates section 2 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_section_3_caret(collapse_open): + """ Rotates section 3 menu caret """ + rotate_caret = { + 'transform': 'rotate(0deg)', + '-ms-transform': 'rotate(0deg)', + '-webkit-transform': 'rotate(0deg)' + } + if collapse_open: + return rotate_caret diff --git a/tabs/forecast.py b/tabs/forecast.py index 9e8b816..0e34cde 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -14,27 +14,10 @@ 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 +from generic import get_forecast_days, layout_view, time_series - -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) - ]) +# MOVED TO GENERIC +# def get_forecast_days(curdate=END_DATE): def gen_ts_marks(ts_type, curdate=END_DATE): """ Generate time slider marks """ @@ -161,69 +144,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 """ diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 3c7e26a..8a79eff 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -3,15 +3,6 @@ from data_handler import VARS from data_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')" diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index bb209b8..121aa81 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -94,22 +94,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(): diff --git a/tests/test_generic.py b/tests/test_generic.py new file mode 100644 index 0000000..bf4e850 --- /dev/null +++ b/tests/test_generic.py @@ -0,0 +1,20 @@ +import pytest +import importlib +code = importlib.import_module('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 0000000..d816795 --- /dev/null +++ b/tests/test_generic_callbacks.py @@ -0,0 +1,17 @@ +import pytest +import importlib +code = importlib.import_module('generic_callbacks') + +# =======================START CARET TESTS =========================== +def test_rotate_section_1_caret(): + assert code.rotate_section_1_caret(True) == None + assert code.rotate_section_1_caret(False) == {'top': '.05rem', 'transform': 'rotate(180deg)', '-ms-transform': 'rotate(180deg)', '-webkit-transform': 'rotate(180deg)'} + +def test_rotate_section_2_caret(): + assert code.rotate_section_2_caret(False) == None + assert code.rotate_section_2_caret(True) == {'transform': 'rotate(0deg)', '-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)'} + +def test_rotate_section_3_caret(): + assert code.rotate_section_3_caret(False) == None + assert code.rotate_section_3_caret(True) == {'transform': 'rotate(0deg)', '-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)'} +# =======================END CARET TESTS =========================== -- GitLab From 39996b3599467e19dcf918a013d5d062e88ee3b3 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 15 Jun 2023 15:08:33 +0200 Subject: [PATCH 19/71] Merge changes from forecast.py refactoring. Tests updated but there is still and issue with xarray and dash. --- data_handler.py | 3 -- preproc/interp.py | 94 -------------------------------------- tests/test_data_handler.py | 6 +-- tests/test_forecast.py | 6 +-- tests/test_interp.py | 29 ------------ tests/test_tools.py | 2 +- 6 files changed, 7 insertions(+), 133 deletions(-) delete mode 100644 preproc/interp.py delete mode 100644 tests/test_interp.py diff --git a/data_handler.py b/data_handler.py index ac66eda..1af2618 100644 --- a/data_handler.py +++ b/data_handler.py @@ -57,9 +57,6 @@ 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'))) -TAB_1 = json.load(open(os.path.join(DIR_PATH, 'conf/tab_1.json'))) -TAB_2 = json.load(open(os.path.join(DIR_PATH, 'conf/tab_2.json'))) -TAB_3 = json.load(open(os.path.join(DIR_PATH, 'conf/tab_3.json'))) MODEBAR_CONFIG = MODEBARS['config'] MODEBAR_CONFIG_TS = MODEBARS['config_ts'] diff --git a/preproc/interp.py b/preproc/interp.py deleted file mode 100644 index 7a211fa..0000000 --- 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/tests/test_data_handler.py b/tests/test_data_handler.py index 2e20ba3..a633c4f 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -57,7 +57,7 @@ def test_retrieve_timeseries_1(TSHandler): 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)' + assert run.layout.title.text =='Dust Optical Depth @ lat = 35 and lon = 15' # =================== FIGURE HANDLER ============================ @pytest.fixture @@ -135,13 +135,13 @@ def test_get_mapbox(ScoresFigureHandler): 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)['name'] == 'MBE' 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].name == 'MBE' assert ScoresFigureHandler.retrieve_scores('median', aspect=(1,1), center=None).data[1].type == 'scattermapbox' # =================== Vis Handler ============================ diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 8a79eff..f60df17 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -56,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='caret1', 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'}, {'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', 'value': 'silam'}, {'label': 'LOTOS-EUROS', 'value': 'lotos-euros'}, {'label': 'ICON-ART', 'value': 'icon-art'}, {'label': 'NOA-WRF-CHEM', 'value': 'noa'}, {'label': 'WRF-NEMO', 'value': 'wrf-nemo'}, {'label': 'ALADIN', 'value': 'aladin'}, {'label': 'ZAMG-WRF-CHEM', 'value': 'zamg'}, {'label': 'MOCAGE', 'value': 'mocage'}], 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='caret1', 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'}, {'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')) #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='caret1', 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'}, {'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='was', country='chad')) diff --git a/tests/test_interp.py b/tests/test_interp.py deleted file mode 100644 index 15c7ad2..0000000 --- a/tests/test_interp.py +++ /dev/null @@ -1,29 +0,0 @@ -import pytest -import importlib -import xarray as xr -import dask -import numpy as np -code = importlib.import_module('preproc.interp') - -# 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( - np.random.randn(10, 3, 3), - dims=("time", "lat", "lon"), - coords={"time": range(10), "lat": [10, 20, 30], "lon": [-120, -110, -100]}, - ) - ds = xr.Dataset({"var": data}) - - # Test that preprocess function keeps only first 5 timesteps - 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_tools.py b/tests/test_tools.py index 6c2a78e..dbc24e3 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -35,7 +35,7 @@ def test_get_timeseries(): 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' + assert str(run1.data[1].name) == 'MBE' run2 = code.get_scores_figure('modis', 'median', 'bias', EDATE_OBJ.strftime("%Y%m")) assert str(run2.data[0]) == 'Scattermapbox()' -- GitLab From 3e5c89a0010fabed49ab3aca05b142e107099065 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 15 Jun 2023 15:39:35 +0200 Subject: [PATCH 20/71] Refactor get title and dask to requirements --- conf/prob.json | 4 +- conf/vis.json | 13 ++ data_handler.py | 285 ++++++++++++++++++++----------------- requirements.txt | 14 +- tests/test_data_handler.py | 22 +-- 5 files changed, 193 insertions(+), 145 deletions(-) create mode 100644 conf/vis.json diff --git a/conf/prob.json b/conf/prob.json index 81c5687..9f22304 100644 --- a/conf/prob.json +++ b/conf/prob.json @@ -6,7 +6,7 @@ "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}" + "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"], @@ -15,6 +15,6 @@ "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}" + "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/vis.json b/conf/vis.json new file mode 100644 index 0000000..cff4235 --- /dev/null +++ b/conf/vis.json @@ -0,0 +1,13 @@ +{ + "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, + "colors": ["#714921", "#da7230", "#fcd775", "CadetBlue"], + "labels": ["< 1 km", "1 - 2 km", "2 - 5 km", "Haze < 5 km"], + "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" +} \ No newline at end of file diff --git a/data_handler.py b/data_handler.py index ce3b3ab..260140f 100644 --- a/data_handler.py +++ b/data_handler.py @@ -54,6 +54,7 @@ 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'))) 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'))) @@ -114,6 +115,62 @@ if HOSTNAME in HOSTNAMES: else: PATHNAME = HOSTNAMES['default'] +class MapHandler: + + def __init__(self): + + # TODO: Add common variables from all figure handlers here + + return + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + # 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 """ + + tstep = int(tstep) + time_attr = int(self.timesteps[tstep]) + + 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 class Observations1dHandler: """ Class which handles 1D obs data """ @@ -528,12 +585,14 @@ class TimeSeriesHandler: return fig -class FigureHandler: +class FigureHandler(MapHandler): """ Class to manage the figure creation """ def __init__(self, model=None, selected_date=None): """ FigureHandler init """ + super(FigureHandler, self).__init__() + self.st_time = time.time() if isinstance(model, list): model = model[0] @@ -574,7 +633,7 @@ class FigureHandler: self.bounds = None self.varlist = None self.rdatetime = None - self.tim = [0] + self.timesteps = [0] elif filepath is not None: self.input_file = nc_file(filepath) if 'lon' in self.input_file.variables: @@ -584,7 +643,7 @@ class FigureHandler: lon = self.input_file.variables['longitude'][:] lat = self.input_file.variables['latitude'][:] time_obj = self.input_file.variables['time'] - self.tim = time_obj[:] + self.timesteps = time_obj[:] tim_units = time_obj.units.split() if len(tim_units) == 3: self.what, _, rdate = tim_units @@ -665,21 +724,6 @@ class FigureHandler: # 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 @@ -877,33 +921,28 @@ class FigureHandler: ), ) - def get_title(self, varname, tstep=0): - """ return title according to the date """ + def get_title(self, **kwargs): + """ Return title according to the date """ + + varname = kwargs['varname'] + tstep = kwargs['tstep'] + if self.model in OBS: - name = OBS[self.model]['name'] - title = OBS[self.model]['title'] + self.base_title = " ".join([OBS[self.model]['name'], 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), - }) + 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(FigureHandler, self).get_title() + + return title 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)): + for step in range(len(self.timesteps)): if self.retrieve_cdatetime(step) == cdatetime: return step @@ -969,7 +1008,8 @@ class FigureHandler: else: fig_title = html.P(html.B( [ - item for sublist in self.get_title(varname, tstep).split('
') for item in [sublist, html.Br()] + item for sublist in self.get_title(varname=varname, tstep=tstep).split('
') + for item in [sublist, html.Br()] ][:-1] )) if self.model is not None: @@ -1202,22 +1242,23 @@ class ScoresFigureHandler: return self.fig -class VisFigureHandler: +class VisFigureHandler(MapHandler): """ 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', '^') + + super(VisFigureHandler, self).__init__() + + 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.colors = VIS['colors'] + self.labels = VIS['labels'] + self.values = VIS['values'] + self.markers = VIS['markers'] if selected_date: self.selected_date_plain = selected_date @@ -1228,6 +1269,8 @@ class VisFigureHandler: self.selected_date_plain = None self.selected_date = None + self.rdatetime = datetime.strptime(self.selected_date_plain, '%Y%m%d') + def set_data(self, tstep=0): """ Set time dependent data """ tstep0 = tstep @@ -1350,9 +1393,14 @@ class VisFigureHandler: ) if list(visibility): - cur_title = self.get_title(tstep) + cur_title = html.P(html.B( + [ + item for sublist in self.get_title(tstep=tstep).split('
') + for item in [sublist, html.Br()] + ][:-1] + )) else: - cur_title = self.get_title() + cur_title = "NO DATA AVAILABLE" info = html.Div( children=cur_title, @@ -1362,15 +1410,20 @@ class VisFigureHandler: ) return df, [geojson, info, legend] - def get_title(self, tstep=None): - """ return title according to the date """ + def get_title(self, **kwargs): + """ Return title according to the date """ + + tstep = None if 'tstep' not in kwargs else kwargs['tstep'] 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] + 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 retrieve_var_tstep(self, tstep=0, hour=None, static=True, aspect=(1,1), center=None): """ run plot """ @@ -1384,12 +1437,14 @@ class VisFigureHandler: return None -class ProbFigureHandler: +class ProbFigureHandler(MapHandler): """ Class to manage the figure creation """ def __init__(self, var=None, prob=None, selected_date=None): """ Initialization with variable, prob and date """ + super(ProbFigureHandler, self).__init__() + if var is None: var = DEFAULT_VAR @@ -1421,7 +1476,7 @@ class ProbFigureHandler: lon = self.input_file.variables['longitude'][:] lat = self.input_file.variables['latitude'][:] time_obj = self.input_file.variables['time'] - self.tim = time_obj[:] + self.timesteps = time_obj[:] tim_units = time_obj.units.split() if len(tim_units) == 3: self.what, _, rdate = tim_units @@ -1454,21 +1509,6 @@ class ProbFigureHandler: 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 @@ -1516,31 +1556,27 @@ class ProbFigureHandler: 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) + def get_title(self, **kwargs): + """ Return title according to the date """ + + varname = kwargs['varname'] + tstep = kwargs['tstep'] - mod_avail = 0 + self.members = 0 for model in MODELS: if model == 'median': continue - path_template = '{}{}.nc'.format(rdatetime.strftime("%Y%m%d"), MODELS[model]['template']) + 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): - 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"), - ) + self.members += 1 + + self.base_title = PROB[varname]['title'] + self.cdatetime = self.rdatetime + relativedelta(days=tstep) + + title = super(ProbFigureHandler, self).get_title() + + return title def retrieve_var_tstep(self, varname=None, day=0, static=True, aspect=(1,1)): """ run plot """ @@ -1570,7 +1606,8 @@ class ProbFigureHandler: 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()] + item for sublist in self.get_title(varname=varname, tstep=tstep).split('
') + for item in [sublist, html.Br()] ][:-1] )) @@ -1583,11 +1620,14 @@ class ProbFigureHandler: return geojson, colorbar, info -class WasFigureHandler: +class WasFigureHandler(MapHandler): """ 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 """ + + super(WasFigureHandler, self).__init__() + self.model = model self.was = was self.variable = variable @@ -1604,7 +1644,7 @@ class WasFigureHandler: if os.path.exists(filepath): self.input_file = nc_file(filepath) time_obj = self.input_file.variables['time'] - self.tim = time_obj[:] + self.timesteps = time_obj[:] tim_units = time_obj.units.split() if len(tim_units) == 3: self.what, _, rdate = tim_units @@ -1624,6 +1664,9 @@ class WasFigureHandler: self.selected_date = datetime.strptime( selected_date, "%Y%m%d").strftime("%Y-%m-%d") + # Correct timesteps to show first timestep of each day ([0, 24, 48, ...]) + self.timesteps = self.timesteps[::8] + self.fig = None def get_regions_data(self, day=0): @@ -1660,20 +1703,6 @@ class WasFigureHandler: 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 """ @@ -1732,21 +1761,15 @@ class WasFigureHandler: 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 get_title(self, **kwargs): + """ Return title according to the date """ + + tstep = kwargs['day'] + self.base_title = WAS[self.was]['title'] + self.cdatetime = self.retrieve_cdatetime(tstep) + title = super(WasFigureHandler, self).get_title() + + return title def retrieve_var_tstep(self, day=0, static=True, aspect=(1,1)): """ run plot """ @@ -1759,7 +1782,7 @@ class WasFigureHandler: fig_title = html.P(html.B( [ item - for sublist in self.get_title(day).split('
') + for sublist in self.get_title(day=day).split('
') for item in [sublist, html.Br()] ][:-1] )) diff --git a/requirements.txt b/requirements.txt index 3ab9cf5..fa44ac9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ chardet==3.0.4 click==8.1.3 click-plugins==1.1.1 cligj==0.7.2 +cloudpickle==2.2.1 configobj==5.0.6 contourpy==1.0.7 cycler==0.10.0 @@ -22,6 +23,7 @@ dash-html-components==2.0.0 dash-leaflet==0.1.23 dash-renderer==1.9.1 dash-table==5.0.0 +dask==2023.6.0 decorator==4.2.1 EditorConfig==0.12.3 feather-format==0.4.1 @@ -29,12 +31,15 @@ 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.13.2 gevent==22.10.2 greenlet==2.0.2 gunicorn==20.1.0 idna==2.5 +importlib-metadata==6.6.0 +iniconfig==2.0.0 ipython==7.16.1 ipython-genutils==0.1.0 itsdangerous==2.1.2 @@ -43,6 +48,7 @@ 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.1 more-itertools==9.1.0 @@ -54,10 +60,12 @@ 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==9.5.0 plotly==5.14.1 +pluggy==1.0.0 ply==3.9 prompt-toolkit==2.0.10 protobuf==3.19.4 @@ -68,9 +76,11 @@ 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 @@ -80,11 +90,13 @@ 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 +zope.interface==6.0 \ No newline at end of file diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index 2e20ba3..d3a0ce3 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -106,8 +106,8 @@ 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)) + assert FigureHandler.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)) + assert FigureHandler.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_step(FigureHandler): assert FigureHandler.hour_to_step(0) == 0 @@ -160,14 +160,14 @@ def test_generate_var_tstep_trace(VisFigureHandler): 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][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)) + 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)) def test_vis_retrieve_var_tstep(VisFigureHandler): result = VisFigureHandler.retrieve_var_tstep(tstep=0, hour=None, static=True, aspect=(1,1), center=None) @@ -215,8 +215,8 @@ def test_prob_generate_contour_tstep_trace_SCONC(ProbFigureHandler): 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)) + assert ProbFigureHandler.get_title(varname='OD550_DUST', tstep=0)[-30:] == '{edate} Valid: {edate}'.format(edate=EDATE_OBJ.strftime(FMT_MON)) + assert ProbFigureHandler.get_title(varname='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) @@ -275,8 +275,8 @@ def test_was_generate_contour_tstep_trace(WasFigureHandler): 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)) + assert str(WasFigureHandler.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(WasFigureHandler.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)) 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) -- GitLab From 1027ddbc7dd7e5c85c96287839eed41ac49adc2f Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 15 Jun 2023 16:24:05 +0200 Subject: [PATCH 21/71] Add forecast final day to json --- conf/init.json | 1 + data_handler.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/conf/init.json b/conf/init.json index 747a638..ec57457 100644 --- a/conf/init.json +++ b/conf/init.json @@ -1,6 +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/data_handler.py b/data_handler.py index db1fe9b..05f0719 100644 --- a/data_handler.py +++ b/data_handler.py @@ -66,6 +66,7 @@ DISCLAIMERS = json.load(open(os.path.join(DIR_PATH, 'conf/disclaimers.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'] @@ -74,7 +75,6 @@ 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")) -FORECAST_FINAL_DAY = 3 # Set up dashboard basic properties GRAPH_HEIGHT = DASH_STYLE['graph_height'] -- GitLab From 80ad0597350756743d12df03ae4495d8ad32a01e Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Tue, 20 Jun 2023 14:50:25 +0200 Subject: [PATCH 22/71] Refactor caret flip menus into one function. Move paths to conf files. Update tests. --- assets/sidebar.css | 4 ++ conf/satellite_image_src.json | 4 ++ dash_server.py | 3 +- data_handler.py | 1 + generic_callbacks.py | 46 -------------- tabs/forecast.py | 16 ++--- tabs/forecast_callbacks.py | 69 +++----------------- generic.py => tabs/generic.py | 0 tabs/generic_callbacks.py | 108 ++++++++++++++++++++++++++++++++ tabs/observations_callbacks.py | 5 +- tests/test_forecast.py | 6 +- tests/test_generic.py | 2 +- tests/test_generic_callbacks.py | 15 ++--- tests/test_tools.py | 13 ++-- 14 files changed, 156 insertions(+), 136 deletions(-) create mode 100644 conf/satellite_image_src.json delete mode 100644 generic_callbacks.py rename generic.py => tabs/generic.py (100%) create mode 100644 tabs/generic_callbacks.py diff --git a/assets/sidebar.css b/assets/sidebar.css index 706dbe3..cb1319d 100644 --- a/assets/sidebar.css +++ b/assets/sidebar.css @@ -379,6 +379,10 @@ span>#was-apply { -webkit-transform: rotate(180deg); } +#group-1-toggle span { + top: .15rem; +} + .Select-arrow-zone { bottom: .3em; right: .5em; diff --git a/conf/satellite_image_src.json b/conf/satellite_image_src.json new file mode 100644 index 0000000..28d3523 --- /dev/null +++ b/conf/satellite_image_src.json @@ -0,0 +1,4 @@ +{ + "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" +} diff --git a/dash_server.py b/dash_server.py index 33f7b3b..4a29155 100755 --- a/dash_server.py +++ b/dash_server.py @@ -96,6 +96,7 @@ if DEBUG: print('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 index 05f0719..61b3a92 100644 --- a/data_handler.py +++ b/data_handler.py @@ -62,6 +62,7 @@ STYLES = json.load(open(os.path.join(DIR_PATH, 'conf/map_layers.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'))) # Set up initial conditions FREQ = INIT['frequency'] diff --git a/generic_callbacks.py b/generic_callbacks.py deleted file mode 100644 index 9580c9e..0000000 --- a/generic_callbacks.py +++ /dev/null @@ -1,46 +0,0 @@ -import dash -from dash.dependencies import Output -from dash.dependencies import Input - -@dash.callback( - Output('caret1', 'style'), - [Input('collapse-1', 'is_open')], -) -def rotate_section_1_caret(collapse_open): - """ Rotates section 1 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_section_2_caret(collapse_open): - """ Rotates section 2 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_section_3_caret(collapse_open): - """ Rotates section 3 menu caret """ - rotate_caret = { - 'transform': 'rotate(0deg)', - '-ms-transform': 'rotate(0deg)', - '-webkit-transform': 'rotate(0deg)' - } - if collapse_open: - return rotate_caret diff --git a/tabs/forecast.py b/tabs/forecast.py index bbaf53c..809bc56 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -15,7 +15,7 @@ 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 -from generic import get_forecast_days, layout_view, time_series +from tabs.generic import get_forecast_days, layout_view, time_series # MOVED TO GENERIC # def get_forecast_days(curdate=END_DATE): @@ -361,12 +361,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([ @@ -398,12 +398,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([ @@ -426,12 +426,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([ diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 85eaba5..c28f8e0 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -38,9 +38,9 @@ from 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 +48,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, @@ -138,48 +138,10 @@ def update_models_dropdown(variable, checked): 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'), @@ -467,7 +429,6 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups if popups is None: popups = {} - import pdb; pdb.set_trace() trigger = orjson.loads(ctxt) if DEBUG: print('TRIGGER', trigger, type(trigger)) @@ -672,7 +633,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'), @@ -745,15 +705,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'), diff --git a/generic.py b/tabs/generic.py similarity index 100% rename from generic.py rename to tabs/generic.py diff --git a/tabs/generic_callbacks.py b/tabs/generic_callbacks.py new file mode 100644 index 0000000..e5475d0 --- /dev/null +++ b/tabs/generic_callbacks.py @@ -0,0 +1,108 @@ +import dash +from dash.dependencies import ALL, MATCH +from dash.exceptions import PreventUpdate +from dash.dependencies import Input, State, Output +from data_handler import DEBUG, FREQ +from data_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 + + +# @dash.callback( +# [Output('slider-interval', 'disabled'), +# Output('slider-interval', 'n_intervals'), +# Output('open-timeseries', 'style'), +# Output('btn-play', 'className'), +# ], +# Input('btn-play', 'n_clicks'), +# [State('slider-interval', 'disabled'), +# State('model-slider-graph', 'value')], +# prevent_initial_call=True +# ) +# def start_stop_autoslider(n_play, 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-play' and disabled: +# ts_style = { 'display': 'none' } +# return not disabled, int(value/FREQ), ts_style, 'fa fa-pause text-center' +# elif button_id == 'btn-play' and not disabled: +# ts_style = { 'display': 'block' } +# return not disabled, int(value/FREQ), ts_style, 'fa fa-play text-center' +# +# raise PreventUpdate +# start/stop animation + +# @dash.callback( +# [Output('obs-slider-interval', 'disabled'), +# Output('obs-slider-interval', 'n_intervals'), +# Output('btn-obs-play', 'className')], +# Input('btn-obs-play', 'n_clicks'), +# [State('obs-slider-interval', 'disabled'), +# State('obs-slider-graph', 'value')], +# prevent_initial_call=True +# ) +# @cache.memoize(timeout=cache_timeout) +# def start_stop_obs_autoslider(n_play, 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-play' and disabled: +# return not disabled, int(value), 'fa fa-pause text-center' +# elif button_id == 'btn-obs-play' and not disabled: +# return not disabled, int(value), 'fa fa-play text-center' +# +# raise PreventUpdate + +# @dash.callback( +# [Output({'type':'slider-interval', 'id': MATCH}, 'disabled'), +# Output({'type':'slider-interval', 'id': MATCH}, 'n_intervals'), +# Output('open-timeseries', 'style'), +# Output({ 'tag': 'btn-play', 'id': MATCH}, 'className'), +# ], +# [Input({ 'tag': 'btn-play', 'id': MATCH}, 'n_clicks')], +# [State({'type':'slider-interval', 'id': MATCH}, 'disabled'), +# State({ 'tag': 'time-step-slider-graphs', 'index': MATCH}, 'value')], +# prevent_initial_call=True +# ) +# def start_stop_autoslider(n_play, disabled, value): +# """ Play/Pause map animation """ +# ctx = dash.callback_context +# if DEBUG: print("VALUE", value) +# if not value: +# value = 0 +# +# if ctx.triggered: +# import pdb; pdb.set_trace() +# import json +# button_id = json.loads(ctx.triggered[0]["prop_id"].split(".")[0]) +# # if button_id == 'btn-play' and disabled: +# if button_id['tag'] == 'btn-play' and disabled: +# ts_style = { 'display': 'none' } +# return not disabled, int(value/FREQ), ts_style, ['fa fa-pause text-center'] +# elif button_id['tag'] == 'btn-play' and not disabled: +# ts_style = { 'display': 'block' } +# return not disabled, int(value/FREQ), ts_style, ['fa fa-play text-center'] +# +# raise PreventUpdate diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index a540e32..bb0e66c 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -7,6 +7,7 @@ from dash.dependencies import ALL from dash.exceptions import PreventUpdate from data_handler import DEBUG from data_handler import START_DATE, END_DATE +from data_handler import SATELLITE_IMAGE_SRC from tabs.observations import tab_observations @@ -145,11 +146,11 @@ def update_image_src(btn_fulldisc, btn_middleeast, date, tstep, btn_fulldisc_act if DEBUG: print('BUTTONS', 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 diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 05441d8..6f4cbab 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -56,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', class_name='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', 'value': 'silam'}, {'label': 'LOTOS-EUROS', 'value': 'lotos-euros'}, {'label': 'ICON-ART', 'value': 'icon-art'}, {'label': 'NOA-WRF-CHEM', 'value': 'noa'}, {'label': 'WRF-NEMO', 'value': 'wrf-nemo'}, {'label': 'ALADIN', 'value': 'aladin'}, {'label': 'ZAMG-WRF-CHEM', 'value': 'zamg'}, {'label': 'MOCAGE', 'value': 'mocage'}], value=['median'])" 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', class_name='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', class_name='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='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_generic.py b/tests/test_generic.py index bf4e850..c61db66 100644 --- a/tests/test_generic.py +++ b/tests/test_generic.py @@ -1,6 +1,6 @@ import pytest import importlib -code = importlib.import_module('generic') +code = importlib.import_module('tabs.generic') #=============================== START LAYOUT LAYERS ======================== def test_layout_layers_single_input(): diff --git a/tests/test_generic_callbacks.py b/tests/test_generic_callbacks.py index d816795..9522f45 100644 --- a/tests/test_generic_callbacks.py +++ b/tests/test_generic_callbacks.py @@ -1,17 +1,10 @@ import pytest import importlib -code = importlib.import_module('generic_callbacks') +code = importlib.import_module('tabs.generic_callbacks') # =======================START CARET TESTS =========================== -def test_rotate_section_1_caret(): - assert code.rotate_section_1_caret(True) == None - assert code.rotate_section_1_caret(False) == {'top': '.05rem', 'transform': 'rotate(180deg)', '-ms-transform': 'rotate(180deg)', '-webkit-transform': 'rotate(180deg)'} +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)'} -def test_rotate_section_2_caret(): - assert code.rotate_section_2_caret(False) == None - assert code.rotate_section_2_caret(True) == {'transform': 'rotate(0deg)', '-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)'} - -def test_rotate_section_3_caret(): - assert code.rotate_section_3_caret(False) == None - assert code.rotate_section_3_caret(True) == {'transform': 'rotate(0deg)', '-ms-transform': 'rotate(0deg)', '-webkit-transform': 'rotate(0deg)'} # =======================END CARET TESTS =========================== diff --git a/tests/test_tools.py b/tests/test_tools.py index dbc24e3..78a8edf 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -53,12 +53,13 @@ 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 + # use import pdb; pdb.set_trace() to open interactive debugger to examine what is returned + #import pdb; pdb.set_trace() + code_run = code.get_vis_figure(tstep=0, selected_date='20220808') + assert str(type(code_run[0])) == "" + assert code_run[1][1].children.children.children[2] == '08 Aug 2022 00-06 UTC' + assert code_run[1][1].id == 'vis-info' + assert code_run[1][2].children[0].children[0].className == 'vis-legend-point' def test_get_models_figure(): result = f"/dashboard/assets/geojsons/median/geojson/{END_DATE}/00_{END_DATE}_OD550_DUST.geojson" -- GitLab From 728b824bc1a4bfcd9e1f32686ecc87091c5d8dd0 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 20 Jun 2023 15:39:35 +0200 Subject: [PATCH 23/71] Remove mapbox from data handler and create independent INES handler --- assets/custom-functions.js | 10 +- assets/style.css | 4 + conf/colorbars.json | 24 + conf/obs.json | 46 +- conf/prob.json | 22 +- conf/scores.json | 6 + conf/vis.json | 13 +- conf/was_prod.json | 70 +-- data_handler.py | 798 ++++++++++++--------------------- ines_core_data_handler.py | 170 +++++++ tabs/evaluation.py | 20 +- tabs/evaluation_callbacks.py | 27 +- tabs/forecast_callbacks.py | 1 - tabs/observations_callbacks.py | 5 +- tests/test_data_handler.py | 60 +-- tests/test_tools.py | 13 +- tools.py | 17 +- 17 files changed, 633 insertions(+), 673 deletions(-) create mode 100644 conf/colorbars.json create mode 100644 conf/scores.json create mode 100644 ines_core_data_handler.py diff --git a/assets/custom-functions.js b/assets/custom-functions.js index f7fa9f3..d0be4f0 100644 --- a/assets/custom-functions.js +++ b/assets/custom-functions.js @@ -40,8 +40,14 @@ window.forecastTab = Object.assign({}, window.forecastTab, { window.evaluationTab = Object.assign({}, window.evaluationTab, { evaluationMaps: { - pointToLayer: function(feature, latlng, context){ - const {circleOptions} = context.props.hideout; + 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); }, diff --git a/assets/style.css b/assets/style.css index 4e5b95a..b2396c1 100644 --- a/assets/style.css +++ b/assets/style.css @@ -1031,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/conf/colorbars.json b/conf/colorbars.json new file mode 100644 index 0000000..272b235 --- /dev/null +++ b/conf/colorbars.json @@ -0,0 +1,24 @@ +{ + "fig": {"colorbar": {"position": "topleft", + "width": 270, + "height": 15, + "style": {"top": "55px"} + } + }, + "prob": {"colorbar": {"position": "topleft", + "width": 330, + "height": 8, + "style": {"top": "65px", + "overflow": "hidden", + "white-space": "nowrap"} + } + }, + "scores": {"colorbar": {"position": "topleft", + "width": 330, + "height": 8, + "style": {"top": "65px", + "overflow": "hidden", + "white-space": "nowrap"} + } + } +} \ No newline at end of file diff --git a/conf/obs.json b/conf/obs.json index c96ef81..4e6a3fc 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/prob.json b/conf/prob.json index 9f22304..6e9ffe2 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)sµg/m³
ENS members: %(members)s Run: %(rday)s %(rmonth)s %(ryear)s Valid: %(sday)s %(smonth)s %(syear)s" + "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)s
ENS members: %(members)s Run: %(rday)s %(rmonth)s %(ryear)s Valid: %(sday)s %(smonth)s %(syear)s" - } + "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/scores.json b/conf/scores.json new file mode 100644 index 0000000..b7abb78 --- /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 index cff4235..4ce65db 100644 --- a/conf/vis.json +++ b/conf/vis.json @@ -5,9 +5,16 @@ "ylat": [0, 65], "ec": "none", "size": 80, - "colors": ["#714921", "#da7230", "#fcd775", "CadetBlue"], - "labels": ["< 1 km", "1 - 2 km", "2 - 5 km", "Haze < 5 km"], + "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" + "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 059b9f0..c9c2f94 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], @@ -44,11 +44,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], @@ -81,11 +81,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], @@ -127,11 +127,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], @@ -160,11 +160,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], @@ -191,11 +191,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], @@ -227,11 +227,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], diff --git a/data_handler.py b/data_handler.py index 05f0719..b42ea3d 100644 --- a/data_handler.py +++ b/data_handler.py @@ -7,6 +7,8 @@ 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 @@ -27,6 +29,10 @@ from utils import retrieve_timeseries from utils import retrieve_single_point from utils import get_colorscale +from ines_core_data_handler import MapHandler +from ines_core_data_handler import PointsFigureHandler +from ines_core_data_handler import ContourFigureHandler + from pathlib import Path from flask_caching import Cache import uuid @@ -55,6 +61,7 @@ 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'))) @@ -100,7 +107,6 @@ 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'] # Set up disclaimers DISCLAIMER_MODELS = [html.Span(html.P(DISCLAIMERS['models']), id='models-disclaimer')] @@ -116,67 +122,16 @@ if HOSTNAME in HOSTNAMES: else: PATHNAME = HOSTNAMES['default'] -class MapHandler: - - def __init__(self): - - # TODO: Add common variables from all figure handlers here - - return - - def get_title(self, **kwargs): - """ Return title from base title and elements """ - - # 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 """ - - tstep = int(tstep) - time_attr = int(self.timesteps[tstep]) - 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 - -class Observations1dHandler: +class Observations1dHandler(PointsFigureHandler): """ Class which handles 1D obs data """ def __init__(self, sdate, edate, obs): + + self.name = 'obs' + super(Observations1dHandler, self).__init__() + self.circle_options = OBS[obs]['circle_options'] + 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') @@ -218,36 +173,29 @@ class Observations1dHandler: 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] + + # Create dataframe df = pd.DataFrame({ 'lon': clon.round(2), 'lat': clat.round(2), 'stations': cstations }) # .T, columns=['lon', 'lat', 'station']) dicts = df.to_dict('records') - 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), - ) - ) + # 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") + geojson = self.retrieve_geojson(geojson_data, namespace) + + return df, geojson class ObsTimeSeriesHandler: @@ -586,18 +534,18 @@ class TimeSeriesHandler: return fig -class FigureHandler(MapHandler): +class FigureHandler(ContourFigureHandler): """ Class to manage the figure creation """ def __init__(self, model=None, selected_date=None): """ FigureHandler init """ + self.name = 'fig' super(FigureHandler, self).__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: @@ -753,22 +701,20 @@ class FigureHandler(MapHandler): 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])] + # 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])] + indices = list(range(len(ctg) + 2)) + self.colorbar_info.update({'min': 0, + 'max': len(ctg)+1, + 'classes': indices, + 'colorscale': colorscale, + 'tickValues': indices[1:-1], + 'tickText': ctg}) + colorbar = self.retrieve_colorbar() + 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") @@ -782,148 +728,8 @@ class FigureHandler(MapHandler): 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('records') - 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, **kwargs): - """ Return title according to the date """ + """ Return title from base title and elements """ varname = kwargs['varname'] tstep = kwargs['tstep'] @@ -996,7 +802,7 @@ class FigureHandler(MapHandler): if DEBUG: print('Update layout ...') if not varname: - fig_title=html.P("") + self.fig_title=html.P("") elif varname and not self.filedir: if self.model in MODELS: curr_name = MODELS[self.model]['name'] @@ -1005,28 +811,23 @@ class FigureHandler(MapHandler): else: curr_name = '' - fig_title = html.P(html.B("{} - DATA NOT AVAILABLE".format(curr_name))) + self.fig_title = html.P(html.B("{} - DATA NOT AVAILABLE".format(curr_name))) else: - fig_title = html.P(html.B( + self.fig_title = html.P(html.B( [ item for sublist in self.get_title(varname=varname, tstep=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" + self.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 - ) + self.info_style['fontSize'] = "{}px".format(int(INFO_STYLE['fontSize'][:-2])-aspect[0]+ 0.3) + # Get title information element + info = self.retrieve_info(name=self.model) else: info = None @@ -1083,171 +884,210 @@ class FigureHandler(MapHandler): return fig -class ScoresFigureHandler: +class ScoresFigureHandler(PointsFigureHandler): """ Class to manage the figure creation """ - def __init__(self, network, statistic, selection=None): + def __init__(self, network, statistic, model, selection): + + self.name = 'scores' + super(ScoresFigureHandler, 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}) + + # Read dataframe + self.read_data() + + # 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 = datetime(year, month, 1) - if network == 'aeronet': - self.sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[network]['sites'])) - self.size = 15 else: - self.sites = None - self.size = 7 + self.data = None - network_name = OBS[network]['name'] - filedir = OBS[network]['path'] - filename = "{}_{}.h5".format(selection, statistic) - tab_name = "{}_{}".format(statistic, selection) + def read_data(self): + + # Get path and file + filedir = OBS[self.network]['path'] + filename = "{}_{}.h5".format(self.selection, self.statistic) + tab_name = "{}_{}".format(self.statistic, self.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) + self.data = pd.read_hdf(filepath, tab_name) + + if DEBUG: print('SCORES filepath', filepath, 'SELECTION', self.selection, 'TAB', tab_name) - months = ' - '.join([datetime.strptime(sel, '%Y%m').strftime("%B %Y") for sel in selection.split('-')]) + def select_data(self): - 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 + if self.model in self.data.columns: + + # 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 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 - ) + self.fig_title = "NO DATA AVAILABLE" - if not relayout: - return mapbox_dict + return None - return dict( - args=["mapbox", mapbox_dict], - label=STYLES[style].capitalize(), - method="relayout" - ) + def retrieve_scores(self): + """ Run plot """ - 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}
%{meta}: %{text}" + if DEBUG: print('Update layout ...') + + if self.data is not None: + lon, lat, stations, scores, res = self.select_data() + if DEBUG: print('Adding one point ...') + # 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] + )) + layers = self.generate_var_tstep_trace(lon, lat, stations, scores, res) else: - hovertemplate="%{customdata}
Lon: %{lon:.2f}
Lat: %{lat:.2f}
%{meta}: %{text}" - name = '{}'.format(STATS_CONF[self.stat]['name']) - return dict( - type='scattermapbox', - lon=xlon, - lat=ylat, - text=vals, - customdata=stats, - name=name, - meta=[name], - hovertemplate=hovertemplate, - opacity=0.8, - hoverlabel={"bgcolor": "#fff", "bordercolor": "#e2e2e1", "font": {"color": "#000"}}, - 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, - ) - ), - ) + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + layers = [None, None] - 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' + # Get title information element + info = self.retrieve_info(self.name) - else: - data_available='
NO DATA AVAILABLE' + return layers + [info] - 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) + def generate_var_tstep_trace(self, lon, lat, stations, scores, res): + """ Generate trace to be added to data, per variable and timestep """ - 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}, - ) + # Create dataframe + df = pd.DataFrame({ + 'lon': np.array(lon).round(2), + 'lat': np.array(lat).round(2), + 'score': scores, + 'value': res + }) + if stations is not None: + df['station'] = stations + + # Set up hover information + dicts = df.to_dict('records') + for item in dicts: + if DEBUG: + print("-------------", item, "-----------------") + tooltip = """""" + if stations is not None: + tooltip += """{station}""".format(station=item['station']) + tooltip += """
Lat: {lat} +
Lon: {lon} +
Score: {score:.1f}""".format(lat=item['lat'], lon=item['lon'], + score=float(item['score'])) + tooltip += """
""" + item["tooltip"] = tooltip + + # Create 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}) + colorbar = self.retrieve_colorbar() + + # Create geojson + geojson_data = dlx.dicts_to_geojson(dicts, lon="lon") + namespace = Namespace("evaluationTab", "evaluationMaps") + geojson = self.retrieve_geojson(geojson_data, namespace) + + return [geojson, colorbar] - # if DEBUG: print('Returning fig of size {}'.format(sys.getsizeof(self.fig))) - return self.fig + 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(ScoresFigureHandler, self).get_title() + + return title -class VisFigureHandler(MapHandler): +class VisFigureHandler(PointsFigureHandler): """ Class to manage the figure creation """ def __init__(self, selected_date=None): + self.name = 'vis' super(VisFigureHandler, self).__init__() self.path_tpl = VIS['path'] @@ -1256,10 +1096,11 @@ class VisFigureHandler(MapHandler): self.ec = VIS['ec'] self.size = VIS['size'] self.freq = VIS['freq'] - self.colors = VIS['colors'] - self.labels = VIS['labels'] + self.colormap = VIS['colormap'] + self.colors = list(VIS['colormap'].values()) + self.labels = list(VIS['colormap'].keys()) self.values = VIS['values'] - self.markers = VIS['markers'] + self.circle_options = VIS['circle_options'] if selected_date: self.selected_date_plain = selected_date @@ -1274,6 +1115,7 @@ class VisFigureHandler(MapHandler): def set_data(self, tstep=0): """ Set time dependent data """ + tstep0 = tstep tstep1 = tstep + self.freq @@ -1314,40 +1156,25 @@ class VisFigureHandler(MapHandler): 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): + def generate_var_tstep_trace(self, xlon, ylat, stats, visibility, humidity, values, color, labels, 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" - ) + + # Create legend + legend = self.create_legend(self.colormap) + + # Assign colors to values + n_points = len(xlon) + res = np.zeros((n_points)) + for i, (value, label) in enumerate(zip(values, labels)): + res[value] = i if DEBUG: - print('VIS ___', xlon, ylat, '\n*********', visibility, '\n*********', humidity) + print(self.name, '___', xlon, ylat, '\n*********', visibility, '\n*********', humidity) + + # Create dataframe df = pd.DataFrame({ 'lon': np.array(xlon).round(2), 'lat': np.array(ylat).round(2), @@ -1356,6 +1183,8 @@ class VisFigureHandler(MapHandler): 'humidity': humidity, 'value': res }) + + # Set up hover information dicts = df.to_dict('records') for item in dicts: if DEBUG: @@ -1375,44 +1204,28 @@ class VisFigureHandler(MapHandler): 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), - ) - ) + # Create geojson + geojson_data = dlx.dicts_to_geojson(dicts, lon="lon") + namespace = Namespace("observationsTab", "observationsMaps") + geojson = self.retrieve_geojson(geojson_data, namespace) if list(visibility): - cur_title = html.P(html.B( + 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: - cur_title = "NO DATA AVAILABLE" + self.fig_title = "NO DATA AVAILABLE" - info = html.Div( - children=cur_title, - id="vis-info", - className="info", - style=INFO_STYLE - ) - return df, [geojson, info, legend] + # Get title information element + info = self.retrieve_info(self.name) + + return [geojson, info, legend] def get_title(self, **kwargs): - """ Return title according to the date """ + """ Return title from base title and elements """ tstep = None if 'tstep' not in kwargs else kwargs['tstep'] if tstep is None: @@ -1431,19 +1244,22 @@ class VisFigureHandler(MapHandler): tstep = int(tstep) - xlon, ylat, stats, visibility, humidity, vals = self.set_data(tstep) + xlon, ylat, stats, visibility, humidity, values = 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) + return self.generate_var_tstep_trace(xlon, ylat, stats, visibility, humidity, values, + self.colors, self.labels, tstep) if DEBUG: print('Adding one point ...') + return None -class ProbFigureHandler(MapHandler): +class ProbFigureHandler(ContourFigureHandler): """ Class to manage the figure creation """ def __init__(self, var=None, prob=None, selected_date=None): """ Initialization with variable, prob and date """ + self.name = 'prob' super(ProbFigureHandler, self).__init__() if var is None: @@ -1529,21 +1345,17 @@ class ProbFigureHandler(MapHandler): style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) - # Create colorbar. + # 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'} - ) + self.colorbar_info.update({'min': -0.1, + 'max': len(ctg)-.7, + 'classes': indices, + 'colorscale': colorscale, + 'tickValues': indices, + 'tickText': ctg}) + colorbar = self.retrieve_colorbar() # Geojson rendering logic, must be JavaScript as it is executed in clientside. ns = Namespace("forecastTab", "forecastMaps") @@ -1558,7 +1370,7 @@ class ProbFigureHandler(MapHandler): return geojson, colorbar def get_title(self, **kwargs): - """ Return title according to the date """ + """ Return title from base title and elements """ varname = kwargs['varname'] tstep = kwargs['tstep'] @@ -1599,25 +1411,22 @@ class ProbFigureHandler(MapHandler): if DEBUG: print('Update layout ...') if not varname: if DEBUG: print('ONE') - fig_title = '' + self.fig_title = '' elif varname and not os.path.exists(self.filepath): if DEBUG: print('TWO') - fig_title = html.P(html.B("DATA NOT AVAILABLE")) + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) else: if DEBUG: print('THREE') - fig_title = html.P(html.B( + self.fig_title = html.P(html.B( [ item for sublist in self.get_title(varname=varname, tstep=tstep).split('
') for item in [sublist, html.Br()] ][:-1] )) - info = html.Div( - children=fig_title, - id="prob-info", - className="info", - style=INFO_STYLE - ) + # Get title information element + info = self.retrieve_info(self.name) + return geojson, colorbar, info @@ -1627,11 +1436,13 @@ class WasFigureHandler(MapHandler): def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=None): """ Initialize WasFigureHandler with shapefile and netCDF data """ + self.name = 'was' super(WasFigureHandler, self).__init__() self.model = model self.was = was self.variable = variable + self.colormap = WAS[self.was]['colormap'] if self.model and selected_date: # read nc file @@ -1715,34 +1526,8 @@ class WasFigureHandler(MapHandler): 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" - ) + # Create legend + legend = self.create_legend(self.colormap) # Geojson rendering logic, must be JavaScript as it is executed in clientside. ns = Namespace("forecastTab", "wasMaps") @@ -1754,8 +1539,8 @@ class WasFigureHandler(MapHandler): # 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()))], + hideout=dict(colorscale=list(self.colormap.values()), + bounds=[i for i in range(len(self.colormap.values()))], style=style, colorProp="value") ) # url to geojson file @@ -1763,7 +1548,7 @@ class WasFigureHandler(MapHandler): return geojson, legend def get_title(self, **kwargs): - """ Return title according to the date """ + """ Return title from base title and elements """ tstep = kwargs['day'] self.base_title = WAS[self.was]['title'] @@ -1774,13 +1559,15 @@ class WasFigureHandler(MapHandler): 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( + self.fig_title = html.P(html.B( [ item for sublist in self.get_title(day=day).split('
') @@ -1788,12 +1575,9 @@ class WasFigureHandler(MapHandler): ][:-1] )) else: - fig_title = html.P(html.B("DATA NOT AVAILABLE")) + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + + # Get title information element + info = self.retrieve_info(self.name) - info = html.Div( - children=fig_title, - id="was-info", - className="info", - style=INFO_STYLE - ) return geojson, legend, info diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py new file mode 100644 index 0000000..c9a8aad --- /dev/null +++ b/ines_core_data_handler.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +""" Core """ + +import os +import json +import copy +from dash import html +import dash_leaflet as dl +import numpy as np +from dateutil.relativedelta import relativedelta + +DIR_PATH = os.path.dirname(os.path.realpath(__file__)) +COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) +MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json'))) + +class MapHandler: + + def __init__(self): + + # TODO: Add common variables from all figure handlers here + + if self.name in COLORBARS: + self.colorbar_info = COLORBARS[self.name]['colorbar'] + + self.info_style = MODEBARS['info_style'] + + return None + + def get_title(self, **kwargs): + """ Return title from base title and elements """ + + print('================= 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 """ + + 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): + + print('================= 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): + + print('================= Retrieving figure title box...') + + # 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): + + print('================= Retrieving colorbar...') + + # Create colorbar + colorbar = dl.Colorbar(**self.colorbar_info) + + return colorbar + +class PointsFigureHandler(MapHandler): + + def __init__(self): + + super(PointsFigureHandler, self).__init__() + + # TODO: Add common variables from VisFigureHandler, Observations1dHandler and ScoresFigureHandler + + return None + + def retrieve_geojson(self, geojson_data, namespace): + + print('================= 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 + +class ContourFigureHandler(MapHandler): + + def __init__(self): + + super(ContourFigureHandler, self).__init__() + + # TODO: Add common variables from FigureHandler and ProbFigureHandler + + return None + diff --git a/tabs/evaluation.py b/tabs/evaluation.py index d9314bc..5cff299 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -36,7 +36,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 +52,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,11 +61,9 @@ scores_maps = dbc.Spinner( style={ 'width': '10rem' }, className="linetool", ), - dcc.Graph( - id='scores-map-modalbody', - figure={}, - config=MODEBAR_CONFIG, # {"displayModeBar": False} - ), + html.Div(children={}, + id="scores-map-modalbody" + ), html.Div(DISCLAIMER_MODELS, className='disclaimer') ] )], @@ -234,7 +232,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" ) ], @@ -253,7 +251,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" ) ], @@ -271,7 +269,7 @@ def tab_evaluation(window='nrt'): {'label': 'Annual', 'value': 'annual'}, ], placeholder='Select timescale', - value='montly', + # value='monthly', clearable=False, searchable=False )], @@ -285,7 +283,7 @@ def tab_evaluation(window='nrt'): options=[ ], placeholder='Select month', - # value='montly', + # value='monthly', clearable=False, searchable=False )], diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 7dfdf5c..3d527f3 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -173,7 +173,7 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) @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')], @@ -188,7 +188,10 @@ 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 + from tools import get_models_figure + mb = MODEBAR_LAYOUT_TS ctx = dash.callback_context @@ -196,14 +199,15 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, if DEBUG: print(':::', 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 + layers = get_scores_figure(network, model, score, selection) + fig = get_models_figure(model=None, var=score, layer=layers) + return fig, True, model, score raise PreventUpdate @@ -211,15 +215,16 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, if DEBUG: print(':::', 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_scores_figure(network, model, score, selection) + fig = get_models_figure(model=None, var=curr_score, layer=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 + layers = get_scores_figure(network, model, score, selection) + fig = get_models_figure(model=None, var=score, layer=layers) + return fig, True, model, score return dash.no_update, False, dash.no_update, dash.no_update # PreventUpdate diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 85eaba5..a5a3145 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -467,7 +467,6 @@ def models_popup(click_data, map_ids, res_list, date, tstep, var, coords, popups if popups is None: popups = {} - import pdb; pdb.set_trace() trigger = orjson.loads(ctxt) if DEBUG: print('TRIGGER', trigger, type(trigger)) diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index a540e32..3b0778b 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -279,7 +279,6 @@ def update_vis_figure(date, tstep, zoom, center): # 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') + layers = get_vis_figure(tstep=tstep, selected_date=date) + fig = get_models_figure(model=None, var=None, layer=layers, zoom=zoom, center=center, tag='obs-vis') return fig diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index 542d7f0..1e78613 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -85,26 +85,6 @@ def test_generate_contour_tstep_trace_leaflet(FigureHandler): 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(varname='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(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)) @@ -123,26 +103,7 @@ def test_retrieve_var_tstep(FigureHandler): # =================== 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' - 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' - assert ScoresFigureHandler.retrieve_scores('median', aspect=(1,1), center=None).data[1].type == 'scattermapbox' + return code.ScoresFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) # =================== Vis Handler ============================ @pytest.fixture @@ -156,12 +117,10 @@ def VisFigureHandler(): # 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' + returned = VisFigureHandler.generate_var_tstep_trace([], [], [], [], [], (), ('#714921', '#da7230', '#fcd775', 'CadetBlue'), ('<1 km', '1 - 2 km', '2 - 5 km', 'Haze'), 6) + assert len(returned) == 3 + assert returned[1].id == 'vis-info' + assert returned[1].children == "NO DATA AVAILABLE" 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)) @@ -169,15 +128,6 @@ def test_vis_get_title(VisFigureHandler): 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)) -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(): diff --git a/tests/test_tools.py b/tests/test_tools.py index dbc24e3..c134b33 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -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' - - 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] @@ -55,8 +46,8 @@ def test_get_was_figure(): 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]) + run1 = str(code.get_vis_figure(tstep=0, selected_date='20220808')[1]) + run2 = str(code.get_vis_figure(tstep=0, selected_date='20220808')[2]) assert result in run1 assert result2 in run2 diff --git a/tools.py b/tools.py index 2df73cb..f947ee4 100644 --- a/tools.py +++ b/tools.py @@ -94,10 +94,21 @@ def get_obs1d(sdate, edate, obs, var): return obs_handler.generate_obs1d_tstep_trace(var) -def get_scores_figure(network, model, statistic, selection=END_DATE): +def get_scores_figure(network=None, model=None, statistic=None, selection=None): """ Retrieve 1D observation """ - fh = ScoresFigureHandler(network, statistic, selection) - return fh.retrieve_scores(model) + + if DEBUG: + print('SERVER: SCORES Figure init ... ') + scores_handler = ScoresFigureHandler(network, statistic, model, selection=selection) + + if network and model and statistic: + if DEBUG: + print('SERVER: SCORES Figure generation ... ') + else: + if DEBUG: + print('SERVER: NO SCORES Figure') + + return scores_handler.retrieve_scores() def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): -- GitLab From 9b87d585697209d37f9c77c5f818c23faa934428 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 21 Jun 2023 15:59:20 +0200 Subject: [PATCH 24/71] Fix bug in tooltip --- data_handler.py | 8 ++++---- ines_core_data_handler.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/data_handler.py b/data_handler.py index b42ea3d..b7cd65f 100644 --- a/data_handler.py +++ b/data_handler.py @@ -1043,10 +1043,10 @@ class ScoresFigureHandler(PointsFigureHandler): print("-------------", item, "-----------------") tooltip = """""" if stations is not None: - tooltip += """{station}""".format(station=item['station']) - tooltip += """
Lat: {lat} -
Lon: {lon} -
Score: {score:.1f}""".format(lat=item['lat'], lon=item['lon'], + tooltip += """{station}
""".format(station=item['station']) + tooltip += """Lat: {lat}
+ Lon: {lon}
+ Score: {score:.1f}""".format(lat=item['lat'], lon=item['lon'], score=float(item['score'])) tooltip += """
""" item["tooltip"] = tooltip diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index c9a8aad..648fef4 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -167,4 +167,3 @@ class ContourFigureHandler(MapHandler): # TODO: Add common variables from FigureHandler and ProbFigureHandler return None - -- GitLab From b595051fa406870c79f419122d8c77a144a60068 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 21 Jun 2023 16:21:16 +0200 Subject: [PATCH 25/71] Fix error in scores map --- tabs/evaluation_callbacks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 3d527f3..291c285 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -216,7 +216,7 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, print(':::', orig_model, orig_stats, ':::') curr_model = [mod for mod in MODELS if mod in orig_model][0] curr_score = [sc for sc in SCORES if sc in orig_stats][0] - layers = get_scores_figure(network, model, score, selection) + layers = get_scores_figure(network, curr_model, curr_score, selection) fig = get_models_figure(model=None, var=curr_score, layer=layers) return fig, True, curr_model, curr_score -- GitLab From c52d44125b973190a3e37f3c51ef5baa8ca8167f Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 21 Jun 2023 17:14:50 +0200 Subject: [PATCH 26/71] Fix cache and refactor tooltip --- conf/cache.json | 5 +-- data_handler.py | 73 ++++++++++++--------------------------- ines_core_data_handler.py | 40 ++++++++++++++++++--- 3 files changed, 61 insertions(+), 57 deletions(-) diff --git a/conf/cache.json b/conf/cache.json index 8b37fe6..8f23923 100644 --- a/conf/cache.json +++ b/conf/cache.json @@ -1,4 +1,5 @@ { - "DEBUG": true, - "CACHE_TYPE": "FileSystemCache" + "config": {"DEBUG": true, + "CACHE_TYPE": "FileSystemCache"}, + "timeout": 86400 } \ No newline at end of file diff --git a/data_handler.py b/data_handler.py index 2164bba..fc0e226 100644 --- a/data_handler.py +++ b/data_handler.py @@ -42,11 +42,12 @@ DIR_PATH = os.path.dirname(os.path.realpath(__file__)) DEBUG = True # False # Set up cache -cache_config = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) +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 = 86400 +cache_timeout = CACHE['timeout'] # Set up base url HOSTNAME = socket.gethostbyname_ex(socket.gethostname())[0] @@ -1031,26 +1032,17 @@ class ScoresFigureHandler(PointsFigureHandler): df = pd.DataFrame({ 'lon': np.array(lon).round(2), 'lat': np.array(lat).round(2), - 'score': scores, + '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'] - # Set up hover information - dicts = df.to_dict('records') - for item in dicts: - if DEBUG: - print("-------------", item, "-----------------") - tooltip = """""" - if stations is not None: - tooltip += """{station}
""".format(station=item['station']) - tooltip += """Lat: {lat}
- Lon: {lon}
- Score: {score:.1f}""".format(lat=item['lat'], lon=item['lon'], - score=float(item['score'])) - tooltip += """
""" - item["tooltip"] = tooltip + # Add tooltips (hover information) to map + data_dict = self.get_tooltip(df, var_list=var_list) # Create colorbar bounds = STATS_CONF[self.statistic]['bounds'] @@ -1066,7 +1058,7 @@ class ScoresFigureHandler(PointsFigureHandler): colorbar = self.retrieve_colorbar() # Create geojson - geojson_data = dlx.dicts_to_geojson(dicts, lon="lon") + geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") namespace = Namespace("evaluationTab", "evaluationMaps") geojson = self.retrieve_geojson(geojson_data, namespace) @@ -1151,16 +1143,16 @@ class VisFigureHandler(PointsFigureHandler): xlon = data['LON'].values ylat = data['LAT'].values - stats = data['STATION'].values + stations = 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)) + if DEBUG: print("VIS DATA", xlon, ylat, stations, (c0, c1, c2, cx)) - return xlon, ylat, stats, visibility, humidity, (c0, c1, c2, cx) + return xlon, ylat, stations, visibility, humidity, (c0, c1, c2, cx) - def generate_var_tstep_trace(self, xlon, ylat, stats, visibility, humidity, values, color, labels, tstep=0): + def generate_var_tstep_trace(self, xlon, ylat, stations, visibility, humidity, values, color, labels, tstep=0): """ Generate trace to be added to data, per variable and timestep """ # Create legend @@ -1172,41 +1164,22 @@ class VisFigureHandler(PointsFigureHandler): for i, (value, label) in enumerate(zip(values, labels)): res[value] = i - if DEBUG: - print(self.name, '___', xlon, ylat, '\n*********', visibility, '\n*********', humidity) - # Create dataframe df = pd.DataFrame({ + 'station': stations, 'lon': np.array(xlon).round(2), 'lat': np.array(ylat).round(2), - 'stat': stats, - 'visibility': visibility, - 'humidity': humidity, + 'visibility': (visibility/1e3).round(2), + 'humidity': humidity.astype(int), 'value': res }) - # Set up hover information - dicts = df.to_dict('records') - 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 '') - ) + # Add tooltips (hover information) to map + data_dict = self.get_tooltip(df, + var_list=['station', 'lon', 'lat', 'visibility', 'humidity']) # Create geojson - geojson_data = dlx.dicts_to_geojson(dicts, lon="lon") + geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") namespace = Namespace("observationsTab", "observationsMaps") geojson = self.retrieve_geojson(geojson_data, namespace) @@ -1245,9 +1218,9 @@ class VisFigureHandler(PointsFigureHandler): tstep = int(tstep) - xlon, ylat, stats, visibility, humidity, values = self.set_data(tstep) + xlon, ylat, stations, visibility, humidity, values = self.set_data(tstep) if tstep is not None: - return self.generate_var_tstep_trace(xlon, ylat, stats, visibility, humidity, values, + return self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, self.colors, self.labels, tstep) if DEBUG: print('Adding one point ...') diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 648fef4..76c95b7 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -12,6 +12,7 @@ from dateutil.relativedelta import relativedelta DIR_PATH = os.path.dirname(os.path.realpath(__file__)) COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json'))) +DEBUG = True class MapHandler: @@ -29,7 +30,8 @@ class MapHandler: def get_title(self, **kwargs): """ Return title from base title and elements """ - print('================= Retrieving figure title...') + if DEBUG: + print('================= Retrieving figure title...') # Get selected and current datetime details rhour = self.rdatetime.strftime("%H") @@ -80,7 +82,8 @@ class MapHandler: def create_legend(self, colormap): - print('================= Retrieving legend...') + if DEBUG: + print('================= Retrieving legend...') # Create categorical legend given the labels and its colors legend_items = [] @@ -109,7 +112,8 @@ class MapHandler: def retrieve_info(self, name): - print('================= Retrieving figure title box...') + if DEBUG: + print('================= Retrieving figure title box...') # Create figure title box info = html.Div( @@ -123,7 +127,8 @@ class MapHandler: def retrieve_colorbar(self): - print('================= Retrieving colorbar...') + if DEBUG: + print('================= Retrieving colorbar...') # Create colorbar colorbar = dl.Colorbar(**self.colorbar_info) @@ -142,7 +147,8 @@ class PointsFigureHandler(MapHandler): def retrieve_geojson(self, geojson_data, namespace): - print('================= Retrieving geojson...') + if DEBUG: + print('================= Retrieving geojson...') # Get points properties hideout = {"circleOptions": self.circle_options} @@ -158,6 +164,30 @@ class PointsFigureHandler(MapHandler): return geojson + def get_tooltip(self, df, var_list): + + if DEBUG: + print('================= Retrieving tooltip...') + + data_dict = df.to_dict('records') + for item in data_dict: + if DEBUG: + print("-------------", item, "-----------------") + tooltip = """""" + tooltip_keys = [key for key in var_list if key in item.keys()] + for key in tooltip_keys: + if item[key] in (False, ''): + 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): def __init__(self): -- GitLab From c5325e01137f50a552cc53b3d3cb5339c8dc968e Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 21 Jun 2023 17:33:16 +0200 Subject: [PATCH 27/71] Rename Observations1DHandler --- data_handler.py | 4 ++-- tabs/evaluation_callbacks.py | 12 ++++++------ tools.py | 14 ++++++++------ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/data_handler.py b/data_handler.py index fc0e226..0cf2349 100644 --- a/data_handler.py +++ b/data_handler.py @@ -125,13 +125,13 @@ else: PATHNAME = HOSTNAMES['default'] -class Observations1dHandler(PointsFigureHandler): +class EvaluationVisualComparisonFigureHandler(PointsFigureHandler): """ Class which handles 1D obs data """ def __init__(self, sdate, edate, obs): self.name = 'obs' - super(Observations1dHandler, self).__init__() + super(EvaluationVisualComparisonFigureHandler, self).__init__() self.circle_options = OBS[obs]['circle_options'] fday = sdate[:-2] + '01' diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 291c285..f277fe2 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -189,7 +189,7 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) 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 + from tools import get_evaluation_scores_figure from tools import get_models_figure mb = MODEBAR_LAYOUT_TS @@ -205,7 +205,7 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, if model is not None and score is not None: if DEBUG: print('::: 1 :::') - layers = get_scores_figure(network, model, score, selection) + layers = get_evaluation_scores_figure(network, model, score, selection) fig = get_models_figure(model=None, var=score, layer=layers) return fig, True, model, score @@ -216,13 +216,13 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, print(':::', orig_model, orig_stats, ':::') curr_model = [mod for mod in MODELS if mod in orig_model][0] curr_score = [sc for sc in SCORES if sc in orig_stats][0] - layers = get_scores_figure(network, curr_model, curr_score, selection) + layers = get_evaluation_scores_figure(network, curr_model, curr_score, selection) fig = get_models_figure(model=None, var=curr_score, layer=layers) return fig, True, curr_model, curr_score else: print('::: 2.5 :::') - layers = get_scores_figure(network, model, score, selection) + layers = get_evaluation_scores_figure(network, model, score, selection) fig = get_models_figure(model=None, var=score, layer=layers) return fig, True, model, score @@ -704,7 +704,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): raise PreventUpdate from tools import get_models_figure - from tools import get_obs1d + from tools import get_evaluation_visual_comparison_figure if DEBUG: print('SERVER: calling figure from EVAL picker callback') if DEBUG: print('SERVER: SDATE', str(sdate)) @@ -730,7 +730,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): else: edate = END_DATE - stations, points_layer = get_obs1d(sdate, edate, obs, DEFAULT_VAR) + stations, points_layer = get_evaluation_visual_comparison_figure(sdate, edate, obs, DEFAULT_VAR) fig = get_models_figure(model=None, var=DEFAULT_VAR, layer=points_layer) return stations.to_dict(), fig diff --git a/tools.py b/tools.py index f947ee4..183f33a 100644 --- a/tools.py +++ b/tools.py @@ -13,7 +13,7 @@ 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 EvaluationVisualComparisonFigureHandler from data_handler import DEBUG from data_handler import MODELS from data_handler import END_DATE, DELAY, DELAY_DATE @@ -88,14 +88,16 @@ def get_single_point(model, date, tstep, var, lat, lon): return th.retrieve_single_point(tstep, lat, lon) -def get_obs1d(sdate, edate, obs, var): - """ Retrieve 1D observation """ - obs_handler = Observations1dHandler(sdate, edate, obs) +def get_evaluation_visual_comparison_figure(sdate, edate, obs, var): + """ Retrieve evaluation visual comparison figure """ + + obs_handler = EvaluationVisualComparisonFigureHandler(sdate, edate, obs) + return obs_handler.generate_obs1d_tstep_trace(var) -def get_scores_figure(network=None, model=None, statistic=None, selection=None): - """ Retrieve 1D observation """ +def get_evaluation_scores_figure(network=None, model=None, statistic=None, selection=None): + """ Retrieve evaluation scores figure """ if DEBUG: print('SERVER: SCORES Figure init ... ') -- GitLab From a8e8ddf93b1e00a3191cd1f3dca01f9537d82f05 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 22 Jun 2023 09:07:55 +0200 Subject: [PATCH 28/71] Refactor geojsons for countour and shapefile maps --- conf/was_prod.json | 30 ++++++++++++++------ data_handler.py | 60 ++++++++++++++++++--------------------- ines_core_data_handler.py | 38 +++++++++++++++++++++++++ tools.py | 4 +-- 4 files changed, 89 insertions(+), 43 deletions(-) diff --git a/conf/was_prod.json b/conf/was_prod.json index c9c2f94..5bba778 100644 --- a/conf/was_prod.json +++ b/conf/was_prod.json @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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": { @@ -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/data_handler.py b/data_handler.py index 0cf2349..5263a4e 100644 --- a/data_handler.py +++ b/data_handler.py @@ -32,6 +32,7 @@ from utils import get_colorscale from ines_core_data_handler import MapHandler from ines_core_data_handler import PointsFigureHandler from ines_core_data_handler import ContourFigureHandler +from ines_core_data_handler import ShapefileFigureHandler from pathlib import Path from flask_caching import Cache @@ -200,7 +201,7 @@ class EvaluationVisualComparisonFigureHandler(PointsFigureHandler): return df, geojson -class ObsTimeSeriesHandler: +class EvaluationVisualComparisonTimeSeriesHandler: """ Class to handle evaluation time series """ def __init__(self, obs, start_date, end_date, variable, models=None): @@ -719,14 +720,9 @@ class FigureHandler(ContourFigureHandler): if DEBUG: print("CTG", ctg) # 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 + namespace = Namespace("forecastTab", "forecastMaps") + hideout = dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") + geojson = self.retrieve_geojson(geojson_url, namespace, hideout) return geojson, colorbar @@ -885,6 +881,12 @@ class FigureHandler(ContourFigureHandler): if DEBUG: print("*** FIGURE EXECUTION TIME: {} ***".format(str(time.time() - self.st_time))) return fig +class ForecastModelsFigureHandler(ContourFigureHandler): + + def __init__(self, model): + + self.name = 'model' + super(ForecastModelsFigureHandler, self).__init__() class ScoresFigureHandler(PointsFigureHandler): """ Class to manage the figure creation """ @@ -1332,14 +1334,9 @@ class ProbFigureHandler(ContourFigureHandler): colorbar = self.retrieve_colorbar() # 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 + namespace = Namespace("forecastTab", "forecastMaps") + hideout = dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") + geojson = self.retrieve_geojson(geojson_url, namespace, hideout) return geojson, colorbar @@ -1404,7 +1401,7 @@ class ProbFigureHandler(ContourFigureHandler): return geojson, colorbar, info -class WasFigureHandler(MapHandler): +class WasFigureHandler(ShapefileFigureHandler): """ Class to manage the figure creation """ def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=None): @@ -1417,6 +1414,10 @@ class WasFigureHandler(MapHandler): self.was = was self.variable = variable 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 and selected_date: # read nc file @@ -1498,26 +1499,19 @@ class WasFigureHandler(MapHandler): 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 legend legend = self.create_legend(self.colormap) # 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(self.colormap.values()), - bounds=[i for i in range(len(self.colormap.values()))], - style=style, - colorProp="value") - ) # url to geojson file + namespace = Namespace("forecastTab", "wasMaps") + geojson_url = 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) + geojson = self.retrieve_geojson(geojson_url, namespace, hideout, hover_style) return geojson, legend diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 76c95b7..0913f66 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -197,3 +197,41 @@ class ContourFigureHandler(MapHandler): # TODO: Add common variables from FigureHandler and ProbFigureHandler return None + + def retrieve_geojson(self, geojson_url, namespace, hideout): + + if DEBUG: + print('================= Retrieving geojson...') + + # Get geojson + style_handle = namespace("styleHandle") + geojson = dl.GeoJSON(url=geojson_url, + options=dict(style=style_handle), + hideout=hideout) + + return geojson + + +class ShapefileFigureHandler(MapHandler): + + def __init__(self): + + super(ShapefileFigureHandler, self).__init__() + + # TODO: Add variables from WasFigureHandler that could be reused in other shapefile figures + + return None + + def retrieve_geojson(self, geojson_url, namespace, hideout, hover_style): + + if DEBUG: + print('================= Retrieving geojson...') + + # Get geojson + style_handle = namespace("styleHandle") + geojson = dl.GeoJSON(url=geojson_url, + options=dict(style=style_handle), + hoverStyle=hover_style, + hideout=hideout) + + return geojson \ No newline at end of file diff --git a/tools.py b/tools.py index 183f33a..e518de5 100644 --- a/tools.py +++ b/tools.py @@ -12,7 +12,7 @@ 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 EvaluationVisualComparisonTimeSeriesHandler from data_handler import EvaluationVisualComparisonFigureHandler from data_handler import DEBUG from data_handler import MODELS @@ -62,7 +62,7 @@ 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) + th = EvaluationVisualComparisonTimeSeriesHandler(obs, start_date, end_date, var) if DEBUG: print('SERVER: OBS TS generation ... ') return th.retrieve_timeseries(idx, name, model) -- GitLab From 164d43af5ec72116f3f3cc965983742a170eef51 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 22 Jun 2023 10:59:13 +0200 Subject: [PATCH 29/71] Move hard coded paths into json files. --- conf/paths.json | 4 ++ conf/satellite_image_src.json | 4 +- data_handler.py | 1 + tabs/forecast.py | 5 +- tabs/generic_callbacks.py | 88 ------------------------------ tabs/observations.py | 5 +- tabs/observations_callbacks.py | 99 +--------------------------------- 7 files changed, 15 insertions(+), 191 deletions(-) create mode 100644 conf/paths.json diff --git a/conf/paths.json b/conf/paths.json new file mode 100644 index 0000000..331e6a3 --- /dev/null +++ b/conf/paths.json @@ -0,0 +1,4 @@ +{ + "user_guide": "/products/overview/user-guide/@@download", + "netcdf": "/products/data-download" +} diff --git a/conf/satellite_image_src.json b/conf/satellite_image_src.json index 28d3523..29c6eea 100644 --- a/conf/satellite_image_src.json +++ b/conf/satellite_image_src.json @@ -1,4 +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": "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/data_handler.py b/data_handler.py index cb25b20..39b41c8 100644 --- a/data_handler.py +++ b/data_handler.py @@ -70,6 +70,7 @@ 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'))) # Set up initial conditions FREQ = INIT['frequency'] diff --git a/tabs/forecast.py b/tabs/forecast.py index 809bc56..9448065 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -15,6 +15,7 @@ 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 +from data_handler import PATHS from tabs.generic import get_forecast_days, layout_view, time_series # MOVED TO GENERIC @@ -481,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."""), @@ -519,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/generic_callbacks.py b/tabs/generic_callbacks.py index e5475d0..990133d 100644 --- a/tabs/generic_callbacks.py +++ b/tabs/generic_callbacks.py @@ -18,91 +18,3 @@ def rotate_section_caret(collapse_open): '-webkit-transform': 'rotate({}deg)'.format(rotation) } return rotate_caret - - -# @dash.callback( -# [Output('slider-interval', 'disabled'), -# Output('slider-interval', 'n_intervals'), -# Output('open-timeseries', 'style'), -# Output('btn-play', 'className'), -# ], -# Input('btn-play', 'n_clicks'), -# [State('slider-interval', 'disabled'), -# State('model-slider-graph', 'value')], -# prevent_initial_call=True -# ) -# def start_stop_autoslider(n_play, 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-play' and disabled: -# ts_style = { 'display': 'none' } -# return not disabled, int(value/FREQ), ts_style, 'fa fa-pause text-center' -# elif button_id == 'btn-play' and not disabled: -# ts_style = { 'display': 'block' } -# return not disabled, int(value/FREQ), ts_style, 'fa fa-play text-center' -# -# raise PreventUpdate -# start/stop animation - -# @dash.callback( -# [Output('obs-slider-interval', 'disabled'), -# Output('obs-slider-interval', 'n_intervals'), -# Output('btn-obs-play', 'className')], -# Input('btn-obs-play', 'n_clicks'), -# [State('obs-slider-interval', 'disabled'), -# State('obs-slider-graph', 'value')], -# prevent_initial_call=True -# ) -# @cache.memoize(timeout=cache_timeout) -# def start_stop_obs_autoslider(n_play, 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-play' and disabled: -# return not disabled, int(value), 'fa fa-pause text-center' -# elif button_id == 'btn-obs-play' and not disabled: -# return not disabled, int(value), 'fa fa-play text-center' -# -# raise PreventUpdate - -# @dash.callback( -# [Output({'type':'slider-interval', 'id': MATCH}, 'disabled'), -# Output({'type':'slider-interval', 'id': MATCH}, 'n_intervals'), -# Output('open-timeseries', 'style'), -# Output({ 'tag': 'btn-play', 'id': MATCH}, 'className'), -# ], -# [Input({ 'tag': 'btn-play', 'id': MATCH}, 'n_clicks')], -# [State({'type':'slider-interval', 'id': MATCH}, 'disabled'), -# State({ 'tag': 'time-step-slider-graphs', 'index': MATCH}, 'value')], -# prevent_initial_call=True -# ) -# def start_stop_autoslider(n_play, disabled, value): -# """ Play/Pause map animation """ -# ctx = dash.callback_context -# if DEBUG: print("VALUE", value) -# if not value: -# value = 0 -# -# if ctx.triggered: -# import pdb; pdb.set_trace() -# import json -# button_id = json.loads(ctx.triggered[0]["prop_id"].split(".")[0]) -# # if button_id == 'btn-play' and disabled: -# if button_id['tag'] == 'btn-play' and disabled: -# ts_style = { 'display': 'none' } -# return not disabled, int(value/FREQ), ts_style, ['fa fa-pause text-center'] -# elif button_id['tag'] == 'btn-play' and not disabled: -# ts_style = { 'display': 'block' } -# return not disabled, int(value/FREQ), ts_style, ['fa fa-play text-center'] -# -# raise PreventUpdate diff --git a/tabs/observations.py b/tabs/observations.py index c5ac711..bc42c92 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -10,6 +10,7 @@ from data_handler import MODELS from data_handler import START_DATE, END_DATE from data_handler import STYLES from data_handler import DISCLAIMER_MODELS +from data_handler import SATELLITE_IMAGE_SRC # from tabs.forecast import layout_view from utils import get_vis_edate @@ -163,7 +164,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 +198,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( diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index 42093ee..34e99c8 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -53,68 +53,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'), @@ -207,43 +145,8 @@ def update_obs_slider(n): if DEBUG: print('SERVER: updating slider-graph ' + 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'), -- GitLab From de798c9453b3dac16abc83e351d754f921a9361b Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 22 Jun 2023 15:27:54 +0200 Subject: [PATCH 30/71] Move hardcoded path out of tools to paths.json. Rename tools to callback_tools --- tools.py => callback_tools.py | 3 ++- conf/paths.json | 3 ++- router.py | 36 +++++++++++++++++----------------- tabs/evaluation_callbacks.py | 20 +++++++++---------- tabs/forecast_callbacks.py | 18 ++++++++--------- tabs/observations_callbacks.py | 4 ++-- tests/test_tools.py | 2 +- 7 files changed, 44 insertions(+), 42 deletions(-) rename tools.py => callback_tools.py (98%) diff --git a/tools.py b/callback_tools.py similarity index 98% rename from tools.py rename to callback_tools.py index f947ee4..825367e 100644 --- a/tools.py +++ b/callback_tools.py @@ -16,6 +16,7 @@ from data_handler import ObsTimeSeriesHandler from data_handler import Observations1dHandler from data_handler import DEBUG from data_handler import MODELS +from data_handler import PATHS from data_handler import END_DATE, DELAY, DELAY_DATE from utils import get_currdate_tstep @@ -23,7 +24,7 @@ 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}" + filepath = PATHS['gif_link'] if len(models) == 1: model = models[0] diff --git a/conf/paths.json b/conf/paths.json index 331e6a3..95b7628 100644 --- a/conf/paths.json +++ b/conf/paths.json @@ -1,4 +1,5 @@ { "user_guide": "/products/overview/user-guide/@@download", - "netcdf": "/products/data-download" + "netcdf": "/products/data-download", + "gif_link" : "assets/comparison/{model}/{variable}/{year}/{month}/{curdate}_{model}_{tstep}.{ext}" } diff --git a/router.py b/router.py index 013c57a..1ee150f 100644 --- a/router.py +++ b/router.py @@ -125,22 +125,22 @@ 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() + 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() return children diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 3d527f3..ed1109b 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -189,8 +189,8 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) 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 - from tools import get_models_figure + from callback_tools import get_scores_figure + from callback_tools import get_models_figure mb = MODEBAR_LAYOUT_TS @@ -449,7 +449,7 @@ def aeronet_scores_tables_retrieve(n, *args): @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 + from callback_tools import get_timeseries if coords is None or nclicks == 0: raise PreventUpdate @@ -485,7 +485,7 @@ def show_eval_modis_timeseries(nclicks, coords, date, obs, model): ) def modis_popup(click_data, mapid, date, model): """ Manages popup info for modis """ - from tools import get_single_point + from callback_tools import get_single_point if DEBUG: print("CLICK:", str(click_data)) if not click_data: raise PreventUpdate @@ -645,7 +645,7 @@ def stations_popup(click_data, mapid, stations): @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 + from callback_tools import get_eval_timeseries ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] if DEBUG: print("CTXT", ctxt, type(ctxt)) if not ctxt or ctxt is None: @@ -703,8 +703,8 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): if sdate is None or edate is None or obs != 'aeronet': raise PreventUpdate - from tools import get_models_figure - from tools import get_obs1d + from callback_tools import get_models_figure + from callback_tools import get_obs1d if DEBUG: print('SERVER: calling figure from EVAL picker callback') if DEBUG: print('SERVER: SDATE', str(sdate)) @@ -759,7 +759,7 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): if date is None or mod is None or obs != 'modis': raise PreventUpdate - from tools import get_models_figure + from callback_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'] @@ -802,8 +802,8 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): @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 + from callback_tools import get_models_figure + # from callback_tools import get_obs1d if DEBUG: print('SERVER: calling figure from EVAL picker callback') # if DEBUG: print('SERVER: interval ' + str(n)) diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index c28f8e0..41ed6fb 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -181,8 +181,8 @@ 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) try: @@ -229,8 +229,8 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente 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_models_figure if not zoom or previous != was: zoom = WAS[was]['zoom'] else: @@ -294,8 +294,8 @@ def update_prob_timeslider(date): @cache.memoize(timeout=cache_timeout) def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): """ Update Probability maps """ - from tools import get_prob_figure - from tools import get_models_figure + from callback_tools import get_prob_figure + from callback_tools import get_models_figure # if not prob in case user navigates to section via URL if not prob: @@ -413,7 +413,7 @@ def update_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 + from callback_tools import get_single_point if DEBUG: print("CLICK:", str(click_data)) if click_data.count(None) == len(click_data): raise PreventUpdate @@ -548,7 +548,7 @@ 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: @@ -731,7 +731,7 @@ 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 + from callback_tools import get_models_figure if DEBUG: print('SERVER: calling figure from picker callback') st_time = time.time() diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index 34e99c8..df5a91a 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -158,8 +158,8 @@ 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 + from callback_tools import get_vis_figure + from callback_tools import get_models_figure if DEBUG: print("*************", date, tstep, zoom, center) if date is not None: date = date.split(' ')[0] diff --git a/tests/test_tools.py b/tests/test_tools.py index 3060475..780626a 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -2,7 +2,7 @@ import pytest from datetime import datetime from datetime import timedelta import importlib -code = importlib.import_module('tools') +code = importlib.import_module('callback_tools') from data_handler import END_DATE from data_handler import FREQ -- GitLab From 511d11fe9a0188583f6901b70cb0e9eb4fd12804 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Fri, 23 Jun 2023 15:01:43 +0200 Subject: [PATCH 31/71] Refactor custom-functions.js and break out sections into different files for easier navigation. Refactor url.js to combine similar functions, and rename file to match with the python file. --- assets/custom-functions.js | 227 ------------------------------------- assets/datepicker.js | 68 +++++++++++ assets/fullscreen.js | 31 +++++ assets/gif_logos.js | 24 ++++ assets/resize_colorbars.js | 21 ++++ assets/router.js | 111 ++++++++++++++++++ assets/stats_carets.js | 81 +++++++++++++ assets/url.js | 176 ---------------------------- 8 files changed, 336 insertions(+), 403 deletions(-) create mode 100644 assets/datepicker.js create mode 100644 assets/fullscreen.js create mode 100644 assets/gif_logos.js create mode 100644 assets/resize_colorbars.js create mode 100644 assets/router.js create mode 100644 assets/stats_carets.js delete mode 100644 assets/url.js diff --git a/assets/custom-functions.js b/assets/custom-functions.js index d0be4f0..84d1b3b 100644 --- a/assets/custom-functions.js +++ b/assets/custom-functions.js @@ -81,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 0000000..3c7c20a --- /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 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); + }; + }); +}); + +// //================= 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/fullscreen.js b/assets/fullscreen.js new file mode 100644 index 0000000..c1febe6 --- /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 0000000..f3441c1 --- /dev/null +++ b/assets/gif_logos.js @@ -0,0 +1,24 @@ +// 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); + } + }); +}); diff --git a/assets/resize_colorbars.js b/assets/resize_colorbars.js new file mode 100644 index 0000000..e99837d --- /dev/null +++ b/assets/resize_colorbars.js @@ -0,0 +1,21 @@ +//==================Functions to resize colorbar ====================== + +function setWidthForColorbars() { + 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 ================== + diff --git a/assets/router.js b/assets/router.js new file mode 100644 index 0000000..c079db4 --- /dev/null +++ b/assets/router.js @@ -0,0 +1,111 @@ +// ==================== 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('.form-check-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 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 A DICTIONARY OF IDs TO CREATE URL OUTPUTS +$(document).ready(function () { + var toggleMap = { + "#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" + }; + // NOW MATCH THE ID WITH THE TOGGLEMAP KEY AND OUTPUT URL VALUE + $(document).on('click', Object.keys(toggleMap).join(', '), function () { + var selector = "#" + $(this).attr('id'); + var url = toggleMap[selector]; + sendURL(url); + }); +}); + + diff --git a/assets/stats_carets.js b/assets/stats_carets.js new file mode 100644 index 0000000..fcb77d9 --- /dev/null +++ b/assets/stats_carets.js @@ -0,0 +1,81 @@ +//================== 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 =============================================== + + diff --git a/assets/url.js b/assets/url.js deleted file mode 100644 index 56d8694..0000000 --- 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); - }) -}); - -- GitLab From 127560117f8bae6c2593f3108d33b96aec92554c Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Mon, 26 Jun 2023 10:18:07 +0200 Subject: [PATCH 32/71] Remove html2canvas file and add CDN to dash_server. --- assets/download-img.js | 2 +- assets/html2canvas.min.js | 20 -------------------- dash_server.py | 6 ++++-- 3 files changed, 5 insertions(+), 23 deletions(-) delete mode 100644 assets/html2canvas.min.js diff --git a/assets/download-img.js b/assets/download-img.js index cca2022..f4be793 100644 --- a/assets/download-img.js +++ b/assets/download-img.js @@ -97,7 +97,7 @@ function makeChanges() { addLogos(); //remove the attribution in the bottom right corner const attribution = document.querySelector('.leaflet-control-attribution'); - attribution.remove(); + attribution.style.display = "none"; } function removeChanges() { diff --git a/assets/html2canvas.min.js b/assets/html2canvas.min.js deleted file mode 100644 index aed6bfd..0000000 --- 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 Date: Mon, 26 Jun 2023 17:28:48 +0200 Subject: [PATCH 33/71] New class names and main reorganization --- conf/colorbars.json | 10 +- data_handler.py | 1836 +++++++++++++++----------------- ines_core_data_handler.py | 108 +- tabs/evaluation_callbacks.py | 55 +- tabs/forecast_callbacks.py | 53 +- tabs/observations_callbacks.py | 4 +- tools.py | 141 ++- 7 files changed, 1115 insertions(+), 1092 deletions(-) diff --git a/conf/colorbars.json b/conf/colorbars.json index 272b235..904412e 100644 --- a/conf/colorbars.json +++ b/conf/colorbars.json @@ -1,9 +1,9 @@ { - "fig": {"colorbar": {"position": "topleft", - "width": 270, - "height": 15, - "style": {"top": "55px"} - } + "model": {"colorbar": {"position": "topleft", + "width": 270, + "height": 15, + "style": {"top": "55px"} + } }, "prob": {"colorbar": {"position": "topleft", "width": 330, diff --git a/data_handler.py b/data_handler.py index 5263a4e..5e5cc4e 100644 --- a/data_handler.py +++ b/data_handler.py @@ -40,7 +40,7 @@ import uuid import socket DIR_PATH = os.path.dirname(os.path.realpath(__file__)) -DEBUG = True # False +DEBUG = True # Set up cache CACHE = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) @@ -126,236 +126,131 @@ else: PATHNAME = HOSTNAMES['default'] -class EvaluationVisualComparisonFigureHandler(PointsFigureHandler): - """ Class which handles 1D obs data """ +class ForecastModelsFigureHandler(ContourFigureHandler): - def __init__(self, sdate, edate, obs): - - self.name = 'obs' - super(EvaluationVisualComparisonFigureHandler, self).__init__() - self.circle_options = OBS[obs]['circle_options'] + def __init__(self, var=None, model=None, tstep=0, hour=None, selected_date=None): - 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' + if isinstance(model, list): + self.model = model[0] 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(DIR_PATH, 'conf/', OBS[obs]['sites'])) - - 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]) - } - + self.model = model + self.name = self.model + super(ForecastModelsFigureHandler, self).__init__() - def generate_obs1d_tstep_trace(self, var): - """ Generate trace to be added to data, per variable and timestep """ + self.var = var + self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') - 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] - - # Create dataframe - df = pd.DataFrame({ - 'lon': clon.round(2), - 'lat': clat.round(2), - 'stations': cstations - }) # .T, columns=['lon', 'lat', 'station']) - dicts = 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") - geojson = self.retrieve_geojson(geojson_data, namespace) - - return df, geojson - - -class EvaluationVisualComparisonTimeSeriesHandler: - """ 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) + self.filedir = MODELS[self.model]['path'] + filepath = NETCDF_TEMPLATE.format(self.filedir, selected_date, MODELS[self.model]['template']) + self.input_file = nc_file(filepath) + time_obj = self.input_file.variables['time'] + self.timesteps = 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") + + self.selected_date_plain = selected_date + self.selected_date = datetime.strptime( + selected_date, "%Y%m%d").strftime("%Y-%m-%d") + + def generate_var_tstep_trace(self, tstep=0): + """ Generate trace to be added to data, per variable and timestep """ + from dash_server import app - 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) + data_path = os.path.basename(MODELS[self.model]['path']) + if DEBUG: print(data_path) + geojson_url = app.get_asset_url(os.path.join('geojsons', + GEOJSON_TEMPLATE.format(data_path, + self.selected_date_plain, tstep, self.selected_date_plain, + self.var))) + + if DEBUG: print("MODEL", self.model, "GEOJSON_URL", geojson_url) - 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) + style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) - 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) + # Create 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() + if DEBUG: print("BOUNDS", self.bounds) + if DEBUG: print("CTG", ctg) - def retrieve_timeseries(self, idx, st_name, model): - """ Return traces with timeseries of observations, filtering variable and - all models """ + # Geojson rendering logic, must be JavaScript as it is executed in clientside. + namespace = Namespace("forecastTab", "forecastMaps") + hideout = dict(colorscale=COLORS, bounds=self.bounds, style=style, colorProp="value") + self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) - #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 }) + def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): + """ run plot """ - 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 hour is not None: + tstep = int(self.hour_to_step(hour)) + else: + tstep = int(tstep) - if DEBUG: print("SELECTING COORDS") - if 'lat' in df.columns: - lat_col = 'lat' - lon_col = 'lon' - else: - lat_col = 'latitude' - lon_col = 'longitude' + # Get trace + self.generate_var_tstep_trace(tstep) - 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() + if DEBUG: print('Update layout ...') + if not self.var: + self.fig_title = "" + elif self.var and not self.filedir: + if self.model in MODELS: + title = "{} - DATA NOT AVAILABLE".format(MODELS[self.model]['name']) 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 + title = "DATA NOT AVAILABLE" + self.fig_title = html.P(html.B(title)) + else: + 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] + )) - if mod == 'median': - line['dash'] = 'dash' + 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) + else: + self.info = None - 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 - ) - ) + return [self.geojson, self.colorbar, self.info] - title = "{} @ {} (lat = {:.2f}, lon = {:.2f})".format( - VARS[self.variable]['name'], st_name, cur_lat, cur_lon, - ) + def get_title(self, **kwargs): + """ Return title from base title and elements """ - 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"), - ]) - ) - ) + varname = kwargs['varname'] + tstep = kwargs['tstep'] - if DEBUG: print('FIG TYPE', type(fig)) - return fig + 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 -class TimeSeriesHandler: +class ForecastModelsTimeSeriesHandler: """ Class to handle forecast time series """ def __init__(self, model, date, variable): @@ -537,57 +432,42 @@ class TimeSeriesHandler: return fig -class FigureHandler(ContourFigureHandler): +class ForecastProbFigureHandler(ContourFigureHandler): """ Class to manage the figure creation """ - def __init__(self, model=None, selected_date=None): - """ FigureHandler init """ + def __init__(self, var=None, prob=None, selected_date=None): + """ Initialization with variable, prob and date """ - self.name = 'fig' - super(FigureHandler, self).__init__() + self.name = 'prob' + super(ForecastProbFigureHandler, self).__init__() - self.st_time = time.time() - if isinstance(model, list): - model = model[0] - self.model = model + if var is None: + self.var = DEFAULT_VAR + else: + self.var = var - 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) + probs = PROB[self.var]['prob_thresh'] + if prob is None: + prob = probs[0] - elif self.model in MODELS: - if DEBUG: print("MODEL", model) - self.filedir = MODELS[self.model]['path'] - self.filevars = VARS - self.confvars = None + self.bounds = np.arange(0, 110, 10) + + self.prob = prob + + geojson_path = PROB[self.var]['geojson_path'] + geojson_file = PROB[self.var]['geojson_template'] + netcdf_path = PROB[self.var]['netcdf_path'] + netcdf_file = PROB[self.var]['netcdf_template'] + + self.geojson = os.path.join(geojson_path, geojson_file).format(prob=prob, + date=selected_date, + var=self.var) + self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=prob, date=selected_date, + var=self.var) + + if os.path.exists(self.filepath): + self.input_file = nc_file(self.filepath) - 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.timesteps = [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'][:] @@ -606,946 +486,930 @@ class FigureHandler(ContourFigureHandler): 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) + varlist = [var for var in self.input_file.variables if var in VARS] 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 generate_contour_tstep_trace_leaflet(self, varname, tstep=0): + + def generate_var_tstep_trace(self, 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("##############", tstep) - if DEBUG: print(data_path) - colorscale = COLORS + geojson_file = self.geojson.format(step=tstep) + geojson_url = app.get_asset_url(geojson_file.replace('/data/daily_dashboard', 'geojsons')) - 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) + name = VARS[self.var]['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 = ["{:d}".format(int(cls)) if cls.as_integer_ratio()[1] == 1 else "{:.1f}".format(cls) - for i, cls in enumerate(bounds[1:-1])] - indices = list(range(len(ctg) + 2)) - self.colorbar_info.update({'min': 0, - 'max': len(ctg)+1, + ctg = ["{:.1f}".format(cls) if '.' in str(cls) else "{:d}".format(cls) + for i, cls in enumerate(bounds)] + indices = list(range(len(ctg))) + self.colorbar_info.update({'min': -0.1, + 'max': len(ctg)-.7, 'classes': indices, 'colorscale': colorscale, - 'tickValues': indices[1:-1], + 'tickValues': indices, 'tickText': ctg}) - colorbar = self.retrieve_colorbar() - - if DEBUG: print("BOUNDS", bounds) - if DEBUG: print("CTG", ctg) + self.colorbar = self.retrieve_colorbar() # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "forecastMaps") hideout = dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") - geojson = self.retrieve_geojson(geojson_url, namespace, hideout) + self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) - return geojson, colorbar + return [self.geojson, self.colorbar] def get_title(self, **kwargs): """ Return title from base title and elements """ - + varname = kwargs['varname'] tstep = kwargs['tstep'] - if self.model in OBS: - self.base_title = " ".join([OBS[self.model]['name'], OBS[self.model]['title']]) - else: - 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(FigureHandler, self).get_title() - - return title + 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 - def hour_to_step(self, hour): - """ Convert hour to relative tstep """ - cdatetime = self.rdatetime.date() + relativedelta(hours=hour) + self.base_title = PROB[varname]['title'] + self.cdatetime = self.rdatetime + relativedelta(days=tstep) - for step in range(len(self.timesteps)): - if self.retrieve_cdatetime(step) == cdatetime: - return step + title = super(ForecastProbFigureHandler, self).get_title() - return 0 + return title - 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'): + def get_figure_layers(self, day=0): """ run plot """ + tstep = int(day) - 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("***", self.var, day, static, self.geojson, self.filepath) + if DEBUG: print('Adding contours ...') + if self.var and os.path.exists(self.geojson.format(step=day)): + self.generate_var_tstep_trace(tstep) if DEBUG: print('Update layout ...') - if not varname: - self.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 = '' - - self.fig_title = html.P(html.B("{} - DATA NOT AVAILABLE".format(curr_name))) + if not self.var: + self.fig_title = "" + elif self.var and not os.path.exists(self.filepath): + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) else: self.fig_title = html.P(html.B( [ - item for sublist in self.get_title(varname=varname, tstep=tstep).split('
') + item for sublist in self.get_title(varname=self.var, tstep=tstep).split('
') for item in [sublist, html.Br()] ][:-1] )) - if self.model is not None: - if colorbar is not None: - self.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: - self.info_style['fontSize'] = "{}px".format(int(INFO_STYLE['fontSize'][:-2])-aspect[0]+ 0.3) - # Get title information element - info = self.retrieve_info(name=self.model) - 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" + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.colorbar, self.info] + - 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 ForecastWasFigureHandler(ShapefileFigureHandler): + """ Class to manage the figure creation """ -class ForecastModelsFigureHandler(ContourFigureHandler): + def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=None): + """ Initialize WasFigureHandler with shapefile and netCDF data """ + + self.name = 'was' + super(ForecastWasFigureHandler, self).__init__() - def __init__(self, model): + self.model = model + self.was = was + self.variable = variable + 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}) - self.name = 'model' - super(ForecastModelsFigureHandler, self).__init__() + 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'] + ) -class ScoresFigureHandler(PointsFigureHandler): - """ Class to manage the figure creation """ + if os.path.exists(filepath): + self.input_file = nc_file(filepath) + time_obj = self.input_file.variables['time'] + self.timesteps = 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 - def __init__(self, network, statistic, model, selection): + if selected_date: + self.selected_date_plain = selected_date - self.name = 'scores' - super(ScoresFigureHandler, self).__init__() + self.selected_date = datetime.strptime( + selected_date, "%Y%m%d").strftime("%Y-%m-%d") - self.extent = SCORES['extent'] - self.model = model - self.network = network - self.statistic = statistic - self.selection = selection + # Correct timesteps to show first timestep of each day ([0, 24, 48, ...]) + self.timesteps = self.timesteps[::8] - if self.network and self.model and self.statistic and self.selection: + self.fig = None - # 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'] + 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') - # 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}) + input_path = os.path.join(input_dir, input_file) - # Read dataframe - self.read_data() - - # 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 = datetime(year, month, 1) + if DEBUG: print("INPUT PATH", input_path, self.selected_date_plain) + if not os.path.exists(input_path): + return [], [], [] - else: - self.data = None + df = pd.read_hdf(input_path, 'was_{}'.format(self.selected_date_plain)).set_index('day') - def read_data(self): - - # Get path and file - filedir = OBS[self.network]['path'] - filename = "{}_{}.h5".format(self.selection, self.statistic) - tab_name = "{}_{}".format(self.statistic, self.selection) - filepath = os.path.join(filedir, "h5", filename) - self.data = pd.read_hdf(filepath, tab_name) - - if DEBUG: print('SCORES filepath', filepath, 'SELECTION', self.selection, 'TAB', tab_name) + names, colors, definitions = df.loc['Day{}'.format(day)].values.T + return names, colors, definitions - def select_data(self): + 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 - if self.model in self.data.columns: + pathlist = os.path.normpath(geojsons_dir).split(os.sep) + geojsons_dir_cleaned = os.sep.join(pathlist[pathlist.index('was'):]) - # 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) + geojson_path = os.path.join(geojsons_dir_cleaned, geojson_file) + geojson_url = app.get_asset_url(os.path.join('geojsons', geojson_path)) - # Remove continents and stations without coordinates / model score - data = data.dropna(subset=['lat', 'lon', self.model]) + if DEBUG: print("WAS GEOJSON URL", geojson_url) - # Transform into numeric (if needed) - for column in ['lat', 'lon', self.model]: - data[column] = pd.to_numeric(data[column]) + return geojson_url - # 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 + def generate_var_tstep_trace(self, day=0): + """ Generate trace to be added to data, per variable and timestep """ - return lon, lat, stations, scores, res + if DEBUG: print('Adding contours ...') + if not hasattr(self, 'was'): + self.geojson = self.legend = None else: - self.fig_title = "NO DATA AVAILABLE" - - return None + day = int(day) - def retrieve_scores(self): - """ Run plot """ + names, colors, definitions = self.get_regions_data(day=day) + colors = np.array(colors) + if DEBUG: print("::::::::::", 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_url = 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_url, 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): + """ run plot """ + + self.generate_var_tstep_trace(day) if DEBUG: print('Update layout ...') - if self.data is not None: - lon, lat, stations, scores, res = self.select_data() - if DEBUG: print('Adding one point ...') - # Get figure title + if self.input_file is not None: self.fig_title = html.P(html.B( [ - item for sublist in self.get_title().split('
') + item + for sublist in self.get_title(day=day).split('
') for item in [sublist, html.Br()] ][:-1] )) - layers = self.generate_var_tstep_trace(lon, lat, stations, scores, res) else: self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) - layers = [None, None] # Get title information element - info = self.retrieve_info(self.name) + self.info = self.retrieve_info(self.name) - return layers + [info] + return [self.geojson, self.legend, self.info] - def generate_var_tstep_trace(self, lon, lat, stations, scores, res): - """ Generate trace to be added to data, per variable and timestep """ - # 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'] +class EvaluationGroundFigureHandler(PointsFigureHandler): + """ Class which handles AERONET observations data """ + + def __init__(self, sdate=None, edate=None, obs=None, var=None): - # Add tooltips (hover information) to map - data_dict = self.get_tooltip(df, var_list=var_list) + self.name = obs + super(EvaluationGroundFigureHandler, self).__init__() + self.circle_options = OBS[obs]['circle_options'] - # Create 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}) - colorbar = self.retrieve_colorbar() + 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' - # Create geojson - geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") - namespace = Namespace("evaluationTab", "evaluationMaps") - geojson = self.retrieve_geojson(geojson_data, namespace) - - return [geojson, colorbar] + lon = input_files[0].variables[lon_var][:] + lat = input_files[0].variables[lat_var][:] + self.var = [var for var in input_files[0].variables if var == OBS[obs]['obs_var']][0] + if DEBUG: print('VARNAME', self.var) - def get_title(self, **kwargs): - """ Return title from base title and elements """ + sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[obs]['sites'])) - 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(ScoresFigureHandler, self).get_title() - - return title + 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 -class VisFigureHandler(PointsFigureHandler): - """ Class to manage the figure creation """ + if DEBUG: print('IDXS', idxs) + if DEBUG: print('ST_NAMES', self.station_names) - def __init__(self, selected_date=None): - - self.name = 'vis' - super(VisFigureHandler, self).__init__() + self.clon = lon[idxs.astype(int)] + self.clat = lat[idxs.astype(int)] - 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'] + self.values = { + self.var: np.concatenate([input_file.variables[self.var][:, idxs.astype(int)] for input_file in input_files]) + } - if selected_date: - self.selected_date_plain = selected_date + def generate_var_tstep_trace(self): + """ Generate trace to be added to data, per variable and timestep """ + + 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] - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") - else: - self.selected_date_plain = None - self.selected_date = None + # 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') - self.rdatetime = datetime.strptime(self.selected_date_plain, '%Y%m%d') + # 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) - def set_data(self, tstep=0): - """ Set time dependent data """ + def get_figure_layers(self): + + self.generate_var_tstep_trace() - tstep0 = tstep - tstep1 = tstep + self.freq + return self.df, [self.geojson] - 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 [], [], [], [], [], () +class EvaluationGroundTimeSeriesHandler: + """ Class to handle evaluation time series """ - data = pd.read_table(filepath, na_filter=False) + 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') - # uncertain - cx = np.where((data['WW'].astype(str) == "HZ") | (data['WW'].astype(str) == "5")| (data['WW'].astype(str) == "05")) + fname_tpl = os.path.join(OBS[obs]['path'], + 'feather', + '{{}}-{dat}-{{}}_interp.ft') - # 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),) + months = np.unique([d.strftime("%Y%m") for d in self.date_range.to_pydatetime()]) - # 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),) + 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') - # 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),) + if DEBUG: + print('MONTHS', months) + if DEBUG: + print('DATE_INDEX', self.date_index) - xlon = data['LON'].values - ylat = data['LAT'].values - stations = data['STATION'].values + 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) - visibility = 'VV' in data and data['VV'] - humidity = 'HUMIDITY' in data and data['HUMIDITY'] + 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) - if DEBUG: print("VIS DATA", xlon, ylat, stations, (c0, c1, c2, cx)) + 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) - return xlon, ylat, stations, visibility, humidity, (c0, c1, c2, cx) - def generate_var_tstep_trace(self, xlon, ylat, stations, visibility, humidity, values, color, labels, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ + 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 }) - # Create legend - legend = self.create_legend(self.colormap) + 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 - # Assign colors to values - n_points = len(xlon) - res = np.zeros((n_points)) - for i, (value, label) in enumerate(zip(values, labels)): - res[value] = i + if DEBUG: print("SELECTING COORDS") + if 'lat' in df.columns: + lat_col = 'lat' + lon_col = 'lon' + else: + lat_col = 'latitude' + lon_col = 'longitude' - # Create dataframe - df = pd.DataFrame({ - 'station': stations, - 'lon': np.array(xlon).round(2), - 'lat': np.array(ylat).round(2), - 'visibility': (visibility/1e3).round(2), - 'humidity': humidity.astype(int), - 'value': res - }) - - # Add tooltips (hover information) to map - data_dict = self.get_tooltip(df, - var_list=['station', 'lon', 'lat', 'visibility', 'humidity']) - - # Create geojson - geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") - namespace = Namespace("observationsTab", "observationsMaps") - geojson = self.retrieve_geojson(geojson_data, namespace) - - if list(visibility): - 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.fig_title = "NO DATA AVAILABLE" + 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 - # Get title information element - info = self.retrieve_info(self.name) + if mod == 'median': + line['dash'] = 'dash' - return [geojson, info, legend] + 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 + ) + ) - def get_title(self, **kwargs): - """ Return title from base title and elements """ + title = "{} @ {} (lat = {:.2f}, lon = {:.2f})".format( + VARS[self.variable]['name'], st_name, cur_lat, cur_lon, + ) - 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() + 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"), + ]) + ) + ) - return title + if DEBUG: print('FIG TYPE', type(fig)) + return fig - def retrieve_var_tstep(self, tstep=0, hour=None, static=True, aspect=(1,1), center=None): - """ run plot """ - tstep = int(tstep) +class EvaluationSatelliteFigureHandler(ContourFigureHandler): - xlon, ylat, stations, visibility, humidity, values = self.set_data(tstep) - if tstep is not None: - return self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, - self.colors, self.labels, tstep) - if DEBUG: print('Adding one point ...') + def __init__(self, var, obs, tstep, selected_date): + + self.obs = obs + self.name = self.obs + super(EvaluationSatelliteFigureHandler, self).__init__() + + self.var = var + self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') + + self.filedir = OBS[self.obs]['path'] + filetpl = OBS[self.obs]['template'].format(OBS[self.obs]['obs_var'], selected_date) + '.nc' + self.filepath = os.path.join(self.filedir, 'netcdf', filetpl) + self.input_file = nc_file(self.filepath) + time_obj = self.input_file.variables['time'] + self.timesteps = 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") + + self.selected_date_plain = selected_date + self.selected_date = datetime.strptime( + selected_date, "%Y%m%d").strftime("%Y-%m-%d") + + def generate_var_tstep_trace(self, tstep): - return None + from dash_server import app + data_path = os.path.basename(OBS[self.obs]['path'][:-1]) + + geojson_url = app.get_asset_url(os.path.join('geojsons', + GEOJSON_TEMPLATE.format(data_path, + self.selected_date_plain, tstep, self.selected_date_plain, + OBS[self.obs]['obs_var']))) + + print('PATH', geojson_url) -class ProbFigureHandler(ContourFigureHandler): - """ Class to manage the figure creation """ + if DEBUG: print("OBS", self.obs, "GEOJSON_URL", geojson_url) - def __init__(self, var=None, prob=None, selected_date=None): - """ Initialization with variable, prob and date """ + style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) - self.name = 'prob' - super(ProbFigureHandler, self).__init__() + # Create 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() - if var is None: - var = DEFAULT_VAR + if DEBUG: print("BOUNDS", self.bounds) + if DEBUG: print("CTG", ctg) - self.varname = var + # Geojson rendering logic, must be JavaScript as it is executed in clientside. + namespace = Namespace("forecastTab", "forecastMaps") + hideout = dict(colorscale=COLORS, bounds=self.bounds, style=style, colorProp="value") + self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) - probs = PROB[var]['prob_thresh'] - if prob is None: - prob = probs[0] + def get_title(self, **kwargs): + """ Return title from base title and elements """ - self.bounds = np.arange(0, 110, 10) + varname = kwargs['varname'] + tstep = kwargs['tstep'] - self.prob = prob + 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 - geojson_path = PROB[var]['geojson_path'] - geojson_file = PROB[var]['geojson_template'] - netcdf_path = PROB[var]['netcdf_path'] - netcdf_file = PROB[var]['netcdf_template'] + def get_figure_layers(self, tstep=0): + """ Run plot """ - 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) + self.generate_var_tstep_trace(tstep) - if os.path.exists(self.filepath): - self.input_file = nc_file(self.filepath) + if DEBUG: print('Update layout ...') - if 'lon' in self.input_file.variables: - lon = self.input_file.variables['lon'][:] - lat = self.input_file.variables['lat'][:] + if self.var and not os.path.exists(self.filepath): + if self.obs in VARS[OBS]: + title = "{} - DATA NOT AVAILABLE".format(OBS[self.obs]['name']) else: - lon = self.input_file.variables['longitude'][:] - lat = self.input_file.variables['latitude'][:] - time_obj = self.input_file.variables['time'] - self.timesteps = 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) + title = "DATA NOT AVAILABLE" + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + else: + 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] + )) - if selected_date: - self.selected_date_plain = selected_date + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.colorbar, self.info] - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") - self.fig = None +class EvaluationStatisticsFigureHandler(PointsFigureHandler): + """ Class to manage the figure creation """ - 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] + def __init__(self, network=None, statistic=None, model=None, selection=None): - return xlon, ylat, var + self.name = 'scores' + super(EvaluationStatisticsFigureHandler, self).__init__() - 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 + # self.extent = SCORES['extent'] + self.model = model + self.network = network + self.statistic = statistic + self.selection = selection - if DEBUG: print("##############", tstep) + if self.network and self.model and self.statistic and self.selection: - geojson_file = self.geojson.format(step=tstep) - geojson_url = app.get_asset_url(geojson_file.replace('/data/daily_dashboard', 'geojsons')) + # 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'] - name = VARS[varname]['name'] - bounds = self.bounds - colorscale = COLORS_PROB + # 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}) - if DEBUG: print("GEOJSON_URL", geojson_url) + # Read data + self.read_data() + + # 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 = datetime(year, month, 1) - style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) + else: + self.data = None - # 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))) - self.colorbar_info.update({'min': -0.1, - 'max': len(ctg)-.7, - 'classes': indices, - 'colorscale': colorscale, - 'tickValues': indices, - 'tickText': ctg}) - colorbar = self.retrieve_colorbar() + def read_data(self): + + # Get path and file + filedir = OBS[self.network]['path'] + filename = "{}_{}.h5".format(self.selection, self.statistic) + tab_name = "{}_{}".format(self.statistic, self.selection) + filepath = os.path.join(filedir, "h5", filename) + self.data = pd.read_hdf(filepath, tab_name) + + if DEBUG: print('SCORES filepath', filepath, 'SELECTION', self.selection, 'TAB', tab_name) - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - namespace = Namespace("forecastTab", "forecastMaps") - hideout = dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") - geojson = self.retrieve_geojson(geojson_url, namespace, hideout) + def select_data(self): - return geojson, colorbar + if self.model in self.data.columns: - def get_title(self, **kwargs): - """ Return title from base title and elements """ + # 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] - varname = kwargs['varname'] - tstep = kwargs['tstep'] + # Transform empty values into np.nan + data = self.data.replace('-', np.nan) - 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 + # Remove continents and stations without coordinates / model score + data = data.dropna(subset=['lat', 'lon', self.model]) - self.base_title = PROB[varname]['title'] - self.cdatetime = self.rdatetime + relativedelta(days=tstep) + # Transform into numeric (if needed) + for column in ['lat', 'lon', self.model]: + data[column] = pd.to_numeric(data[column]) - title = super(ProbFigureHandler, self).get_title() + # 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 + + return None + + def get_figure_layers(self): + """ Run plot """ + + if DEBUG: print('Update layout ...') + + if self.data is not None: + + # Get data + lon, lat, stations, scores, res = self.select_data() - return title + # Get trace + self.generate_var_tstep_trace(lon, lat, stations, scores, res) - 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') - self.fig_title = '' - elif varname and not os.path.exists(self.filepath): - if DEBUG: print('TWO') - self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) - else: - if DEBUG: print('THREE') + # Get figure title self.fig_title = html.P(html.B( [ - item for sublist in self.get_title(varname=varname, tstep=tstep).split('
') + item for sublist in self.get_title().split('
') for item in [sublist, html.Br()] ][:-1] )) - # Get title information element - info = self.retrieve_info(self.name) + # Get title information element + self.info = self.retrieve_info(self.name) - return geojson, colorbar, info + return[self.geojson, self.colorbar, self.info] + else: + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + return [None, None, None] + def generate_var_tstep_trace(self, lon, lat, stations, scores, res): + """ Generate trace to be added to data, per variable and timestep """ -class WasFigureHandler(ShapefileFigureHandler): - """ Class to manage the figure creation """ + # 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) - def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=None): - """ Initialize WasFigureHandler with shapefile and netCDF data """ + # Create 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() + + # 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 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() - self.name = 'was' - super(WasFigureHandler, self).__init__() + return title - self.model = model - self.was = was - self.variable = variable - 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}) +class VisFigureHandler(PointsFigureHandler): + """ Class to manage the figure creation """ - 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'] - ) + def __init__(self, selected_date=None): + + self.name = 'vis' + super(VisFigureHandler, self).__init__() - if os.path.exists(filepath): - self.input_file = nc_file(filepath) - time_obj = self.input_file.variables['time'] - self.timesteps = 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 + 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 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 - # Correct timesteps to show first timestep of each day ([0, 24, 48, ...]) - self.timesteps = self.timesteps[::8] + self.rdatetime = datetime.strptime(self.selected_date_plain, '%Y%m%d') - self.fig = None + def set_data(self, tstep=0): + """ Set time dependent data """ - 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') + tstep0 = tstep + tstep1 = tstep + self.freq - input_path = os.path.join(input_dir, input_file) + 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') - if DEBUG: print("INPUT PATH", input_path, self.selected_date_plain) - if not os.path.exists(input_path): - return [], [], [] + 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 [], [], [], [], [], () - df = pd.read_hdf(input_path, 'was_{}'.format(self.selected_date_plain)).set_index('day') + data = pd.read_table(filepath, na_filter=False) - names, colors, definitions = df.loc['Day{}'.format(day)].values.T - return names, colors, definitions + # uncertain + cx = np.where((data['WW'].astype(str) == "HZ") | (data['WW'].astype(str) == "5")| (data['WW'].astype(str) == "05")) - 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 + # 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),) - pathlist = os.path.normpath(geojsons_dir).split(os.sep) - geojsons_dir_cleaned = os.sep.join(pathlist[pathlist.index('was'):]) + # 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),) - geojson_path = os.path.join(geojsons_dir_cleaned, geojson_file) - geojson_url = app.get_asset_url(os.path.join('geojsons', geojson_path)) + # 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),) - if DEBUG: print("WAS GEOJSON URL", geojson_url) + xlon = data['LON'].values + ylat = data['LAT'].values + stations = data['STATION'].values - return geojson_url + visibility = 'VV' in data and data['VV'] + humidity = 'HUMIDITY' in data and data['HUMIDITY'] - def generate_contour_tstep_trace(self, day=0): - """ Generate trace to be added to data, per variable and timestep """ + if DEBUG: print("VIS DATA", xlon, ylat, stations, (c0, c1, c2, cx)) - if not hasattr(self, 'was'): - return None, None + return xlon, ylat, stations, visibility, humidity, (c0, c1, c2, cx) - names, colors, definitions = self.get_regions_data(day=day) - colors = np.array(colors) - if DEBUG: print("::::::::::", names, colors, definitions) + def generate_var_tstep_trace(self, xlon, ylat, stations, visibility, humidity, values, color, labels, tstep=0): + """ Generate trace to be added to data, per variable and timestep """ # Create legend - legend = self.create_legend(self.colormap) - - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - namespace = Namespace("forecastTab", "wasMaps") - geojson_url = 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) - geojson = self.retrieve_geojson(geojson_url, namespace, hideout, hover_style) - - return geojson, legend - - 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(WasFigureHandler, self).get_title() + self.legend = self.create_legend(self.colormap) - return title + # Assign colors to values + n_points = len(xlon) + res = np.zeros((n_points)) + for i, (value, label) in enumerate(zip(values, labels)): + res[value] = i - def retrieve_var_tstep(self, day=0, static=True, aspect=(1,1)): - """ run plot """ + # Create dataframe + df = pd.DataFrame({ + 'station': stations, + 'lon': np.array(xlon).round(2), + 'lat': np.array(ylat).round(2), + 'visibility': (visibility/1e3).round(2), + 'humidity': humidity.astype(int), + 'value': res + }) - 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 ...') + # Add tooltips (hover information) to map + data_dict = self.get_tooltip(df, + var_list=['station', 'lon', 'lat', 'visibility', '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) - if self.input_file is not None: + if list(visibility): self.fig_title = html.P(html.B( [ - item - for sublist in self.get_title(day=day).split('
') + item for sublist in self.get_title(tstep=tstep).split('
') for item in [sublist, html.Br()] ][:-1] )) else: - self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) + self.fig_title = "NO DATA AVAILABLE" # Get title information element - info = self.retrieve_info(self.name) + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.info, self.legend] + + 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, hour=None): + """ run plot """ + + tstep = int(tstep) + + xlon, ylat, stations, visibility, humidity, values = self.set_data(tstep) + if tstep is not None: + return self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, + self.colors, self.labels, tstep) + if DEBUG: print('Adding one point ...') - return geojson, legend, info + return None diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 0913f66..3168d7d 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -2,6 +2,7 @@ """ Core """ import os +import time import json import copy from dash import html @@ -12,6 +13,7 @@ from dateutil.relativedelta import relativedelta DIR_PATH = os.path.dirname(os.path.realpath(__file__)) COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json'))) +STYLES = json.load(open(os.path.join(DIR_PATH, 'conf/map_layers.json'))) DEBUG = True class MapHandler: @@ -20,9 +22,6 @@ class MapHandler: # TODO: Add common variables from all figure handlers here - if self.name in COLORBARS: - self.colorbar_info = COLORBARS[self.name]['colorbar'] - self.info_style = MODEBARS['info_style'] return None @@ -115,6 +114,10 @@ class MapHandler: if DEBUG: print('================= Retrieving figure title box...') + # Box width to match colorbar width + if hasattr(self, 'colorbar'): + self.info_style['width'] = str(self.colorbar.width) + "px" + # Create figure title box info = html.Div( children=self.fig_title, @@ -125,7 +128,7 @@ class MapHandler: return info - def retrieve_colorbar(self): + def retrieve_colorbar(self, aspect=(1,1)): if DEBUG: print('================= Retrieving colorbar...') @@ -133,14 +136,106 @@ class MapHandler: # 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 hour_to_step(self, hour): + """ Convert hour to relative tstep """ + cdatetime = self.rdatetime.date() + relativedelta(hours=hour) + + for step in range(len(self.timesteps)): + if self.retrieve_cdatetime(step) == cdatetime: + return step + + return 0 + + 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 retrieve_fig(self, aspect=(1,1), center=None, selected_tiles='carto-positron', + zoom=None, tag='empty', index=None, layers=None): + """ run plot """ + + self.st_time = time.time() + + if DEBUG: + print('================= Retrieving figure...') + + 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 DEBUG: print("ZOOM", zoom) + if DEBUG: print("CENTER", 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] + + if DEBUG: print("TAG", tag) + if DEBUG: print("INDEX", index) + fig = dl.Map(children=[ + dl.TileLayer( + id=dict( + tag=tag_template_tile.format(tag), + index=index + ), + url=STYLES[selected_tiles]['url'], + attribution=STYLES[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", + ) + + if DEBUG: print("*** FIGURE EXECUTION TIME: {} ***".format(str(time.time() - self.st_time))) + + return fig + class PointsFigureHandler(MapHandler): def __init__(self): super(PointsFigureHandler, self).__init__() + if self.name in COLORBARS: + self.colorbar_info = COLORBARS[self.name]['colorbar'] + # TODO: Add common variables from VisFigureHandler, Observations1dHandler and ScoresFigureHandler return None @@ -194,6 +289,11 @@ class ContourFigureHandler(MapHandler): super(ContourFigureHandler, self).__init__() + if self.name in COLORBARS: + self.colorbar_info = COLORBARS[self.name]['colorbar'] + else: + self.colorbar_info = COLORBARS['model']['colorbar'] + # TODO: Add common variables from FigureHandler and ProbFigureHandler return None diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index f277fe2..9137754 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -189,8 +189,8 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, orig_stats): """ Read scores tables and plot maps """ - from tools import get_evaluation_scores_figure - from tools import get_models_figure + from tools import get_evaluation_statistics_figure + from tools import get_figure mb = MODEBAR_LAYOUT_TS @@ -205,8 +205,8 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, if model is not None and score is not None: if DEBUG: print('::: 1 :::') - layers = get_evaluation_scores_figure(network, model, score, selection) - fig = get_models_figure(model=None, var=score, layer=layers) + layers = get_evaluation_statistics_figure(network, model, score, selection) + fig = get_figure(layers=layers) return fig, True, model, score raise PreventUpdate @@ -216,14 +216,14 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, print(':::', orig_model, orig_stats, ':::') curr_model = [mod for mod in MODELS if mod in orig_model][0] curr_score = [sc for sc in SCORES if sc in orig_stats][0] - layers = get_evaluation_scores_figure(network, curr_model, curr_score, selection) - fig = get_models_figure(model=None, var=curr_score, layer=layers) + 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 :::') - layers = get_evaluation_scores_figure(network, model, score, selection) - fig = get_models_figure(model=None, var=score, layer=layers) + 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 @@ -703,8 +703,9 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): if sdate is None or edate is None or obs != 'aeronet': raise PreventUpdate - from tools import get_models_figure - from tools import get_evaluation_visual_comparison_figure + from tools import get_figure + from tools import get_evaluation_comparison_aeronet_figure + if DEBUG: print('SERVER: calling figure from EVAL picker callback') if DEBUG: print('SERVER: SDATE', str(sdate)) @@ -730,8 +731,8 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): else: edate = END_DATE - stations, points_layer = get_evaluation_visual_comparison_figure(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(sdate, edate, obs, DEFAULT_VAR) + fig = get_figure(layers=points_layer) return stations.to_dict(), fig @@ -747,6 +748,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): @cache.memoize(timeout=cache_timeout) def update_eval_modis(n_clicks, date, mod, obs, mod_div): """ Update MODIS evaluation figure according to all parameters """ + ctx = dash.callback_context if not ctx.triggered: raise PreventUpdate @@ -759,7 +761,10 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): if date is None or mod is None or obs != 'modis': raise PreventUpdate - from tools import get_models_figure + from tools import get_model_figure + from tools import get_evaluation_comparison_modis_figure + from tools import get_figure + if DEBUG: print('SERVER: calling figure from EVAL picker callback') if DEBUG: print(mod_div) mod_center = mod_div['props']['center'] @@ -783,12 +788,18 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): 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) + print("================================", 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') + return fig_obs, fig_mod @@ -802,7 +813,7 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): @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_figure # from tools import get_obs1d if DEBUG: print('SERVER: calling figure from EVAL picker callback') # if DEBUG: print('SERVER: interval ' + str(n)) @@ -825,7 +836,7 @@ def update_eval(obs): eval_graph = html.Div([ html.Div( - get_models_figure(), + get_figure(), id='graph-eval-aeronet' ), html.Div( @@ -851,8 +862,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( diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index c28f8e0..122875c 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -181,7 +181,7 @@ 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 get_figure from tools import download_image_link if DEBUG: print('GIF', models, variable, date) @@ -229,8 +229,10 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente 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 tools import get_figure + if not zoom or previous != was: zoom = WAS[was]['zoom'] else: @@ -259,8 +261,8 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente print("WAS figure " + 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') + layers = get_was_figure(was, day, selected_date=date) + fig = get_figure(view=view, zoom=zoom, center=center, tag='was', layers=layers) return fig, previous raise PreventUpdate @@ -294,9 +296,7 @@ def update_prob_timeslider(date): @cache.memoize(timeout=cache_timeout) def update_prob_figure(n_clicks, date, day, prob, var, view, 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' @@ -315,6 +315,9 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): if button_id != 'prob-apply' and var is None and day is None: raise PreventUpdate + from tools import get_prob_figure + from tools import get_figure + if date is not None: date = date.split(' ')[0] try: @@ -329,8 +332,9 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): 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') + layers = get_prob_figure(var, prob, day, selected_date=date) + fig = get_figure(view=view, zoom=zoom, center=center, tag='prob', layers=layers) + if DEBUG: print("FIG", fig) return fig raise PreventUpdate @@ -731,7 +735,9 @@ 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 + from tools import get_model_figure + from tools import get_figure + if DEBUG: print('SERVER: calling figure from picker callback') st_time = time.time() @@ -783,15 +789,28 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z 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 + # 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) + if DEBUG: print("MOD", mod, "VARIABLE", variable, "ZOOM", mod_zoom, "CENTER", mod_center, 'VIEW', view) + + # Get current tag + if mod in MODELS: + tag = 'model' + else: + tag = isinstance(tag, str) and tag or str(tag) - figure = get_models_figure(mod, variable, date, tstep, - static=static, aspect=(nrows, ncols), view=view, - center=mod_center, zoom=mod_zoom) + # 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(static=static, aspect=(nrows, ncols), view=view, center=mod_center, + zoom=mod_zoom, tag=tag, index=index, layers=layers) figure.style = { 'height': '{}vh'.format(int(GRAPH_HEIGHT/nrows)), diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index 42093ee..c80ab62 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -256,7 +256,7 @@ 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 + from tools import get_figure if DEBUG: print("*************", date, tstep, zoom, center) if date is not None: date = date.split(' ')[0] @@ -281,5 +281,5 @@ def update_vis_figure(date, tstep, zoom, center): # view = list(STYLES.keys())[view.index(True)] if DEBUG: print('SERVER: VIS callback date {}, tstep {}'.format(date, tstep)) layers = get_vis_figure(tstep=tstep, selected_date=date) - fig = get_models_figure(model=None, var=None, layer=layers, zoom=zoom, center=center, tag='obs-vis') + fig = get_figure(layers=layers, zoom=zoom, center=center, tag='obs-vis') return fig diff --git a/tools.py b/tools.py index e518de5..3543f89 100644 --- a/tools.py +++ b/tools.py @@ -6,14 +6,16 @@ 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 MapHandler +from data_handler import ForecastModelsFigureHandler +from data_handler import ForecastModelsTimeSeriesHandler +from data_handler import ForecastProbFigureHandler +from data_handler import ForecastWasFigureHandler +from data_handler import EvaluationGroundFigureHandler +from data_handler import EvaluationGroundTimeSeriesHandler +from data_handler import EvaluationSatelliteFigureHandler +from data_handler import EvaluationStatisticsFigureHandler from data_handler import VisFigureHandler -from data_handler import ScoresFigureHandler -from data_handler import TimeSeriesHandler -from data_handler import EvaluationVisualComparisonTimeSeriesHandler -from data_handler import EvaluationVisualComparisonFigureHandler from data_handler import DEBUG from data_handler import MODELS from data_handler import END_DATE, DELAY, DELAY_DATE @@ -62,7 +64,7 @@ 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 = EvaluationVisualComparisonTimeSeriesHandler(obs, start_date, end_date, var) + th = EvaluationGroundTimeSeriesHandler(obs, start_date, end_date, var) if DEBUG: print('SERVER: OBS TS generation ... ') return th.retrieve_timeseries(idx, name, model) @@ -72,7 +74,7 @@ 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) + th = ForecastModelsTimeSeriesHandler(model, date, var) if DEBUG: print('SERVER: TS generation ... ') return th.retrieve_timeseries(lat, lon, method='feather', forecast=forecast) @@ -82,26 +84,34 @@ 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) + th = ForecastModelsTimeSeriesHandler(model, date, var) if DEBUG: print('SERVER: SINGLE POINT generation ... ') return th.retrieve_single_point(tstep, lat, lon) -def get_evaluation_visual_comparison_figure(sdate, edate, obs, var): +def get_evaluation_comparison_aeronet_figure(sdate, edate, obs, var): """ Retrieve evaluation visual comparison figure """ - obs_handler = EvaluationVisualComparisonFigureHandler(sdate, edate, obs) + fh = EvaluationGroundFigureHandler(sdate, edate, obs, var) - return obs_handler.generate_obs1d_tstep_trace(var) + return fh.get_figure_layers() -def get_evaluation_scores_figure(network=None, model=None, statistic=None, selection=None): +def get_evaluation_comparison_modis_figure(var, obs, tstep=0, selected_date=END_DATE): + """ Retrieve evaluation visual comparison figure """ + + 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 scores figure """ if DEBUG: print('SERVER: SCORES Figure init ... ') - scores_handler = ScoresFigureHandler(network, statistic, model, selection=selection) + fh = EvaluationStatisticsFigureHandler(network, statistic, model, selection=selection) if network and model and statistic: if DEBUG: @@ -110,7 +120,48 @@ def get_evaluation_scores_figure(network=None, model=None, statistic=None, selec if DEBUG: print('SERVER: NO SCORES Figure') - return scores_handler.retrieve_scores() + return fh.get_figure_layers() + + +def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, aspect=(1, 1)): + """ Retrieve figure """ + + if DEBUG: + print("***", model, var, selected_date, tstep, hour, "***") + try: + selected_date = dt.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 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(var, model, tstep, hour, selected_date) + if var: + if DEBUG: + print('SERVER: MODELS Figure init ... ') + fh = ForecastModelsFigureHandler(var=var, model=model, tstep=tstep, hour=hour, selected_date=selected_date) + if DEBUG: + print('SERVER: MODELS Figure generation ... ') + return fh.get_figure_layers(tstep=tstep, hour=hour, aspect=aspect) + if DEBUG: + print('SERVER: NO MODELS Figure') + + return ForecastModelsFigureHandler().get_figure_layers() def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): @@ -127,13 +178,13 @@ def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): if prob: if DEBUG: print('SERVER: PROB Figure init ... ') - fh = ProbFigureHandler(var=var, prob=prob, selected_date=selected_date) + fh = ForecastProbFigureHandler(var=var, prob=prob, selected_date=selected_date) if DEBUG: print('SERVER: PROB Figure generation ... ') - return fh.retrieve_var_tstep(day=day) + return fh.get_figure_layers(day=day) if DEBUG: print('SERVER: NO PROB Figure') - return ProbFigureHandler().retrieve_var_tstep() + return ForecastProbFigureHandler().get_figure_layers() def get_was_figure(was=None, day=0, selected_date=END_DATE): @@ -150,13 +201,13 @@ def get_was_figure(was=None, day=0, selected_date=END_DATE): if was: if DEBUG: print('SERVER: WAS Figure init ... ') - fh = WasFigureHandler(was=was, selected_date=selected_date) + fh = ForecastWasFigureHandler(was=was, selected_date=selected_date) if DEBUG: print('SERVER: WAS Figure generation ... ') - return fh.retrieve_var_tstep(day=day) + return fh.get_figure_layers(day=day) if DEBUG: print('SERVER: NO WAS Figure') - return WasFigureHandler().retrieve_var_tstep() + return ForecastWasFigureHandler().get_figure_layers() def get_vis_figure(tstep=0, selected_date=END_DATE): @@ -176,46 +227,24 @@ def get_vis_figure(tstep=0, selected_date=END_DATE): fh = VisFigureHandler(selected_date=selected_date) if DEBUG: print('SERVER: VIS Figure generation ... ') - return fh.retrieve_var_tstep(tstep=tstep) + return fh.get_figure_layers(tstep=tstep) if DEBUG: print('SERVER: NO VIS Figure') - return VisFigureHandler().retrieve_var_tstep() + return VisFigureHandler().get_figure_layers() -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'): +def get_figure(selected_date=END_DATE, tstep=0, static=True, aspect=(1, 1), center=None, + view='carto-positron', zoom=None, layers=None, index=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) + print('***** SERVER: Figure generation: CURR_DATE', selected_date, 'TSTEP', tstep, '*****') + + fh = MapHandler() + return fh.retrieve_fig(aspect=aspect, center=center, selected_tiles=view, + zoom=zoom, tag=tag, index=index, layers=layers) if DEBUG: print('SERVER: No Figure') - return FigureHandler().retrieve_var_tstep(layer=layer, center=center, selected_tiles=view, zoom=zoom, tag=tag) + + return MapHandler().retrieve_fig() -- GitLab From 82960a4ecc598e9a9cef5f6ee10075fef87cb6d2 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Tue, 27 Jun 2023 15:28:58 +0200 Subject: [PATCH 34/71] Remove bundle.js --- assets/bundle.js | 2 -- assets/bundle.js.map | 1 - 2 files changed, 3 deletions(-) delete mode 100644 assets/bundle.js delete mode 100644 assets/bundle.js.map diff --git a/assets/bundle.js b/assets/bundle.js deleted file mode 100644 index 184bb88..0000000 --- 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 -- GitLab From 21714d684e49a4b69048e4e1c06b192ae3c00b82 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 28 Jun 2023 12:45:30 +0200 Subject: [PATCH 35/71] update test_tools --- tests/test_tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_tools.py b/tests/test_tools.py index 780626a..3a02c9d 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -47,9 +47,9 @@ def test_get_vis_figure(): # use import pdb; pdb.set_trace() to open interactive debugger to examine what is returned #import pdb; pdb.set_trace() code_run = code.get_vis_figure(tstep=0, selected_date='20220808') - assert code_run[1][1].children.children.children[2] == '08 Aug 2022 00-06 UTC' - assert code_run[1][1].id == 'vis-info' - assert code_run[1][2].children[0].children[0].className == 'vis-legend-point' + assert code_run[1].children.children.children[2] == '08 Aug 2022 00-06 UTC' + assert code_run[1].id == 'vis-info' + assert code_run[2].children[0].children[0].className == 'vis-legend-point' def test_get_models_figure(): result = f"/dashboard/assets/geojsons/median/geojson/{END_DATE}/00_{END_DATE}_OD550_DUST.geojson" -- GitLab From aaaa444deeb990ad3d5456b8e25ebfbf60696db0 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 28 Jun 2023 14:39:47 +0200 Subject: [PATCH 36/71] Fix most broken tests --- data_handler.py | 454 +++++++++++++++++++---------------- ines_core_data_handler.py | 6 +- preproc/nc2scores_aeronet.py | 4 +- preproc/nc2scores_modis.py | 4 +- tests/test_data_handler.py | 309 +++++++++++------------- tests/test_tools.py | 29 ++- tools.py | 3 +- 7 files changed, 405 insertions(+), 404 deletions(-) diff --git a/data_handler.py b/data_handler.py index 5e5cc4e..876ba6c 100644 --- a/data_handler.py +++ b/data_handler.py @@ -141,20 +141,22 @@ class ForecastModelsFigureHandler(ContourFigureHandler): self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') self.filedir = MODELS[self.model]['path'] - filepath = NETCDF_TEMPLATE.format(self.filedir, selected_date, MODELS[self.model]['template']) - self.input_file = nc_file(filepath) - time_obj = self.input_file.variables['time'] - self.timesteps = 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") + self.filepath = NETCDF_TEMPLATE.format(self.filedir, selected_date, MODELS[self.model]['template']) + + if os.path.exists(self.filepath): + self.input_file = nc_file(self.filepath) + time_obj = self.input_file.variables['time'] + self.timesteps = 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") self.selected_date_plain = selected_date self.selected_date = datetime.strptime( @@ -193,29 +195,26 @@ class ForecastModelsFigureHandler(ContourFigureHandler): # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "forecastMaps") hideout = dict(colorscale=COLORS, bounds=self.bounds, style=style, colorProp="value") + self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): """ run plot """ - if hour is not None: - tstep = int(self.hour_to_step(hour)) - else: - tstep = int(tstep) - - # Get trace - self.generate_var_tstep_trace(tstep) - if DEBUG: print('Update layout ...') - if not self.var: - self.fig_title = "" - elif self.var and not self.filedir: - if self.model in MODELS: - title = "{} - DATA NOT AVAILABLE".format(MODELS[self.model]['name']) + + if os.path.exists(self.filepath) and self.var: + + # Get timestep + if hour is not None: + tstep = int(self.hour_to_step(hour)) else: - title = "DATA NOT AVAILABLE" - self.fig_title = html.P(html.B(title)) - else: + tstep = int(tstep) + + # Get trace + 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('
') @@ -223,16 +222,29 @@ class ForecastModelsFigureHandler(ContourFigureHandler): ][:-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) - else: - self.info = None + + # Get title information element + self.info = self.retrieve_info(name=self.model) return [self.geojson, self.colorbar, self.info] @@ -459,21 +471,14 @@ class ForecastProbFigureHandler(ContourFigureHandler): netcdf_path = PROB[self.var]['netcdf_path'] netcdf_file = PROB[self.var]['netcdf_template'] - self.geojson = os.path.join(geojson_path, geojson_file).format(prob=prob, - date=selected_date, - var=self.var) + self.geojsonpath = os.path.join(geojson_path, geojson_file).format(prob=prob, + date=selected_date, + var=self.var) self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=prob, date=selected_date, var=self.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.timesteps = time_obj[:] tim_units = time_obj.units.split() @@ -486,24 +491,20 @@ class ForecastProbFigureHandler(ContourFigureHandler): 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 generate_var_tstep_trace(self, tstep=0): """ Generate trace to be added to data, per variable and timestep """ from dash_server import app if DEBUG: print("##############", tstep) - geojson_file = self.geojson.format(step=tstep) + geojson_file = self.geojsonpath.format(step=tstep) geojson_url = app.get_asset_url(geojson_file.replace('/data/daily_dashboard', 'geojsons')) name = VARS[self.var]['name'] @@ -536,7 +537,6 @@ class ForecastProbFigureHandler(ContourFigureHandler): def get_title(self, **kwargs): """ Return title from base title and elements """ - varname = kwargs['varname'] tstep = kwargs['tstep'] self.members = 0 @@ -548,7 +548,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): if os.path.exists(fpath): self.members += 1 - self.base_title = PROB[varname]['title'] + self.base_title = PROB[self.var]['title'] self.cdatetime = self.rdatetime + relativedelta(days=tstep) title = super(ForecastProbFigureHandler, self).get_title() @@ -557,26 +557,36 @@ class ForecastProbFigureHandler(ContourFigureHandler): def get_figure_layers(self, day=0): """ run plot """ - tstep = int(day) + + if DEBUG: print("***", self.var, day, self.geojsonpath, self.filepath) + + if DEBUG: print('Update layout ...') - if DEBUG: print("***", self.var, day, static, self.geojson, self.filepath) + if self.var and os.path.exists(self.filepath): + + # Get timestep + tstep = int(day) - if DEBUG: print('Adding contours ...') - if self.var and os.path.exists(self.geojson.format(step=day)): + # Get trace + if DEBUG: print('Adding contours...') self.generate_var_tstep_trace(tstep) - if DEBUG: print('Update layout ...') - if not self.var: - self.fig_title = "" - elif self.var and not os.path.exists(self.filepath): - self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) - else: + # 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) @@ -678,11 +688,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): if DEBUG: print('Adding contours ...') - if not hasattr(self, 'was'): - self.geojson = self.legend = None - else: - day = int(day) - + day = int(day) names, colors, definitions = self.get_regions_data(day=day) colors = np.array(colors) if DEBUG: print("::::::::::", names, colors, definitions) @@ -712,12 +718,14 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): def get_figure_layers(self, day=0): """ run plot """ - - self.generate_var_tstep_trace(day) if DEBUG: print('Update layout ...') if self.input_file is not None: + # Get trace + self.generate_var_tstep_trace(day) + + # Get figure title self.fig_title = html.P(html.B( [ item @@ -726,6 +734,10 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): ][:-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 @@ -806,6 +818,7 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): def get_figure_layers(self): + # Get trace self.generate_var_tstep_trace() return self.df, [self.geojson] @@ -1008,8 +1021,6 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): self.selected_date_plain, tstep, self.selected_date_plain, OBS[self.obs]['obs_var']))) - print('PATH', geojson_url) - if DEBUG: print("OBS", self.obs, "GEOJSON_URL", geojson_url) style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) @@ -1050,17 +1061,23 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): def get_figure_layers(self, tstep=0): """ Run plot """ - self.generate_var_tstep_trace(tstep) - if DEBUG: print('Update layout ...') if self.var and not os.path.exists(self.filepath): + self.geojson = None + self.colorbar = None + + # Get figure title if self.obs in VARS[OBS]: title = "{} - DATA NOT AVAILABLE".format(OBS[self.obs]['name']) else: title = "DATA NOT AVAILABLE" self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) else: + # Get trace + 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('
') @@ -1107,9 +1124,6 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): else: self.sites = None self.circle_options.update({'radius': 2}) - - # Read data - self.read_data() # Get selected month / year # Set day to first date of the month to transform into datetime and avoid errors creating title @@ -1117,81 +1131,80 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): month = int(self.selection[4:6]) self.rdatetime = datetime(year, month, 1) - else: - self.data = None - def read_data(self): - # Get path and file - filedir = OBS[self.network]['path'] - filename = "{}_{}.h5".format(self.selection, self.statistic) - tab_name = "{}_{}".format(self.statistic, self.selection) - filepath = os.path.join(filedir, "h5", filename) - self.data = pd.read_hdf(filepath, tab_name) - - if DEBUG: print('SCORES filepath', filepath, 'SELECTION', self.selection, 'TAB', tab_name) + if self.network and self.model and self.statistic and self.selection: + filedir = OBS[self.network]['path'] + filename = "{}_{}.h5".format(self.selection, self.statistic) + tab_name = "{}_{}".format(self.statistic, self.selection) + filepath = os.path.join(filedir, "h5", filename) + if os.path.exists(filepath): + self.data = pd.read_hdf(filepath, tab_name) + if DEBUG: print('SCORES filepath', filepath, 'SELECTION', self.selection, 'TAB', tab_name) + else: + self.data = None + else: + self.data = None def select_data(self): - if self.model in self.data.columns: - - # 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 - - return None + # 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 get_figure_layers(self): """ Run plot """ if DEBUG: print('Update layout ...') - if self.data is not None: - + # Read data + self.read_data() + + if (self.data is not None) and (self.model in self.data.columns): # Get data lon, lat, stations, scores, res = self.select_data() @@ -1206,14 +1219,15 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): for item in [sublist, html.Br()] ][:-1] )) - - # Get title information element - self.info = self.retrieve_info(self.name) - - return[self.geojson, self.colorbar, self.info] else: + self.geojson = None + self.colorbar = None self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) - return [None, None, None] + + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.colorbar, self.info] def generate_var_tstep_trace(self, lon, lat, stations, scores, res): """ Generate trace to be added to data, per variable and timestep """ @@ -1294,9 +1308,8 @@ class VisFigureHandler(PointsFigureHandler): self.rdatetime = datetime.strptime(self.selected_date_plain, '%Y%m%d') - def set_data(self, tstep=0): - """ Set time dependent data """ - + def read_data(self, tstep): + tstep0 = tstep tstep1 = tstep + self.freq @@ -1306,85 +1319,86 @@ class VisFigureHandler(PointsFigureHandler): 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 [], [], [], [], [], () + if os.path.exists(filepath): + self.data = pd.read_table(filepath, na_filter=False) + else: + self.data = None - data = pd.read_table(filepath, na_filter=False) + def select_data(self, tstep=0): + """ Set time dependent data """ - # uncertain - cx = np.where((data['WW'].astype(str) == "HZ") | (data['WW'].astype(str) == "5")| (data['WW'].astype(str) == "05")) + # Uncertain + cx = np.where((self.data['WW'].astype(str) == "HZ") | (self.data['WW'].astype(str) == "5")| (self.data['WW'].astype(str) == "05")) - # vis <= 1km - c0t = np.where((data['VV'] <= 1000))[0] + # 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),) - # vis 1km <= 2km - c1t = np.where((data['VV'] > 1000) & (data['VV'] <= 2000))[0] + # 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),) - # vis 2km <= 5km - c2t = np.where((data['VV'] > 2000) & (data['VV'] <= 5000))[0] + # 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),) - xlon = data['LON'].values - ylat = data['LAT'].values - stations = data['STATION'].values + xlon = self.data['LON'].values + ylat = 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 [] - visibility = 'VV' in data and data['VV'] - humidity = 'HUMIDITY' in data and data['HUMIDITY'] - - if DEBUG: print("VIS DATA", xlon, ylat, stations, (c0, c1, c2, cx)) + # 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) - return xlon, ylat, stations, visibility, humidity, (c0, c1, c2, cx) + return xlon, ylat, stations, np.array(visibility), np.array(humidity), (c0, c1, c2, cx) def generate_var_tstep_trace(self, xlon, ylat, stations, visibility, humidity, values, color, labels, tstep=0): """ Generate trace to be added to data, per variable and timestep """ - # Create legend - self.legend = self.create_legend(self.colormap) - - # Assign colors to values - n_points = len(xlon) - res = np.zeros((n_points)) - for i, (value, label) in enumerate(zip(values, labels)): - res[value] = i - - # Create dataframe - df = pd.DataFrame({ - 'station': stations, - 'lon': np.array(xlon).round(2), - 'lat': np.array(ylat).round(2), - 'visibility': (visibility/1e3).round(2), - 'humidity': humidity.astype(int), - 'value': res - }) + if list(xlon) and list(ylat) and list(stations) and list(visibility) and list(humidity): + + # Create legend + self.legend = self.create_legend(self.colormap) + + # Assign colors to values + n_points = len(xlon) + res = np.zeros((n_points)) + for i, (value, label) in enumerate(zip(values, labels)): + res[value] = i + + # Create dataframe + df = pd.DataFrame({ + 'station': stations, + 'lon': xlon.round(2), + 'lat': ylat.round(2), + 'visibility': (visibility/1e3).round(2), + 'humidity': humidity.astype(int), + 'value': res + }) - # Add tooltips (hover information) to map - data_dict = self.get_tooltip(df, - var_list=['station', 'lon', 'lat', 'visibility', 'humidity']) + # Replace -999 by np.nan to revert previous change + df['humidity'] = df['humidity'].replace(-999, float('nan')) - # Create geojson - geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") - namespace = Namespace("observationsTab", "observationsMaps") - self.geojson = self.retrieve_geojson(geojson_data, namespace) + # Add tooltips (hover information) to map + data_dict = self.get_tooltip(df, + var_list=['station', 'lon', 'lat', 'visibility', '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) - if list(visibility): - 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.fig_title = "NO DATA AVAILABLE" - - # Get title information element - self.info = self.retrieve_info(self.name) - - return [self.geojson, self.info, self.legend] + self.geojson = None + self.legend = None def get_title(self, **kwargs): """ Return title from base title and elements """ @@ -1404,12 +1418,26 @@ class VisFigureHandler(PointsFigureHandler): def get_figure_layers(self, tstep=0, hour=None): """ run plot """ - tstep = int(tstep) + # Read data + self.read_data(tstep) - xlon, ylat, stations, visibility, humidity, values = self.set_data(tstep) - if tstep is not None: - return self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, - self.colors, self.labels, tstep) - if DEBUG: print('Adding one point ...') + if self.data is not None: + tstep = int(tstep) + xlon, ylat, stations, visibility, humidity, values = self.select_data(tstep) + self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, + self.colors, self.labels, tstep) + 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 + self.fig_title = "NO DATA AVAILABLE" - return None + # Get title information element + self.info = self.retrieve_info(self.name) + + return [self.geojson, self.info, self.legend] diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 3168d7d..2597253 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -116,7 +116,8 @@ class MapHandler: # Box width to match colorbar width if hasattr(self, 'colorbar'): - self.info_style['width'] = str(self.colorbar.width) + "px" + if self.colorbar is not None: + self.info_style['width'] = str(self.colorbar.width) + "px" # Create figure title box info = html.Div( @@ -271,7 +272,8 @@ class PointsFigureHandler(MapHandler): tooltip = """""" tooltip_keys = [key for key in var_list if key in item.keys()] for key in tooltip_keys: - if item[key] in (False, ''): + # 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]) diff --git a/preproc/nc2scores_aeronet.py b/preproc/nc2scores_aeronet.py index c616294..0fca75b 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 data_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 be0d6bb..6263107 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 data_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/tests/test_data_handler.py b/tests/test_data_handler.py index 1e78613..0aafb85 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -17,27 +17,21 @@ 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")) +# =================== AERONET OBSERVATIONS HANDLER ============================ @pytest.fixture -def aeronet_instance(): - return code.Observations1dHandler(EDATE_PREV, END_DATE , 'aeronet') +def EvaluationGroundFigureHandler(): + return code.EvaluationGroundFigureHandler(sdate=EDATE_PREV, edate=END_DATE , obs='aeronet', + var='OD550_DUST') -def test_generate_obs1d_tstep_trace1(aeronet_instance): - run = aeronet_instance.generate_obs1d_tstep_trace('OD550_DUST') - assert str(type(run[1])) == "" +def test_aeronet_get_figure_layers(EvaluationGroundFigureHandler): -# @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' + EvaluationGroundFigureHandler.get_figure_layers() + assert str(type(EvaluationGroundFigureHandler.geojson)) == "" - # =================== TIME SERIES HANDLER ============================ @pytest.fixture def TSHandler(): - return code.TimeSeriesHandler('median', END_DATE, 'OD550_DUST') + return code.ForecastModelsTimeSeriesHandler('median', END_DATE, 'OD550_DUST') # def test_TimeSeriesHandler(TSHandler): # run = TSHandler.retrieve_single_point(1, 45, 45, model='median') @@ -59,68 +53,33 @@ 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' -# =================== 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_get_title(FigureHandler): - assert FigureHandler.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)) - assert FigureHandler.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_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 ============================ +# =================== Evaluation Statistics Figure Handler ============================ @pytest.fixture -def ScoresFigureHandler(): - return code.ScoresFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) +def EvaluationStatisticsFigureHandler(): + return code.EvaluationStatisticsFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) -# =================== Vis Handler ============================ +# =================== Visibility Figure 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'), 6) - assert len(returned) == 3 - assert returned[1].id == 'vis-info' - assert returned[1].children == "NO DATA AVAILABLE" + return code.VisFigureHandler(selected_date=END_DATE) + +def test_vis_generate_var_tstep_trace_empty(VisFigureHandler): + returned = VisFigureHandler.generate_var_tstep_trace([], [], [], [], [], (), + ('#714921', '#da7230', '#fcd775', 'CadetBlue'), + ('<1 km', '1 - 2 km', '2 - 5 km', 'Haze'), + 6) + assert VisFigureHandler.geojson is None + assert VisFigureHandler.legend is None + +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 VisFigureHandler.info.children == "NO DATA 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)) @@ -130,106 +89,118 @@ def test_vis_get_title(VisFigureHandler): # =================== 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(varname='OD550_DUST', tstep=0)[-30:] == '{edate} Valid: {edate}'.format(edate=EDATE_OBJ.strftime(FMT_MON)) - assert ProbFigureHandler.get_title(varname='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'}} - +def ForecastProbFigureHandler(): + ForecastProbFigureHandler_OD550_DUST = code.ForecastProbFigureHandler(var='OD550_DUST', prob=0.1, selected_date=END_DATE) + ForecastProbFigureHandler_SCONC_DUST = code.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_generate_var_tstep_trace_AOD(ForecastProbFigureHandler): + # day 0 for od550_dust + assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=0)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/00_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=0)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=0)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + # day 1 for od550_dust + assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=1)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/01_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=1)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=1)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +def test_prob_generate_var_tstep_trace_SCONC(ForecastProbFigureHandler): + # day 0 for sconc_dust + assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=0)[0].url == '/dashboard/assets/geojsons/prob/sconc_dust/50/geojson/{edate}/00_{edate}_SCONC_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=0)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=0)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + + # day 1 for sconc_dust + assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=1)[0].url == '/dashboard/assets/geojsons/prob/sconc_dust/50/geojson/{edate}/01_{edate}_SCONC_DUST.geojson'.format(edate=END_DATE) + assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=1)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} + assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=1)[1].classes == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + +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 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 ForecastWasFigureHandler(): + return code.ForecastWasFigureHandler(was='burkinafaso', model='median', variable='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) == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) + assert ForecastWasFigureHandler.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_retrieve_cdatetime(ForecastWasFigureHandler): + assert str(ForecastWasFigureHandler.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_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(WasFigureHandler): - assert str(WasFigureHandler.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(WasFigureHandler.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)) - -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'}} +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_tools.py b/tests/test_tools.py index 3060475..1d6da37 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -44,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(): - # use import pdb; pdb.set_trace() to open interactive debugger to examine what is returned - #import pdb; pdb.set_trace() - code_run = code.get_vis_figure(tstep=0, selected_date='20220808') - assert code_run[1][1].children.children.children[2] == '08 Aug 2022 00-06 UTC' - assert code_run[1][1].id == 'vis-info' - assert code_run[1][2].children[0].children[0].className == 'vis-legend-point' + code_run = code.get_vis_figure(tstep=0, selected_date='20230321') + assert code_run[1].children.children.children[2] == '21 Mar 2023 00-06 UTC' + assert code_run[1].id == 'vis-info' + assert code_run[2].children[0].children[0].className == 'vis-legend-point' 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/tools.py b/tools.py index 3543f89..2319f03 100644 --- a/tools.py +++ b/tools.py @@ -128,6 +128,7 @@ def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, asp if DEBUG: print("***", model, var, selected_date, tstep, hour, "***") + try: selected_date = dt.strptime( selected_date, "%Y-%m-%d").strftime("%Y%m%d") @@ -148,7 +149,7 @@ def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, asp # 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(var, model, tstep, hour, selected_date) if var: -- GitLab From 3231c69ef617383d185591c52394f5908c60f524 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 28 Jun 2023 15:19:52 +0200 Subject: [PATCH 37/71] Add INES core tests --- tests/test_data_handler.py | 5 --- tests/test_ines_data_handler.py | 56 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) create mode 100644 tests/test_ines_data_handler.py diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index 0aafb85..b7e62c3 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -9,11 +9,6 @@ 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) diff --git a/tests/test_ines_data_handler.py b/tests/test_ines_data_handler.py new file mode 100644 index 0000000..cf20963 --- /dev/null +++ b/tests/test_ines_data_handler.py @@ -0,0 +1,56 @@ +import pytest +from datetime import datetime +from datetime import timedelta +import importlib +code = importlib.import_module('ines_core_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_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_step(MapHandler): + assert MapHandler.hour_to_step(0) == 0 + assert MapHandler.hour_to_step(3) == int(3/FREQ) + assert MapHandler.hour_to_step(6) == int(6/FREQ) + assert MapHandler.hour_to_step(36) == int(36/FREQ) -- GitLab From e460aed3969b53b98c8f347ff12b257aab4e29ff Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 28 Jun 2023 16:46:49 +0200 Subject: [PATCH 38/71] Add logging to all application files. Data handlers still need coverage --- dash_server.py | 10 ++- data_handler.py | 2 + router.py | 48 ++++++------- tabs/evaluation_callbacks.py | 115 ++++++++++++++--------------- tabs/forecast.py | 7 +- tabs/forecast_callbacks.py | 112 ++++++++++++++--------------- tabs/observations.py | 7 +- tabs/observations_callbacks.py | 127 +++------------------------------ tools.py | 103 +++++++++----------------- utils.py | 21 +++--- 10 files changed, 206 insertions(+), 346 deletions(-) diff --git a/dash_server.py b/dash_server.py index 4a29155..4c684cd 100755 --- a/dash_server.py +++ b/dash_server.py @@ -7,12 +7,16 @@ from dash import html from flask.app import Flask from data_handler import DEBUG +from data_handler import DASH_LOG_LEVEL from data_handler import cache from data_handler import PATHNAME from data_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" @@ -40,7 +44,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,12 +90,12 @@ 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 * diff --git a/data_handler.py b/data_handler.py index 2164bba..9b507b6 100644 --- a/data_handler.py +++ b/data_handler.py @@ -37,9 +37,11 @@ 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 = True # False +DASH_LOG_LEVEL = logging.DEBUG # INFO WARNING WARNING ERROR # Set up cache cache_config = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) diff --git a/router.py b/router.py index 013c57a..e73b1c0 100644 --- a/router.py +++ b/router.py @@ -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): @@ -73,10 +74,7 @@ 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(): @@ -118,29 +116,29 @@ def render404(): 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.info('===== 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.debug("ERROR 404 %s", str(err)) + children = render404() return children diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 291c285..ff03604 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -30,6 +30,7 @@ import pandas as pd import orjson import os.path from random import random +import logging SCORES = list(STATS.keys()) @@ -150,7 +151,7 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) stat = ['model'] + stat - if DEBUG: print("###########", models, stat, network, timescale, selection, n) + logging.debug("########### %s %s %s %s %s %s", models, stat, network, timescale, selection, n) filedir = OBS[network]['path'] filename = f"{selection}_scores.h5" tab_name = f"total_{selection}" @@ -160,9 +161,8 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) df = pd.read_hdf(filepath, tab_name) ret = df.loc[df['model'].isin(models), stat] 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() @@ -196,15 +196,13 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, 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 :::') + logging.debug('::: 1 :::') layers = get_scores_figure(network, model, score, selection) fig = get_models_figure(model=None, var=score, layer=layers) return fig, True, model, score @@ -212,8 +210,7 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, 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_score = [sc for sc in SCORES if sc in orig_stats][0] layers = get_scores_figure(network, curr_model, curr_score, selection) @@ -221,7 +218,7 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, return fig, True, curr_model, curr_score else: - print('::: 2.5 :::') + logging.debug('::: 2.5 :::') layers = get_scores_figure(network, model, score, selection) fig = get_models_figure(model=None, var=score, layer=layers) return fig, True, model, score @@ -300,14 +297,14 @@ def aeronet_scores_tables_retrieve(n, *args): 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 @@ -327,7 +324,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, len(tables)) filedir = OBS[network]['path'] stat_idxs = [SCORES.index(st) for st in stat] @@ -353,15 +350,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) @@ -374,15 +371,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 @@ -400,9 +396,8 @@ def aeronet_scores_tables_retrieve(n, *args): 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 @@ -431,8 +426,8 @@ 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 @@ -454,15 +449,15 @@ def show_eval_modis_timeseries(nclicks, coords, date, obs, model): raise PreventUpdate 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 or ctxt != 'ts-eval-modis-button': # or nclicks == 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) @@ -486,23 +481,23 @@ def show_eval_modis_timeseries(nclicks, coords, date, obs, model): 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)) + 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: @@ -557,15 +552,15 @@ def stations_popup(click_data, mapid, stations): if not click_data: 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 @@ -575,7 +570,7 @@ def stations_popup(click_data, mapid, stations): 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 @@ -613,19 +608,19 @@ 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()) + logging.debug("MARKER %s", marker.to_plotly_json()) last = mapid[-1] if last['type'] == 'Popup': mapid[-1] = marker.to_plotly_json() else: mapid.append(marker.to_plotly_json()) - print("LAST", type(mapid), type(last), last) + logging.debug("LAST %s %s %s", type(mapid), type(last), last) for _, log in enumerate(mapid): if log is not None: # mapid[pos]['id']['random'] = if DEBUG: - print("********", type(log), log.keys()) - # print(log['type']) + 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 @@ -647,7 +642,7 @@ def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, mode """ Retrieve AERONET evaluation timeseries according to station selected """ from 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 @@ -657,8 +652,8 @@ def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, mode 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] @@ -668,7 +663,7 @@ def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, mode figure = get_eval_timeseries(obs, start_date, end_date, DEFAULT_VAR, idx, stat, 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, stat, start_date, end_date) return dbc.ModalBody( dcc.Graph( id='timeseries-eval-modal', @@ -694,7 +689,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): 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: @@ -705,8 +700,8 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): 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)) + logging.debug('SERVER: calling figure from EVAL picker callback') + logging.debug('SERVER: SDATE %s', str(sdate)) sdate = sdate.split()[0] try: @@ -716,7 +711,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): sdate = END_DATE pass if DEBUG: - print(f'SERVER: callback start_date {sdate}') + logging.debug('SERVER: callback start_date %s', sdate) if edate is not None: edate = edate.split()[0] @@ -726,7 +721,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): except: pass if DEBUG: - print(f'SERVER: callback end_date {edate}') + logging.debug('SERVER: callback end_date %s', edate) else: edate = END_DATE @@ -752,7 +747,7 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): 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 @@ -760,8 +755,8 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): raise PreventUpdate from tools import get_models_figure - if DEBUG: print('SERVER: calling figure from EVAL picker callback') - if DEBUG: print(mod_div) + logging.debug('SERVER: calling figure from EVAL picker callback') + logging.debug(" %s ", mod_div) mod_center = mod_div['props']['center'] mod_zoom = mod_div['props']['zoom'] @@ -773,12 +768,12 @@ 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: @@ -788,7 +783,7 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): 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("MODIS %s", fig_obs) return fig_obs, fig_mod @@ -804,8 +799,8 @@ 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)) + logging.debug('SERVER: calling figure from EVAL picker callback') + # logging.debug('SERVER: interval %s', str(n)) start_date = OBS[obs]['start_date'] diff --git a/tabs/forecast.py b/tabs/forecast.py index 809bc56..465cacc 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -16,6 +16,7 @@ from data_handler import START_DATE, END_DATE, DELAY, DELAY_DATE from data_handler import WAS from data_handler import DISCLAIMER_MODELS from tabs.generic import get_forecast_days, layout_view, time_series +import logging # MOVED TO GENERIC # def get_forecast_days(curdate=END_DATE): @@ -23,7 +24,7 @@ from tabs.generic import get_forecast_days, layout_view, time_series 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()) @@ -78,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'] = '' @@ -127,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([ diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index c28f8e0..720a9c8 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 @@ -134,7 +135,7 @@ 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 @@ -159,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( @@ -184,13 +185,13 @@ def download_anim_link(models, variable, date, tstep): # from tools import get_models_figure from 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( @@ -223,7 +224,7 @@ def update_was_timeslider(date): ) def update_was_figure(n_clicks, date, day, was, var, previous, view, 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, view, zoom, center) ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] @@ -242,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: @@ -250,13 +251,11 @@ 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) @@ -308,7 +307,7 @@ 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, view, zoom, center) ctx = dash.callback_context if ctx.triggered: button_id = ctx.triggered[0]["prop_id"].split(".")[0] @@ -322,7 +321,7 @@ 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 @@ -331,7 +330,7 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): 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) + logging.debug("FIG %s", fig) return fig raise PreventUpdate @@ -378,23 +377,22 @@ def update_styles_button(*args): 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)) + logging.debug("BUTTON ID %s %s", str(button_id), type(button_id)) if button_id['index'] in STYLES: - if DEBUG: print("CURRENT ARGS", str(args)) + logging.debug("CURRENT ARGS %s", str(args)) active = args[-1] - if DEBUG: print("NUM GRAPHS", num_graphs) + logging.debug("NUM GRAPHS %s", 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) + 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( @@ -414,15 +412,15 @@ def update_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)) + 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 @@ -430,7 +428,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 @@ -443,7 +441,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 @@ -464,9 +462,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=[ @@ -505,16 +503,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] } @@ -525,8 +522,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 @@ -553,11 +550,11 @@ def show_timeseries(ts_button, mod, date, variable, coords, popups): 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 @@ -571,7 +568,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( @@ -642,7 +639,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 @@ -665,14 +662,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( @@ -693,11 +690,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) @@ -732,12 +729,11 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z raise PreventUpdate from tools import get_models_figure - if DEBUG: print('SERVER: calling figure from picker callback') + 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] @@ -746,7 +742,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 @@ -761,8 +757,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) } @@ -777,17 +773,17 @@ 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) + logging.debug('#### ZOOM, CENTER: %s %s %s %s %s', 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 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, view) figure = get_models_figure(mod, variable, date, tstep, static=static, aspect=(nrows, ncols), view=view, @@ -799,13 +795,13 @@ 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) + logging.debug('STATIC %s %s', static, int(GRAPH_HEIGHT)/nrows) 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( @@ -823,5 +819,5 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z 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/observations.py b/tabs/observations.py index c5ac711..1c681b6 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -15,6 +15,7 @@ from utils import get_vis_edate from datetime import datetime as dt from datetime import timedelta +import logging aod_end_date = '20210318' @@ -42,7 +43,7 @@ layout_view = html.Div([ 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 +54,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 +89,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' diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index 42093ee..1406b7a 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -13,12 +13,10 @@ 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 """ - @dash.callback( [Output('observations-tab', 'children'), Output('rgb', 'style'), @@ -35,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: @@ -53,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'), @@ -130,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] @@ -143,7 +76,7 @@ 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 = SATELLITE_IMAGE_SRC['middle_east'] @@ -159,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 @@ -177,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: @@ -197,54 +130,16 @@ 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) -# 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'), [Input('obs-vis-date-picker', 'date'), @@ -257,7 +152,7 @@ def update_obs_slider(n): 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) + logging.debug("************* %s %s %s %s", date, tstep, zoom, center) if date is not None: date = date.split(' ')[0] try: @@ -279,7 +174,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)) + logging.debug('SERVER: VIS callback date %s tstep %s', date, tstep) layers = get_vis_figure(tstep=tstep, selected_date=date) fig = get_models_figure(model=None, var=None, layer=layers, zoom=zoom, center=center, tag='obs-vis') return fig diff --git a/tools.py b/tools.py index f947ee4..9e64045 100644 --- a/tools.py +++ b/tools.py @@ -5,6 +5,7 @@ from datetime import datetime as dt from datetime import timedelta import os +import logging from data_handler import FigureHandler from data_handler import WasFigureHandler @@ -21,28 +22,23 @@ 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()) + logging.debug('CURRDIR %s', 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) + logging.debug('DOWNLOAD MODELS %s', model) else: - if DEBUG: - print('DOWNLOAD ALL MODELS') + logging.debug('DOWNLOAD ALL MODELS') model = "all" if anim: tstep = "loop" ext = "gif" - if DEBUG: - print('DOWNLOAD LOOP') + logging.debug('DOWNLOAD LOOP') else: tstep = "%02d" % tstep ext = "png" - if DEBUG: - print('DOWNLOAD PNG', tstep) + logging.debug('DOWNLOAD PNG %s', tstep) filename = filepath.format( model=model, @@ -53,38 +49,31 @@ def download_image_link(models, variable, curdate, tstep=0, anim=False): tstep=tstep, ext=ext ) - if DEBUG: - print('DOWNLOAD FILENAME', filename) + logging.debug('DOWNLOAD FILENAME %s', 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))) + logging.debug('SERVER: OBS TS init for obs %s ... ', str(obs)) th = ObsTimeSeriesHandler(obs, start_date, end_date, var) - if DEBUG: - print('SERVER: OBS TS generation ... ') + logging.debug('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))) + logging.debug('SERVER: TS init for models %s ... ', str(model)) th = TimeSeriesHandler(model, date, var) - if DEBUG: - print('SERVER: TS generation ... ') + 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): """ Retrieve sigle point """ - if DEBUG: - print('SERVER: SINGLE POINT init for models {} ... '.format(str(model))) + logging.debug('SERVER: SINGLE POINT init for models %s ... ',str(model)) th = TimeSeriesHandler(model, date, var) - if DEBUG: - print('SERVER: SINGLE POINT generation ... ') + logging.debug('SERVER: SINGLE POINT generation ... ') return th.retrieve_single_point(tstep, lat, lon) @@ -97,86 +86,68 @@ def get_obs1d(sdate, edate, obs, var): def get_scores_figure(network=None, model=None, statistic=None, selection=None): """ Retrieve 1D observation """ - if DEBUG: - print('SERVER: SCORES Figure init ... ') + logging.debug('SERVER: SCORES Figure init ... ') scores_handler = ScoresFigureHandler(network, statistic, model, selection=selection) if network and model and statistic: - if DEBUG: - print('SERVER: SCORES Figure generation ... ') + logging.debug('SERVER: SCORES Figure generation ... ') else: - if DEBUG: - print('SERVER: NO SCORES Figure') + logging.debug('SERVER: NO SCORES Figure') return scores_handler.retrieve_scores() def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): """ Retrieve figure """ - if DEBUG: - print(prob, day, selected_date) + logging.debug(' %s %s %s',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) + logging.debug(' %s %s %s', prob, day, selected_date) if prob: - if DEBUG: - print('SERVER: PROB Figure init ... ') + logging.debug('SERVER: PROB Figure init ... ') fh = ProbFigureHandler(var=var, prob=prob, selected_date=selected_date) - if DEBUG: - print('SERVER: PROB Figure generation ... ') + logging.debug('SERVER: PROB Figure generation ... ') return fh.retrieve_var_tstep(day=day) - if DEBUG: - print('SERVER: NO PROB Figure') + logging.debug('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) + logging.debug(' %s %s %s', 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) + logging.debug(' %s %s %s', was, day, selected_date) if was: - if DEBUG: - print('SERVER: WAS Figure init ... ') + logging.debug('SERVER: WAS Figure init ... ') fh = WasFigureHandler(was=was, selected_date=selected_date) - if DEBUG: - print('SERVER: WAS Figure generation ... ') + logging.debug('SERVER: WAS Figure generation ... ') return fh.retrieve_var_tstep(day=day) - if DEBUG: - print('SERVER: NO WAS Figure') + logging.debug('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) + logging.debug(' %s %s', 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) + logging.debug(' %s %s', tstep, selected_date) if tstep is not None: - if DEBUG: - print('SERVER: VIS Figure init ... ') + logging.debug('SERVER: VIS Figure init ... ') fh = VisFigureHandler(selected_date=selected_date) - if DEBUG: - print('SERVER: VIS Figure generation ... ') + logging.debug('SERVER: VIS Figure generation ... ') return fh.retrieve_var_tstep(tstep=tstep) - if DEBUG: - print('SERVER: NO VIS Figure') + logging.debug('SERVER: NO VIS Figure') return VisFigureHandler().retrieve_var_tstep() @@ -184,8 +155,7 @@ 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, "***") + logging.debug("*** %s %s %s %s %s ***", model, var, selected_date, tstep, hour) try: selected_date = dt.strptime( selected_date, "%Y-%m-%d %H:%M:%S").strftime("%Y%m%d") @@ -196,8 +166,7 @@ def get_models_figure(model=None, var=None, selected_date=END_DATE, tstep=0, 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""" + 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} * @@ -207,13 +176,11 @@ def get_models_figure(model=None, var=None, selected_date=END_DATE, tstep=0, # 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, '*****') + logging.debug('***** SERVER: Figure generation: CURR_DATE %s TSTEP %s *****', selected_date, 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') + logging.debug('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 cbacd77..c8848fd 100644 --- a/utils.py +++ b/utils.py @@ -12,6 +12,7 @@ import xarray as xr import numpy as np import pandas as pd import feather +import logging def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None): @@ -21,7 +22,7 @@ def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=Non opaths = [fname_tpl.format(month, variable) for month in months if os.path.exists(fname_tpl.format(month, variable))] - print("__________", opaths, "__________") + logging.debug("__________ %s __________", opaths) if not opaths: return None, None @@ -52,16 +53,16 @@ def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=Non def retrieve_single_point(fname, tstep, lat, lon, variable): """ """ from data_handler import DEBUG - if DEBUG: print(fname, tstep, lat, lon, variable) + 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() - # print('TIMESERIES', fname, variable, lon, lat) + # 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') - if DEBUG: print(da) + logging.debug('%s', da) return da.values[tstep] @@ -99,7 +100,7 @@ def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=Fal preprocess=preprocess) if variable not in ds.variables: variable = variable.lower() - # print('TIMESERIES', fname, variable, lon, lat) + # 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' @@ -208,7 +209,7 @@ def get_vis_edate(end_date, hour=None): curr1 = curr1 - half_day if (edate >= curr + delay) and (edate < curr1 + delay): - if DEBUG: print("NOW", edate, "CURR", curr, "H", curr.hour) + if DEBUG: logging.debug("NOW %s CURR %s H %s", edate, curr, curr.hour) break if curr is not None: @@ -221,7 +222,7 @@ def get_currdate_tstep(model_start, model_start_before, current_time_before, del """ Returns date and timestep """ # MODELS starting at 00 with 3 days of forecast - print("Starting at 0 and NOT DELAYED: 3 days 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) @@ -229,7 +230,7 @@ def get_currdate_tstep(model_start, model_start_before, current_time_before, del 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!") + 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") - @@ -239,7 +240,7 @@ def get_currdate_tstep(model_start, model_start_before, current_time_before, del tstep = int(tstep) - 4 cdo_tsteps = "1/21" else: - print("Starting at 12 and NOT DELAYED: 3 days forecast!") + 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") @@ -247,7 +248,7 @@ def get_currdate_tstep(model_start, model_start_before, current_time_before, del cdo_tsteps = "5/29" # MODELS starting at 00 with 2 days of forecast elif delayed: - print("Starting at 0 and DELAYED: 2 days forecast!") + logging.debug("Starting at 0 and DELAYED: 2 days forecast!") cdo_tsteps = "5/25" return selected_date, tstep, cdo_tsteps -- GitLab From 67cbec4e01966e5518877c6f2b2f7484c1347bf9 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 28 Jun 2023 16:52:59 +0200 Subject: [PATCH 39/71] Remove selected_data_plain and create get_time_details func --- data_handler.py | 330 +++++++++++++++-------------------- ines_core_data_handler.py | 19 ++ tabs/evaluation_callbacks.py | 1 + tests/test_data_handler.py | 8 - 4 files changed, 159 insertions(+), 199 deletions(-) diff --git a/data_handler.py b/data_handler.py index 876ba6c..0473607 100644 --- a/data_handler.py +++ b/data_handler.py @@ -15,14 +15,13 @@ 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 +import time +import calendar from datetime import timedelta +from datetime import datetime +from dateutil.relativedelta import relativedelta from utils import concat_dataframes from utils import retrieve_timeseries @@ -40,7 +39,7 @@ import uuid import socket DIR_PATH = os.path.dirname(os.path.realpath(__file__)) -DEBUG = True +DEBUG = False # Set up cache CACHE = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) @@ -83,8 +82,8 @@ 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")) +END_DATE = END_DATE or (DELAY and (datetime.now() - + timedelta(days=1)).strftime("%Y%m%d") or datetime.now().strftime("%Y%m%d")) # Set up dashboard basic properties GRAPH_HEIGHT = DASH_STYLE['graph_height'] @@ -138,29 +137,18 @@ class ForecastModelsFigureHandler(ContourFigureHandler): super(ForecastModelsFigureHandler, self).__init__() self.var = var + self.selected_date = selected_date + self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') - self.filedir = MODELS[self.model]['path'] - self.filepath = NETCDF_TEMPLATE.format(self.filedir, selected_date, MODELS[self.model]['template']) - - if os.path.exists(self.filepath): - self.input_file = nc_file(self.filepath) - time_obj = self.input_file.variables['time'] - self.timesteps = 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") - - self.selected_date_plain = selected_date - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") + 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() def generate_var_tstep_trace(self, tstep=0): """ Generate trace to be added to data, per variable and timestep """ @@ -169,8 +157,7 @@ class ForecastModelsFigureHandler(ContourFigureHandler): data_path = os.path.basename(MODELS[self.model]['path']) if DEBUG: print(data_path) geojson_url = app.get_asset_url(os.path.join('geojsons', - GEOJSON_TEMPLATE.format(data_path, - self.selected_date_plain, tstep, self.selected_date_plain, + GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, self.var))) if DEBUG: print("MODEL", self.model, "GEOJSON_URL", geojson_url) @@ -462,41 +449,28 @@ class ForecastProbFigureHandler(ContourFigureHandler): if prob is None: prob = probs[0] - self.bounds = np.arange(0, 110, 10) - self.prob = prob + self.selected_date = selected_date + + self.bounds = np.arange(0, 110, 10) geojson_path = PROB[self.var]['geojson_path'] geojson_file = PROB[self.var]['geojson_template'] netcdf_path = PROB[self.var]['netcdf_path'] netcdf_file = PROB[self.var]['netcdf_template'] + # Get GeoJSON file path self.geojsonpath = os.path.join(geojson_path, geojson_file).format(prob=prob, - date=selected_date, + date=self.selected_date, var=self.var) - self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=prob, date=selected_date, + # Get NetCDF file path + self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=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.input_file = nc_file(self.filepath) - time_obj = self.input_file.variables['time'] - self.timesteps = 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") - - if selected_date: - self.selected_date_plain = selected_date - - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") + self.timesteps, self.what, self.rdatetime = self.get_time_details() def generate_var_tstep_trace(self, tstep=0): """ Generate trace to be added to data, per variable and timestep """ @@ -606,68 +580,52 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): self.model = model self.was = was self.variable = variable + 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 and selected_date: - # read nc file - if DEBUG: print("MODEL", model) - filepath = NETCDF_TEMPLATE.format( + 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(filepath): - self.input_file = nc_file(filepath) - time_obj = self.input_file.variables['time'] - self.timesteps = 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") - - # Correct timesteps to show first timestep of each day ([0, 24, 48, ...]) - self.timesteps = self.timesteps[::8] - - self.fig = None + + 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] 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_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.variable, format='h5') input_path = os.path.join(input_dir, input_file) - if DEBUG: print("INPUT PATH", input_path, self.selected_date_plain) + if DEBUG: print("INPUT PATH", input_path, self.selected_date) if not os.path.exists(input_path): return [], [], [] - df = pd.read_hdf(input_path, 'was_{}'.format(self.selected_date_plain)).set_index('day') + 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): 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)) + 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.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) @@ -721,7 +679,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): if DEBUG: print('Update layout ...') - if self.input_file is not None: + if os.path.exists(self.filepath): # Get trace self.generate_var_tstep_trace(day) @@ -762,36 +720,41 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): 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.var = [var for var in input_files[0].variables if var == OBS[obs]['obs_var']][0] - if DEBUG: print('VARNAME', self.var) - - sites = pd.read_csv(os.path.join(DIR_PATH, 'conf/', OBS[obs]['sites'])) - - 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.input_files = [nc_file(filepath) for filepath in filepaths if os.path.exists(filepath)] + + if self.input_files: - self.values = { - self.var: np.concatenate([input_file.variables[self.var][:, idxs.astype(int)] for input_file in 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] + if DEBUG: print('VARNAME', 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 + + 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.var: np.concatenate([input_file.variables[self.var][:, idxs.astype(int)] + for input_file in self.input_files]) + } def generate_var_tstep_trace(self): """ Generate trace to be added to data, per variable and timestep """ @@ -818,8 +781,13 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): def get_figure_layers(self): - # Get trace - self.generate_var_tstep_trace() + if self.input_files: + # Get trace + self.generate_var_tstep_trace() + + else: + self.geojson = None + self.df = pd.DataFrame([]) return self.df, [self.geojson] @@ -987,28 +955,19 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): super(EvaluationSatelliteFigureHandler, self).__init__() self.var = var + self.selected_date = selected_date + self.bounds = np.array(VARS[self.var.upper()]['bounds']).astype('float32') - self.filedir = OBS[self.obs]['path'] - filetpl = OBS[self.obs]['template'].format(OBS[self.obs]['obs_var'], selected_date) + '.nc' - self.filepath = os.path.join(self.filedir, 'netcdf', filetpl) - self.input_file = nc_file(self.filepath) - time_obj = self.input_file.variables['time'] - self.timesteps = 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") - - self.selected_date_plain = selected_date - self.selected_date = datetime.strptime( - selected_date, "%Y%m%d").strftime("%Y-%m-%d") + 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() def generate_var_tstep_trace(self, tstep): @@ -1017,8 +976,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): data_path = os.path.basename(OBS[self.obs]['path'][:-1]) geojson_url = app.get_asset_url(os.path.join('geojsons', - GEOJSON_TEMPLATE.format(data_path, - self.selected_date_plain, tstep, self.selected_date_plain, + GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, OBS[self.obs]['obs_var']))) if DEBUG: print("OBS", self.obs, "GEOJSON_URL", geojson_url) @@ -1068,7 +1026,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): self.colorbar = None # Get figure title - if self.obs in VARS[OBS]: + if self.obs in OBS[self.obs]: title = "{} - DATA NOT AVAILABLE".format(OBS[self.obs]['name']) else: title = "DATA NOT AVAILABLE" @@ -1131,20 +1089,21 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): month = int(self.selection[4:6]) self.rdatetime = datetime(year, month, 1) + # Read data + self.read_data() + def read_data(self): - if self.network and self.model and self.statistic and self.selection: - filedir = OBS[self.network]['path'] - filename = "{}_{}.h5".format(self.selection, self.statistic) - tab_name = "{}_{}".format(self.statistic, self.selection) - filepath = os.path.join(filedir, "h5", filename) - if os.path.exists(filepath): - self.data = pd.read_hdf(filepath, tab_name) - if DEBUG: print('SCORES filepath', filepath, 'SELECTION', self.selection, 'TAB', tab_name) - else: - self.data = None - else: - self.data = None + # 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) + if DEBUG: print('SCORES FILEPATH', self.filepath, 'SELECTION', self.selection, 'TAB', tab_name) def select_data(self): @@ -1200,11 +1159,8 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): """ Run plot """ if DEBUG: print('Update layout ...') - - # Read data - self.read_data() - if (self.data is not None) and (self.model in self.data.columns): + if (os.path.exists(self.filepath)) and (self.model in self.data.columns): # Get data lon, lat, stations, scores, res = self.select_data() @@ -1285,6 +1241,8 @@ class VisFigureHandler(PointsFigureHandler): 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']) @@ -1297,32 +1255,24 @@ class VisFigureHandler(PointsFigureHandler): self.values = VIS['values'] self.circle_options = VIS['circle_options'] - 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 - - self.rdatetime = datetime.strptime(self.selected_date_plain, '%Y%m%d') + if self.selected_date: + self.rdatetime = datetime.strptime(self.selected_date, '%Y%m%d') def read_data(self, tstep): + # Get CSV file 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 os.path.exists(filepath): - self.data = pd.read_table(filepath, na_filter=False) - else: - self.data = None + year = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%Y') + month = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%m') + day = datetime.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) + if DEBUG: print("VIS FILEPATH", self.filepath) def select_data(self, tstep=0): """ Set time dependent data """ @@ -1360,7 +1310,8 @@ class VisFigureHandler(PointsFigureHandler): return xlon, ylat, stations, np.array(visibility), np.array(humidity), (c0, c1, c2, cx) - def generate_var_tstep_trace(self, xlon, ylat, stations, visibility, humidity, values, color, labels, tstep=0): + def generate_var_tstep_trace(self, xlon, ylat, stations, visibility, humidity, values, color, + labels, tstep=0): """ Generate trace to be added to data, per variable and timestep """ if list(xlon) and list(ylat) and list(stations) and list(visibility) and list(humidity): @@ -1395,10 +1346,6 @@ class VisFigureHandler(PointsFigureHandler): geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") namespace = Namespace("observationsTab", "observationsMaps") self.geojson = self.retrieve_geojson(geojson_data, namespace) - - else: - self.geojson = None - self.legend = None def get_title(self, **kwargs): """ Return title from base title and elements """ @@ -1419,9 +1366,10 @@ class VisFigureHandler(PointsFigureHandler): """ run plot """ # Read data - self.read_data(tstep) + if self.selected_date: + self.read_data(tstep) - if self.data is not None: + if os.path.exists(self.filepath): tstep = int(tstep) xlon, ylat, stations, visibility, humidity, values = self.select_data(tstep) self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 2597253..d64cf56 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -9,6 +9,8 @@ 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 DIR_PATH = os.path.dirname(os.path.realpath(__file__)) COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) @@ -146,6 +148,23 @@ class MapHandler: return colorbar + def get_time_details(self): + + 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 = datetime.strptime("{} {}".format(rdate, rtime), "%Y-%m-%d %H:%M") + + return timesteps, what, rdatetime + def hour_to_step(self, hour): """ Convert hour to relative tstep """ cdatetime = self.rdatetime.date() + relativedelta(hours=hour) diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 9137754..4836d4a 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -733,6 +733,7 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): stations, points_layer = get_evaluation_comparison_aeronet_figure(sdate, edate, obs, DEFAULT_VAR) fig = get_figure(layers=points_layer) + return stations.to_dict(), fig diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index b7e62c3..c1ebbc3 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -58,14 +58,6 @@ def EvaluationStatisticsFigureHandler(): def VisFigureHandler(): return code.VisFigureHandler(selected_date=END_DATE) -def test_vis_generate_var_tstep_trace_empty(VisFigureHandler): - returned = VisFigureHandler.generate_var_tstep_trace([], [], [], [], [], (), - ('#714921', '#da7230', '#fcd775', 'CadetBlue'), - ('<1 km', '1 - 2 km', '2 - 5 km', 'Haze'), - 6) - assert VisFigureHandler.geojson is None - assert VisFigureHandler.legend is None - def test_vis_get_figure_layers_empty(VisFigureHandler): VisFigureHandler.path_tpl = "fakepath" VisFigureHandler.get_figure_layers(tstep=0) -- GitLab From 501e02eadfdc76a57f95d48406844da1c287d91a Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 29 Jun 2023 12:40:23 +0200 Subject: [PATCH 40/71] Update data handlers to use logging instead of print statements --- .gitignore | 4 ++ data_handler.py | 96 +++++++++++++++++++-------------------- ines_core_data_handler.py | 45 ++++++++---------- tools.py | 25 ++++------ 4 files changed, 79 insertions(+), 91 deletions(-) diff --git a/.gitignore b/.gitignore index d36a28c..3b4f12f 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/data_handler.py b/data_handler.py index 7b9ae14..b92e534 100644 --- a/data_handler.py +++ b/data_handler.py @@ -41,7 +41,7 @@ import logging DIR_PATH = os.path.dirname(os.path.realpath(__file__)) DEBUG = True # False -DASH_LOG_LEVEL = logging.DEBUG # INFO WARNING WARNING ERROR +DASH_LOG_LEVEL = logging.DEBUG # DEBUG INFO WARNING ERROR # Set up cache CACHE = json.load(open(os.path.join(DIR_PATH, 'conf/cache.json'))) @@ -157,12 +157,12 @@ class ForecastModelsFigureHandler(ContourFigureHandler): from dash_server import app data_path = os.path.basename(MODELS[self.model]['path']) - if DEBUG: print(data_path) + logging.debug('%s', data_path) geojson_url = app.get_asset_url(os.path.join('geojsons', GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, self.var))) - if DEBUG: print("MODEL", self.model, "GEOJSON_URL", geojson_url) + logging.debug("MODEL %s GEOJSON_URL %s", self.model, geojson_url) style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) @@ -178,8 +178,8 @@ class ForecastModelsFigureHandler(ContourFigureHandler): 'tickText': ctg}) self.colorbar = self.retrieve_colorbar() - if DEBUG: print("BOUNDS", self.bounds) - if DEBUG: print("CTG", ctg) + logging.debug("BOUNDS %s", self.bounds) + logging.debug("CTG %s", ctg) # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "forecastMaps") @@ -190,7 +190,7 @@ class ForecastModelsFigureHandler(ContourFigureHandler): def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): """ run plot """ - if DEBUG: print('Update layout ...') + logging.debug('Update layout ...') if os.path.exists(self.filepath) and self.var: @@ -272,7 +272,7 @@ class ForecastModelsTimeSeriesHandler: if not model: model = self.model[0] - if DEBUG: print("----------", model) + logging.debug("---------- %s", 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 \ @@ -295,7 +295,7 @@ class ForecastModelsTimeSeriesHandler: if not model: model = self.model - # if DEBUG: print("----------", model) + # logging.debug("---------- %s", model) obs_eval = model[0] not in MODELS and model[0] in OBS if obs_eval: @@ -340,22 +340,22 @@ class ForecastModelsTimeSeriesHandler: fig = go.Figure() for mod, fpath in zip(all_models, self.fpaths): - # print(mod, fpath) + # logging.debug(' %s %s', 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.") + logging.debug("NOT retrieving %s File doesn't exist.", fpath) continue - if DEBUG: print('Retrieving *** FPATH ***', fpath) + logging.debug('Retrieving *** FPATH *** %s', 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)) + logging.debug("NOT retrieving %s ERROR: %s", fpath, str(e)) continue if forecast is True or mod in ('cams', 'ema-regcm4'): @@ -478,7 +478,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): """ Generate trace to be added to data, per variable and timestep """ from dash_server import app - if DEBUG: print("##############", tstep) + logging.debug("############## %s", tstep) geojson_file = self.geojsonpath.format(step=tstep) geojson_url = app.get_asset_url(geojson_file.replace('/data/daily_dashboard', 'geojsons')) @@ -487,7 +487,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): bounds = self.bounds colorscale = COLORS_PROB - if DEBUG: print("GEOJSON_URL", geojson_url) + logging.debug("GEOJSON_URL %s", geojson_url) style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) @@ -534,9 +534,9 @@ class ForecastProbFigureHandler(ContourFigureHandler): def get_figure_layers(self, day=0): """ run plot """ - if DEBUG: print("***", self.var, day, self.geojsonpath, self.filepath) + logging.debug("*** %s %s %s %s", self.var, day, self.geojsonpath, self.filepath) - if DEBUG: print('Update layout ...') + logging.debug('Update layout ...') if self.var and os.path.exists(self.filepath): @@ -544,7 +544,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): tstep = int(day) # Get trace - if DEBUG: print('Adding contours...') + logging.debug('Adding contours...') self.generate_var_tstep_trace(tstep) # Get figure title @@ -612,7 +612,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): input_path = os.path.join(input_dir, input_file) - if DEBUG: print("INPUT PATH", input_path, self.selected_date) + logging.debug("INPUT PATH %s %s", input_path, self.selected_date) if not os.path.exists(input_path): return [], [], [] @@ -630,7 +630,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): '_{}.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) + logging.debug("ERROR: WAS GEOJSON URL %s", geojson_filepath) return None pathlist = os.path.normpath(geojsons_dir).split(os.sep) @@ -639,19 +639,19 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): 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) + logging.debug("WAS GEOJSON URL %s", geojson_url) return geojson_url def generate_var_tstep_trace(self, day=0): """ Generate trace to be added to data, per variable and timestep """ - if DEBUG: print('Adding contours ...') + logging.debug('Adding contours ...') day = int(day) names, colors, definitions = self.get_regions_data(day=day) colors = np.array(colors) - if DEBUG: print("::::::::::", names, colors, definitions) + logging.debug(":::::::::: %s %s %s", names, colors, definitions) # Create legend self.legend = self.create_legend(self.colormap) @@ -679,7 +679,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): def get_figure_layers(self, day=0): """ run plot """ - if DEBUG: print('Update layout ...') + logging.debug('Update layout ...') if os.path.exists(self.filepath): # Get trace @@ -736,7 +736,7 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): 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] - if DEBUG: print('VARNAME', self.var) + logging.debug('VARNAME %s', self.var) sites_path = os.path.join(DIR_PATH, 'conf/', OBS[obs]['sites']) if os.path.exists(sites_path): @@ -747,8 +747,8 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): 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) + 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)] @@ -804,7 +804,7 @@ class EvaluationGroundTimeSeriesHandler: self.model = models self.variable = variable self.dataframe = [] - if DEBUG: print("ObsTimeSeries", start_date, end_date) + 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'], @@ -817,10 +817,8 @@ class EvaluationGroundTimeSeriesHandler: '{}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) + 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, variable, @@ -849,16 +847,16 @@ class EvaluationGroundTimeSeriesHandler: 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) + logging.debug("RETRIEVE TS %s %s %s %s", 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) + logging.debug("MOD %s COLS %s", mod, df.columns) if df.columns[-1].upper() == self.variable: df = df.rename(columns = { df.columns[-1]: self.variable }) - if DEBUG: print("BUILDING TIME-SERIES") + logging.debug("BUILDING TIME-SERIES") try: tmp_df = df[df['station']==dict_idx[idx]].set_index('time') tmp_df.drop_duplicates(keep='first') @@ -867,10 +865,10 @@ class EvaluationGroundTimeSeriesHandler: if timeseries[self.variable].isnull().all(): continue except Exception as e: - if DEBUG: print("ERROR timeseries", str(e)) + logging.debug("ERROR timeseries %s", str(e)) continue - if DEBUG: print("SELECTING COORDS") + logging.debug("SELECTING COORDS") if 'lat' in df.columns: lat_col = 'lat' lon_col = 'lon' @@ -879,14 +877,14 @@ class EvaluationGroundTimeSeriesHandler: lon_col = 'longitude' if mod == self.obs: - if DEBUG: print("OBSERVATION") + logging.debug("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) + logging.debug("MODEL %s", mod) try: sc_mode = 'lines' marker = {} @@ -896,13 +894,13 @@ class EvaluationGroundTimeSeriesHandler: 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()) + logging.debug("ERROR MODEL %s %s %s", mod, str(e), timeseries[lat_col].dropna()) continue if mod == 'median': line['dash'] = 'dash' - if DEBUG: print("ADD TRACE") + logging.debug("ADD TRACE") fig.add_trace(dict( type='scatter', name=name, @@ -944,7 +942,7 @@ class EvaluationGroundTimeSeriesHandler: ) ) - if DEBUG: print('FIG TYPE', type(fig)) + logging.debug('FIG TYPE %s', type(fig)) return fig @@ -981,7 +979,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, OBS[self.obs]['obs_var']))) - if DEBUG: print("OBS", self.obs, "GEOJSON_URL", geojson_url) + logging.debug("OBS %s GEOJSON_URL %s", self.obs, geojson_url) style = dict(weight=0, opacity=0, color='white', dashArray='', fillOpacity=OPACITY) @@ -997,8 +995,8 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): 'tickText': ctg}) self.colorbar = self.retrieve_colorbar() - if DEBUG: print("BOUNDS", self.bounds) - if DEBUG: print("CTG", ctg) + logging.debug("BOUNDS %s", self.bounds) + logging.debug("CTG %s", ctg) # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "forecastMaps") @@ -1021,7 +1019,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): def get_figure_layers(self, tstep=0): """ Run plot """ - if DEBUG: print('Update layout ...') + logging.debug('Update layout ...') if self.var and not os.path.exists(self.filepath): self.geojson = None @@ -1105,7 +1103,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): # Read data if os.path.exists(self.filepath): self.data = pd.read_hdf(self.filepath, tab_name) - if DEBUG: print('SCORES FILEPATH', self.filepath, 'SELECTION', self.selection, 'TAB', tab_name) + logging.debug('SCORES FILEPATH %s SELECTION %s TAB %s', self.filepath, self.selection, tab_name) def select_data(self): @@ -1160,7 +1158,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): def get_figure_layers(self): """ Run plot """ - if DEBUG: print('Update layout ...') + logging.debug('Update layout ...') if (os.path.exists(self.filepath)) and (self.model in self.data.columns): # Get data @@ -1169,7 +1167,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): # Get trace self.generate_var_tstep_trace(lon, lat, stations, scores, res) - if DEBUG: print('Adding one point ...') + logging.debug('Adding one point ...') # Get figure title self.fig_title = html.P(html.B( [ @@ -1274,7 +1272,7 @@ class VisFigureHandler(PointsFigureHandler): # Read data if os.path.exists(self.filepath): self.data = pd.read_table(self.filepath, na_filter=False) - if DEBUG: print("VIS FILEPATH", self.filepath) + logging.debug("VIS FILEPATH %s", self.filepath) def select_data(self, tstep=0): """ Set time dependent data """ diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index d64cf56..8bb701a 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -11,6 +11,7 @@ import numpy as np from dateutil.relativedelta import relativedelta from netCDF4 import Dataset as nc_file from datetime import datetime +import logging DIR_PATH = os.path.dirname(os.path.realpath(__file__)) COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) @@ -31,8 +32,7 @@ class MapHandler: def get_title(self, **kwargs): """ Return title from base title and elements """ - if DEBUG: - print('================= Retrieving figure title...') + logging.debug('================= Retrieving figure title...') # Get selected and current datetime details rhour = self.rdatetime.strftime("%H") @@ -83,8 +83,7 @@ class MapHandler: def create_legend(self, colormap): - if DEBUG: - print('================= Retrieving legend...') + logging.debug('================= Retrieving legend...') # Create categorical legend given the labels and its colors legend_items = [] @@ -113,8 +112,7 @@ class MapHandler: def retrieve_info(self, name): - if DEBUG: - print('================= Retrieving figure title box...') + logging.debug('================= Retrieving figure title box...') # Box width to match colorbar width if hasattr(self, 'colorbar'): @@ -133,8 +131,7 @@ class MapHandler: def retrieve_colorbar(self, aspect=(1,1)): - if DEBUG: - print('================= Retrieving colorbar...') + logging.debug('================= Retrieving colorbar...') # Create colorbar colorbar = dl.Colorbar(**self.colorbar_info) @@ -194,16 +191,15 @@ class MapHandler: self.st_time = time.time() - if DEBUG: - print('================= Retrieving figure...') + logging.debug('================= Retrieving figure...') - if DEBUG: print("ASPECT", aspect) + logging.debug("ASPECT %s", aspect) if center is None: center = self.get_center(center) if zoom is None: zoom = 3.5 -(aspect[0]-aspect[0]*0.4) - if DEBUG: print("ZOOM", zoom) - if DEBUG: print("CENTER", center) + logging.debug("ZOOM %s", zoom) + logging.debug("CENTER %s", center) # Get index and template names index = str(index) @@ -213,8 +209,8 @@ class MapHandler: if not isinstance(layers, list): layers = [layers] - if DEBUG: print("TAG", tag) - if DEBUG: print("INDEX", index) + logging.debug("TAG %s", tag) + logging.debug("INDEX %s", index) fig = dl.Map(children=[ dl.TileLayer( id=dict( @@ -243,7 +239,7 @@ class MapHandler: #className="graph-with-slider", ) - if DEBUG: print("*** FIGURE EXECUTION TIME: {} ***".format(str(time.time() - self.st_time))) + logging.debug("*** FIGURE EXECUTION TIME: {} ***".format(str(time.time() - self.st_time))) return fig @@ -262,8 +258,7 @@ class PointsFigureHandler(MapHandler): def retrieve_geojson(self, geojson_data, namespace): - if DEBUG: - print('================= Retrieving geojson...') + logging.debug('================= Retrieving geojson...') # Get points properties hideout = {"circleOptions": self.circle_options} @@ -281,13 +276,11 @@ class PointsFigureHandler(MapHandler): def get_tooltip(self, df, var_list): - if DEBUG: - print('================= Retrieving tooltip...') + logging.debug('================= Retrieving tooltip...') data_dict = df.to_dict('records') for item in data_dict: - if DEBUG: - print("-------------", item, "-----------------") + logging.debug("------------- %s -----------------", item) tooltip = """""" tooltip_keys = [key for key in var_list if key in item.keys()] for key in tooltip_keys: @@ -321,8 +314,7 @@ class ContourFigureHandler(MapHandler): def retrieve_geojson(self, geojson_url, namespace, hideout): - if DEBUG: - print('================= Retrieving geojson...') + logging.debug('================= Retrieving geojson...') # Get geojson style_handle = namespace("styleHandle") @@ -345,8 +337,7 @@ class ShapefileFigureHandler(MapHandler): def retrieve_geojson(self, geojson_url, namespace, hideout, hover_style): - if DEBUG: - print('================= Retrieving geojson...') + logging.debug('================= Retrieving geojson...') # Get geojson style_handle = namespace("styleHandle") @@ -355,4 +346,4 @@ class ShapefileFigureHandler(MapHandler): hoverStyle=hover_style, hideout=hideout) - return geojson \ No newline at end of file + return geojson diff --git a/tools.py b/tools.py index 2d2b772..c58790d 100644 --- a/tools.py +++ b/tools.py @@ -124,29 +124,24 @@ def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, asp 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} * - ********************************************************************************* - """) + 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) - if DEBUG: - print(var, model, tstep, hour, selected_date) + logging.debug(' %s %s %s %s %s', var, model, tstep, hour, selected_date) if var: - if DEBUG: - print('SERVER: MODELS Figure init ... ') + logging.debug('SERVER: MODELS Figure init ... ') fh = ForecastModelsFigureHandler(var=var, model=model, tstep=tstep, hour=hour, selected_date=selected_date) - if DEBUG: - print('SERVER: MODELS Figure generation ... ') + logging.debug('SERVER: MODELS Figure generation ... ') return fh.get_figure_layers(tstep=tstep, hour=hour, aspect=aspect) - if DEBUG: - print('SERVER: NO MODELS Figure') + logging.debug('SERVER: NO MODELS Figure') return ForecastModelsFigureHandler().get_figure_layers() -- GitLab From 8cfbb90a8a1bbeb9221cfa0092ab22e7700d6b49 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 29 Jun 2023 14:41:07 +0200 Subject: [PATCH 41/71] Update tests for forecast_callbacks --- tests/test_forecast_callbacks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index 121aa81..08fc067 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -232,7 +232,7 @@ def test_update_was_figure(): 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 "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(): @@ -241,7 +241,7 @@ def test_update_was_figure_zooms(): 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) + 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=========================== -- GitLab From 59efec31ec80abed54e7744c231144fb85c709d7 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 29 Jun 2023 15:46:09 +0200 Subject: [PATCH 42/71] Move get_asset_url and create INES conf --- conf/colorbars.json | 41 +++++----- conf/info_style.json | 9 +++ conf/modebars.json | 9 --- {conf => conf_ines_core}/map_layers.json | 4 +- data_handler.py | 95 ++++++++---------------- ines_core_data_handler.py | 37 +++++---- tabs/evaluation.py | 1 - tabs/forecast.py | 1 - tabs/forecast_callbacks.py | 16 ++-- tabs/generic.py | 8 +- tabs/observations.py | 8 +- 11 files changed, 101 insertions(+), 128 deletions(-) create mode 100644 conf/info_style.json rename {conf => conf_ines_core}/map_layers.json (78%) diff --git a/conf/colorbars.json b/conf/colorbars.json index 904412e..c6ca2da 100644 --- a/conf/colorbars.json +++ b/conf/colorbars.json @@ -1,24 +1,21 @@ { - "model": {"colorbar": {"position": "topleft", - "width": 270, - "height": 15, - "style": {"top": "55px"} - } - }, - "prob": {"colorbar": {"position": "topleft", - "width": 330, - "height": 8, - "style": {"top": "65px", - "overflow": "hidden", - "white-space": "nowrap"} - } - }, - "scores": {"colorbar": {"position": "topleft", - "width": 330, - "height": 8, - "style": {"top": "65px", - "overflow": "hidden", - "white-space": "nowrap"} - } - } + "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/info_style.json b/conf/info_style.json new file mode 100644 index 0000000..31b568b --- /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/modebars.json b/conf/modebars.json index c6b7048..22a06e6 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/map_layers.json b/conf_ines_core/map_layers.json similarity index 78% rename from conf/map_layers.json rename to conf_ines_core/map_layers.json index c770f3c..6a1ce10 100644 --- a/conf/map_layers.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/data_handler.py b/data_handler.py index 0473607..c53e643 100644 --- a/data_handler.py +++ b/data_handler.py @@ -66,7 +66,6 @@ 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'))) -STYLES = json.load(open(os.path.join(DIR_PATH, 'conf/map_layers.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'))) @@ -140,7 +139,8 @@ class ForecastModelsFigureHandler(ContourFigureHandler): 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) + if self.selected_date: # Get NetCDF file path self.filepath = NETCDF_TEMPLATE.format(MODELS[self.model]['path'], selected_date, @@ -152,17 +152,6 @@ class ForecastModelsFigureHandler(ContourFigureHandler): def generate_var_tstep_trace(self, tstep=0): """ Generate trace to be added to data, per variable and timestep """ - from dash_server import app - - data_path = os.path.basename(MODELS[self.model]['path']) - if DEBUG: print(data_path) - geojson_url = app.get_asset_url(os.path.join('geojsons', - GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, - self.var))) - - 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) @@ -180,10 +169,12 @@ class ForecastModelsFigureHandler(ContourFigureHandler): if DEBUG: print("CTG", ctg) # Geojson rendering logic, must be JavaScript as it is executed in clientside. + 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") - hideout = dict(colorscale=COLORS, bounds=self.bounds, style=style, colorProp="value") - - self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) + hideout = dict(colorscale=COLORS, bounds=self.bounds, style=self.style, colorProp="value") + self.geojson = self.retrieve_geojson(geojson_path, namespace, hideout) def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): """ run plot """ @@ -453,18 +444,13 @@ class ForecastProbFigureHandler(ContourFigureHandler): 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) - geojson_path = PROB[self.var]['geojson_path'] - geojson_file = PROB[self.var]['geojson_template'] + # Get NetCDF file path netcdf_path = PROB[self.var]['netcdf_path'] netcdf_file = PROB[self.var]['netcdf_template'] - - # Get GeoJSON file path - self.geojsonpath = os.path.join(geojson_path, geojson_file).format(prob=prob, - date=self.selected_date, - var=self.var) - # Get NetCDF file path - self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=prob, + self.filepath = os.path.join(netcdf_path, netcdf_file).format(prob=self.prob, date=self.selected_date, var=self.var) @@ -474,37 +460,29 @@ class ForecastProbFigureHandler(ContourFigureHandler): def generate_var_tstep_trace(self, tstep=0): """ Generate trace to be added to data, per variable and timestep """ - from dash_server import app - - if DEBUG: print("##############", tstep) - - geojson_file = self.geojsonpath.format(step=tstep) - geojson_url = app.get_asset_url(geojson_file.replace('/data/daily_dashboard', 'geojsons')) - - name = VARS[self.var]['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)] + 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': colorscale, + 'colorscale': self.colorscale, 'tickValues': indices, 'tickText': ctg}) self.colorbar = self.retrieve_colorbar() - # Geojson rendering logic, must be JavaScript as it is executed in clientside. + # Geojson rendering logic, must be JavaScript as it is executed in clientside + 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") - hideout = dict(colorscale=colorscale, bounds=bounds, style=style, colorProp="value") - self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) + hideout = dict(colorscale=self.colorscale, bounds=self.bounds, style=self.style, + colorProp="value") + self.geojson = self.retrieve_geojson(geojson_path, namespace, hideout) return [self.geojson, self.colorbar] @@ -635,7 +613,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): 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)) + geojson_url = os.path.join('geojsons', geojson_path) if DEBUG: print("WAS GEOJSON URL", geojson_url) @@ -656,13 +634,13 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "wasMaps") - geojson_url = self.get_geojson_url(day=day) + 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_url, namespace, hideout, 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 """ @@ -958,11 +936,13 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): 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) 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' + 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): @@ -971,18 +951,6 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): def generate_var_tstep_trace(self, tstep): - from dash_server import app - - data_path = os.path.basename(OBS[self.obs]['path'][:-1]) - - geojson_url = app.get_asset_url(os.path.join('geojsons', - GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, - OBS[self.obs]['obs_var']))) - - if DEBUG: print("OBS", self.obs, "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(self.bounds[1:-1])] @@ -1000,8 +968,11 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "forecastMaps") - hideout = dict(colorscale=COLORS, bounds=self.bounds, style=style, colorProp="value") - self.geojson = self.retrieve_geojson(geojson_url, namespace, hideout) + hideout = dict(colorscale=COLORS, bounds=self.bounds, style=self.style, colorProp="value") + 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, hideout) def get_title(self, **kwargs): """ Return title from base title and elements """ diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index d64cf56..b5929fd 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -11,11 +11,12 @@ import numpy as np from dateutil.relativedelta import relativedelta from netCDF4 import Dataset as nc_file from datetime import datetime +from dash_server import app DIR_PATH = os.path.dirname(os.path.realpath(__file__)) COLORBARS = json.load(open(os.path.join(DIR_PATH, 'conf/colorbars.json'))) -MODEBARS = json.load(open(os.path.join(DIR_PATH, 'conf/modebars.json'))) -STYLES = json.load(open(os.path.join(DIR_PATH, 'conf/map_layers.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: @@ -24,7 +25,7 @@ class MapHandler: # TODO: Add common variables from all figure handlers here - self.info_style = MODEBARS['info_style'] + self.info_style = INFO_STYLE return None @@ -221,8 +222,8 @@ class MapHandler: tag=tag_template_tile.format(tag), index=index ), - url=STYLES[selected_tiles]['url'], - attribution=STYLES[selected_tiles]['attribution'] + url=MAP_LAYERS[selected_tiles]['url'], + attribution=MAP_LAYERS[selected_tiles]['attribution'] ), dl.FullscreenControl( position='topright', @@ -254,9 +255,10 @@ class PointsFigureHandler(MapHandler): super(PointsFigureHandler, self).__init__() if self.name in COLORBARS: - self.colorbar_info = COLORBARS[self.name]['colorbar'] + self.colorbar_info = COLORBARS[self.name] - # TODO: Add common variables from VisFigureHandler, Observations1dHandler and ScoresFigureHandler + # TODO: Add common variables from: + # VisFigureHandler, EvaluationGroundFigureHandler and EvaluationStatisticsFigureHandler return None @@ -310,26 +312,30 @@ class ContourFigureHandler(MapHandler): super(ContourFigureHandler, self).__init__() + # TODO: Add common variables from old FigureHandler and ProbFigureHandler + if self.name in COLORBARS: - self.colorbar_info = COLORBARS[self.name]['colorbar'] + self.colorbar_info = COLORBARS[self.name] else: - self.colorbar_info = COLORBARS['model']['colorbar'] - - # TODO: Add common variables from FigureHandler and ProbFigureHandler + self.colorbar_info = COLORBARS['model'] return None - def retrieve_geojson(self, geojson_url, namespace, hideout): + def retrieve_geojson(self, geojson_path, namespace, hideout): if DEBUG: print('================= 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), hideout=hideout) + if DEBUG: + print("GEOJSON_URL", geojson_url) + return geojson @@ -339,16 +345,17 @@ class ShapefileFigureHandler(MapHandler): super(ShapefileFigureHandler, self).__init__() - # TODO: Add variables from WasFigureHandler that could be reused in other shapefile figures + # TODO: Add variables from ForecastWasFigureHandler that could be reused in other figures return None - def retrieve_geojson(self, geojson_url, namespace, hideout, hover_style): + def retrieve_geojson(self, geojson_path, namespace, hideout, hover_style): if DEBUG: print('================= 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), diff --git a/tabs/evaluation.py b/tabs/evaluation.py index 5cff299..a42a348 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -8,7 +8,6 @@ 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 diff --git a/tabs/forecast.py b/tabs/forecast.py index 809bc56..4de4962 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -11,7 +11,6 @@ from dash import html from data_handler import FORECAST_MAX 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 diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 122875c..b56d9b1 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -19,12 +19,12 @@ from dash.dependencies import ALL from dash.exceptions import PreventUpdate import dash_leaflet as dl +from ines_core_data_handler import MAP_LAYERS 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 @@ -260,7 +260,7 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente if DEBUG: print("WAS figure " + date, was, day) if was: - view = list(STYLES.keys())[view.index(True)] + view = list(MAP_LAYERS.keys())[view.index(True)] layers = get_was_figure(was, day, selected_date=date) fig = get_figure(view=view, zoom=zoom, center=center, tag='was', layers=layers) return fig, previous @@ -331,7 +331,7 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): if prob: prob = prob.replace('prob_', '') - view = list(STYLES.keys())[view.index(True)] + view = list(MAP_LAYERS.keys())[view.index(True)] layers = get_prob_figure(var, prob, day, selected_date=date) fig = get_figure(view=view, zoom=zoom, center=center, tag='prob', layers=layers) @@ -383,14 +383,14 @@ def update_styles_button(*args): 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 button_id['index'] in MAP_LAYERS: if DEBUG: print("CURRENT ARGS", 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)] + 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: @@ -788,7 +788,7 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z center = [None for _ in model] if DEBUG: print('#### ZOOM, CENTER:', zoom, center, model, ncols, nrows) - view = list(STYLES.keys())[view.index(True)] + view = list(MAP_LAYERS.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 diff --git a/tabs/generic.py b/tabs/generic.py index 5ad6d68..ca2be7a 100644 --- a/tabs/generic.py +++ b/tabs/generic.py @@ -1,6 +1,6 @@ import dash_bootstrap_components as dbc from dash import html -from data_handler import STYLES +from ines_core_data_handler import MAP_LAYERS from data_handler import DELAY, DELAY_DATE, END_DATE, FORECAST_FINAL_DAY from datetime import datetime from datetime import timedelta @@ -35,16 +35,16 @@ def layout_view(): label='VIEW', children=[ dbc.DropdownMenuItem( - STYLES[style]['name'], + MAP_LAYERS[style]['name'], id=dict( tag='view-style', index=style ), active=active ) - for style, active in zip(list(STYLES.keys()), + for style, active in zip(list(MAP_LAYERS.keys()), [True if i == 'carto-positron' - else False for i in STYLES]) + else False for i in MAP_LAYERS]) ], direction="up", in_navbar=True, diff --git a/tabs/observations.py b/tabs/observations.py index c5ac711..0b2ed8c 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -1,6 +1,7 @@ from dash import dcc import dash_bootstrap_components as dbc from dash import html +from ines_core_data_handler import MAP_LAYERS from data_handler import DEBUG from data_handler import DEFAULT_VAR from data_handler import DEFAULT_MODEL @@ -8,7 +9,6 @@ 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_MODELS # from tabs.forecast import layout_view from utils import get_vis_edate @@ -26,15 +26,15 @@ 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", ), -- GitLab From 5363992edbbdef42c66da4b27133b5167a02024b Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 29 Jun 2023 16:10:09 +0200 Subject: [PATCH 43/71] Fix bug in dash server --- dash_server.py | 1 - data_handler.py | 7 ++++--- ines_core_data_handler.py | 10 +++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/dash_server.py b/dash_server.py index fccce1e..3def1a8 100755 --- a/dash_server.py +++ b/dash_server.py @@ -6,7 +6,6 @@ from dash import dcc from dash import html from flask.app import Flask -from data_handler import DEBUG from data_handler import DASH_LOG_LEVEL from data_handler import cache from data_handler import PATHNAME diff --git a/data_handler.py b/data_handler.py index cc4c505..8e16c70 100644 --- a/data_handler.py +++ b/data_handler.py @@ -40,8 +40,8 @@ import socket import logging DIR_PATH = os.path.dirname(os.path.realpath(__file__)) -DEBUG = True # False -DASH_LOG_LEVEL = logging.DEBUG # DEBUG INFO WARNING ERROR +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'))) @@ -513,7 +513,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): def get_figure_layers(self, day=0): """ run plot """ - logging.debug("*** %s %s %s %s", self.var, day, self.geojsonpath, self.filepath) + logging.debug("*** %s %s %s %s", self.var, day, self.filepath) logging.debug('Update layout ...') @@ -1235,6 +1235,7 @@ class VisFigureHandler(PointsFigureHandler): # Get CSV file tstep0 = tstep tstep1 = tstep + self.freq + logging.info("%s %s %s", self.selected_date, tstep0, tstep1) year = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%Y') month = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%m') day = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%d') diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 25cae98..9de5e6d 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -11,7 +11,6 @@ import numpy as np from dateutil.relativedelta import relativedelta from netCDF4 import Dataset as nc_file from datetime import datetime -from dash_server import app import logging DIR_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -315,6 +314,8 @@ class ContourFigureHandler(MapHandler): return None def retrieve_geojson(self, geojson_path, namespace, hideout): + + from dash_server import app if DEBUG: print('================= Retrieving geojson...') @@ -326,8 +327,7 @@ class ContourFigureHandler(MapHandler): options=dict(style=style_handle), hideout=hideout) - if DEBUG: - print("GEOJSON_URL", geojson_url) + logging.info("GEOJSON URL %s", geojson_url) return geojson @@ -343,6 +343,8 @@ class ShapefileFigureHandler(MapHandler): return None def retrieve_geojson(self, geojson_path, namespace, hideout, hover_style): + + from dash_server import app logging.debug('================= Retrieving geojson...') @@ -354,4 +356,6 @@ class ShapefileFigureHandler(MapHandler): hoverStyle=hover_style, hideout=hideout) + logging.info("GEOJSON URL %s", geojson_url) + return geojson -- GitLab From a0120bcfc62b11023aee2597e1ed5ce7b89d65ad Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Thu, 29 Jun 2023 17:13:52 +0200 Subject: [PATCH 44/71] Update JS comments and fix test --- assets/custom-functions.js | 24 ++++++++++++------------ assets/datepicker.js | 8 ++++---- assets/download-img.js | 28 +++++++++++++++------------- assets/gif_logos.js | 2 +- assets/resize_colorbars.js | 4 +--- assets/router.js | 12 ++++++------ assets/stats_carets.js | 23 ++++++++++------------- js/create_model_loop_zoom_fit.js | 26 +++++++++++++------------- tests/test_data_handler.py | 4 ++-- 9 files changed, 64 insertions(+), 67 deletions(-) diff --git a/assets/custom-functions.js b/assets/custom-functions.js index 84d1b3b..3369b9f 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; }, @@ -48,7 +48,7 @@ window.evaluationTab = Object.assign({}, window.evaluationTab, { } else { var {circleOptions} = context.props.hideout; } - // sender a simple circle marker. + // Sender a simple circle marker. return L.circleMarker(latlng, circleOptions); }, } @@ -60,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); }, } diff --git a/assets/datepicker.js b/assets/datepicker.js index 3c7c20a..18cc0c0 100644 --- a/assets/datepicker.js +++ b/assets/datepicker.js @@ -38,12 +38,12 @@ $(document).ready(function() { this.setSelectionRange(newCaretPosition, newCaretPosition); }); }); -// -// Move datepicker above the time series area + +// 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 + // We still want the menu to go below on eval/vis for modis const evalVis = document.getElementById('eval-date'); if (evalVis != null) { return @@ -56,7 +56,7 @@ $(document).ready(function () { }); }); -// //================= Function to clear datepicker without resetting ================================= +// CLEAR DATEPICKER WITHOUT RESETTING $(document).ready(function () { $(document).on('click', "#clear_button", function () { const date = document.getElementById('date'); diff --git a/assets/download-img.js b/assets/download-img.js index f4be793..710a8f1 100644 --- a/assets/download-img.js +++ b/assets/download-img.js @@ -1,7 +1,7 @@ $(document).ready(function () { $(document).on('click', "#btn-frame-download", function () { - //first add the logos - //Adjust colorbar, as it is appearing covered by the info bar + // 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) { @@ -11,7 +11,7 @@ $(document).ready(function () { } } getCanvas(element); - //The changes need to be remove to ensure the map appears normal + // The changes need to be remove to ensure the map appears normal removeChanges(); }); }); @@ -43,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){ @@ -83,28 +83,30 @@ function addLogos() { }; } +// REMOVE LOGOS AFTER SCREENSHOT IS TAKEN function removeLogos() { - //This function removes the added logos after screenshot is taken var logos = document.getElementById('logos'); logos.remove(); }; +// PUSH DOWN COLORBAR FOR SCREENSHOT function makeChanges() { - //colorbar needs pushed down for picture + // Push down const colorbar = document.querySelector('.leaflet-control-colorbar'); colorbar.style.paddingTop = '60px'; - // add the logos for the screenshot + // Add the logos for the screenshot addLogos(); - //remove the attribution in the bottom right corner + // 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 + // Put colorbar back in place const colorbar = document.querySelector('.leaflet-control-colorbar'); colorbar.style.paddingTop = '0px'; - //remove added logos + // Remove added logos removeLogos(); } diff --git a/assets/gif_logos.js b/assets/gif_logos.js index f3441c1..2a29e16 100644 --- a/assets/gif_logos.js +++ b/assets/gif_logos.js @@ -1,4 +1,4 @@ -// Add logos for the animated gifs +// ADD LOGOS FOR THE ANIMATED GIF $(document).ready(function () { $(document).on('click', "#model-slider-graph", function () { const logos = document.getElementById('logos'); diff --git a/assets/resize_colorbars.js b/assets/resize_colorbars.js index e99837d..229626f 100644 --- a/assets/resize_colorbars.js +++ b/assets/resize_colorbars.js @@ -1,4 +1,4 @@ -//==================Functions to resize colorbar ====================== +// RESIZE COLORBAR function setWidthForColorbars() { const colorbars = document.querySelectorAll('.leaflet-control-colorbar'); @@ -17,5 +17,3 @@ $(document).ready(function () { $(document).ready(function () { setTimeout(setWidthForColorbars, 1500); }); -//==================END Funcfions to resize colorbar ================== - diff --git a/assets/router.js b/assets/router.js index c079db4..f771297 100644 --- a/assets/router.js +++ b/assets/router.js @@ -1,5 +1,5 @@ -// ==================== ROUTING FUNCTIONS ============================= -//convert date from DD MON YYYY to YYYYMMDD +// ROUTING FUNCTIONS +// Convert date from DD MON YYYY to YYYYMMDD function getDate() { const dict = { 'Jan': '01', @@ -23,7 +23,7 @@ function getDate() { // LISTEN FOR MESSAGES FROM CMS window.addEventListener("message", (event) => { - // CREATE LIST OF ADDRESSES + // Create list of addresses const hosts = [ 'http://bscesdust02.bsc.es', 'http://bscesdust02.bsc.es/products/daily-dust-products', @@ -42,13 +42,13 @@ window.addEventListener("message", (event) => { } }, false); -// NOW ASSESS HOW THE URL SHOULD BE OUTPUT +// ASSESS HOW THE URL SHOULD BE OUTPUT function sendURL(url) { window.history.pushState("Models", "Models", url); parent.postMessage(url, '*'); } -//GET THE VARIABLE VALUE +// GET THE VARIABLE VALUE function getVar() { curvar = document.querySelector('.Select-value-label').innerHTML; curvar = curvar.split(' ').join('_') @@ -100,7 +100,7 @@ $(document).ready(function () { "#rgb": "?tab=observations§ion=eumetsat-rgb", "#visibility": "?tab=observations§ion=visibility" }; - // NOW MATCH THE ID WITH THE TOGGLEMAP KEY AND OUTPUT URL VALUE + // Match ID with the tooglemap key and output url value $(document).on('click', Object.keys(toggleMap).join(', '), function () { var selector = "#" + $(this).attr('id'); var url = toggleMap[selector]; diff --git a/assets/stats_carets.js b/assets/stats_carets.js index fcb77d9..f33277b 100644 --- a/assets/stats_carets.js +++ b/assets/stats_carets.js @@ -3,7 +3,7 @@ // 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 +// changing its state // ADD FUNCTION TO WAIT FOR TABLE TO FINISH CHANGING function waitForMutation(selector, func) { @@ -27,12 +27,12 @@ function waitForMutation(selector, func) { }); } -//FIND THE REGIONS TO REMOVE THE CARETS BEFORE FLIPPING THEM +// 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 + // 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 + // 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'); @@ -42,19 +42,19 @@ function targetRegions(mutations) { flipCarets(); } -//CREATE FUNCTION TO FIND REGIONS AND FLIP CARETS AS NEEDED +// 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 + // 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 + // 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'); }; @@ -70,12 +70,9 @@ function flipCarets() { }; }; -//ADD FUNCTION TO WAIT FOR CLICKS ON AREAS THAT SHOULD TRIGGER CARET FLIPS +// 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 =============================================== - - diff --git a/js/create_model_loop_zoom_fit.js b/js/create_model_loop_zoom_fit.js index a465850..32fdcd6 100644 --- a/js/create_model_loop_zoom_fit.js +++ b/js/create_model_loop_zoom_fit.js @@ -16,7 +16,7 @@ const modelDict = {'od550_dust':'AOD', 'dust_load':'Load', 'dust_ext_sfc':'Extinction'}; -//READ COORDINATES AND CENTER FROM CONF FILE +// 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); @@ -54,7 +54,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { waitUntil: 'networkidle0', }); await page.waitForSelector("#graph-collection"); - // select variable + // Select variable try { const sel = await page.$('#variable-dropdown-forecast'); await sel.click(); @@ -69,7 +69,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { } catch (err) { process.stdout.write("ERR0: " + err + "\n"); } - // select all models + // Select all models if (curmodel === "all") { try { process.stdout.write("SELECT ALL MODELS\n"); @@ -84,7 +84,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { process.stdout.write("ERR1: " + err + "\n"); } } - // select only one model + // Select only one model else { try { process.stdout.write("SELECT MODEL: " + curmodel + "\n"); @@ -105,7 +105,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { process.stdout.write("ERR2: " + err + "\n"); } } - // apply button + // Apply button try { for (const model of await page.$$('.custom-control-input')) { const checked = await model.evaluate(elem => elem.checked); @@ -118,7 +118,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { } catch (err) { process.stdout.write("ERR3: " + err + "\n"); } - // apply none fit button + // Apply none fit button try { var zoom = coords[fit].zoom; var lat = coords[fit].lat; @@ -165,21 +165,21 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { num = tstep; } } - // wait for alert div to disappear to start grabbing elements + // 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 + // Change graph height to fill output gif let graphHeight = await page.$('.leaflet-container'); await graphHeight.evaluate((el) => el.style.height = '93vh'); - // remove timeslider + // 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 + // Remove zoom panel process.stdout.write("REMOVING ZOOM PANEL(S)" + "\n"); await page.waitForSelector(".leaflet-bar"); await page.evaluate((sel) => { @@ -191,10 +191,10 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { if (coords[fit].logos == true){ try { - //reveal logos + // Reveal logos process.stdout.write("REVEALING LOGOS" + "\n"); - //style logos + // Style logos let logos = await page.$('#logos'); await logos.evaluate((el) => el.style.display = 'inline'); await logos.evaluate((el, margin) => {el.style.marginTop = margin}, coords[fit].marginTop); @@ -208,7 +208,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, 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 + // 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'; diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index c1ebbc3..30c0ede 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -170,8 +170,8 @@ def test_was_get_regions_data(ForecastWasFigureHandler): assert ForecastWasFigureHandler.get_regions_data(day=2)[2][-1] =='Normal' def test_was_get_geojson_url(ForecastWasFigureHandler): - assert ForecastWasFigureHandler.get_geojson_url(day=1) == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_1.geojson'.format(edate=END_DATE) - assert ForecastWasFigureHandler.get_geojson_url(day=2) == '/dashboard/assets/geojsons/was/burkinafaso/geojson/{edate}/{edate}_SCONC_DUST_2.geojson'.format(edate=END_DATE) + 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)) -- GitLab From e1e5835eabf94617288a5659d812590bdf8eabcd Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Fri, 30 Jun 2023 14:24:16 +0200 Subject: [PATCH 45/71] Merge and correct all tests. --- tests/test_evaluation_callbacks.py | 4 +- tests/test_forecast_callbacks.py | 133 +++------------------------ tests/test_nc2timeseries.py | 2 +- tests/test_observations_callbacks.py | 9 -- 4 files changed, 15 insertions(+), 133 deletions(-) diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py index 8ca8676..16bd0e7 100644 --- a/tests/test_evaluation_callbacks.py +++ b/tests/test_evaluation_callbacks.py @@ -62,7 +62,7 @@ def test_update_time_selection_monthly(): ctx = copy_context() output = ctx.run(run_callback) - assert "{'label': '2018', 'value': '201801-201812'}], 'Select year')" in str(output) + 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(): @@ -235,7 +235,7 @@ def test_update_eval_modis(): assert output[0].children[0].id == {'tag': 'modis-tile-layer', 'index': 'None'} 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': 'median', 'tag': 'model-map'} + assert output[1].id == {'index': 'None', 'tag': 'empty-map'} #===================== test update_eval('modis') =============================== def test_update_eval_test_modis(): diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index 5a3c4b4..7cbfbde 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -136,27 +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'})" -<<<<<<< HEAD -======= - - assert "([{'label': 'MULTI-MODEL', 'value': 'median', 'disabled': True}, {'label': 'MONARCH', 'value': 'monarch', 'disabled': False}, {'label': 'CAMS-IFS', 'value': 'cams', 'disabled': True}, {'label': 'DREAM8-CAMS', 'value': 'dream8-macc', 'disabled': True}, {'label': 'NASA-GEOS', 'value': 'nasa-geos', 'disabled': True}, {'label': 'MetOffice-UM', 'value': 'metoffice', 'disabled': True}, {'label': 'NCEP-GEFS', 'value': 'ncep-gefs', 'disabled': True}, {'label': 'EMA-RegCM4', 'value': 'ema-regcm4', 'disabled': True}, {'label': 'SILAM', 'value': 'silam', 'disabled': True}, {'label': 'LOTOS-EUROS', 'value': 'lotos-euros', 'disabled': True}, {'label': 'ICON-ART', 'value': 'icon-art', 'disabled': True}, {'label': 'NOA-WRF-CHEM', 'value': 'noa', 'disabled': True}, {'label': 'WRF-NEMO', 'value': 'wrf-nemo', 'disabled': True}, {'label': 'ALADIN', 'value': 'aladin', 'disabled': True}, {'label': 'ZAMG-WRF-CHEM', 'value': 'zamg', 'disabled': True}, {'label': 'MOCAGE', 'value': 'mocage', 'disabled': True}], ['monarch'], {'display': 'block'})" in str(code.update_models_dropdown('DUST_DEPW', ['cams', 'silam'])) -# =======================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 =========================== ->>>>>>> dev-more-tests # =======================START SIDEBAR_BOTTOM TESTS =========================== def test_sidebar_bottom_info(): @@ -209,7 +188,6 @@ 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]]) -<<<<<<< HEAD # =======================TEST ZOOMS=========================== def test_zooms(): def run_callback(): @@ -237,65 +215,6 @@ def test_update_styles_button(): output = ctx.run(run_callback) assert output == (['https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'], ["© OpenStreetMap contributors"], [], [], [], [], [False, True, False, False]) -def test_update_styles_button_from_open_streets(): - def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{'prop_id': '{"index":"esri-world","tag":"view-style"}.n_clicks', 'value': 1}]})) - return code.update_styles_button.uncached([None, 1, None, 1], ['https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'], [], [], [False, True, False, False]) - - ctx = copy_context() - output = ctx.run(run_callback) - assert output == (['https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], ['Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'], [], [], [], [], [False, False, False, True]) - -# ORJSON ERROR and INPUT IS EXTREMELY LONG -# def test_models_popup(): -======= -# ORJSON ERROR -# def test_zooms(): -# def run_callback(): -# 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) -# -# -def test_update_was_styles_button_carto(): - def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"carto-positron","tag":"view-style"}.n_clicks'},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) - return code.update_was_styles_button.uncached([1, None, 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}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png'], ["© OpenStreetMap " "contributors © CARTO"]) - -def test_update_was_styles_button_open_street(): - def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"open-street-map","tag":"view-style"}.n_clicks'},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) - return code.update_was_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']) - -def test_update_was_styles_button_esri(): - def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"esri-world","tag":"view-style"}.n_clicks'},{"prop_id": "was-tile-layer.url"},{"prop_id": "view-style.active"}]})) - return code.update_was_styles_button.uncached([None, None, None, 1], ['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://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}'], ['Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, ' 'Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community']) - -def test_update_was_styles_button_terrain(): - def run_callback(): - context_value.set(AttributeDict(**{"triggered_inputs": [{"prop_id":'{"index":"stamen-terrain","tag":"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 == (['https://stamen-tiles-{s}.a.ssl.fastly.net/terrain/{z}/{x}/{y}{r}.png'], ["Map tiles by Stamen Design, CC BY 3.0 — Map " 'data © OpenStreetMap " 'contributors']) - -# =======================Start models popup tests=========================== def test_models_popup(): def run_callback(): @@ -308,20 +227,21 @@ def test_models_popup(): assert output[1] == {} # ASSERT OUTPUT[2] == RETURNS DASH.NO_UPDATE -# NEED TO DEAL WITH THE SECOND INPUT, WHICH IS EXTREMELY LONG -# def test_models_popup_full_input(): -# # FIRST TEST INSUFFICIENT INPUTS TO RETURN EMPTY VALUES AND 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":1}.click_lat_lng'}]})) -# return code.models_popup([[56.739260373724775, 91.93359375]],[{'tag': 'model-map', 'index': 'median', 'n_clicks': 1}], [], '20230404', 0, 'OD550_DUST', None, {}) -# +# 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) +# +# # =======================Start show_timeseries=========================== #SHOULD RETURN 3 ITEMS, CURRENTLY ONLY RETURNING NONE # def test_show_timeseries(): ->>>>>>> dev-more-tests # def run_callback(): # 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}) @@ -370,7 +290,7 @@ def test_update_model_figure(): ctx = copy_context() output = ctx.run(run_callback) 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 ", 'fontSize': '14px', 'fontWeight': 'bold', 'width': '305px'}), None], id={'tag': 'model-map', 'index': 'monarch', 'n_clicks': 1}, animate=False, center=[43.93333333333334, 19.450000000000003], inertia=True, minZoom=2, preferCanvas=True, style={'height': '90vh'}, wheelDebounceTime=80, wheelPxPerZoomLevel=120, zoom=2.9, zoomSnap=0.1), width=12)], align='start', no_gutters=True)]" 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) # =======================START UPDATE PROB FIGURE TESTS=========================== def test_update_prob_figure(): @@ -384,20 +304,16 @@ def test_update_prob_figure(): 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] == None - assert output.children[3] == None - assert output.children[4] == None - assert output.children[5].url == '/dashboard/assets/geojsons/prob/od550_dust/0.2/geojson/20230404/01_20230404_OD550_DUST.geojson' - assert output.id == {'index': 'None', 'tag': 'prob-map'} + 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_prob(): +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[5].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/20230404/01_20230404_OD550_DUST.geojson' + 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(): @@ -436,28 +352,3 @@ def test_update_was_figure_zooms(): 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=========================== - - - - - - - - - - - - - - - - - - - - - - - - -#some comment diff --git a/tests/test_nc2timeseries.py b/tests/test_nc2timeseries.py index c33af38..9a51a63 100644 --- a/tests/test_nc2timeseries.py +++ b/tests/test_nc2timeseries.py @@ -2,7 +2,7 @@ import pytest import importlib import numpy as np import xarray as xr -code = importlib.import_module('preproc.nc2timeseries') +code = importlib.import_module('preproc.nc2timeseries_interp') def counter(model): diff --git a/tests/test_observations_callbacks.py b/tests/test_observations_callbacks.py index a421f57..8877994 100644 --- a/tests/test_observations_callbacks.py +++ b/tests/test_observations_callbacks.py @@ -43,16 +43,7 @@ def test_render_observations_tab_visibility(): _, default_tstep = get_vis_edate('20220831', hour=hour) ctx = copy_context() output = ctx.run(run_callback) -<<<<<<< HEAD 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]) -======= - check_date = output[0].children[3].children[0].children[0].children[0].children[0].date - check_tstep = output[0].children[3].children[0].children[0].children[2].children.value - assert check_date == code.END_DATE - assert check_tstep == default_tstep - assert "Tab(children=[Span(children=P('Visibility'), className='description-title'), Span(children=P([B('You can explore key observations that can be used to track dust events. '), 'All observations are kindly offered by Partners of the WMO Barcelona Dust Regional Center. The reduction of VISIBILITY is an indirect measure of the occurrence of sand and dust storms on the surface.'])" in str(output[0]) - assert "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')" in str(output[0]) ->>>>>>> dev-more-tests assert output[1:] == ({ 'font-weight': 'normal' }, { 'font-weight': 'bold' }, 'visibility') -- GitLab From b21c65f199e6fa56750a11f7eb2ac0a73929abf3 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 11:23:14 +0200 Subject: [PATCH 46/71] Documentation for INES core --- callback_tools.py | 10 +- data_handler.py | 25 ++-- ines_core_data_handler.py | 209 +++++++++++++++++++++++++++----- tests/test_ines_data_handler.py | 6 - 4 files changed, 195 insertions(+), 55 deletions(-) diff --git a/callback_tools.py b/callback_tools.py index 997e799..e66be15 100644 --- a/callback_tools.py +++ b/callback_tools.py @@ -110,10 +110,10 @@ def get_evaluation_statistics_figure(network=None, model=None, statistic=None, s return fh.get_figure_layers() -def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, aspect=(1, 1)): +def get_model_figure(var, model, tstep=0, selected_date=END_DATE, aspect=(1, 1)): """ Retrieve figure """ - logging.debug("*** %s %s %s %s %s *****", model, var, selected_date, tstep, hour) + logging.debug("*** %s %s %s %s %s *****", model, var, selected_date, tstep) try: selected_date = dt.strptime( @@ -135,12 +135,12 @@ def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, asp # 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) + logging.debug(' %s %s %s %s %s', var, model, tstep, selected_date) if var: logging.debug('SERVER: MODELS Figure init ... ') - fh = ForecastModelsFigureHandler(var=var, model=model, tstep=tstep, hour=hour, selected_date=selected_date) + fh = ForecastModelsFigureHandler(var=var, model=model, tstep=tstep, selected_date=selected_date) logging.debug('SERVER: MODELS Figure generation ... ') - return fh.get_figure_layers(tstep=tstep, hour=hour, aspect=aspect) + return fh.get_figure_layers(tstep=tstep, aspect=aspect) logging.debug('SERVER: NO MODELS Figure') return ForecastModelsFigureHandler().get_figure_layers() diff --git a/data_handler.py b/data_handler.py index 8e16c70..daa1981 100644 --- a/data_handler.py +++ b/data_handler.py @@ -143,6 +143,7 @@ class ForecastModelsFigureHandler(ContourFigureHandler): 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 @@ -163,7 +164,7 @@ class ForecastModelsFigureHandler(ContourFigureHandler): self.colorbar_info.update({'min': 0, 'max': len(ctg)+1, 'classes': indices, - 'colorscale': COLORS, + 'colorscale': self.colorscale, 'tickValues': indices[1:-1], 'tickText': ctg}) self.colorbar = self.retrieve_colorbar() @@ -176,23 +177,17 @@ class ForecastModelsFigureHandler(ContourFigureHandler): geojson_path = os.path.join('geojsons', GEOJSON_TEMPLATE.format(data_path, self.selected_date, tstep, self.selected_date, self.var)) namespace = Namespace("forecastTab", "forecastMaps") - hideout = dict(colorscale=COLORS, bounds=self.bounds, style=self.style, colorProp="value") - self.geojson = self.retrieve_geojson(geojson_path, namespace, hideout) + self.geojson = self.retrieve_geojson(geojson_path, namespace) - def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): + def get_figure_layers(self, tstep=0, aspect=(1,1)): """ run plot """ logging.debug('Update layout ...') if os.path.exists(self.filepath) and self.var: - # Get timestep - if hour is not None: - tstep = int(self.hour_to_step(hour)) - else: - tstep = int(tstep) - # Get trace + tstep = int(tstep) self.generate_var_tstep_trace(tstep) # Get figure title @@ -482,10 +477,8 @@ class ForecastProbFigureHandler(ContourFigureHandler): date=self.selected_date, var=self.var) geojson_path = geojson_path.format(step=tstep).replace('/data/daily_dashboard', 'geojsons') - namespace = Namespace("forecastTab", "forecastMaps") - hideout = dict(colorscale=self.colorscale, bounds=self.bounds, style=self.style, - colorProp="value") - self.geojson = self.retrieve_geojson(geojson_path, namespace, hideout) + namespace = Namespace("forecastTab", "forecastMaps") + self.geojson = self.retrieve_geojson(geojson_path, namespace) return [self.geojson, self.colorbar] @@ -938,6 +931,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): 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 @@ -969,11 +963,10 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): # Geojson rendering logic, must be JavaScript as it is executed in clientside. namespace = Namespace("forecastTab", "forecastMaps") - hideout = dict(colorscale=COLORS, bounds=self.bounds, style=self.style, colorProp="value") 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, hideout) + self.geojson = self.retrieve_geojson(geojson_path, namespace) def get_title(self, **kwargs): """ Return title from base title and elements """ diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 9de5e6d..cb7d390 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -30,7 +30,13 @@ class MapHandler: return None def get_title(self, **kwargs): - """ Return title from base title and elements """ + """ Return title from base title and elements + + Returns + ------- + title : str + Map title + """ logging.debug('================= Retrieving figure title...') @@ -64,7 +70,18 @@ class MapHandler: return title def retrieve_cdatetime(self, tstep=0): - """ Retrieve data from NetCDF file """ + """ 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]) @@ -82,7 +99,19 @@ class MapHandler: 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 @@ -111,7 +140,19 @@ class MapHandler: 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 @@ -130,7 +171,19 @@ class MapHandler: 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 @@ -146,6 +199,17 @@ class MapHandler: 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'] @@ -162,42 +226,66 @@ class MapHandler: return timesteps, what, rdatetime - def hour_to_step(self, hour): - """ Convert hour to relative tstep """ - cdatetime = self.rdatetime.date() + relativedelta(hours=hour) - - for step in range(len(self.timesteps)): - if self.retrieve_cdatetime(step) == cdatetime: - return step + def get_center(self, center=None): + """ Returns center of map - return 0 + Parameters + ---------- + center : list, optional + Map center, by default None - def get_center(self, center=None): - """ Returns center of map """ + Returns + ------- + center : list + Map center + """ - 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 ] + 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): - """ run plot """ + """ 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 + ------- + fig : dash_leaflet.Map + Figure + """ self.st_time = time.time() logging.debug('================= Retrieving figure...') - logging.debug("ASPECT %s", aspect) + # 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) @@ -211,6 +299,7 @@ class MapHandler: logging.debug("TAG %s", tag) logging.debug("INDEX %s", index) + fig = dl.Map(children=[ dl.TileLayer( id=dict( @@ -258,6 +347,20 @@ class PointsFigureHandler(MapHandler): 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...') @@ -276,6 +379,20 @@ class PointsFigureHandler(MapHandler): 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...') @@ -313,13 +430,31 @@ class ContourFigureHandler(MapHandler): return None - def retrieve_geojson(self, geojson_path, namespace, hideout): - + 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 if DEBUG: print('================= 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") @@ -343,7 +478,25 @@ class ShapefileFigureHandler(MapHandler): 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...') diff --git a/tests/test_ines_data_handler.py b/tests/test_ines_data_handler.py index cf20963..8a652f7 100644 --- a/tests/test_ines_data_handler.py +++ b/tests/test_ines_data_handler.py @@ -48,9 +48,3 @@ def test_get_title(MapHandler): 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_step(MapHandler): - assert MapHandler.hour_to_step(0) == 0 - assert MapHandler.hour_to_step(3) == int(3/FREQ) - assert MapHandler.hour_to_step(6) == int(6/FREQ) - assert MapHandler.hour_to_step(36) == int(36/FREQ) -- GitLab From f7f711e10134fa709219beda72242c27598a0fdd Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 12:30:43 +0200 Subject: [PATCH 47/71] Reintroduce hour as arg --- callback_tools.py | 10 +++++----- data_handler.py | 9 +++++++-- ines_core_data_handler.py | 20 ++++++++++++++++++++ tests/test_ines_data_handler.py | 6 ++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/callback_tools.py b/callback_tools.py index e66be15..997e799 100644 --- a/callback_tools.py +++ b/callback_tools.py @@ -110,10 +110,10 @@ def get_evaluation_statistics_figure(network=None, model=None, statistic=None, s return fh.get_figure_layers() -def get_model_figure(var, model, tstep=0, selected_date=END_DATE, aspect=(1, 1)): +def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, aspect=(1, 1)): """ Retrieve figure """ - logging.debug("*** %s %s %s %s %s *****", model, var, selected_date, tstep) + logging.debug("*** %s %s %s %s %s *****", model, var, selected_date, tstep, hour) try: selected_date = dt.strptime( @@ -135,12 +135,12 @@ def get_model_figure(var, model, tstep=0, selected_date=END_DATE, aspect=(1, 1)) # 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, selected_date) + 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, selected_date=selected_date) + 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, aspect=aspect) + return fh.get_figure_layers(tstep=tstep, hour=hour, aspect=aspect) logging.debug('SERVER: NO MODELS Figure') return ForecastModelsFigureHandler().get_figure_layers() diff --git a/data_handler.py b/data_handler.py index daa1981..1f86c1c 100644 --- a/data_handler.py +++ b/data_handler.py @@ -179,15 +179,20 @@ class ForecastModelsFigureHandler(ContourFigureHandler): namespace = Namespace("forecastTab", "forecastMaps") self.geojson = self.retrieve_geojson(geojson_path, namespace) - def get_figure_layers(self, tstep=0, aspect=(1,1)): + def get_figure_layers(self, tstep=0, hour=None, aspect=(1,1)): """ run plot """ logging.debug('Update 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 - tstep = int(tstep) self.generate_var_tstep_trace(tstep) # Get figure title diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index cb7d390..6bc8eb0 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -226,6 +226,26 @@ class MapHandler: 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 diff --git a/tests/test_ines_data_handler.py b/tests/test_ines_data_handler.py index 8a652f7..97ef850 100644 --- a/tests/test_ines_data_handler.py +++ b/tests/test_ines_data_handler.py @@ -48,3 +48,9 @@ def test_get_title(MapHandler): 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) -- GitLab From 7f1f10c3fcec287d7db18be4851edcc10b3d23e8 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 12:47:36 +0200 Subject: [PATCH 48/71] Document ForecastModelsFigureHandler --- data_handler.py | 92 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 67 insertions(+), 25 deletions(-) diff --git a/data_handler.py b/data_handler.py index 1f86c1c..fac608b 100644 --- a/data_handler.py +++ b/data_handler.py @@ -130,6 +130,21 @@ else: class ForecastModelsFigureHandler(ContourFigureHandler): def __init__(self, var=None, model=None, tstep=0, hour=None, selected_date=None): + """ Initialise 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] @@ -148,16 +163,32 @@ class ForecastModelsFigureHandler(ContourFigureHandler): if self.selected_date: # Get NetCDF file path self.filepath = NETCDF_TEMPLATE.format(MODELS[self.model]['path'], selected_date, - MODELS[self.model]['template']) + 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() - + def generate_var_tstep_trace(self, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ + """ Generate GeoJSON trace to be added per timestep - # Create colorbar + 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)) @@ -172,15 +203,38 @@ class ForecastModelsFigureHandler(ContourFigureHandler): logging.debug("BOUNDS %s", self.bounds) logging.debug("CTG %s", ctg) - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - 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 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)): - """ run plot """ + """ 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('Update layout ...') @@ -192,8 +246,9 @@ class ForecastModelsFigureHandler(ContourFigureHandler): else: tstep = int(tstep) - # Get trace + # Get trace and colorbar self.generate_var_tstep_trace(tstep) + self.generate_colorbar() # Get figure title self.fig_title = html.P(html.B( @@ -229,19 +284,6 @@ class ForecastModelsFigureHandler(ContourFigureHandler): return [self.geojson, self.colorbar, self.info] - 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 - class ForecastModelsTimeSeriesHandler: """ Class to handle forecast time series """ -- GitLab From c545f29347e6a33be332cfa6ce7c0ec878c309af Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 13:28:06 +0200 Subject: [PATCH 49/71] Clean and document data handler --- data_handler.py | 473 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 372 insertions(+), 101 deletions(-) diff --git a/data_handler.py b/data_handler.py index fac608b..85a7576 100644 --- a/data_handler.py +++ b/data_handler.py @@ -130,7 +130,7 @@ else: class ForecastModelsFigureHandler(ContourFigureHandler): def __init__(self, var=None, model=None, tstep=0, hour=None, selected_date=None): - """ Initialise ForecastModelsFigureHandler class + """ Initialize ForecastModelsFigureHandler class Parameters ---------- @@ -168,7 +168,9 @@ class ForecastModelsFigureHandler(ContourFigureHandler): # 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 @@ -468,11 +470,20 @@ class ForecastModelsTimeSeriesHandler: class ForecastProbFigureHandler(ContourFigureHandler): - """ Class to manage the figure creation """ def __init__(self, var=None, prob=None, selected_date=None): - """ Initialization with variable, prob and date """ + """ Initialize ForecastProbFigureHandler class + Parameters + ---------- + var : str, optional + Variable name, by default None + prob : str, optional + Probability threshold, by default None + selected_date : str, optional + Selected date, by default None + """ + self.name = 'prob' super(ForecastProbFigureHandler, self).__init__() @@ -503,10 +514,30 @@ class ForecastProbFigureHandler(ContourFigureHandler): 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 trace to be added to data, per variable and timestep """ + """ 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 """ - # Create 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))) @@ -518,16 +549,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): 'tickText': ctg}) self.colorbar = self.retrieve_colorbar() - # Geojson rendering logic, must be JavaScript as it is executed in clientside - 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 [self.geojson, self.colorbar] + return None def get_title(self, **kwargs): """ Return title from base title and elements """ @@ -551,7 +573,18 @@ class ForecastProbFigureHandler(ContourFigureHandler): return title def get_figure_layers(self, day=0): - """ run plot """ + """ 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("*** %s %s %s %s", self.var, day, self.filepath) @@ -562,8 +595,8 @@ class ForecastProbFigureHandler(ContourFigureHandler): # Get timestep tstep = int(day) - # Get trace - logging.debug('Adding contours...') + # Get trace and colorbar + self.generate_colorbar() self.generate_var_tstep_trace(tstep) # Get figure title @@ -590,10 +623,22 @@ class ForecastProbFigureHandler(ContourFigureHandler): class ForecastWasFigureHandler(ShapefileFigureHandler): - """ 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 """ + def __init__(self, was='burkinafaso', model='median', variable='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' + variable : str, optional + Variable name, by default 'SCONC_DUST' + selected_date : str, optional + Selected date, by default None + """ self.name = 'was' super(ForecastWasFigureHandler, self).__init__() @@ -625,7 +670,26 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): # 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.variable, format='h5') @@ -638,10 +702,23 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): 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): - from dash_server import app + """ 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.variable, @@ -663,7 +740,12 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): return geojson_url def generate_var_tstep_trace(self, day=0): - """ Generate trace to be added to data, per variable and timestep """ + """ Generate GeoJSON trace to be added per day + Parameters + ---------- + day : int, optional + Day, by default 0 + """ logging.debug('Adding contours ...') @@ -696,7 +778,18 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): return title def get_figure_layers(self, day=0): - """ run plot """ + """ 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('Update layout ...') @@ -777,9 +870,11 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): for input_file in self.input_files]) } + return None + def generate_var_tstep_trace(self): - """ Generate trace to be added to data, per variable and timestep """ - + """ 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] @@ -800,8 +895,19 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): 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.DataFrame + Dataframe with stations information + list + Figure layers + """ + if self.input_files: # Get trace self.generate_var_tstep_trace() @@ -968,7 +1074,20 @@ class EvaluationGroundTimeSeriesHandler: class EvaluationSatelliteFigureHandler(ContourFigureHandler): 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__() @@ -991,9 +1110,28 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): # 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 """ - # Create 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)) @@ -1008,12 +1146,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): logging.debug("BOUNDS %s", self.bounds) logging.debug("CTG %s", ctg) - # Geojson rendering logic, must be JavaScript as it is executed in clientside. - 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 get_title(self, **kwargs): """ Return title from base title and elements """ @@ -1029,7 +1162,18 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): return title def get_figure_layers(self, tstep=0): - """ Run plot """ + """ 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('Update layout ...') @@ -1044,7 +1188,8 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): title = "DATA NOT AVAILABLE" self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) else: - # Get trace + # Get trace and colorbar + self.generate_colorbar() self.generate_var_tstep_trace(tstep) # Get figure title @@ -1062,9 +1207,21 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): class EvaluationStatisticsFigureHandler(PointsFigureHandler): - """ Class to manage the figure creation """ 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__() @@ -1104,8 +1261,11 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): # 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) @@ -1117,7 +1277,24 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): 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: @@ -1167,38 +1344,22 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): return lon, lat, stations, scores, res - def get_figure_layers(self): - """ Run plot """ - - logging.debug('Update 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 - self.generate_var_tstep_trace(lon, lat, stations, scores, res) - - logging.debug('Adding one point ...') - # 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] - def generate_var_tstep_trace(self, lon, lat, stations, scores, res): - """ Generate trace to be added to data, per variable and timestep """ + """ 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({ @@ -1216,7 +1377,14 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): # Add tooltips (hover information) to map data_dict = self.get_tooltip(df, var_list=var_list) - # Create colorbar + # 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)] @@ -1229,10 +1397,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): 'tickText': ctg}) self.colorbar = self.retrieve_colorbar() - # Create geojson - geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") - namespace = Namespace("evaluationTab", "evaluationMaps") - self.geojson = self.retrieve_geojson(geojson_data, namespace) + return None def get_title(self, **kwargs): """ Return title from base title and elements """ @@ -1245,11 +1410,55 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): return title + def get_figure_layers(self): + """ Get figure layers (GeoJSON trace, colorbar and information box with title) + + Returns + ------- + list + Figure layers + """ + + logging.debug('Update 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) + + logging.debug('Adding one point ...') + # 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 to manage the figure creation """ 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__() @@ -1270,8 +1479,17 @@ class VisFigureHandler(PointsFigureHandler): if self.selected_date: self.rdatetime = datetime.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 @@ -1287,8 +1505,31 @@ class VisFigureHandler(PointsFigureHandler): 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): - """ Set time dependent data """ + """ 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")) @@ -1308,8 +1549,8 @@ class VisFigureHandler(PointsFigureHandler): c2x = np.where([c2t == i for i in cx[0]])[-1] c2 = (np.delete(c2t, c2x),) - xlon = self.data['LON'].values - ylat = self.data['LAT'].values + 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 [] @@ -1321,28 +1562,46 @@ class VisFigureHandler(PointsFigureHandler): # 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) - return xlon, ylat, stations, np.array(visibility), np.array(humidity), (c0, c1, c2, cx) + # 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, xlon, ylat, stations, visibility, humidity, values, color, - labels, tstep=0): - """ Generate trace to be added to data, per variable and timestep """ + 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(xlon) and list(ylat) and list(stations) and list(visibility) and list(humidity): + if list(lon) and list(lat) and list(stations) and list(visibility) and list(humidity): # Create legend self.legend = self.create_legend(self.colormap) - # Assign colors to values - n_points = len(xlon) - res = np.zeros((n_points)) - for i, (value, label) in enumerate(zip(values, labels)): - res[value] = i - # Create dataframe df = pd.DataFrame({ 'station': stations, - 'lon': xlon.round(2), - 'lat': ylat.round(2), + 'lon': lon.round(2), + 'lat': lat.round(2), 'visibility': (visibility/1e3).round(2), 'humidity': humidity.astype(int), 'value': res @@ -1360,6 +1619,8 @@ class VisFigureHandler(PointsFigureHandler): 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 """ @@ -1375,8 +1636,19 @@ class VisFigureHandler(PointsFigureHandler): return title - def get_figure_layers(self, tstep=0, hour=None): - """ run plot """ + 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 + """ # Read data if self.selected_date: @@ -1384,9 +1656,8 @@ class VisFigureHandler(PointsFigureHandler): if os.path.exists(self.filepath): tstep = int(tstep) - xlon, ylat, stations, visibility, humidity, values = self.select_data(tstep) - self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, values, - self.colors, self.labels, tstep) + xlon, ylat, stations, visibility, humidity, res = self.select_data(tstep) + self.generate_var_tstep_trace(xlon, ylat, stations, visibility, humidity, res, tstep) self.fig_title = html.P(html.B( [ item for sublist in self.get_title(tstep=tstep).split('
') @@ -1401,4 +1672,4 @@ class VisFigureHandler(PointsFigureHandler): # Get title information element self.info = self.retrieve_info(self.name) - return [self.geojson, self.info, self.legend] + return [self.geojson, self.legend, self.info] -- GitLab From c15e00b45805a164f23daaf7dfcb3df9daf1c491 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 16:25:08 +0200 Subject: [PATCH 50/71] Fix tests --- tests/test_data_handler.py | 22 ---------------------- tests/test_tools.py | 6 +++--- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index c9b2f52..b52edc0 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -90,28 +90,6 @@ 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_generate_var_tstep_trace_AOD(ForecastProbFigureHandler): - # day 0 for od550_dust - assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=0)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/00_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=0)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=0)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - # day 1 for od550_dust - assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=1)[0].url == '/dashboard/assets/geojsons/prob/od550_dust/0.1/geojson/{edate}/01_{edate}_OD550_DUST.geojson'.format(edate=END_DATE) - assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=1)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ForecastProbFigureHandler[0].generate_var_tstep_trace(tstep=1)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - -def test_prob_generate_var_tstep_trace_SCONC(ForecastProbFigureHandler): - # day 0 for sconc_dust - assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=0)[0].url == '/dashboard/assets/geojsons/prob/sconc_dust/50/geojson/{edate}/00_{edate}_SCONC_DUST.geojson'.format(edate=END_DATE) - assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=0)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=0)[1].classes ==[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - - # day 1 for sconc_dust - assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=1)[0].url == '/dashboard/assets/geojsons/prob/sconc_dust/50/geojson/{edate}/01_{edate}_SCONC_DUST.geojson'.format(edate=END_DATE) - assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=1)[0].options == {'style': {'variable': 'forecastTab.forecastMaps.styleHandle'}} - assert ForecastProbFigureHandler[1].generate_var_tstep_trace(tstep=1)[1].classes == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - 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)) diff --git a/tests/test_tools.py b/tests/test_tools.py index 7149799..54323ad 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -45,9 +45,9 @@ def test_get_was_figure(): def test_get_vis_figure(): code_run = code.get_vis_figure(tstep=0, selected_date='20230321') - assert code_run[1].children.children.children[2] == '21 Mar 2023 00-06 UTC' - assert code_run[1].id == 'vis-info' - assert code_run[2].children[0].children[0].className == 'vis-legend-point' + 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(): result1 = f"/dashboard/assets/geojsons/median/geojson/{END_DATE}/00_{END_DATE}_OD550_DUST.geojson" -- GitLab From e80808b456ee17a6f117ac95e1ea7c05393b0d1c Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 17:39:36 +0200 Subject: [PATCH 51/71] Move timeseries functions to timeseries_handler --- callback_tools.py | 4 +- data_handler.py | 372 ++--------------------------- preproc/nc2scores_aeronet.py | 2 +- preproc/nc2scores_modis.py | 2 +- tabs/evaluation.py | 1 - tests/test_data_handler.py | 20 +- timeseries_handler.py | 446 +++++++++++++++++++++++++++++++++++ 7 files changed, 479 insertions(+), 368 deletions(-) create mode 100644 timeseries_handler.py diff --git a/callback_tools.py b/callback_tools.py index 997e799..d8b222d 100644 --- a/callback_tools.py +++ b/callback_tools.py @@ -9,11 +9,9 @@ import logging from data_handler import MapHandler from data_handler import ForecastModelsFigureHandler -from data_handler import ForecastModelsTimeSeriesHandler from data_handler import ForecastProbFigureHandler from data_handler import ForecastWasFigureHandler from data_handler import EvaluationGroundFigureHandler -from data_handler import EvaluationGroundTimeSeriesHandler from data_handler import EvaluationSatelliteFigureHandler from data_handler import EvaluationStatisticsFigureHandler from data_handler import VisFigureHandler @@ -21,6 +19,8 @@ from data_handler import DEBUG from data_handler import MODELS from data_handler import PATHS from data_handler import END_DATE, DELAY, DELAY_DATE +from timeseries_handler import ForecastModelsTimeSeriesHandler +from timeseries_handler import EvaluationGroundTimeSeriesHandler from utils import get_currdate_tstep def download_image_link(models, variable, curdate, tstep=0, anim=False): diff --git a/data_handler.py b/data_handler.py index 85a7576..3f90c55 100644 --- a/data_handler.py +++ b/data_handler.py @@ -1,7 +1,6 @@ # -*- 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 @@ -23,11 +22,6 @@ from datetime import timedelta from datetime import datetime from dateutil.relativedelta import relativedelta -from utils import concat_dataframes -from utils import retrieve_timeseries -from utils import retrieve_single_point -from utils import get_colorscale - from ines_core_data_handler import MapHandler from ines_core_data_handler import PointsFigureHandler from ines_core_data_handler import ContourFigureHandler @@ -238,7 +232,7 @@ class ForecastModelsFigureHandler(ContourFigureHandler): Figure layers """ - logging.debug('Update layout ...') + logging.debug('Updating layout ...') if os.path.exists(self.filepath) and self.var: @@ -287,188 +281,6 @@ class ForecastModelsFigureHandler(ContourFigureHandler): return [self.geojson, self.colorbar, self.info] -class ForecastModelsTimeSeriesHandler: - """ 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] - - logging.debug("---------- %s", 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 - - # 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.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): - # logging.debug(' %s %s', 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): - 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, variable, 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((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 ForecastProbFigureHandler(ContourFigureHandler): def __init__(self, var=None, prob=None, selected_date=None): @@ -586,9 +398,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): Figure layers """ - logging.debug("*** %s %s %s %s", self.var, day, self.filepath) - - logging.debug('Update layout ...') + logging.debug('Updating layout ...') if self.var and os.path.exists(self.filepath): @@ -624,8 +434,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): class ForecastWasFigureHandler(ShapefileFigureHandler): - def __init__(self, was='burkinafaso', model='median', variable='SCONC_DUST', - selected_date=None): + def __init__(self, was='burkinafaso', model='median', var='SCONC_DUST', selected_date=None): """ Initialize ForecastWasFigureHandler class Parameters @@ -634,7 +443,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): Region name, by default 'burkinafaso' model : str, optional Model name, by default 'median' - variable : str, optional + var : str, optional Variable name, by default 'SCONC_DUST' selected_date : str, optional Selected date, by default None @@ -645,7 +454,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): self.model = model self.was = was - self.variable = variable + self.var = var self.selected_date = selected_date self.colormap = WAS[self.was]['colormap'] @@ -691,7 +500,8 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): """ 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.variable, format='h5') + 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) @@ -721,7 +531,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): 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.variable, + 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) @@ -791,7 +601,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): Figure layers """ - logging.debug('Update layout ...') + logging.debug('Updating layout ...') if os.path.exists(self.filepath): # Get trace @@ -908,6 +718,8 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): Figure layers """ + logging.debug('Updating layout ...') + if self.input_files: # Get trace self.generate_var_tstep_trace() @@ -919,158 +731,6 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): return self.df, [self.geojson] -class EvaluationGroundTimeSeriesHandler: - """ 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 = [] - 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((datetime.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, 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)) - logging.debug("RETRIEVE TS %s %s %s %s", 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 - logging.debug("MOD %s COLS %s", mod, df.columns) - if df.columns[-1].upper() == self.variable: - df = df.rename(columns = { df.columns[-1]: self.variable }) - - 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.variable].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.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"), - ]) - ) - ) - - logging.debug('FIG TYPE %s', type(fig)) - return fig - - class EvaluationSatelliteFigureHandler(ContourFigureHandler): def __init__(self, var, obs, tstep, selected_date): @@ -1175,7 +835,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): Figure layers """ - logging.debug('Update layout ...') + logging.debug('Updating layout ...') if self.var and not os.path.exists(self.filepath): self.geojson = None @@ -1419,7 +1079,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): Figure layers """ - logging.debug('Update layout ...') + logging.debug('Updating layout ...') if (os.path.exists(self.filepath)) and (self.model in self.data.columns): # Get data @@ -1429,7 +1089,6 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): self.generate_colorbar() self.generate_var_tstep_trace(lon, lat, stations, scores, res) - logging.debug('Adding one point ...') # Get figure title self.fig_title = html.P(html.B( [ @@ -1650,14 +1309,19 @@ class VisFigureHandler(PointsFigureHandler): 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('
') diff --git a/preproc/nc2scores_aeronet.py b/preproc/nc2scores_aeronet.py index 0fca75b..60d1941 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 EvaluationGroundTimeSeriesHandler +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'))) diff --git a/preproc/nc2scores_modis.py b/preproc/nc2scores_modis.py index 6263107..d2588e6 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 EvaluationGroundTimeSeriesHandler +from timeseries_handler import EvaluationGroundTimeSeriesHandler from data_handler import STATS diff --git a/tabs/evaluation.py b/tabs/evaluation.py index a42a348..981e788 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -10,7 +10,6 @@ from data_handler import MODELS from data_handler import OBS from data_handler import DATES from data_handler import STATS -from data_handler import MODEBAR_CONFIG from data_handler import DISCLAIMER_MODELS from datetime import datetime as dt diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index b52edc0..6885153 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -2,7 +2,8 @@ import pytest from datetime import datetime from datetime import timedelta import importlib -code = importlib.import_module('data_handler') +data_handler = importlib.import_module('data_handler') +timeseries_handler = importlib.import_module('timeseries_handler') from data_handler import END_DATE from data_handler import FREQ @@ -15,8 +16,8 @@ EDATE_PREV = (datetime.strptime(END_DATE, FMT_ISO) - timedelta(days=7)).strftime # =================== AERONET OBSERVATIONS HANDLER ============================ @pytest.fixture def EvaluationGroundFigureHandler(): - return code.EvaluationGroundFigureHandler(sdate=EDATE_PREV, edate=END_DATE , obs='aeronet', - var='OD550_DUST') + return data_handler.EvaluationGroundFigureHandler(sdate=EDATE_PREV, edate=END_DATE , obs='aeronet', + var='OD550_DUST') def test_aeronet_get_figure_layers(EvaluationGroundFigureHandler): @@ -26,7 +27,7 @@ def test_aeronet_get_figure_layers(EvaluationGroundFigureHandler): # =================== TIME SERIES HANDLER ============================ @pytest.fixture def TSHandler(): - return code.ForecastModelsTimeSeriesHandler('median', END_DATE, 'OD550_DUST') + 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') @@ -51,12 +52,12 @@ def test_retrieve_timeseries_2(TSHandler): # =================== Evaluation Statistics Figure Handler ============================ @pytest.fixture def EvaluationStatisticsFigureHandler(): - return code.EvaluationStatisticsFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) + return data_handler.EvaluationStatisticsFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) # =================== Visibility Figure Handler ============================ @pytest.fixture def VisFigureHandler(): - return code.VisFigureHandler(selected_date=END_DATE) + return data_handler.VisFigureHandler(selected_date=END_DATE) def test_vis_get_figure_layers_empty(VisFigureHandler): VisFigureHandler.path_tpl = "fakepath" @@ -77,8 +78,8 @@ def test_vis_get_title(VisFigureHandler): # =================== Prob Handler ============================ @pytest.fixture def ForecastProbFigureHandler(): - ForecastProbFigureHandler_OD550_DUST = code.ForecastProbFigureHandler(var='OD550_DUST', prob=0.1, selected_date=END_DATE) - ForecastProbFigureHandler_SCONC_DUST = code.ForecastProbFigureHandler(var='SCONC_DUST', prob=50, selected_date=END_DATE) + ForecastProbFigureHandler_OD550_DUST = data_handler.ForecastProbFigureHandler(var='OD550_DUST', prob=0.1, selected_date=END_DATE) + ForecastProbFigureHandler_SCONC_DUST = data_handler.ForecastProbFigureHandler(var='SCONC_DUST', prob=50, selected_date=END_DATE) return ForecastProbFigureHandler_OD550_DUST, ForecastProbFigureHandler_SCONC_DUST # def test_prob_set_data(ForecastProbFigureHandler): @@ -118,7 +119,8 @@ def test_prob_get_figure_layers(ForecastProbFigureHandler): # =================== Was Handler ============================ @pytest.fixture def ForecastWasFigureHandler(): - return code.ForecastWasFigureHandler(was='burkinafaso', model='median', variable='SCONC_DUST', selected_date=END_DATE) + return data_handler.ForecastWasFigureHandler(was='burkinafaso', model='median', var='SCONC_DUST', + selected_date=END_DATE) def test_was_get_regions_data(ForecastWasFigureHandler): # day 1 diff --git a/timeseries_handler.py b/timeseries_handler.py new file mode 100644 index 0000000..9476638 --- /dev/null +++ b/timeseries_handler.py @@ -0,0 +1,446 @@ +# -*- coding: utf-8 -*- +""" Timeseries Handler """ + +import os +from datetime import datetime +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 utils import concat_dataframes +from utils import retrieve_timeseries +from utils import retrieve_single_point + +from data_handler import DASH_LOG_LEVEL +from data_handler import MODELS +from data_handler import MODEBAR_CONFIG +from data_handler import MODEBAR_CONFIG_TS +from data_handler import MODEBAR_LAYOUT +from data_handler import MODEBAR_LAYOUT_TS +from data_handler import DELAY +from data_handler import DELAY_DATE +from data_handler import FREQ +from data_handler import VARS +from data_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, variable): + """ Initialize ForecastModelsTimeSeriesHandler class + + Parameters + ---------- + model : str + _description_ + date : str + Selected date + variable : str + Variable name + """ + + 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") + + 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 single point + """ + + 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 (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): + """ 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 + Variable to indicate 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.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): + # logging.debug(' %s %s', 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): + 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, variable, 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((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 EvaluationGroundTimeSeriesHandler: + + def __init__(self, obs, start_date, end_date, variable, models=None): + """ Initialize EvaluationGroundTimeSeriesHandler class + + Parameters + ---------- + obs : str + Observations name + start_date : str + Start date + end_date : str + End date + variable : 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.variable = variable + 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((datetime.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, 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) + + return None + + def retrieve_timeseries(self, idx, st_name, model): + """ Retrieve timeseries plot for station in modal window + + Parameters + ---------- + idx : int + Station index + st_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], st_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.variable: + df = df.rename(columns = { df.columns[-1]: self.variable }) + + 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.variable].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.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"), + ]) + ) + ) + + logging.debug('FIG TYPE %s', type(fig)) + + return fig -- GitLab From 04a26d7476f7554fb57fc32295f75fd00f4d1a5a Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 4 Jul 2023 17:44:11 +0200 Subject: [PATCH 52/71] Improve documentation --- data_handler.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/data_handler.py b/data_handler.py index 3f90c55..6eb1cdc 100644 --- a/data_handler.py +++ b/data_handler.py @@ -122,6 +122,7 @@ else: 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 @@ -282,6 +283,7 @@ class ForecastModelsFigureHandler(ContourFigureHandler): class ForecastProbFigureHandler(ContourFigureHandler): + """ Class which handles forecast probability data """ def __init__(self, var=None, prob=None, selected_date=None): """ Initialize ForecastProbFigureHandler class @@ -433,6 +435,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): 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 @@ -551,6 +554,7 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): def generate_var_tstep_trace(self, day=0): """ Generate GeoJSON trace to be added per day + Parameters ---------- day : int, optional @@ -632,7 +636,20 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): """ Class which handles AERONET observations data """ def __init__(self, sdate=None, edate=None, obs=None, var=None): - + """_summary_ + + Parameters + ---------- + sdate : str, optional + Start date, by default None + edate : 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'] @@ -732,6 +749,7 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): class EvaluationSatelliteFigureHandler(ContourFigureHandler): + """ Class which handles MODIS sensor data """ def __init__(self, var, obs, tstep, selected_date): """ Initialize EvaluationSatelliteFigureHandler class @@ -867,6 +885,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): class EvaluationStatisticsFigureHandler(PointsFigureHandler): + """ Class which handles evaluation statistics data """ def __init__(self, network=None, statistic=None, model=None, selection=None): """ Initialize EvaluationStatisticsFigureHandler class @@ -1108,6 +1127,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): class VisFigureHandler(PointsFigureHandler): + """ Class which handles visibility data """ def __init__(self, selected_date=None): """ Initialize VisFigureHandler class -- GitLab From 40b134b9c05e6c570b230f66399d48b7129cf19c Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 09:41:53 +0200 Subject: [PATCH 53/71] Arrange fig title --- data_handler.py | 26 ++++++++++++++------------ tests/test_data_handler.py | 2 +- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/data_handler.py b/data_handler.py index 6eb1cdc..8d907d6 100644 --- a/data_handler.py +++ b/data_handler.py @@ -855,17 +855,7 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): logging.debug('Updating layout ...') - if self.var and not os.path.exists(self.filepath): - 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("DATA NOT AVAILABLE")) - else: + if self.var and os.path.exists(self.filepath): # Get trace and colorbar self.generate_colorbar() self.generate_var_tstep_trace(tstep) @@ -877,6 +867,16 @@ class EvaluationSatelliteFigureHandler(ContourFigureHandler): 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) @@ -1351,7 +1351,9 @@ class VisFigureHandler(PointsFigureHandler): else: self.geojson = None self.legend = None - self.fig_title = "NO DATA AVAILABLE" + + # Get figure title + self.fig_title = html.P(html.B("DATA NOT AVAILABLE")) # Get title information element self.info = self.retrieve_info(self.name) diff --git a/tests/test_data_handler.py b/tests/test_data_handler.py index 6885153..5a697b1 100644 --- a/tests/test_data_handler.py +++ b/tests/test_data_handler.py @@ -63,7 +63,7 @@ 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 VisFigureHandler.info.children == "NO DATA AVAILABLE" + assert str(VisFigureHandler.info.children) == "P(B('DATA NOT AVAILABLE'))" def test_vis_get_figure_layers(VisFigureHandler): VisFigureHandler.get_figure_layers(tstep=0) -- GitLab From 3178e21c7ceb9cf15c1d3cd3d89bd3ae2579c359 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 10:44:15 +0200 Subject: [PATCH 54/71] Document utils and improve doc in other files --- data_handler.py | 2 +- timeseries_handler.py | 2 +- utils.py | 244 +++++++++++++++++++++++++++++++++++++++--- 3 files changed, 230 insertions(+), 18 deletions(-) diff --git a/data_handler.py b/data_handler.py index 8d907d6..1b848b5 100644 --- a/data_handler.py +++ b/data_handler.py @@ -729,7 +729,7 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): Returns ------- - pandas.DataFrame + pandas.core.frame.DataFrame Dataframe with stations information list Figure layers diff --git a/timeseries_handler.py b/timeseries_handler.py index 9476638..cb1f05e 100644 --- a/timeseries_handler.py +++ b/timeseries_handler.py @@ -77,7 +77,7 @@ class ForecastModelsTimeSeriesHandler: Returns ------- float - Variable value at single point + Variable value at closest available point from (lat, lon) """ if not model: diff --git a/utils.py b/utils.py index c8848fd..5a094a6 100644 --- a/utils.py +++ b/utils.py @@ -16,9 +16,30 @@ import logging def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None): - """ Concatenate monthly dataframes """ - - # build feather files paths + """ 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))] @@ -26,12 +47,12 @@ def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=Non if not opaths: return None, None - # read monthly dataframes and concatenate into one + # 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 + # In case of models we don't rename the variable column else: mon_dfs = pd.concat([feather.read_dataframe(opath) for opath in opaths]) @@ -51,23 +72,77 @@ def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=Non 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 data_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 data_handler import DEBUG + if method == 'feather' and not forecast: df = feather.read_dataframe(fname) if 'lat' in df.columns: @@ -100,7 +175,9 @@ def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=Fal 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' @@ -109,16 +186,45 @@ def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=Fal 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 """ + """ 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 """ + """ 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])): @@ -128,32 +234,95 @@ def find_nearest2(array, value): def calc_matrix(n): - """ Calculate the mosaic optimum matrix shape """ + """ 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 """ + """ 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 """ + """ 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_colorscale(bounds, colormap, discrete=True): - """ Create colorscale """ + """ Create colorscale (it was used in mapbox, now it is not being applied anywhere) + + Parameters + ---------- + bounds : list + Bounds, e.g. [-0.1, -0.08, -0.06, -0.04, -0.02, 0., 0.02, 0.04, 0.06, 0.08, 0.1] + colormap : str + Colormap, e.g. 'viridis' + discrete : bool, optional + Indicates if the colorscale is going to be discrete or not, by default True + + Returns + ------- + list + Colorscale + """ + if isinstance(colormap, str): colormap = cm.get_cmap(colormap) @@ -185,8 +354,25 @@ def get_colorscale(bounds, colormap, discrete=True): def get_vis_edate(end_date, hour=None): - """ Return default date and timestep for visibility. """ + """ 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 data_handler import DEBUG + delay = timedelta(hours=8) half_day = timedelta(hours=12) fmt_full = "%Y%m%d %H:%M" @@ -218,8 +404,34 @@ def get_vis_edate(end_date, hour=None): 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 """ +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!") -- GitLab From 73e10a63ef891b081e8c4b71cf04a9b0d92458e0 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 5 Jul 2023 10:47:51 +0200 Subject: [PATCH 55/71] Refactor router.js to make it easier to add url outputs. --- assets/router.js | 58 +++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/assets/router.js b/assets/router.js index f771297..a871c9b 100644 --- a/assets/router.js +++ b/assets/router.js @@ -23,18 +23,16 @@ function getDate() { // LISTEN FOR MESSAGES FROM CMS window.addEventListener("message", (event) => { - // Create list of addresses + // 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); + window.history.pushState("From Dashboard", "", event.data); return; }else{ console.log('DASH: that is not the right address' + event.origin); @@ -42,9 +40,9 @@ window.addEventListener("message", (event) => { } }, false); -// ASSESS HOW THE URL SHOULD BE OUTPUT +// SEND THE URL TO THE CMS function sendURL(url) { - window.history.pushState("Models", "Models", url); + window.history.pushState("URL from Dashboard", "", url); parent.postMessage(url, '*'); } @@ -64,46 +62,46 @@ function getModels(){ return data.join('&'); } -// CREATE OUTPUT URL FOR MODELS-APPLY -$(document).ready(function () { - $(document).on('click', "#models-apply", function () { +// CREATE URL WHEN MODELS-APPLY IS CLICKED +function handleModelsApply () { curvar = getVar() models = getModels() outputDate = getDate(); url = "?" + curvar + models + outputDate; sendURL(url); - }) -}); + }; -// CREATE WAS OUTPUT -$(document).ready(function () { - $(document).on('click', "#was-apply", function () { +// 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, +}; + $(document).ready(function () { - var toggleMap = { - "#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" - }; // Match ID with the tooglemap key and output url value - $(document).on('click', Object.keys(toggleMap).join(', '), function () { + $(document).on('click', Object.keys(urlOuputDict).join(', '), function () { var selector = "#" + $(this).attr('id'); - var url = toggleMap[selector]; + var value = urlOuputDict[selector]; + var url = typeof value === 'function' ? value() : value; sendURL(url); }); }); -- GitLab From bf355e92fa475eb1782001bb042e6db212574e61 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 10:53:27 +0200 Subject: [PATCH 56/71] Remove DEBUG statement --- ines_core_data_handler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 6bc8eb0..8215abf 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -468,8 +468,7 @@ class ContourFigureHandler(MapHandler): from dash_server import app - if DEBUG: - print('================= Retrieving geojson...') + logging.debug('================= Retrieving geojson...') # Get contour properties hideout = dict(colorscale=self.colorscale, bounds=self.bounds, style=self.style, -- GitLab From 150860e667888f1ac92014dc88e7c6ee8b6f85ef Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 5 Jul 2023 11:17:24 +0200 Subject: [PATCH 57/71] Fix formatting in evaluation --- tabs/evaluation.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/tabs/evaluation.py b/tabs/evaluation.py index a42a348..493d44a 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -427,24 +427,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", + ), + ] -- GitLab From c9e5e164a739fd093904056fc9b730804d7e5911 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 11:24:58 +0200 Subject: [PATCH 58/71] Refactor render 404 --- conf/render404.json | 13 +++++++++++ data_handler.py | 1 + router.py | 56 ++++++++++++++++++++++----------------------- 3 files changed, 42 insertions(+), 28 deletions(-) create mode 100644 conf/render404.json diff --git a/conf/render404.json b/conf/render404.json new file mode 100644 index 0000000..69cc03b --- /dev/null +++ b/conf/render404.json @@ -0,0 +1,13 @@ +{ + "forecast": {"name": "Forecast", + "id": "forecast_link", + "path": "/?tab=forecast"}, + + "evaluation": {"name": "Evaluation", + "id": "evaluation_link", + "path": "/?tab=evaluation"}, + + "observations": {"name": "Observations", + "id": "observations_link", + "path": "/?tab=observations"} +} \ No newline at end of file diff --git a/data_handler.py b/data_handler.py index 1b848b5..a01ff2b 100644 --- a/data_handler.py +++ b/data_handler.py @@ -67,6 +67,7 @@ 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'] diff --git a/router.py b/router.py index e73b1c0..c7b888c 100644 --- a/router.py +++ b/router.py @@ -13,6 +13,7 @@ from data_handler import DEBUG from data_handler import PATHNAME from data_handler import ALIASES from data_handler import ROUTE_DEFAULTS +from data_handler import RENDER404 from tabs.forecast import tab_forecast from tabs.forecast import sidebar_forecast @@ -37,13 +38,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): @@ -78,41 +81,38 @@ def render_sidebar(tab='forecast-tab', route_selections=ROUTE_DEFAULTS): 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" + """ 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 helpful links that might help:"),] + + # Define links + for tab_i, tab in enumerate(RENDER404.keys()): + # 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=PATHNAME+RENDER404[tab]['path'], + className='error_links',target='_parent', + refresh=True),]) + # Create page 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) - ], + children=children, ) ] ) ] + return page @dash.callback( -- GitLab From 87cedb0310bbbb5fbae90ae80485000650275f0a Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 12:51:51 +0200 Subject: [PATCH 59/71] Remove get_colorscale and move render404 to utils --- router.py | 43 +++------------------ tests/test_router.py | 7 ---- tests/test_utils.py | 12 ++---- utils.py | 89 ++++++++++++++++++++------------------------ 4 files changed, 49 insertions(+), 102 deletions(-) diff --git a/router.py b/router.py index c7b888c..be0ce4a 100644 --- a/router.py +++ b/router.py @@ -10,10 +10,9 @@ 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 data_handler import RENDER404 +from utils import render404 from tabs.forecast import tab_forecast from tabs.forecast import sidebar_forecast @@ -80,49 +79,16 @@ def render_sidebar(tab='forecast-tab', route_selections=ROUTE_DEFAULTS): } return tabs[tab] -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 helpful links that might help:"),] - - # Define links - for tab_i, tab in enumerate(RENDER404.keys()): - # 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=PATHNAME+RENDER404[tab]['path'], - className='error_links',target='_parent', - refresh=True),]) - - # Create page - page = [html.Div( - className='background', - children=[ - html.Div( - id='error_div', - children=children, - ) - ] - ) - ] - - return page - @dash.callback( Output("content", "children"), Input("url", "href"), ) def router(url): """ Get url search queries and build layout for app""" + route_selections = get_url_queries(url) logging.info('===== route_selections %s', route_selections) + try: children = [ html.Div( @@ -138,7 +104,8 @@ def router(url): go_fullscreen(), ]), ] - except Exception as err: #This handles when user inputs incorrect URL params + except Exception as err: # This handles when user inputs incorrect URL params logging.debug("ERROR 404 %s", str(err)) children = render404() + return children diff --git a/tests/test_router.py b/tests/test_router.py index 50aed5c..34bf611 100644 --- a/tests/test_router.py +++ b/tests/test_router.py @@ -74,10 +74,3 @@ def test_router(): #verify incorrect url lands on default url ="children=[H2(children='404 Error', id='error_title" assert "id='app-tabs', value='forecast-tab')]" in str(code.router(url)) - - #============ TEST render 404 ================================= -def test_render404(): - #verify output of 404 function - assert "Div(children=[Div(children=[H2(children='404 Error', id='error_title')" in str(code.render404()[0]) - assert "P('Here are some helpful 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]) - diff --git a/tests/test_utils.py b/tests/test_utils.py index 7e430e9..f91804a 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -101,14 +101,10 @@ 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_render404(): + assert "Div(children=[Div(children=[H2(children='404 Error', id='error_title')" in str(code.render404()[0]) + assert "P('Here are some helpful 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]) diff --git a/utils.py b/utils.py index 5a094a6..1f06ea5 100644 --- a/utils.py +++ b/utils.py @@ -13,7 +13,10 @@ import numpy as np import pandas as pd import feather import logging - +from dash import dcc +from dash import html +from data_handler import PATHNAME +from data_handler import RENDER404 def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None): """ Concatenate monthly dataframes @@ -305,54 +308,6 @@ def normalize_vals(vals, valsmin, valsmax, rnd=2): return np.around((vals-valsmin)/(valsmax-valsmin), rnd) -def get_colorscale(bounds, colormap, discrete=True): - """ Create colorscale (it was used in mapbox, now it is not being applied anywhere) - - Parameters - ---------- - bounds : list - Bounds, e.g. [-0.1, -0.08, -0.06, -0.04, -0.02, 0., 0.02, 0.04, 0.06, 0.08, 0.1] - colormap : str - Colormap, e.g. 'viridis' - discrete : bool, optional - Indicates if the colorscale is going to be discrete or not, by default True - - Returns - ------- - list - 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 @@ -464,3 +419,39 @@ def get_currdate_tstep(model_start, model_start_before, current_time_before, del cdo_tsteps = "5/25" return selected_date, tstep, cdo_tsteps + + +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 helpful links that might help:"),] + + # Define links + for tab_i, tab in enumerate(RENDER404.keys()): + # 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=PATHNAME+RENDER404[tab]['path'], + className='error_links',target='_parent', + refresh=True),]) + + # Create page + page = [html.Div( + className='background', + children=[ + html.Div( + id='error_div', + children=children, + ) + ] + ) + ] + + return page -- GitLab From 61214eef0664e651b5e8fd9d9d68ba074a162eb6 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 14:54:33 +0200 Subject: [PATCH 60/71] Move get_vis_edate and create ines core router js --- assets/router.js | 35 ++------ callback_tools.py | 83 +++++++++++++++++-- data_handler.py | 14 ++-- ines_core_data_handler.py | 4 +- js_ines_core/router.js | 29 +++++++ router.py | 2 +- tabs/observations.py | 52 +++++++++++- tests/test_observations.py | 27 +++---- tests/test_observations_callbacks.py | 2 +- tests/test_utils.py | 4 - timeseries_handler.py | 22 ++--- utils.py | 115 --------------------------- 12 files changed, 196 insertions(+), 193 deletions(-) create mode 100644 js_ines_core/router.js diff --git a/assets/router.js b/assets/router.js index a871c9b..0186e7e 100644 --- a/assets/router.js +++ b/assets/router.js @@ -1,29 +1,12 @@ // 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]; -} + +import { getDate } from './js_ines_core/router.js'; +import { sendURL } from './js_ines_core/router.js'; + // LISTEN FOR MESSAGES FROM CMS window.addEventListener("message", (event) => { - // CREATE LIST OF ADDRESSES + // Create list of addresses const hosts = [ 'http://bscesdust02.bsc.es', 'https://dust.aemet.es', @@ -40,12 +23,6 @@ window.addEventListener("message", (event) => { } }, false); -// SEND THE URL TO THE CMS -function sendURL(url) { - window.history.pushState("URL from Dashboard", "", url); - parent.postMessage(url, '*'); -} - // GET THE VARIABLE VALUE function getVar() { curvar = document.querySelector('.Select-value-label').innerHTML; @@ -105,5 +82,3 @@ $(document).ready(function () { sendURL(url); }); }); - - diff --git a/callback_tools.py b/callback_tools.py index d8b222d..034d84c 100644 --- a/callback_tools.py +++ b/callback_tools.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """ Tools module with functions related to plots """ -from datetime import datetime as dt +from datetime import datetime from datetime import timedelta import os import logging @@ -21,10 +21,10 @@ from data_handler import PATHS from data_handler import END_DATE, DELAY, DELAY_DATE from timeseries_handler import ForecastModelsTimeSeriesHandler from timeseries_handler import EvaluationGroundTimeSeriesHandler -from utils import get_currdate_tstep def download_image_link(models, variable, curdate, tstep=0, anim=False): """ Generates links to animated gifs """ + logging.debug('CURRDIR %s', os.getcwd()) filepath = "assets/comparison/{model}/{variable}/{year}/{month}/{curdate}_{model}_{tstep}.{ext}" @@ -53,30 +53,37 @@ def download_image_link(models, variable, curdate, tstep=0, anim=False): ext=ext ) logging.debug('DOWNLOAD FILENAME %s', filename) + return filename def get_eval_timeseries(obs, start_date, end_date, var, idx, name, model): """ Retrieve 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, name, model) def get_timeseries(model, date, var, lat, lon, forecast=False): """ Retrieve 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): """ Retrieve sigle point """ + 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) @@ -110,13 +117,75 @@ def get_evaluation_statistics_figure(network=None, model=None, statistic=None, s 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 figure """ logging.debug("*** %s %s %s %s %s *****", model, var, selected_date, tstep, hour) try: - selected_date = dt.strptime( + selected_date = datetime.strptime( selected_date, "%Y-%m-%d").strftime("%Y%m%d") except: pass @@ -124,7 +193,7 @@ def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, asp 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")) + 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} * @@ -150,7 +219,7 @@ def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): """ Retrieve figure """ logging.debug(' %s %s %s',prob, day, selected_date) try: - selected_date = dt.strptime( + selected_date = datetime.strptime( selected_date, "%Y-%m-%d").strftime("%Y%m%d") except: pass @@ -168,7 +237,7 @@ def get_was_figure(was=None, day=0, selected_date=END_DATE): """ Retrieve figure """ logging.debug(' %s %s %s', was, day, selected_date) try: - selected_date = dt.strptime( + selected_date = datetime.strptime( selected_date, "%Y-%m-%d").strftime("%Y%m%d") except: pass @@ -186,7 +255,7 @@ def get_vis_figure(tstep=0, selected_date=END_DATE): """ Retrieve figure """ logging.debug(' %s %s', tstep, selected_date) try: - selected_date = dt.strptime( + selected_date = datetime.strptime( selected_date, "%Y-%m-%d").strftime("%Y%m%d") except: pass diff --git a/data_handler.py b/data_handler.py index a01ff2b..838311f 100644 --- a/data_handler.py +++ b/data_handler.py @@ -19,7 +19,7 @@ import os import time import calendar from datetime import timedelta -from datetime import datetime +from datetime import datetime as dt from dateutil.relativedelta import relativedelta from ines_core_data_handler import MapHandler @@ -79,8 +79,8 @@ 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 (datetime.now() - - timedelta(days=1)).strftime("%Y%m%d") or datetime.now().strftime("%Y%m%d")) +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'] @@ -1157,7 +1157,7 @@ class VisFigureHandler(PointsFigureHandler): self.circle_options = VIS['circle_options'] if self.selected_date: - self.rdatetime = datetime.strptime(self.selected_date, '%Y%m%d') + self.rdatetime = dt.strptime(self.selected_date, '%Y%m%d') return None @@ -1174,9 +1174,9 @@ class VisFigureHandler(PointsFigureHandler): tstep0 = tstep tstep1 = tstep + self.freq logging.info("%s %s %s", self.selected_date, tstep0, tstep1) - year = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%Y') - month = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%m') - day = datetime.strptime(self.selected_date, '%Y%m%d').strftime('%d') + 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) diff --git a/ines_core_data_handler.py b/ines_core_data_handler.py index 8215abf..351d595 100644 --- a/ines_core_data_handler.py +++ b/ines_core_data_handler.py @@ -10,7 +10,7 @@ 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 +from datetime import datetime as dt import logging DIR_PATH = os.path.dirname(os.path.realpath(__file__)) @@ -222,7 +222,7 @@ class MapHandler: what, _, rdate, rtime = tim_units[:4] if len(rtime) > 5: rtime = rtime[:5] - rdatetime = datetime.strptime("{} {}".format(rdate, rtime), "%Y-%m-%d %H:%M") + rdatetime = dt.strptime("{} {}".format(rdate, rtime), "%Y-%m-%d %H:%M") return timesteps, what, rdatetime diff --git a/js_ines_core/router.js b/js_ines_core/router.js new file mode 100644 index 0000000..3f9fba6 --- /dev/null +++ b/js_ines_core/router.js @@ -0,0 +1,29 @@ +// 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, '*'); +} \ No newline at end of file diff --git a/router.py b/router.py index be0ce4a..24cfa61 100644 --- a/router.py +++ b/router.py @@ -105,7 +105,7 @@ def router(url): ]), ] except Exception as err: # This handles when user inputs incorrect URL params - logging.debug("ERROR 404 %s", str(err)) + logging.error("ERROR 404 %s", str(err)) children = render404() return children diff --git a/tabs/observations.py b/tabs/observations.py index bf49e4b..6959996 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -12,7 +12,6 @@ from data_handler import START_DATE, END_DATE from data_handler import DISCLAIMER_MODELS from data_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 @@ -43,6 +42,57 @@ layout_view = html.Div([ )]) +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 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 = 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): # logging.debug("----------- %s %s %s %s -------------\n", start, type(start), step, type(step)) diff --git a/tests/test_observations.py b/tests/test_observations.py index bb58ac3..374d5eb 100644 --- a/tests/test_observations.py +++ b/tests/test_observations.py @@ -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) + _, 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 8877994..0ff96cf 100644 --- a/tests/test_observations_callbacks.py +++ b/tests/test_observations_callbacks.py @@ -37,7 +37,7 @@ def test_render_observations_tab_visibility(): 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('20220831', hour=hour) diff --git a/tests/test_utils.py b/tests/test_utils.py index f91804a..f72a720 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -101,10 +101,6 @@ 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_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_render404(): assert "Div(children=[Div(children=[H2(children='404 Error', id='error_title')" in str(code.render404()[0]) assert "P('Here are some helpful 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]) diff --git a/timeseries_handler.py b/timeseries_handler.py index cb1f05e..bb0aa19 100644 --- a/timeseries_handler.py +++ b/timeseries_handler.py @@ -2,7 +2,7 @@ """ Timeseries Handler """ import os -from datetime import datetime +from datetime import datetime as dt from datetime import timedelta from dateutil.relativedelta import relativedelta import plotly.graph_objs as go @@ -52,11 +52,11 @@ class ForecastModelsTimeSeriesHandler: 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") + 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 = datetime.strptime(date, "%Y-%m-%d").strftime("%Y%m") - self.currdate = datetime.strptime(date, "%Y-%m-%d").strftime("%Y%m%d") + 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 @@ -86,9 +86,9 @@ class ForecastModelsTimeSeriesHandler: logging.debug("---------- %s", 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 \ + 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 = (datetime.strptime(self.currdate, "%Y%m%d") - + mod_date = (dt.strptime(self.currdate, "%Y%m%d") - timedelta(days=1)).strftime("%Y%m%d") else: mod_date = self.currdate @@ -148,9 +148,9 @@ class ForecastModelsTimeSeriesHandler: 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 \ + 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 = (datetime.strptime(self.currdate, "%Y%m%d") - + mod_date = (dt.strptime(self.currdate, "%Y%m%d") - timedelta(days=1)).strftime("%Y%m%d") else: mod_date = self.currdate @@ -192,7 +192,7 @@ class ForecastModelsTimeSeriesHandler: 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') + + '{}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) @@ -298,7 +298,7 @@ class EvaluationGroundTimeSeriesHandler: 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') + + '{}01'.format((dt.strptime(months[-1], '%Y%m') + relativedelta(days=31)).strftime('%Y%m')), freq=f'{FREQ}H') logging.debug('MONTHS %s', months) diff --git a/utils.py b/utils.py index 1f06ea5..8e0312b 100644 --- a/utils.py +++ b/utils.py @@ -4,8 +4,6 @@ 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 @@ -308,119 +306,6 @@ def normalize_vals(vals, valsmin, valsmax, rnd=2): return np.around((vals-valsmin)/(valsmax-valsmin), rnd) -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 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: 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 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 render404(): """ Create a 404 page """ -- GitLab From ddffd1bb55e4d1a1b77e15fb1e84f65542028873 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 15:01:18 +0200 Subject: [PATCH 61/71] Rename data_handler to map_handler --- ines_core_data_handler.py => ines_core_map_handler.py | 0 data_handler.py => map_handler.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename ines_core_data_handler.py => ines_core_map_handler.py (100%) rename data_handler.py => map_handler.py (100%) diff --git a/ines_core_data_handler.py b/ines_core_map_handler.py similarity index 100% rename from ines_core_data_handler.py rename to ines_core_map_handler.py diff --git a/data_handler.py b/map_handler.py similarity index 100% rename from data_handler.py rename to map_handler.py -- GitLab From 8693aced0329381a3d9d75452c285c0eebe19f0f Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Wed, 5 Jul 2023 15:12:43 +0200 Subject: [PATCH 62/71] Rename data handler to map handler in all files --- callback_tools.py | 24 ++++++++-------- dash_server.py | 8 +++--- map_handler.py | 10 +++---- router.py | 10 +++---- tabs/evaluation.py | 18 ++++++------ tabs/evaluation_callbacks.py | 22 +++++++-------- tabs/forecast.py | 14 +++++----- tabs/forecast_callbacks.py | 28 +++++++++---------- tabs/generic.py | 4 +-- tabs/generic_callbacks.py | 4 +-- tabs/observations.py | 22 +++++++-------- tabs/observations_callbacks.py | 8 +++--- tests/test_evaluation_callbacks.py | 2 +- tests/test_forecast.py | 4 +-- tests/test_forecast_callbacks.py | 2 +- ...ta_handler.py => test_ines_map_handler.py} | 6 ++-- ...st_data_handler.py => test_map_handler.py} | 18 ++++++------ tests/test_observations.py | 6 ++-- tests/test_observations_callbacks.py | 2 +- tests/test_router.py | 2 +- tests/test_tools.py | 4 +-- tests/test_utils.py | 4 +-- timeseries_handler.py | 22 +++++++-------- utils.py | 8 +++--- 24 files changed, 126 insertions(+), 126 deletions(-) rename tests/{test_ines_data_handler.py => test_ines_map_handler.py} (95%) rename tests/{test_data_handler.py => test_map_handler.py} (92%) diff --git a/callback_tools.py b/callback_tools.py index 034d84c..06bac81 100644 --- a/callback_tools.py +++ b/callback_tools.py @@ -7,18 +7,18 @@ from datetime import timedelta import os import logging -from data_handler import MapHandler -from data_handler import ForecastModelsFigureHandler -from data_handler import ForecastProbFigureHandler -from data_handler import ForecastWasFigureHandler -from data_handler import EvaluationGroundFigureHandler -from data_handler import EvaluationSatelliteFigureHandler -from data_handler import EvaluationStatisticsFigureHandler -from data_handler import VisFigureHandler -from data_handler import DEBUG -from data_handler import MODELS -from data_handler import PATHS -from data_handler import END_DATE, DELAY, DELAY_DATE +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 diff --git a/dash_server.py b/dash_server.py index 3def1a8..0d3d3ef 100755 --- a/dash_server.py +++ b/dash_server.py @@ -6,10 +6,10 @@ from dash import dcc from dash import html from flask.app import Flask -from data_handler import DASH_LOG_LEVEL -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 * diff --git a/map_handler.py b/map_handler.py index 838311f..7a8b62e 100644 --- a/map_handler.py +++ b/map_handler.py @@ -22,10 +22,10 @@ from datetime import timedelta from datetime import datetime as dt from dateutil.relativedelta import relativedelta -from ines_core_data_handler import MapHandler -from ines_core_data_handler import PointsFigureHandler -from ines_core_data_handler import ContourFigureHandler -from ines_core_data_handler import ShapefileFigureHandler +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 @@ -936,7 +936,7 @@ class EvaluationStatisticsFigureHandler(PointsFigureHandler): # 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 = datetime(year, month, 1) + self.rdatetime = dt(year, month, 1) # Read data self.read_data() diff --git a/router.py b/router.py index 24cfa61..7d1906b 100644 --- a/router.py +++ b/router.py @@ -7,11 +7,11 @@ 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 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 utils import render404 from tabs.forecast import tab_forecast diff --git a/tabs/evaluation.py b/tabs/evaluation.py index d0007ef..3fcf96d 100644 --- a/tabs/evaluation.py +++ b/tabs/evaluation.py @@ -2,15 +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 DATES -from data_handler import STATS -from data_handler import DISCLAIMER_MODELS +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 diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index 9887b80..b2d82c9 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -8,17 +8,17 @@ 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_MODELS -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 diff --git a/tabs/forecast.py b/tabs/forecast.py index 7757099..a67012d 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -8,13 +8,13 @@ import dash_bootstrap_components as dbc from dash import dcc from dash import html -from data_handler import FORECAST_MAX -from data_handler import FREQ -from data_handler import DEBUG -from data_handler import START_DATE, END_DATE, DELAY, DELAY_DATE -from data_handler import WAS -from data_handler import DISCLAIMER_MODELS -from data_handler import PATHS +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 diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 78d74d8..415d090 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -20,20 +20,20 @@ from dash.dependencies import ALL from dash.exceptions import PreventUpdate import dash_leaflet as dl -from ines_core_data_handler import MAP_LAYERS -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 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 diff --git a/tabs/generic.py b/tabs/generic.py index ca2be7a..a417cfe 100644 --- a/tabs/generic.py +++ b/tabs/generic.py @@ -1,7 +1,7 @@ import dash_bootstrap_components as dbc from dash import html -from ines_core_data_handler import MAP_LAYERS -from data_handler import DELAY, DELAY_DATE, END_DATE, FORECAST_FINAL_DAY +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 diff --git a/tabs/generic_callbacks.py b/tabs/generic_callbacks.py index 990133d..df0b280 100644 --- a/tabs/generic_callbacks.py +++ b/tabs/generic_callbacks.py @@ -2,8 +2,8 @@ import dash from dash.dependencies import ALL, MATCH from dash.exceptions import PreventUpdate from dash.dependencies import Input, State, Output -from data_handler import DEBUG, FREQ -from data_handler import cache, cache_timeout +from map_handler import DEBUG, FREQ +from map_handler import cache, cache_timeout @dash.callback( Output({'type':'caret', 'index': MATCH}, 'style'), diff --git a/tabs/observations.py b/tabs/observations.py index 6959996..831d186 100644 --- a/tabs/observations.py +++ b/tabs/observations.py @@ -1,16 +1,16 @@ from dash import dcc import dash_bootstrap_components as dbc from dash import html -from ines_core_data_handler import MAP_LAYERS -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 DISCLAIMER_MODELS -from data_handler import SATELLITE_IMAGE_SRC +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 datetime import datetime as dt @@ -60,7 +60,7 @@ def get_vis_edate(end_date, hour=None): Default hour """ - from data_handler import DEBUG + from map_handler import DEBUG delay = timedelta(hours=8) half_day = timedelta(hours=12) diff --git a/tabs/observations_callbacks.py b/tabs/observations_callbacks.py index 9e66039..44bb1ef 100644 --- a/tabs/observations_callbacks.py +++ b/tabs/observations_callbacks.py @@ -5,9 +5,9 @@ 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 data_handler import SATELLITE_IMAGE_SRC +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 @@ -15,7 +15,7 @@ from datetime import datetime as dt import math import logging -from data_handler import cache, cache_timeout +from map_handler import cache, cache_timeout @dash.callback( [Output('observations-tab', 'children'), diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py index 16bd0e7..ce00ed3 100644 --- a/tests/test_evaluation_callbacks.py +++ b/tests/test_evaluation_callbacks.py @@ -7,7 +7,7 @@ import dash from dash._callback_context import context_value from dash._utils import AttributeDict code = importlib.import_module('tabs.evaluation_callbacks') -from data_handler import START_DATE, END_DATE +from map_handler import START_DATE, END_DATE #add equality checker for objects def __eq__(self, other): diff --git a/tests/test_forecast.py b/tests/test_forecast.py index 6f4cbab..cade99f 100644 --- a/tests/test_forecast.py +++ b/tests/test_forecast.py @@ -1,6 +1,6 @@ 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_model_time_bar(): diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index 7cbfbde..6185fa2 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 data_handler import START_DATE +from map_handler import START_DATE # =======================START render forecast test =========================== diff --git a/tests/test_ines_data_handler.py b/tests/test_ines_map_handler.py similarity index 95% rename from tests/test_ines_data_handler.py rename to tests/test_ines_map_handler.py index 97ef850..8989da9 100644 --- a/tests/test_ines_data_handler.py +++ b/tests/test_ines_map_handler.py @@ -2,9 +2,9 @@ import pytest from datetime import datetime from datetime import timedelta import importlib -code = importlib.import_module('ines_core_data_handler') -from data_handler import END_DATE -from data_handler import FREQ +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" diff --git a/tests/test_data_handler.py b/tests/test_map_handler.py similarity index 92% rename from tests/test_data_handler.py rename to tests/test_map_handler.py index 5a697b1..1c1b4d5 100644 --- a/tests/test_data_handler.py +++ b/tests/test_map_handler.py @@ -2,10 +2,10 @@ import pytest from datetime import datetime from datetime import timedelta import importlib -data_handler = importlib.import_module('data_handler') +map_handler = importlib.import_module('map_handler') timeseries_handler = importlib.import_module('timeseries_handler') -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" @@ -16,7 +16,7 @@ EDATE_PREV = (datetime.strptime(END_DATE, FMT_ISO) - timedelta(days=7)).strftime # =================== AERONET OBSERVATIONS HANDLER ============================ @pytest.fixture def EvaluationGroundFigureHandler(): - return data_handler.EvaluationGroundFigureHandler(sdate=EDATE_PREV, edate=END_DATE , obs='aeronet', + return map_handler.EvaluationGroundFigureHandler(sdate=EDATE_PREV, edate=END_DATE , obs='aeronet', var='OD550_DUST') def test_aeronet_get_figure_layers(EvaluationGroundFigureHandler): @@ -52,12 +52,12 @@ def test_retrieve_timeseries_2(TSHandler): # =================== Evaluation Statistics Figure Handler ============================ @pytest.fixture def EvaluationStatisticsFigureHandler(): - return data_handler.EvaluationStatisticsFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) + return map_handler.EvaluationStatisticsFigureHandler('aeronet', 'bias', 'median', '{edate}'.format(edate=EDATE_OBJ.strftime("%Y%m"))) # =================== Visibility Figure Handler ============================ @pytest.fixture def VisFigureHandler(): - return data_handler.VisFigureHandler(selected_date=END_DATE) + return map_handler.VisFigureHandler(selected_date=END_DATE) def test_vis_get_figure_layers_empty(VisFigureHandler): VisFigureHandler.path_tpl = "fakepath" @@ -78,8 +78,8 @@ def test_vis_get_title(VisFigureHandler): # =================== Prob Handler ============================ @pytest.fixture def ForecastProbFigureHandler(): - ForecastProbFigureHandler_OD550_DUST = data_handler.ForecastProbFigureHandler(var='OD550_DUST', prob=0.1, selected_date=END_DATE) - ForecastProbFigureHandler_SCONC_DUST = data_handler.ForecastProbFigureHandler(var='SCONC_DUST', prob=50, selected_date=END_DATE) + 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): @@ -119,7 +119,7 @@ def test_prob_get_figure_layers(ForecastProbFigureHandler): # =================== Was Handler ============================ @pytest.fixture def ForecastWasFigureHandler(): - return data_handler.ForecastWasFigureHandler(was='burkinafaso', model='median', var='SCONC_DUST', + return map_handler.ForecastWasFigureHandler(was='burkinafaso', model='median', var='SCONC_DUST', selected_date=END_DATE) def test_was_get_regions_data(ForecastWasFigureHandler): diff --git a/tests/test_observations.py b/tests/test_observations.py index 374d5eb..0f9ae48 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" diff --git a/tests/test_observations_callbacks.py b/tests/test_observations_callbacks.py index 0ff96cf..3597969 100644 --- a/tests/test_observations_callbacks.py +++ b/tests/test_observations_callbacks.py @@ -5,7 +5,7 @@ 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 +from map_handler import START_DATE from unittest.mock import MagicMock #============ TEST render_evaluation_tab================================= diff --git a/tests/test_router.py b/tests/test_router.py index 34bf611..6bb6cf6 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 54323ad..35e0427 100644 --- a/tests/test_tools.py +++ b/tests/test_tools.py @@ -3,8 +3,8 @@ from datetime import datetime from datetime import timedelta import importlib code = importlib.import_module('callback_tools') -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" diff --git a/tests/test_utils.py b/tests/test_utils.py index f72a720..5030292 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -4,8 +4,8 @@ from datetime import timedelta import importlib code = importlib.import_module('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" diff --git a/timeseries_handler.py b/timeseries_handler.py index bb0aa19..e5e1182 100644 --- a/timeseries_handler.py +++ b/timeseries_handler.py @@ -15,17 +15,17 @@ from utils import concat_dataframes from utils import retrieve_timeseries from utils import retrieve_single_point -from data_handler import DASH_LOG_LEVEL -from data_handler import MODELS -from data_handler import MODEBAR_CONFIG -from data_handler import MODEBAR_CONFIG_TS -from data_handler import MODEBAR_LAYOUT -from data_handler import MODEBAR_LAYOUT_TS -from data_handler import DELAY -from data_handler import DELAY_DATE -from data_handler import FREQ -from data_handler import VARS -from data_handler import OBS +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__)) diff --git a/utils.py b/utils.py index 8e0312b..5f1d01f 100644 --- a/utils.py +++ b/utils.py @@ -13,8 +13,8 @@ import feather import logging from dash import dcc from dash import html -from data_handler import PATHNAME -from data_handler import RENDER404 +from map_handler import PATHNAME +from map_handler import RENDER404 def concat_dataframes(fname_tpl, months, variable, rename_from=None, notnans=None): """ Concatenate monthly dataframes @@ -94,7 +94,7 @@ def retrieve_single_point(fname, tstep, lat, lon, variable): Variable value at closest available point from (lat, lon) """ - from data_handler import DEBUG + from map_handler import DEBUG logging.debug(" %s %s %s %s %s", fname, tstep, lat, lon, variable) ds = xr.open_dataset(fname) @@ -142,7 +142,7 @@ def retrieve_timeseries(fname, lat, lon, variable, method='netcdf', forecast=Fal Data """ - from data_handler import DEBUG + from map_handler import DEBUG if method == 'feather' and not forecast: df = feather.read_dataframe(fname) -- GitLab From a8569f0bbfba6c1701b26b2dd1a7ff5a04fafc87 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 6 Jul 2023 09:41:37 +0200 Subject: [PATCH 63/71] Refactor router.js to make it more generalized. Remove js_ines as importing js is not yet available in Dash. --- assets/router.js | 74 +++++++++++++++++++++++------------------- js_ines_core/router.js | 29 ----------------- 2 files changed, 41 insertions(+), 62 deletions(-) delete mode 100644 js_ines_core/router.js diff --git a/assets/router.js b/assets/router.js index 0186e7e..aa6a763 100644 --- a/assets/router.js +++ b/assets/router.js @@ -1,50 +1,57 @@ // ROUTING FUNCTIONS -import { getDate } from './js_ines_core/router.js'; -import { sendURL } from './js_ines_core/router.js'; - +// 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', - 'https://dust.aemet.es', - 'https://dust02.bsc.es', - 'https://dust03.bsc.es' - ]; - if (hosts.includes(event.origin)){ - console.log('DASH: Success!! ' + event.data); - window.history.pushState("From Dashboard", "", event.data); - return; - }else{ - console.log('DASH: that is not the right address' + event.origin); - return; - } -}, false); +// Send the URL to the CMS +function sendURL(url) { + window.history.pushState("URL from Dashboard", "", url); + parent.postMessage(url, '*'); +} -// GET THE VARIABLE VALUE -function getVar() { - curvar = document.querySelector('.Select-value-label').innerHTML; +// GET THE VALUE SELECTED IN DROPDOWN MENU +function getDropdownValue(selector, key) { + curvar = document.querySelector(selector).innerHTML; curvar = curvar.split(' ').join('_') - return 'var=' + curvar.toLowerCase() + '&'; + //CREATE KEY/VALUE OUTPUT FOR URL SEACH QUERY FORMAT (EX. FRUIT=APPLE) + return key + '=' + curvar.toLowerCase(); } -// GET MODELS CHECKED -function getModels(){ - const data = [...document.querySelectorAll('.form-check-input:checked')].map(e => e.value); +// GET CHECK BOXES CHECKED +function getCheckedBoxes(selector, key){ + const data = [...document.querySelectorAll(selector)].map(e => e.value); data.forEach(function(item, index){ - this[index] = 'model=' + item + //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 = getVar() - models = getModels() + curvar = getDropdownValue('.Select-value-label', 'var') + models = getCheckedBoxes('.form-check-input:checked', 'model') outputDate = getDate(); - url = "?" + curvar + models + outputDate; + url = "?" + curvar + '&' + models + '&' + outputDate; sendURL(url); }; @@ -73,8 +80,9 @@ const urlOuputDict = { "#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 + // 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]; diff --git a/js_ines_core/router.js b/js_ines_core/router.js deleted file mode 100644 index 3f9fba6..0000000 --- a/js_ines_core/router.js +++ /dev/null @@ -1,29 +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]; -} - -// Send the URL to the CMS -function sendURL(url) { - window.history.pushState("URL from Dashboard", "", url); - parent.postMessage(url, '*'); -} \ No newline at end of file -- GitLab From 21a0df55a18a2739c26e7383c4a7665e8ea79359 Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Mon, 10 Jul 2023 10:47:07 +0200 Subject: [PATCH 64/71] Pull out hard coded aspects of script and move to config files. Clean code for readability. --- conf/create_loop_dev.json | 4 + conf/create_loop_prod.json | 4 + conf/create_loop_selectors.json | 20 +++ conf/create_loop_setup.json | 10 ++ js/create_model_loop_zoom_fit.js | 219 +++++++++++++++++-------------- 5 files changed, 161 insertions(+), 96 deletions(-) create mode 100644 conf/create_loop_dev.json create mode 100644 conf/create_loop_prod.json create mode 100644 conf/create_loop_selectors.json create mode 100644 conf/create_loop_setup.json diff --git a/conf/create_loop_dev.json b/conf/create_loop_dev.json new file mode 100644 index 0000000..1efe1e3 --- /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 0000000..ec89cfa --- /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 0000000..5f95b93 --- /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 0000000..7898530 --- /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", + "makeVisibile": "block", + "logoDisplay": "inline" +} diff --git a/js/create_model_loop_zoom_fit.js b/js/create_model_loop_zoom_fit.js index 32fdcd6..26872c0 100644 --- a/js/create_model_loop_zoom_fit.js +++ b/js/create_model_loop_zoom_fit.js @@ -7,25 +7,33 @@ 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-vie;er/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 +47,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,28 +62,29 @@ 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 + + // 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("SELECT ALL MODELS\n") + for (const model of await page.$$(selectors.selectedModels)) { + const checked = await model.evaluate(elem => elem.checked) process.stdout.write("CHECKED BEFORE: " + checked + "\n"); if (!checked) { await model.click(); @@ -84,11 +94,12 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { process.stdout.write("ERR1: " + err + "\n"); } } - // Select only one model + + // ELSE SELECT ONLY ONE MODEL else { try { process.stdout.write("SELECT MODEL: " + curmodel + "\n"); - 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 BEFORE: " + checked + "\n"); const value = await model.evaluate(elem => elem.value); @@ -105,59 +116,63 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { process.stdout.write("ERR2: " + 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"); } - // 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); + // CHANGE HIDDEN INPUTS AND BUTTON TO VISIBLE + let zoomInput = await page.$(selectors.zoomLevel); + await zoomInput.evaluate((el) => el.style.display = setup.makeVisible); + let latInput = await page.$(selectors.lat); + await latInput.evaluate((el) => el.style.display = setup.makeVisible); + let lonInput = await page.$(selectors.lon); + await lonInput.evaluate((el) => el.style.display = setup.makeVisible); + let zoomButton = await page.$(selectors.focus); + await zoomButton.evaluate((el) => el.style.display = 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('#country-focus'); + await page.click(selectors.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'); + // MAKE ELEMENTS INVISIBLE AGAIN + await zoomInput.evaluate((el) => el.style.display = setup.makeInvisible); + await latInput.evaluate((el) => el.style.display = setup.makeInvisible); + await lonInput.evaluate((el) => el.style.display = setup.makeInvisible); + await zoomButton.evaluate((el) => el.style.display = setup.makeInvisible); } catch (err) { process.stdout.write("ERR4: " + 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,72 +180,84 @@ 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'); + // STYLE LOGOS + let logos = await page.$(selectors.logos); + await logos.evaluate((el) => el.style.display = 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) { console.log(err); - process.stdout.write("ERR5:" + err + "\n"); + process.stdout.write("ERR6:" + 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("ERR7:" + err + "\n"); + } }); 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("ERR8:" + err + "\n"); } } } else { -- GitLab From b98e85378087085d94f3518cdd1d02901abedcd2 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 11 Jul 2023 11:23:33 +0200 Subject: [PATCH 65/71] Update matplotlib to 3.7.2 --- requirements.txt | 2 +- utils.py | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index fa44ac9..d9c0aba 100644 --- a/requirements.txt +++ b/requirements.txt @@ -50,7 +50,7 @@ jsbeautifier==1.14.8 kiwisolver==1.1.0 locket==1.0.0 MarkupSafe==2.1.3 -matplotlib==3.7.1 +matplotlib==3.7.2 more-itertools==9.1.0 msgpack==1.0.5 netCDF4==1.6.4 diff --git a/utils.py b/utils.py index 5f1d01f..9c9787e 100644 --- a/utils.py +++ b/utils.py @@ -4,8 +4,6 @@ import math import os.path -import matplotlib as mpl -from matplotlib import cm import xarray as xr import numpy as np import pandas as pd -- GitLab From 011bba26d06b417458c68b9d88db3a8936ab36df Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 11 Jul 2023 12:39:58 +0200 Subject: [PATCH 66/71] Document callback_tools and rename some args --- callback_tools.py | 283 ++++++++++++++++++++++++++++--- map_handler.py | 12 +- tabs/evaluation_callbacks.py | 36 ++-- tabs/forecast_callbacks.py | 26 ++- tests/test_forecast_callbacks.py | 2 +- tests/test_map_handler.py | 4 +- timeseries_handler.py | 58 +++---- 7 files changed, 332 insertions(+), 89 deletions(-) diff --git a/callback_tools.py b/callback_tools.py index 06bac81..0a85c8d 100644 --- a/callback_tools.py +++ b/callback_tools.py @@ -23,7 +23,26 @@ 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 """ + """ 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}" @@ -57,18 +76,62 @@ def download_image_link(models, variable, curdate, tstep=0, anim=False): return filename -def get_eval_timeseries(obs, start_date, end_date, var, idx, name, model): - """ Retrieve timeseries """ +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, name, model) + return th.retrieve_timeseries(idx, station_name, model) def get_timeseries(model, date, var, lat, lon, forecast=False): - """ Retrieve timeseries """ + """ 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) @@ -78,7 +141,28 @@ def get_timeseries(model, date, var, lat, lon, forecast=False): def get_single_point(model, date, tstep, var, lat, lon): - """ Retrieve sigle point """ + """ 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) @@ -87,16 +171,52 @@ def get_single_point(model, date, tstep, var, lat, lon): return th.retrieve_single_point(tstep, lat, lon) -def get_evaluation_comparison_aeronet_figure(sdate, edate, obs, var): - """ Retrieve evaluation visual comparison figure """ +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(sdate, edate, obs, var) + 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 """ + """ 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) @@ -104,7 +224,26 @@ def get_evaluation_comparison_modis_figure(var, obs, tstep=0, selected_date=END_ def get_evaluation_statistics_figure(network=None, model=None, statistic=None, selection=None): - """ Retrieve evaluation scores figure """ + """ 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) @@ -180,7 +319,28 @@ def get_currdate_tstep(model_start, model_start_before, current_time_before, del def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, aspect=(1, 1)): - """ Retrieve figure """ + """ 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) @@ -212,11 +372,30 @@ def get_model_figure(var, model, tstep=0, hour=None, selected_date=END_DATE, asp 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 figure """ + """ 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( @@ -224,17 +403,36 @@ def get_prob_figure(var, prob=None, day=0, selected_date=END_DATE): 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 figure """ + """ 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( @@ -242,38 +440,85 @@ def get_was_figure(was=None, day=0, selected_date=END_DATE): 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 figure """ + """ 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, static=True, aspect=(1, 1), center=None, - view='carto-positron', zoom=None, layers=None, index=None, tag='empty'): - """ Retrieve figure """ +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=view, + return fh.retrieve_fig(aspect=aspect, center=center, selected_tiles= selected_tiles, zoom=zoom, tag=tag, index=index, layers=layers) diff --git a/map_handler.py b/map_handler.py index 7a8b62e..7aeb88c 100644 --- a/map_handler.py +++ b/map_handler.py @@ -293,7 +293,7 @@ class ForecastProbFigureHandler(ContourFigureHandler): ---------- var : str, optional Variable name, by default None - prob : str, optional + prob : float, optional Probability threshold, by default None selected_date : str, optional Selected date, by default None @@ -636,14 +636,14 @@ class ForecastWasFigureHandler(ShapefileFigureHandler): class EvaluationGroundFigureHandler(PointsFigureHandler): """ Class which handles AERONET observations data """ - def __init__(self, sdate=None, edate=None, obs=None, var=None): + def __init__(self, start_date=None, end_date=None, obs=None, var=None): """_summary_ Parameters ---------- - sdate : str, optional + start_date : str, optional Start date, by default None - edate : str, optional + end_date : str, optional End date, by default None obs : str, optional Observations name, by default None @@ -655,8 +655,8 @@ class EvaluationGroundFigureHandler(PointsFigureHandler): super(EvaluationGroundFigureHandler, self).__init__() self.circle_options = OBS[obs]['circle_options'] - fday = sdate[:-2] + '01' - lday = edate[:-2] + str(calendar.monthrange(int(edate[:4]), int(edate[4:6]))[1]) + 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', diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index b2d82c9..e5f017d 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -658,12 +658,12 @@ def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, mode 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) - logging.debug('SHOW AERONET EVAL TS""""" %s %s %s %s %s', 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,7 +684,7 @@ 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): +def update_eval_aeronet(n_clicks, start_date, end_date, obs): """ Update AERONET evaluation figure according to all parameters """ ctx = dash.callback_context if ctx.triggered: @@ -695,38 +695,38 @@ def update_eval_aeronet(n_clicks, sdate, edate, obs): 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 callback_tools import get_figure from callback_tools import get_evaluation_comparison_aeronet_figure logging.debug('SERVER: calling figure from EVAL picker callback') - logging.debug('SERVER: SDATE %s', str(sdate)) + logging.debug('SERVER: start date %s', str(start_date)) - sdate = sdate.split()[0] + 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: - logging.debug('SERVER: callback start_date %s', 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: - logging.debug('SERVER: callback end_date %s', edate) + logging.debug('SERVER: callback end date %s', end_date) else: - edate = END_DATE + end_date = END_DATE - stations, points_layer = get_evaluation_comparison_aeronet_figure(sdate, edate, obs, DEFAULT_VAR) + 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 diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 415d090..e20116c 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -222,9 +222,9 @@ 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 """ - logging.debug('WAS %s %s %s %s %s %s %s %s', 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] @@ -257,9 +257,9 @@ def update_was_figure(n_clicks, date, day, was, var, previous, view, zoom, cente logging.debug("WAS figure %s %s %s", date, was, day) if was: - view = list(MAP_LAYERS.keys())[view.index(True)] + selected_tiles = list(MAP_LAYERS.keys())[selected_tiles.index(True)] layers = get_was_figure(was, day, selected_date=date) - fig = get_figure(view=view, zoom=zoom, center=center, tag='was', layers=layers) + fig = get_figure(selected_tiles=selected_tiles, zoom=zoom, center=center, tag='was', layers=layers) return fig, previous raise PreventUpdate @@ -291,7 +291,7 @@ 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 """ # if not prob in case user navigates to section via URL if not prob: @@ -304,7 +304,7 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): center = None else: center = center[0] - logging.debug('PROB %s %s %s %s %s %s %s %s', 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] @@ -327,9 +327,9 @@ def update_prob_figure(n_clicks, date, day, prob, var, view, zoom, center): if prob: prob = prob.replace('prob_', '') - view = list(MAP_LAYERS.keys())[view.index(True)] + selected_tiles = list(MAP_LAYERS.keys())[selected_tiles.index(True)] layers = get_prob_figure(var, prob, day, selected_date=date) - fig = get_figure(view=view, zoom=zoom, center=center, tag='prob', layers=layers) + fig = get_figure(selected_tiles=selected_tiles, zoom=zoom, center=center, tag='prob', layers=layers) logging.debug("FIG %s", fig) return fig @@ -710,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'), @@ -718,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 @@ -781,12 +780,12 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z center = [None for _ in model] logging.debug('#### ZOOM, CENTER: %s %s %s %s %s', zoom, center, model, ncols, nrows) - view = list(MAP_LAYERS.keys())[view.index(True)] + 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): - logging.debug("MOD %s ZOOM %s CENTER %s VIEW %s", mod, mod_zoom, mod_center, view) + logging.debug("MOD %s ZOOM %s CENTER %s VIEW %s", mod, mod_zoom, mod_center, selected_tiles) # Get current tag if mod in MODELS: @@ -802,7 +801,7 @@ def update_models_figure(n_clicks, tstep, date, model, variable, static, view, z layers = get_model_figure(var=variable, model=mod, tstep=tstep, selected_date=date, aspect=(nrows, ncols)) - figure = get_figure(static=static, aspect=(nrows, ncols), view=view, center=mod_center, + figure = get_figure(aspect=(nrows, ncols), selected_tiles=selected_tiles, center=mod_center, zoom=mod_zoom, tag=tag, index=index, layers=layers) figure.style = { @@ -812,7 +811,6 @@ 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 # logging.debug('FIGURE KEYS 2 %s', figure.id) - logging.debug('STATIC %s %s', static, int(GRAPH_HEIGHT)/nrows) figures.append(figure) if DEBUG: diff --git a/tests/test_forecast_callbacks.py b/tests/test_forecast_callbacks.py index 6185fa2..c56e9bf 100644 --- a/tests/test_forecast_callbacks.py +++ b/tests/test_forecast_callbacks.py @@ -285,7 +285,7 @@ def test_update_slider(): def test_update_model_figure(): def run_callback(): 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], [], []) + return code.update_models_figure.uncached(1, 1, '20230404', ['monarch'], None, [True, False, False, False], [], []) ctx = copy_context() output = ctx.run(run_callback) diff --git a/tests/test_map_handler.py b/tests/test_map_handler.py index 1c1b4d5..dea4bff 100644 --- a/tests/test_map_handler.py +++ b/tests/test_map_handler.py @@ -16,8 +16,8 @@ EDATE_PREV = (datetime.strptime(END_DATE, FMT_ISO) - timedelta(days=7)).strftime # =================== AERONET OBSERVATIONS HANDLER ============================ @pytest.fixture def EvaluationGroundFigureHandler(): - return map_handler.EvaluationGroundFigureHandler(sdate=EDATE_PREV, edate=END_DATE , obs='aeronet', - var='OD550_DUST') + return map_handler.EvaluationGroundFigureHandler(start_date=EDATE_PREV, end_date=END_DATE, + obs='aeronet', var='OD550_DUST') def test_aeronet_get_figure_layers(EvaluationGroundFigureHandler): diff --git a/timeseries_handler.py b/timeseries_handler.py index e5e1182..ffbd067 100644 --- a/timeseries_handler.py +++ b/timeseries_handler.py @@ -33,23 +33,23 @@ DIR_PATH = os.path.dirname(os.path.realpath(__file__)) class ForecastModelsTimeSeriesHandler: """ Class to handle forecast time series """ - def __init__(self, model, date, variable): + def __init__(self, model, date, var): """ Initialize ForecastModelsTimeSeriesHandler class Parameters ---------- model : str - _description_ + Model name date : str Selected date - variable : str + var : str Variable name """ if isinstance(model, str): model = [model] self.model = model - self.variable = variable + self.var = var self.fpaths = [] try: self.month = dt.strptime(date, "%Y%m%d").strftime("%Y%m") @@ -94,11 +94,11 @@ class ForecastModelsTimeSeriesHandler: mod_date = self.currdate path_template = '{}{}.nc'.format(mod_date, MODELS[model]['template'], - self.variable) + self.var) fpath = os.path.join(MODELS[model]['path'], method, path_template) - return retrieve_single_point(fpath, tstep, lat, lon, self.variable) + 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 @@ -114,7 +114,7 @@ class ForecastModelsTimeSeriesHandler: method : str, optional Method name, by default 'netcdf' forecast : bool, optional - Variable to indicate if we are using forecast data, by default False + Indicates if we are using forecast data, by default False Returns ------- @@ -142,9 +142,9 @@ class ForecastModelsTimeSeriesHandler: path_tpl = '{}-{}-{}.ft' # 202010-median-OD550_DUST_interp.ft if method == 'feather': - path_template = path_tpl.format(self.month, mod, self.variable) + path_template = path_tpl.format(self.month, mod, self.var) elif method == 'netcdf': - path_template = '{}*{}.nc'.format(self.month, MODELS[mod]['template'], self.variable) + path_template = '{}*{}.nc'.format(self.month, MODELS[mod]['template'], self.var) if forecast: method = 'netcdf' @@ -154,7 +154,7 @@ class ForecastModelsTimeSeriesHandler: timedelta(days=1)).strftime("%Y%m%d") else: mod_date = self.currdate - path_template = '{}{}.nc'.format(mod_date, MODELS[mod]['template'], self.variable) + path_template = '{}{}.nc'.format(mod_date, MODELS[mod]['template'], self.var) fpath = os.path.join(filedir, method, @@ -162,19 +162,19 @@ class ForecastModelsTimeSeriesHandler: self.fpaths.append(fpath) title = "{} @ lat = {} and lon = {}".format( - VARS[self.variable]['name'], round(lat, 2), round(lon, 2) + VARS[self.var]['name'], round(lat, 2), round(lon, 2) ) - mul = VARS[self.variable]['mul'] + 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: - variable = OBS[mod]['obs_var'] + var = OBS[mod]['obs_var'] else: - variable = self.variable + var = self.var if not os.path.exists(fpath): logging.debug("NOT retrieving %s File doesn't exist.", fpath) @@ -183,7 +183,7 @@ class ForecastModelsTimeSeriesHandler: logging.debug('Retrieving *** FPATH *** %s', fpath) try: ts_lat, ts_lon, ts_index, ts_values = retrieve_timeseries( - fpath, lat, lon, variable, method=method, forecast=forecast) + fpath, lat, lon, var, method=method, forecast=forecast) except Exception as e: logging.debug("NOT retrieving %s ERROR: %s", fpath, str(e)) continue @@ -265,7 +265,7 @@ class ForecastModelsTimeSeriesHandler: class EvaluationGroundTimeSeriesHandler: - def __init__(self, obs, start_date, end_date, variable, models=None): + def __init__(self, obs, start_date, end_date, var, models=None): """ Initialize EvaluationGroundTimeSeriesHandler class Parameters @@ -276,7 +276,7 @@ class EvaluationGroundTimeSeriesHandler: Start date end_date : str End date - variable : str + var : str Variable name models : str, optional Model name, by default None @@ -286,7 +286,7 @@ class EvaluationGroundTimeSeriesHandler: if models is None: models = list(MODELS.keys()) self.model = models - self.variable = variable + 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') @@ -305,32 +305,32 @@ class EvaluationGroundTimeSeriesHandler: logging.debug('DATE_INDEX %s', self.date_index) fname_obs = fname_tpl.format(dat=obs) - notnans, obs_df = concat_dataframes(fname_obs, months, variable, + 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, variable, + _, 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, variable, + _, 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, st_name, model): + def retrieve_timeseries(self, idx, station_name, model): """ Retrieve timeseries plot for station in modal window Parameters ---------- idx : int Station index - st_name : str + station_name : str Station name model : str, optional Model name, by default None @@ -346,14 +346,14 @@ class EvaluationGroundTimeSeriesHandler: 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], st_name, model) + 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.variable: - df = df.rename(columns = { df.columns[-1]: self.variable }) + if df.columns[-1].upper() == self.var: + df = df.rename(columns = { df.columns[-1]: self.var }) logging.debug("BUILDING TIME-SERIES") try: @@ -361,7 +361,7 @@ class EvaluationGroundTimeSeriesHandler: tmp_df.drop_duplicates(keep='first') timeseries = \ tmp_df.reindex(self.date_index) - if timeseries[self.variable].isnull().all(): + if timeseries[self.var].isnull().all(): continue except Exception as e: logging.debug("ERROR timeseries %s", str(e)) @@ -404,7 +404,7 @@ class EvaluationGroundTimeSeriesHandler: type='scatter', name=name, x=timeseries.index, - y=timeseries[self.variable].round(2), + y=timeseries[self.var].round(2), mode=sc_mode, marker=marker, line=line, @@ -414,7 +414,7 @@ class EvaluationGroundTimeSeriesHandler: ) title = "{} @ {} (lat = {:.2f}, lon = {:.2f})".format( - VARS[self.variable]['name'], st_name, cur_lat, cur_lon, + VARS[self.var]['name'], station_name, cur_lat, cur_lon, ) fig.update_layout( -- GitLab From 04f0dc0bb24ab0791276ebd23608db6b40205522 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 11 Jul 2023 13:00:08 +0200 Subject: [PATCH 67/71] Document INES core map handler --- ines_core_map_handler.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ines_core_map_handler.py b/ines_core_map_handler.py index 351d595..13d4ddb 100644 --- a/ines_core_map_handler.py +++ b/ines_core_map_handler.py @@ -20,12 +20,13 @@ MAP_LAYERS = json.load(open(os.path.join(DIR_PATH, 'conf_ines_core/map_layers.js DEBUG = True class MapHandler: + """ Class than handles the creation of maps""" def __init__(self): - - # TODO: Add common variables from all figure handlers here + """ Initialize MapHandler class """ self.info_style = INFO_STYLE + # TODO: Add common variables from all figure handlers here return None @@ -353,8 +354,10 @@ class MapHandler: return fig class PointsFigureHandler(MapHandler): + """ Class that handles the generation of maps with points """ def __init__(self): + """ Initialize PointsFigureHandler class """ super(PointsFigureHandler, self).__init__() @@ -436,8 +439,10 @@ class PointsFigureHandler(MapHandler): class ContourFigureHandler(MapHandler): + """ Class that handles the generation of maps with countours """ def __init__(self): + """ Initialize ContourFigureHandler class """ super(ContourFigureHandler, self).__init__() @@ -487,9 +492,11 @@ class ContourFigureHandler(MapHandler): 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 -- GitLab From 8ed54f538344676908be7c5ca25c391248089afb Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 11 Jul 2023 13:05:52 +0200 Subject: [PATCH 68/71] Move extend_list to utils --- tabs/evaluation_callbacks.py | 18 +++++------------- utils.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index e5f017d..fd30cbb 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -23,6 +23,8 @@ from map_handler import cache, cache_timeout from tabs.evaluation import tab_evaluation from tabs.evaluation import STATS +from utils import extend_list + from datetime import datetime as dt from datetime import timedelta from dateutil.relativedelta import relativedelta @@ -36,16 +38,6 @@ 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): # """ Registering callbacks """ @@ -269,7 +261,7 @@ def alphabetize_stations(dataframe): 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'), @@ -284,7 +276,7 @@ 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')] @@ -310,7 +302,7 @@ def aeronet_scores_tables_retrieve(n, *args): raise PreventUpdate if not n or network != 'aeronet': - return extend_l([[dash.no_update, dash.no_update, { 'display': 'none' }, + return extend_list([[dash.no_update, dash.no_update, { 'display': 'none' }, dash.no_update, dash.no_update] for _ in SCORES]) # ORDER is IMPORTANT diff --git a/utils.py b/utils.py index 9c9787e..72b534e 100644 --- a/utils.py +++ b/utils.py @@ -338,3 +338,26 @@ def render404(): ] 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 \ No newline at end of file -- GitLab From dcf4c61a6f713bf5fe82d8c5f7d5126fb6e15848 Mon Sep 17 00:00:00 2001 From: Alba Vilanova Date: Tue, 11 Jul 2023 17:26:42 +0200 Subject: [PATCH 69/71] Create app utils and document evaluation callbacks --- ines_core_map_handler.py | 2 +- ines_core_utils.py | 363 ++++++++++++++++ map_handler.py | 5 +- router.py | 2 +- tabs/evaluation_callbacks.py | 407 +++++++++++++----- tabs/forecast_callbacks.py | 2 +- tests/test_evaluation_callbacks.py | 7 +- ...ndler.py => test_ines_core_map_handler.py} | 0 ...{test_utils.py => test_ines_core_utils.py} | 5 +- timeseries_handler.py | 6 +- utils.py | 390 +++-------------- 11 files changed, 740 insertions(+), 449 deletions(-) create mode 100644 ines_core_utils.py rename tests/{test_ines_map_handler.py => test_ines_core_map_handler.py} (100%) rename tests/{test_utils.py => test_ines_core_utils.py} (97%) diff --git a/ines_core_map_handler.py b/ines_core_map_handler.py index 13d4ddb..40f78d0 100644 --- a/ines_core_map_handler.py +++ b/ines_core_map_handler.py @@ -293,7 +293,7 @@ class MapHandler: Returns ------- - fig : dash_leaflet.Map + dash_leaflet.Map Figure """ diff --git a/ines_core_utils.py b/ines_core_utils.py new file mode 100644 index 0000000..72b534e --- /dev/null +++ b/ines_core_utils.py @@ -0,0 +1,363 @@ +#!/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 + +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 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 helpful links that might help:"),] + + # Define links + for tab_i, tab in enumerate(RENDER404.keys()): + # 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=PATHNAME+RENDER404[tab]['path'], + 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 \ No newline at end of file diff --git a/map_handler.py b/map_handler.py index 7aeb88c..9b19dd0 100644 --- a/map_handler.py +++ b/map_handler.py @@ -1290,9 +1290,12 @@ class VisFigureHandler(PointsFigureHandler): # 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', 'humidity']) + var_list=['station', 'lon', 'lat', 'visibility', 'relative humidity']) # Create geojson geojson_data = dlx.dicts_to_geojson(data_dict, lon="lon") diff --git a/router.py b/router.py index 7d1906b..6197293 100644 --- a/router.py +++ b/router.py @@ -12,7 +12,7 @@ from map_handler import MODELS from map_handler import DEBUG from map_handler import ALIASES from map_handler import ROUTE_DEFAULTS -from utils import render404 +from ines_core_utils import render404 from tabs.forecast import tab_forecast from tabs.forecast import sidebar_forecast diff --git a/tabs/evaluation_callbacks.py b/tabs/evaluation_callbacks.py index fd30cbb..d5e18f1 100644 --- a/tabs/evaluation_callbacks.py +++ b/tabs/evaluation_callbacks.py @@ -8,6 +8,7 @@ from dash.dependencies import Input from dash.dependencies import State from dash.exceptions import PreventUpdate import dash_leaflet as dl + from map_handler import DEFAULT_VAR from map_handler import VARS from map_handler import MODELS @@ -19,11 +20,11 @@ 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 utils import extend_list +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 @@ -38,7 +39,7 @@ import logging SCORES = list(STATS.keys()) -#def register_callbacks(app, cache, cache_timeout): +# def register_callbacks(app, cache, cache_timeout): # """ Registering callbacks """ @dash.callback( @@ -50,14 +51,31 @@ SCORES = list(STATS.keys()) 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: @@ -74,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 @@ -114,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( @@ -129,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 - logging.debug("########### %s %s %s %s %s %s", 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}" @@ -151,7 +208,7 @@ 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}) logging.debug('--- %s', ret.columns) logging.debug('--- %s', ret.to_dict('records')) @@ -160,7 +217,7 @@ def modis_scores_tables_retrieve(n, models, stat, network, timescale, selection) 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' } @@ -179,7 +236,36 @@ 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 """ + """ 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 @@ -217,48 +303,6 @@ def scores_maps_retrieve(n_clicks, model, score, network, selection, orig_model, 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_list([ @@ -283,8 +327,19 @@ def alphabetize_stations(dataframe): 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)]) @@ -301,7 +356,7 @@ def aeronet_scores_tables_retrieve(n, *args): for score in SCORES]: raise PreventUpdate - if not n or network != 'aeronet': + 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]) @@ -316,7 +371,7 @@ def aeronet_scores_tables_retrieve(n, *args): models = ['station'] + models - logging.debug("@@@@@@@@@@@ %s %s %s %s %s %s %s", 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] @@ -420,6 +475,7 @@ def aeronet_scores_tables_retrieve(n, *args): logging.debug('LEN %s', len(ret_tables)) logging.debug ("TABLES RET %s", ret_tables) + return ret_tables @@ -434,15 +490,37 @@ 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 """ +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 nclicks == 0: + if coords is None or n_clicks == 0: raise PreventUpdate ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] logging.debug("CTXT %s %s", ctxt, type(ctxt)) - if not ctxt or ctxt is None or ctxt != 'ts-eval-modis-button': # or nclicks == 0:P: + if not ctxt or ctxt is None or ctxt != 'ts-eval-modis-button': # or n_clicks == 0:P: raise PreventUpdate logging.debug('TRIGGER %s %s', ctxt, type(ctxt)) @@ -453,6 +531,12 @@ def show_eval_modis_timeseries(nclicks, coords, date, obs, model): 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', @@ -470,9 +554,30 @@ 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 """ +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 @@ -526,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( @@ -539,11 +644,31 @@ 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 - + logging.debug("CLICK: %s", str(click_data)) ctxt = dash.callback_context.triggered[0]["prop_id"].split(".")[0] @@ -558,6 +683,7 @@ def stations_popup(click_data, mapid, stations): 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 @@ -601,21 +727,21 @@ def stations_popup(click_data, mapid, stations): (df_stations['lat'].round(2) == round(lat, 2))].to_dict() logging.debug("MARKER %s", marker.to_plotly_json()) - last = mapid[-1] + 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()) - logging.debug("LAST %s %s %s", 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: 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 + # 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( @@ -630,20 +756,44 @@ 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 """ +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] 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 - + 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) @@ -677,7 +827,27 @@ def show_eval_aeronet_timeseries(nclicks, cdata, start_date, end_date, obs, mode prevent_initial_call=True) @cache.memoize(timeout=cache_timeout) def update_eval_aeronet(n_clicks, start_date, end_date, obs): - """ Update AERONET evaluation figure according to all parameters """ + """ 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] @@ -720,7 +890,7 @@ def update_eval_aeronet(n_clicks, start_date, end_date, obs): 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 @@ -734,8 +904,29 @@ def update_eval_aeronet(n_clicks, start_date, end_date, 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: @@ -754,9 +945,10 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): from callback_tools import get_figure logging.debug('SERVER: calling figure from EVAL picker callback') - logging.debug(" %s ", mod_div) - mod_center = mod_div['props']['center'] - mod_zoom = mod_div['props']['zoom'] + 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] @@ -786,7 +978,7 @@ def update_eval_modis(n_clicks, date, mod, obs, mod_div): # 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') + fig_obs = get_figure(center=mod_center, zoom=mod_zoom, layers=layers, tag='modis', index='modis') return fig_obs, fig_mod @@ -800,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 """ + """ 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 - # from callback_tools import get_obs1d + logging.debug('SERVER: calling figure from EVAL picker callback') - # logging.debug('SERVER: interval %s', str(n)) start_date = OBS[obs]['start_date'] diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index e20116c..e54ed71 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -35,7 +35,7 @@ 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( diff --git a/tests/test_evaluation_callbacks.py b/tests/test_evaluation_callbacks.py index ce00ed3..e78bbc4 100644 --- a/tests/test_evaluation_callbacks.py +++ b/tests/test_evaluation_callbacks.py @@ -15,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(): @@ -231,8 +228,8 @@ def test_update_eval_modis(): ctx = copy_context() output = ctx.run(run_callback) - assert output[0].id == {'index': 'None', 'tag': 'modis-map'} - assert output[0].children[0].id == {'tag': 'modis-tile-layer', 'index': 'None'} + 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'} diff --git a/tests/test_ines_map_handler.py b/tests/test_ines_core_map_handler.py similarity index 100% rename from tests/test_ines_map_handler.py rename to tests/test_ines_core_map_handler.py diff --git a/tests/test_utils.py b/tests/test_ines_core_utils.py similarity index 97% rename from tests/test_utils.py rename to tests/test_ines_core_utils.py index 5030292..815b0fb 100644 --- a/tests/test_utils.py +++ b/tests/test_ines_core_utils.py @@ -2,7 +2,7 @@ import pytest 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 map_handler import END_DATE from map_handler import FREQ @@ -104,3 +104,6 @@ def test_normalize_vals(): 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 helpful 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] \ No newline at end of file diff --git a/timeseries_handler.py b/timeseries_handler.py index ffbd067..d98a39e 100644 --- a/timeseries_handler.py +++ b/timeseries_handler.py @@ -11,9 +11,9 @@ import pandas as pd import json import logging -from utils import concat_dataframes -from utils import retrieve_timeseries -from utils import retrieve_single_point +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 diff --git a/utils.py b/utils.py index 72b534e..164e433 100644 --- a/utils.py +++ b/utils.py @@ -2,362 +2,78 @@ # -*- 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 -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 +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 ---------- - 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 + dataframe : pandas.core.frame.DataFrame + Dataframe 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 + pandas.core.frame.DataFrame + Dataframe with formatted columns """ - 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) + 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 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 helpful links that might help:"),] - - # Define links - for tab_i, tab in enumerate(RENDER404.keys()): - # 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=PATHNAME+RENDER404[tab]['path'], - 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 +def alphabetize_stations(dataframe): + """ Alphabetize the stations for each region in the AERONET statistics table Parameters ---------- - l : list - List + dataframe : pandas.core.frame.DataFrame + Dataframe Returns ------- - list - Flat list + pandas.core.frame.DataFrame + Dataframe with sorted columns """ - res = [] - for x in l: - res.extend(x) - if not isinstance(res, list): - res = [res] - - return res \ No newline at end of file + # 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 -- GitLab From 4ba639dd8318f7a30bef7fa0786351723829ee8d Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Wed, 12 Jul 2023 12:41:53 +0200 Subject: [PATCH 70/71] Clean and refactor gif and png scripts. Update coords.json to better fit logos in monarch gifs. --- bin/gen_country_loop.sh | 45 ++++++++----- conf/coords.json | 4 +- conf/create_loop_setup.json | 2 +- js/create_model_loop_zoom_fit.js | 108 +++++++++++++------------------ 4 files changed, 79 insertions(+), 80 deletions(-) diff --git a/bin/gen_country_loop.sh b/bin/gen_country_loop.sh index a711c62..cdc28bf 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/conf/coords.json b/conf/coords.json index 36ab8f3..73e0c6e 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_setup.json b/conf/create_loop_setup.json index 7898530..0e799b9 100644 --- a/conf/create_loop_setup.json +++ b/conf/create_loop_setup.json @@ -5,6 +5,6 @@ "graphHeight": "93vh", "disclaimerRight": "1px", "makeInvisible": "none", - "makeVisibile": "block", + "makeVisible": "block", "logoDisplay": "inline" } diff --git a/js/create_model_loop_zoom_fit.js b/js/create_model_loop_zoom_fit.js index 26872c0..69f3c43 100644 --- a/js/create_model_loop_zoom_fit.js +++ b/js/create_model_loop_zoom_fit.js @@ -1,16 +1,17 @@ -// 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'); // USE THE CREATE_LOOP_DEV CONF DURING DEVELOPMENT -// const output_conf = path.relative('js/create_model_loop_zoom_fit.js', 'interactive-forecast-vie;er/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 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'); @@ -79,42 +80,24 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { 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.$$(selectors.selectedModels)) { - 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"); - } - } - - // ELSE SELECT ONLY ONE MODEL - else { - 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(); - } - 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"); } // CLICK APPLY BUTTON @@ -128,7 +111,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { await btn.click(); // "button#models-apply"); await delay(500); } catch (err) { - process.stdout.write("ERR3: " + err + "\n"); + process.stdout.write("ERR2: " + err + "\n"); } // APPLY ZOOM AND COORDINATES @@ -140,29 +123,29 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { // CHANGE HIDDEN INPUTS AND BUTTON TO VISIBLE let zoomInput = await page.$(selectors.zoomLevel); - await zoomInput.evaluate((el) => el.style.display = setup.makeVisible); + await zoomInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); let latInput = await page.$(selectors.lat); - await latInput.evaluate((el) => el.style.display = setup.makeVisible); + await latInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); let lonInput = await page.$(selectors.lon); - await lonInput.evaluate((el) => el.style.display = setup.makeVisible); + await lonInput.evaluate((el, visible) => {el.style.display = visible}, setup.makeVisible); let zoomButton = await page.$(selectors.focus); - await zoomButton.evaluate((el) => el.style.display = setup.makeVisible); + 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"); + process.stdout.write("CLICK HIDDEN COUNTRY BUTTON\n"); await page.click(selectors.focus); // MAKE ELEMENTS INVISIBLE AGAIN - await zoomInput.evaluate((el) => el.style.display = setup.makeInvisible); - await latInput.evaluate((el) => el.style.display = setup.makeInvisible); - await lonInput.evaluate((el) => el.style.display = setup.makeInvisible); - await zoomButton.evaluate((el) => el.style.display = setup.makeInvisible); + 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 @@ -188,7 +171,7 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { // CHANGE GRAPH HEIGHT TO FILL OUTPUT GIF let graphHeight = await page.$(selectors.leafletContainer); await graphHeight.evaluate((el, styledHeight) => el.style.height = styledHeight, - setup.graphHeight); + setup.graphHeight); // REMOVE TIMESLIDER await page.waitForSelector(selectors.navTimebar); process.stdout.write("REMOVING NAVBAR" + "\n"); @@ -207,28 +190,26 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { }, selectors.leafletBar); } catch (err) { console.log(err); - process.stdout.write("ERR5:" + err + "\n"); + process.stdout.write("ERR4: " + err + "\n"); } if (coords[fit].logos == true){ try { - // REVEAL LOGOS + // REVEAL AND STYLE LOGOS process.stdout.write("REVEALING LOGOS" + "\n"); - - // STYLE LOGOS let logos = await page.$(selectors.logos); - await logos.evaluate((el) => el.style.display = setup.logoDisplay); + 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) { console.log(err); - process.stdout.write("ERR6:" + err + "\n"); + process.stdout.write("ERR5:" + err + "\n"); } }; // 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); + setup.disclaimerRight); // HANDLE OUTPUT AND TAKE SCREENSHOT // FILENAME SHOULDN'T INCLUDE FIT UNLESS IT IS A SPECIFIED COUNTRY @@ -245,11 +226,12 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { await graph.screenshot({ path: outputFile }); } catch(err) { console.log(err); - process.stdout.write("ERR7:" + err + "\n"); + process.stdout.write("ERR6:" + err + "\n"); } }); + // TAKE SCREENSHOTS FOR ALL TSTEPS TO CREATE GIF if (anim === "true") { for (let i = 0; i < setup.tstepCount; i++) { try { @@ -257,9 +239,10 @@ const RunCluster = async (anim, curmodel, seldate, variable, fit) => { await cluster.queue([i, curmodel, seldate, variable, fit]); } catch (err) { console.log(err); - process.stdout.write("ERR8:" + 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]); @@ -273,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); -- GitLab From 9276207c39ee1fcf74b30067e2ebbaecf8145c7c Mon Sep 17 00:00:00 2001 From: Elliott Rose Date: Thu, 13 Jul 2023 15:43:01 +0200 Subject: [PATCH 71/71] Update the 404 page links to redirect to the iframe version of the dashboard, instead of the fullpage version. Update tests. --- conf/render404.json | 11 +++++++---- ines_core_utils.py | 20 +++++++++++++++++--- router.py | 2 +- tests/test_ines_core_utils.py | 23 ++++++++++++++++++++--- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/conf/render404.json b/conf/render404.json index 69cc03b..769f95f 100644 --- a/conf/render404.json +++ b/conf/render404.json @@ -1,13 +1,16 @@ { "forecast": {"name": "Forecast", "id": "forecast_link", - "path": "/?tab=forecast"}, + "path_prod": "https://dust.aemet.es/products/daily-dust-products/?tab=forecast", + "path_dev": "/?tab=forecast"}, "evaluation": {"name": "Evaluation", "id": "evaluation_link", - "path": "/?tab=evaluation"}, + "path_prod": "https://dust.aemet.es/products/daily-dust-products/?tab=evaluation", + "path_dev": "/?tab=evaluation"}, "observations": {"name": "Observations", "id": "observations_link", - "path": "/?tab=observations"} -} \ No newline at end of file + "path_prod": "https://dust.aemet.es/products/daily-dust-products/?tab=observations", + "path_dev": "/?tab=observations"} +} diff --git a/ines_core_utils.py b/ines_core_utils.py index 72b534e..ac9cdc3 100644 --- a/ines_core_utils.py +++ b/ines_core_utils.py @@ -13,6 +13,7 @@ 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 @@ -303,6 +304,16 @@ def normalize_vals(vals, valsmin, valsmax, 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 """ @@ -310,10 +321,13 @@ def render404(): # 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 helpful links that might help:"),] + 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(), @@ -321,7 +335,7 @@ def render404(): # Add links to tabs children.extend([dcc.Link(RENDER404[tab]['name'], id=RENDER404[tab]['id'], - href=PATHNAME+RENDER404[tab]['path'], + href=link, className='error_links',target='_parent', refresh=True),]) @@ -360,4 +374,4 @@ def extend_list(l): if not isinstance(res, list): res = [res] - return res \ No newline at end of file + return res diff --git a/router.py b/router.py index 6197293..80eeb62 100644 --- a/router.py +++ b/router.py @@ -87,7 +87,7 @@ def router(url): """ Get url search queries and build layout for app""" route_selections = get_url_queries(url) - logging.info('===== route_selections %s', route_selections) + logging.debug('===== route_selections %s', route_selections) try: children = [ diff --git a/tests/test_ines_core_utils.py b/tests/test_ines_core_utils.py index 815b0fb..c351cec 100644 --- a/tests/test_ines_core_utils.py +++ b/tests/test_ines_core_utils.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import MagicMock, patch from datetime import datetime from datetime import timedelta import importlib @@ -101,9 +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_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 helpful 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]) + 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] \ No newline at end of file + assert code.extend_list([[1],[2],[3]]) == [1,2,3] -- GitLab