diff --git a/.gitignore b/.gitignore index abea78e1af02098b09375c46cf37b1c817188008..b831a6a41625ba853d5bd543ca938edc9bb9edbd 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ log *.log *.swp *.swo +/js/tmp/ diff --git a/bin/gen_country_loop.sh b/bin/gen_country_loop.sh new file mode 100755 index 0000000000000000000000000000000000000000..7fe2c5990b8dfe101f9132fd3987dfcea26bc4ae --- /dev/null +++ b/bin/gen_country_loop.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +########################################## +# country should be written with first letter as capital, or how appears in coords.conf +# date currently does not function and will return always 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 + +# generate daily maps for specified couuntry +# usage: script.sh [model] [anim] [variable] [date] [country] + +if [ "$1" != "" ]; then + model="$1" +else + model="all" +fi + +if [ "$2" != "" ]; then + anim="$2" +else + anim="true" +fi + +if [ "$3" != "" ]; then + curvar="$3" +else + curvar="all" +fi + +if [ "$4" != "" ]; then + curdate="$4" +else + curdate=`date '+%y%m%d' -d '-1day'` +fi + +if [ "$5" != "" ]; then + country="$5" +else + country="Spain" +fi + +curyear=`date '+%y' -d ${curdate}` +curmon=`date '+%m' -d ${curdate}` +repodir='/data/daily_dashboard/comparison/' + +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 + tmpdir=${home}/tmp/${var}/${country} + mkdir -p $tmpdir + rm -rf $tmpdir/* + if [ "$variable" != "od550_dust" ] && [ "$variable" != "sconc_dust" ]; then + model="monarch" + node js/create_country_loop.js $anim $model $curdate $variable $country + wait + sleep 1 + convert -loop 0 -delay 25 ${tmpdir}/${curdate}_${model}_${country}_??.png ${tmpdir}/${curdate}_${model}_${country}_loop.gif + currepo=${repodir}/${model}/${variable}/${curyear}/${curmon}/${country} + mkdir -p $currepo + wait + mv ${tmpdir}/${curdate}_${model}_${country}_* $currepo + rm $currepo/${curdate}_${model}_${country}_??.png + + elif [ "$model" == "all" ]; then + for mod in `cat interactive-forecast-viewer/conf/models.json | grep '": {' | sed 's/^.*"\(.*\)".*$/\1/g'` + do + node js/create_country_loop.js $anim $mod $curdate $variable $country + wait + sleep 1 + convert -loop 0 -delay 25 ${tmpdir}/${curdate}_${mod}_${country}_??.png ${tmpdir}/${curdate}_${mod}_${country}_loop.gif + currepo=${repodir}/${mod}/${variable}/${curyear}/${curmon}/${country} + mkdir -p $currepo + wait + mv ${tmpdir}/${curdate}_${mod}_${country}_* $currepo + rm $currepo/${curdate}_${mod}_${country}_??.png + done + else + node js/create_country_loop.js $anim $model $curdate $variable $country + wait + sleep 1 + convert -loop 0 -delay 25 ${tmpdir}/${curdate}_${model}_${country}_??.png ${tmpdir}/${curdate}_${model}_${country}_loop.gif + currepo=${repodir}/${model}/${variable}/${curyear}/${curmon}/${country} + mkdir -p $currepo + wait + mv ${tmpdir}/${curdate}_${model}_${country}_* $currepo + rm $currepo/${curdate}_${model}_${country}_??.png + fi + + wait + sleep 1 +done diff --git a/conf/coords.json b/conf/coords.json new file mode 100644 index 0000000000000000000000000000000000000000..2de3a399a37013a9a01c7a6b724ecfba0f158ad8 --- /dev/null +++ b/conf/coords.json @@ -0,0 +1,7 @@ +{ + "Spain":{ + "zoom":"5", + "lat":"34", + "lon": "-7" + } +} diff --git a/data_handler.py b/data_handler.py index da3b67af223fc8fc637fcd5d9a051ee54599eb8e..0167138fecd0f82ce8b9095ae72e64c19676b110 100644 --- a/data_handler.py +++ b/data_handler.py @@ -1009,7 +1009,7 @@ class FigureHandler(object): if center is None: center = self.get_center(center) if zoom is None: - zoom = 3.5-(aspect[0]-aspect[0]*0.4) + zoom = 3.5 -(aspect[0]-aspect[0]*0.4) if colorbar is not None: colorbar.width = 320 - 25 * aspect[0] if DEBUG: print("ZOOM", zoom) @@ -1080,6 +1080,7 @@ class FigureHandler(object): colorbar, info ] + layer, + zoomSnap=0.1, zoom=zoom, center=center, id=dict( @@ -1089,6 +1090,7 @@ class FigureHandler(object): inertia=True, preferCanvas=True, animate=False, + minZoom=2, className="graph-with-slider", ) # if DEBUG: print("---", fig) diff --git a/js/create_country_loop.js b/js/create_country_loop.js new file mode 100644 index 0000000000000000000000000000000000000000..416d8e5e806a4c8d514f0b217afff6627f71d3ad --- /dev/null +++ b/js/create_country_loop.js @@ -0,0 +1,221 @@ +// CREATE A GIF FOCUSED GIF OR PNG OVER SELECTED COUNTRY +// 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 +// country should have first letter capitalized, or coincide to coords.conf +// node create_country_loop true monarch 20220808 od550_dust Spain + +const { Cluster } = require('puppeteer-cluster'); +const util = require('util'); +const path = require('path'); +const url = 'http://127.0.0.1:9000/daily_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('create_country_loop.js', 'conf/coords.json'); +const coords = require(relpath); + +function delay(time) { + return new Promise(function(resolve) { + setTimeout(resolve, time) + }); +} + +const RunCluster = async (anim, curmodel, seldate, variable, country) => { + const cluster = await Cluster.launch({ + concurrency: Cluster.CONCURRENCY_CONTEXT, + puppeteerOptions: { + headless: true, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + ] + }, + maxConcurrency: 4, + }); + + await cluster.task(async ({ page, data: args }) => { + process.stdout.write("ARGS: " + args + "\n"); + var tstep = args[0]; + var curmodel = args[1]; + var seldate = args[2]; + var variable = args[3]; + var country = args[4]; + process.stdout.write("TSTEP: " + tstep + " -- MOD: " + curmodel + " -- DATE: " + seldate + " -- VAR: " + variable +' country: ' + country + "\n"); + await page.setViewport({ width: 1280, height: 768}); + await page.goto(url, { + waitUntil: 'networkidle0', + }); + await page.waitForSelector("#graph-collection"); + await page.waitForSelector(".graph-with-slider"); + // select variable + try { + const sel = await page.$('#variable-dropdown-forecast'); + 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')]; + selected_variable = options.find(element => element.textContent === dropdown_variable); + selected_variable.click(); + }, dropdown_variable); + } catch (err) { + process.stdout.write("ERR0: " + err + "\n"); + } + // select all models + if (curmodel === "all") { + try { + process.stdout.write("SELECT ALL MODELS\n"); + for (const model of await page.$$('.custom-control-input')) { + const checked = await model.evaluate(elem => elem.checked); + process.stdout.write("CHECKED BEFORE: " + checked + "\n"); + if (!checked) { + await model.click(); + } + } + } catch (err) { + process.stdout.write("ERR1: " + err + "\n"); + } + } + // select only one model + else { + try { + process.stdout.write("SELECT MODEL: " + curmodel + "\n"); + for (const model of await page.$$('.custom-control-input')) { + const checked = await model.evaluate(elem => elem.checked); + process.stdout.write("CHECKED BEFORE: " + checked + "\n"); + const value = await model.evaluate(elem => elem.value); + process.stdout.write("VALUE: " + value + "\n"); + if (!checked && value === curmodel) { + await model.click(); + } + if (checked && value !== curmodel) { + await model.click(); + await delay(500); + } + } + } catch (err) { + process.stdout.write("ERR1: " + err + "\n"); + } + } + // apply button + try { + for (const model of await page.$$('.custom-control-input')) { + 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'); + await btn.click(); // "button#models-apply"); + await delay(500); + } catch (err) { + process.stdout.write("ERR2: " + err + "\n"); + } + // apply none country button + try { + var zoom =coords[country].zoom; + var lat = coords[country].lat; + var lon = coords[country].lon; + process.stdout.write("Zoom: " + zoom + " Lat " + lat + " Lon " + lon +"\n"); + + // Change hidden inputs and button to visible + let zoomInput = await page.$('#country-zoom'); + await zoomInput.evaluate((el) => el.style.display = 'block'); + let latInput = await page.$('#country-lat'); + await latInput.evaluate((el) => el.style.display = 'block'); + let lonInput = await page.$('#country-lon'); + await lonInput.evaluate((el) => el.style.display = 'block'); + let zoomButton = await page.$('#country-focus'); + await zoomButton.evaluate((el) => el.style.display = 'block'); + + // Add data and click + await page.type('#country-zoom', zoom); + await page.type('#country-lat', lat); + await page.type('#country-lon', lon); + process.stdout.write("CLICK HIDDEN COUNTRY BUTTON\n"); + await page.click('#country-focus'); + + // Make elements invisible again + await zoomInput.evaluate((el) => el.style.display = 'none'); + await latInput.evaluate((el) => el.style.display = 'none'); + await lonInput.evaluate((el) => el.style.display = 'none'); + await zoomButton.evaluate((el) => el.style.display = 'none'); + + }catch (err) { + process.stdout.write("ERR2: " + err + "\n"); + } + 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"); + await steps[tstep].click(); + if (tstep<10) { + num = "0"+tstep; + } else { + 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'); + // remove timeslider + await page.waitForSelector(".layout-dropdown"); + process.stdout.write("REMOVING LAYOUT DROPDOWN" + "\n"); + await page.evaluate((sel) => { + let toRemove = document.querySelector(sel); + toRemove.parentNode.removeChild(toRemove); + }, '.layout-dropdown'); + // 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 " + outputFile + "\n"); + await graph.screenshot({ path: outputFile }); + + }); + + if (anim === "true") { + for (let i=0; i<25; i++) { + try { + process.stdout.write("QUEUEING step:" + i + "\n"); + await cluster.queue([i, curmodel, seldate, variable, country]); + } catch (err) { + console.log(err); + process.stdout.write("ERROR:" + err + "\n"); + } + } + } else { + process.stdout.write("QUEUEING current:" + anim + "\n"); + cluster.queue([anim, curmodel, seldate, variable, country]); + } + + await cluster.idle(); + await cluster.close(); +} + +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 country = process.argv[6]; // default: Spain + +process.stdout.write("START -> ANIM: " + anim + " CURMODEL: " + curmodel + " DATE: " + seldate + " VAR: " + variable + " country: " + country + "\n"); +RunCluster(anim, curmodel, seldate, variable, country); + diff --git a/tabs/forecast.py b/tabs/forecast.py index 0ac932f7de6386b273fc17a813193f7f74b2b6bd..2cf77303bd2b20cdcc2e7c33c0a343a9936d3d50 100644 --- a/tabs/forecast.py +++ b/tabs/forecast.py @@ -393,6 +393,13 @@ def sidebar_forecast(variables, default_var, models, default_model): ), html.Span([ html.Button('APPLY', id='models-apply', n_clicks=0), + ]), + #add hidden country fields and button for gifs over entered coords + html.Span([ + dcc.Input(id="country-zoom",type="text",style={'display':'none'}), + dcc.Input(id="country-lat", type="text",style={'display':'none'}), + dcc.Input(id="country-lon", type="text",style={'display':'none'}), + html.Button(id="country-focus", n_clicks=0,style={'display':'none'}), ], )], ) diff --git a/tabs/forecast_callbacks.py b/tabs/forecast_callbacks.py index 78d5273f980fc42e93dbcf97523e8c761ca08865..ef4f47587cc22dc27e99ae2228c3a9ffdac4d20c 100644 --- a/tabs/forecast_callbacks.py +++ b/tabs/forecast_callbacks.py @@ -866,7 +866,6 @@ def register_callbacks(app, cache, cache_timeout): return {}, {}, dash.no_update # raise PreventUpdate - # retrieve timeseries according to coordinates selected @app.callback( [Output('ts-modal', 'children'), @@ -922,6 +921,28 @@ def register_callbacks(app, cache, cache_timeout): return dash.no_update, False, [0 for _ in ts_button] # raise PreventUpdate + @app.callback( + Output({'tag': 'model-map', 'index': ALL, "n_clicks": ALL}, 'zoom'), + Output({'tag': 'model-map', 'index': ALL, "n_clicks": ALL}, 'center'), + Input('country-focus', 'n_clicks'), + [State('slider-graph', 'value'), + State('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('country-zoom', 'value'), + State('country-lat', 'value'), + State('country-lon', 'value'), + State({'tag': 'model-map', 'index': ALL, "n_clicks": ALL}, 'id'), + ], + ) + def zoom_country(n_clicks, tstep, date, model, variable, static, view, zoom, lat, lon, ids): + """Set zoom and center over coordinates entered""" + count = len(model) + zoom=[float(zoom)] + center=[[float(lat),float(lon)]] + return zoom*count, center*count # start/stop animation @app.callback(