|
| 1 | +import os |
| 2 | +from dash import dcc |
| 3 | +from dash import html |
| 4 | +import dash_bootstrap_components as dbc |
| 5 | +from dash import dash_table |
| 6 | +import dash_leaflet as dlf |
| 7 | +import plotly.express as px |
| 8 | +from controls import Block, Sentence, DateNoYear, Number |
| 9 | + |
| 10 | +import numpy as np |
| 11 | +from pathlib import Path |
| 12 | +import calc |
| 13 | +import pingrid |
| 14 | +import pandas as pd |
| 15 | + |
| 16 | +from globals_ import GLOBAL_CONFIG |
| 17 | + |
| 18 | +CONFIG = GLOBAL_CONFIG["maprooms"]["crop_suitability"] |
| 19 | + |
| 20 | +DR_PATH = f'{GLOBAL_CONFIG["datasets"]["daily"]["zarr_path"]}{GLOBAL_CONFIG["datasets"]["daily"]["vars"]["precip"][1]}' |
| 21 | +RR_MRG_ZARR = Path(DR_PATH) |
| 22 | + |
| 23 | +IRI_BLUE = "rgb(25,57,138)" |
| 24 | +IRI_GRAY = "rgb(113,112,116)" |
| 25 | +LIGHT_GRAY = "#eeeeee" |
| 26 | + |
| 27 | + |
| 28 | +def app_layout(): |
| 29 | + |
| 30 | + # Initialization |
| 31 | + rr_mrg = calc.read_zarr_data(RR_MRG_ZARR) |
| 32 | + center_of_the_map = [((rr_mrg["Y"][int(rr_mrg["Y"].size/2)].values)), ((rr_mrg["X"][int(rr_mrg["X"].size/2)].values))] |
| 33 | + lat_res = np.around((rr_mrg["Y"][1]-rr_mrg["Y"][0]).values, decimals=10) |
| 34 | + lat_min = np.around((rr_mrg["Y"][0]-lat_res/2).values, decimals=10) |
| 35 | + lat_max = np.around((rr_mrg["Y"][-1]+lat_res/2).values, decimals=10) |
| 36 | + lon_res = np.around((rr_mrg["X"][1]-rr_mrg["X"][0]).values, decimals=10) |
| 37 | + lon_min = np.around((rr_mrg["X"][0]-lon_res/2).values, decimals=10) |
| 38 | + lon_max = np.around((rr_mrg["X"][-1]+lon_res/2).values, decimals=10) |
| 39 | + lat_label = str(lat_min)+" to "+str(lat_max)+" by "+str(lat_res)+"˚" |
| 40 | + lon_label = str(lon_min)+" to "+str(lon_max)+" by "+str(lon_res)+"˚" |
| 41 | + |
| 42 | + return dbc.Container( |
| 43 | + [ |
| 44 | + dcc.Location(id="location", refresh=True), |
| 45 | + navbar_layout(), |
| 46 | + dbc.Row( |
| 47 | + [ |
| 48 | + dbc.Col( |
| 49 | + controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label), |
| 50 | + sm=12, |
| 51 | + md=4, |
| 52 | + style={ |
| 53 | + "background-color": "white", |
| 54 | + "border-style": "solid", |
| 55 | + "border-color": LIGHT_GRAY, |
| 56 | + "border-width": "0px 1px 0px 0px", |
| 57 | + "overflow":"scroll","height":"95vh",#column that holds text and controls |
| 58 | + }, |
| 59 | + ), |
| 60 | + dbc.Col( |
| 61 | + [ |
| 62 | + dbc.Row( |
| 63 | + [ |
| 64 | + dbc.Col( |
| 65 | + map_layout(center_of_the_map, lon_min, lat_min, lon_max, lat_max), |
| 66 | + width=12, |
| 67 | + style={ |
| 68 | + "background-color": "white", |
| 69 | + }, |
| 70 | + ), |
| 71 | + ], |
| 72 | + style={"overflow":"scroll","height": "55%"}, #box the map is in |
| 73 | + className="g-0", |
| 74 | + ), |
| 75 | + dbc.Row( |
| 76 | + [ |
| 77 | + dbc.Col( |
| 78 | + results_layout(), |
| 79 | + width=12, |
| 80 | + style={ |
| 81 | + "background-color": "white", |
| 82 | + "min-height": "100px", |
| 83 | + "border-style": "solid", |
| 84 | + "border-color": LIGHT_GRAY, |
| 85 | + "border-width": "1px 0px 0px 0px", |
| 86 | + }, |
| 87 | + ), |
| 88 | + ], |
| 89 | + style={"overflow":"scroll","height":"45%"}, #box the plots are in |
| 90 | + className="g-0", |
| 91 | + ), |
| 92 | + ],style={"overflow":"scroll","height":"95vh"},#main column for map and results |
| 93 | + sm=12, |
| 94 | + md=8, |
| 95 | + ), |
| 96 | + ], |
| 97 | + className="g-0", |
| 98 | + ), |
| 99 | + ], |
| 100 | + fluid=True, |
| 101 | + style={"padding-left": "1px", "padding-right": "1px","height":"100%"}, |
| 102 | + ) |
| 103 | + |
| 104 | + |
| 105 | +def navbar_layout(): |
| 106 | + return dbc.Navbar( |
| 107 | + [ |
| 108 | + html.A( |
| 109 | + dbc.Row( |
| 110 | + [ |
| 111 | + dbc.Col( |
| 112 | + html.Img( |
| 113 | + src="assets/" + GLOBAL_CONFIG["logo"], |
| 114 | + height="30px", |
| 115 | + ) |
| 116 | + ), |
| 117 | + dbc.Col( |
| 118 | + dbc.NavbarBrand( |
| 119 | + "Climate and Agriculture / " + CONFIG["crop_suit_title"], |
| 120 | + className="ml-2", |
| 121 | + ) |
| 122 | + ), |
| 123 | + ], |
| 124 | + align="center", |
| 125 | + className="g-0", |
| 126 | + ), |
| 127 | + ), |
| 128 | + dbc.NavbarToggler(id="navbar-toggler"), |
| 129 | + dbc.Collapse( |
| 130 | + id="navbar-collapse", |
| 131 | + navbar=True, |
| 132 | + ), |
| 133 | + ], |
| 134 | + sticky="top", |
| 135 | + color=IRI_GRAY, |
| 136 | + dark=True, |
| 137 | + ) |
| 138 | + |
| 139 | + |
| 140 | +def controls_layout(lat_min, lat_max, lon_min, lon_max, lat_label, lon_label): |
| 141 | + return dbc.Container( |
| 142 | + [ |
| 143 | + html.Div( |
| 144 | + [ |
| 145 | + html.H5(CONFIG["crop_suit_title"]), |
| 146 | + html.P( |
| 147 | + [f""" |
| 148 | + Maproom to explore crop climate suitability using a |
| 149 | + spatial overlay analysis. The output is an index from |
| 150 | + 0-5 |
| 151 | + where 5 meet all conditions and is the most suitable. |
| 152 | + """, |
| 153 | + html.Br(), |
| 154 | + """Select from the layers dropdown to select which data |
| 155 | + to view as timeseries data and as a map layer. You can |
| 156 | + Select a custom point to view for the timeseries data, |
| 157 | + as well as a specific year to view on the map.""", |
| 158 | + html.Br(), |
| 159 | + """Select custom values to determine the crop suitability |
| 160 | + parameters passed into the analysis to determine the |
| 161 | + most suitable conditions for your inquiry.""", |
| 162 | + ] |
| 163 | + ) |
| 164 | + ]+[ |
| 165 | + html.H5("Dataset Documentation"), |
| 166 | + html.P( |
| 167 | + f""" |
| 168 | + Reconstructed gridded rainfall, minimum temperature, and maximum temperature from {GLOBAL_CONFIG["institution"]}. |
| 169 | + The time series data were created by combining |
| 170 | + quality-controlled station observations in |
| 171 | + {GLOBAL_CONFIG["institution"]}’s archive with satellite rainfall estimates. |
| 172 | + Crop suitability values use the rainfall, mimumum and maximum temperature data as inputs to calculate a suitability index where a certain number of conditions of crop suitability are met. |
| 173 | + """ |
| 174 | + ), |
| 175 | + ]+[ |
| 176 | + html.P([html.H6(val["menu_label"]), html.P(val["description"])]) |
| 177 | + for key, val in CONFIG["layers"].items() |
| 178 | + ], |
| 179 | + style={"position":"relative","height":"25%", "overflow":"scroll"},#box holding text |
| 180 | + ), |
| 181 | + html.H3("Controls Panel",style={"padding":".5rem"}), |
| 182 | + html.Div( |
| 183 | + [ |
| 184 | + Block("Data Layers", |
| 185 | + "Choose a data layer to view:", |
| 186 | + dbc.Select( |
| 187 | + id="data_choice", |
| 188 | + value=list(CONFIG["layers"].keys())[0], |
| 189 | + options=[ |
| 190 | + {"label": val["menu_label"], "value": key} |
| 191 | + for key, val in CONFIG["layers"].items() |
| 192 | + ], |
| 193 | + ), |
| 194 | + ), |
| 195 | + Block("Pick a point", |
| 196 | + dbc.Row( |
| 197 | + [ |
| 198 | + dbc.Col( |
| 199 | + dbc.FormFloating([dbc.Input( |
| 200 | + id = "lat_input", |
| 201 | + min=lat_min, |
| 202 | + max=lat_max, |
| 203 | + type="number", |
| 204 | + ), |
| 205 | + dbc.Label("Latitude", style={"font-size": "80%"}), |
| 206 | + dbc.Tooltip( |
| 207 | + f"{lat_label}", |
| 208 | + target="lat_input", |
| 209 | + className="tooltiptext", |
| 210 | + )]), |
| 211 | + ), |
| 212 | + dbc.Col( |
| 213 | + dbc.FormFloating([dbc.Input( |
| 214 | + id = "lng_input", |
| 215 | + min=lon_min, |
| 216 | + max=lon_max, |
| 217 | + type="number", |
| 218 | + ), |
| 219 | + dbc.Label("Longitude", style={"font-size": "80%"}), |
| 220 | + dbc.Tooltip( |
| 221 | + f"{lon_label}", |
| 222 | + target="lng_input", |
| 223 | + className="tooltiptext", |
| 224 | + )]), |
| 225 | + ), |
| 226 | + dbc.Button(id="submit_coords", outline=True, color="primary", children='Submit lat lng'), |
| 227 | + ], |
| 228 | + ), |
| 229 | + "Map year:", |
| 230 | + dbc.Input( |
| 231 | + id = "target_year", |
| 232 | + type = "number", |
| 233 | + min = 1961, |
| 234 | + max = 2018, |
| 235 | + value = CONFIG["param_defaults"]["target_year"], |
| 236 | + ), |
| 237 | + "Season of interest:", |
| 238 | + dbc.Select( |
| 239 | + id="target_season", |
| 240 | + value= CONFIG["param_defaults"]["target_season"], |
| 241 | + options=[ |
| 242 | + {"label":"Dec-Feb", "value":"DJF"}, |
| 243 | + {"label":"Mar-May", "value":"MAM"}, |
| 244 | + {"label":"Jun-Aug", "value":"JJA"}, |
| 245 | + {"label":"Sep-Nov", "value":"SON"}, |
| 246 | + ], |
| 247 | + ), |
| 248 | + ), |
| 249 | + Block( |
| 250 | + "Optimum seasonal total rainfall", |
| 251 | + Sentence( |
| 252 | + "Total rainfall amount between", |
| 253 | + Number("lower_wet_threshold", CONFIG["param_defaults"]["lower_wet_thresh"], min=0, max=99999, html_size=4), |
| 254 | + "and", |
| 255 | + Number("upper_wet_threshold", CONFIG["param_defaults"]["upper_wet_thresh"], min=0, max=99999, html_size=4), |
| 256 | + "mm", |
| 257 | + ), |
| 258 | + ), |
| 259 | + Block( |
| 260 | + "Temperature tolerance", |
| 261 | + Sentence( |
| 262 | + "Temperature range between", |
| 263 | + Number("minimum_temp", CONFIG["param_defaults"]["min_temp"], min=-99, max=999, html_size=2), |
| 264 | + "and", |
| 265 | + Number("maximum_temp", CONFIG["param_defaults"]["max_temp"], min=-99, max=99999, html_size=2), |
| 266 | + "C", |
| 267 | + ), |
| 268 | + ), |
| 269 | + Block( |
| 270 | + "Optimal daily temperature amplitude", |
| 271 | + Sentence( |
| 272 | + "An average daily temperature amplitude of:", |
| 273 | + Number("temp_range", CONFIG["param_defaults"]["temp_range"], min=0, max=99999, html_size=2), |
| 274 | + "C", |
| 275 | + ), |
| 276 | + ), |
| 277 | +# Block( |
| 278 | +# "Season length", |
| 279 | +# Sentence( |
| 280 | +# "The minimum length of the season:", |
| 281 | +# Number("season_length", CONFIG["param_defaults"]["season_length"], min=0, max=99999, html_size=3), |
| 282 | +# "days", |
| 283 | +# ), |
| 284 | +# ), |
| 285 | + Block( |
| 286 | + "Wet days", |
| 287 | + Sentence( |
| 288 | + "The minimum number of wet days within a season:", |
| 289 | + Number("min_wet_days", CONFIG["param_defaults"]["min_wet_days"], min=0, max=99999, html_size=3), |
| 290 | + "days", |
| 291 | + ), |
| 292 | + Sentence( |
| 293 | + "Where a wet day is defined as a day with rainfall more than:", |
| 294 | + Number("wet_day_def", CONFIG["param_defaults"]["wet_day_def"], min=0, max=9999, html_size=3), |
| 295 | + "mm", |
| 296 | + ), |
| 297 | + ), |
| 298 | + ], |
| 299 | + style={"position":"relative","height":"60%", "overflow":"scroll"},#box holding controls |
| 300 | + ), |
| 301 | + html.Div( |
| 302 | + [dbc.Button(id="submit_params", children='Submit Crop Suitability Conditions', style={"position":"fixed", "width":"31%"})], |
| 303 | + ) |
| 304 | + ], #end container |
| 305 | + fluid=True, |
| 306 | + className="scrollable-panel p-3", |
| 307 | + style={"overflow":"scroll","height":"100%","padding-bottom": "1rem", "padding-top": "1rem"}, |
| 308 | + ) #style for container that is returned #95vh |
| 309 | + |
| 310 | +def map_layout(center_of_the_map, lon_min, lat_min, lon_max, lat_max): |
| 311 | + return dbc.Container( |
| 312 | + [ |
| 313 | + dlf.Map( |
| 314 | + [ |
| 315 | + dlf.LayersControl(id="layers_control", position="topleft"), |
| 316 | + dlf.LayerGroup( |
| 317 | + #Dummy position overridden by pick_location initial callback |
| 318 | + #position must be defined |
| 319 | + [dlf.Marker(id="loc_marker", position=[0, 0])], |
| 320 | + id="layers_group" |
| 321 | + ), |
| 322 | + dlf.ScaleControl(imperial=False, position="topright"), |
| 323 | + dlf.Colorbar( |
| 324 | + id="colorbar", |
| 325 | + position="bottomleft", |
| 326 | + width=300, |
| 327 | + height=10, |
| 328 | + opacity=1, |
| 329 | + ) |
| 330 | + ], |
| 331 | + id="map", |
| 332 | + center=center_of_the_map, |
| 333 | + zoom=GLOBAL_CONFIG["zoom"], |
| 334 | + maxBounds = [[lat_min, lon_min],[lat_max, lon_max]], |
| 335 | + minZoom = GLOBAL_CONFIG["zoom"] - 1, |
| 336 | + maxZoom = GLOBAL_CONFIG["zoom"] + 10, #this was completely arbitrary |
| 337 | + style={ |
| 338 | + "width": "100%", |
| 339 | + "height": "87%",#height of the map |
| 340 | + }, |
| 341 | + ), |
| 342 | + html.H6( |
| 343 | + id="map_title" |
| 344 | + ), |
| 345 | + html.H6( |
| 346 | + id="hover_feature_label" |
| 347 | + ) |
| 348 | + ], |
| 349 | + fluid=True, |
| 350 | + style={"padding": "0rem", "height":"100%"},#box that holds map and title |
| 351 | + ) |
| 352 | + |
| 353 | + |
| 354 | +def results_layout(): |
| 355 | + return html.Div( |
| 356 | + [ |
| 357 | + dbc.Spinner(dcc.Graph(id="timeseries_graph")), |
| 358 | + ], |
| 359 | + id="results_div", |
| 360 | + ) |
0 commit comments