From 11d8685108026d6b23be92fe78cf686b0e8ae2b2 Mon Sep 17 00:00:00 2001 From: Goutte Date: Thu, 24 Jan 2019 14:25:25 +0100 Subject: [PATCH] Add support for saturn's aurora HST campaign. --- CHANGELOG.md | 8 +++++++- config.yml | 4 ++++ requirements.txt | 5 +++++ web/run.py | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------- web/static/css/home.css | 456 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ web/static/js/main.js | 575 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------------------------------- web/static/js/swapp.ls | 2 +- web/view/home.html.jinja2 | 439 +++++++++++++++++++++++++++++++++++++++++++++++++------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ 8 files changed, 1151 insertions(+), 459 deletions(-) create mode 100644 web/static/css/home.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 2b16905..263934e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,11 +32,17 @@ et prendre aussi - [ ] Rework the images of Rosetta and Juno - [ ] Optimize data aggregation (numpy vectorization?) - [ ] IE compat, if you can (I can't) -- [ ] Add a README to the download tarball (no tarball anymore) - [ ] Bump D3JS to v5 (and its promises) - [ ] Enable p67 +## 1.8 + +- [x] Auroral Emissions (Saturn) +- [ ] Auroral Emissions Fixes +- [ ] Fix the warmup on the prod server + + ## 1.7 - [x] Only get SWRT after OMNI diff --git a/config.yml b/config.yml index cbd940f..5c7ecf9 100644 --- a/config.yml +++ b/config.yml @@ -59,9 +59,11 @@ layers: - slug: "hstjupiterobservations" name: "HST Jupiter" desc: "Hubble Space Telescope Jupiter Observations" + live: false - slug: "hstsaturnobservations" name: "HST Saturn" desc: "Hubble Space Telescope Saturn Observations" + live: true catalogs: - slug: "cmecatalogs" name: "CME Catalogs" @@ -267,6 +269,8 @@ targets: sb: - slug: 'tao_sat_sw' - slug: 'tao_sat_swrt' + tap: + target_name: 'Saturn' locked: false default: true - type: 'planet' diff --git a/requirements.txt b/requirements.txt index eb8d10c..8044e1e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,6 +13,11 @@ MarkupSafe==1.0 python-slugify==1.2.4 requests==2.19.1 +# Why not pyvo ? +# For obscure reasons within spacepy, only numpy 1.7 or 1.8 will work +# and pyvo requires astropy, that requires a more recent numpy. +#pyvo==0.9.* + ## SECOND LEVEL DEPS # (comment them out if you have compatibility issues) diff --git a/web/run.py b/web/run.py index 4f0330c..c30efa4 100755 --- a/web/run.py +++ b/web/run.py @@ -10,8 +10,9 @@ import tarfile import time import urllib import requests +import re # regex -from csv import writer as csv_writer +from csv import writer as csv_writer, DictWriter as csv_dict_writer from math import sqrt, isnan from os import environ, remove as removefile from os.path import isfile, join, abspath, dirname @@ -356,10 +357,27 @@ def get_active_targets(): return [t for t in all_targets if not ('locked' in t and t['locked'])] -def retrieve_auroral_emissions(target_name): +def validate_tap_target_config(target): + tc = get_target_config(target) + if 'tap' not in tc: + raise Exception("No `tap` configuration for target `%s`." % target) + if 'target_name' not in tc['tap']: + raise Exception("No `target_name` in the `tap` configuration for target `%s`." % target) + return tc + + +# Using pyvo would be best. +# def retrieve_auroral_emissions_vopy(target_name): +# api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync" +# import pyvo as vo +# service = vo.dal.TAPService(api_url) +# # … can't figure out how to install pyvo and spacepy alongside (forking?) + + +def retrieve_auroral_emissions(target_name, d_started_at=None, d_stopped_at=None): """ Work In Progress. - :param target_name: You should probably not let users choose this value, + :param target_name: You should probably not let users define this value, as our sanitizing for ADQL may not be 100% safe. Use values from YAML configuration, instead. Below is a list of the ids we found to be existing. @@ -377,16 +395,34 @@ def retrieve_auroral_emissions(target_name): - Saturn :return: """ + + # Try out the form + # http://voparis-tap-planeto.obspm.fr/__system__/adql/query/form + api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync" - d_started_at = datetime.datetime.now() - t_started_at = time.mktime(d_started_at.timetuple()) - 3600 * 24 * 365 * 10 # fixme - # t_started_at = 1 - d_stopped_at = datetime.datetime.now() + if d_started_at is None: + d_started_at = datetime.datetime.now() + t_started_at = time.mktime(d_started_at.timetuple()) - 3600 * 24 * 365 * 2 + # t_started_at = 1 + else: + t_started_at = time.mktime(d_started_at.timetuple()) + + if d_stopped_at is None: + d_stopped_at = datetime.datetime.now() t_stopped_at = time.mktime(d_stopped_at.timetuple()) - def to_jday(timestamp): + def timestamp_to_jday(timestamp): return timestamp / 86400.0 + 2440587.5 + def jday_to_timestamp(jday): + return (jday - 2440587.5) * 86400.0 + + def jday_to_datetime(jday): + return datetime.datetime.utcfromtimestamp(jday_to_timestamp(jday)) + + # SELECT DISTINCT dataproduct_type FROM apis.epn_core + # > im sp sc + query = """ SELECT time_min, @@ -396,13 +432,13 @@ SELECT FROM apis.epn_core WHERE target_name='{target_name}' AND dataproduct_type='im' -AND time_min > {jday_start} -AND time_min < {jday_stop} +AND time_min >= {jday_start} +AND time_min <= {jday_stop} ORDER BY time_min, granule_gid """.format( target_name=target_name.replace("'", "\\'"), - jday_start=to_jday(t_started_at), - jday_stop=to_jday(t_stopped_at) + jday_start=timestamp_to_jday(t_started_at), + jday_stop=timestamp_to_jday(t_stopped_at) ) # query = """ @@ -427,8 +463,8 @@ ORDER BY time_min, granule_gid rows = [] for row in root.findall(rows_xpath, namespaces): rows.append({ - 'time_min': row[0].text, - 'time_max': row[1].text, + 'time_min': jday_to_datetime(float(row[0].text)), + 'time_max': jday_to_datetime(float(row[1].text)), 'thumbnail_url': row[2].text, 'external_link': row[3].text, }) @@ -1045,7 +1081,7 @@ def increment_hit_counter(): def update_spacepy(): """ - Importing pydcf will fail if the toolbox is not up to date. + Importing pycdf will fail if the toolbox is not up to date. """ try: log.info("Updating spacepy's toolbox…") @@ -1500,6 +1536,61 @@ def download_targets_cdf(targets, inp, started_at, stopped_at): return send_from_directory(CACHE_DIR, cdf_filename) +@app.route("/_auroral_catalog.csv") +def download_auroral_catalog_csv(target): + tc = validate_tap_target_config(target) + log.debug("Requesting auroral emissions CSV for %s..." % tc['name']) + + filename = "%s_auroral_catalog.csv" % (target) + local_csv_file = join(CACHE_DIR, filename) + + + target_name = tc['tap']['target_name'] + emissions = retrieve_auroral_emissions(target_name) + + # Be careful with regexes in python 2 ; best always use the ^$ + thumbnail_url_filter = re.compile("^.*proc(?:_small)?\\.(?:jpe?g|png|webp|gif|bmp|tiff)$") + + # Filter the emissions + def _keep_emission(emission): + ok = thumbnail_url_filter.match(emission['thumbnail_url']) + # print("ok", ok, emission['thumbnail_url']) + return bool(ok) + + emissions = [e for e in emissions if _keep_emission(e)] + + header = ('time_min', 'time_max', 'thumbnail_url', 'external_link') + if len(emissions): + header = emissions[0].keys() + si = StringIO.StringIO() + cw = csv_dict_writer(si, fieldnames=header) + cw.writeheader() + # 'time_min', 'time_max', 'thumbnail_url', 'external_link' + #cw.writerow(head) + + log.debug("Writing auroral emissions CSV for %s..." % tc['name']) + cw.writerows(emissions) + + log.info("Generated auroral emissions CSV contents for %s." % tc['name']) + return si.getvalue() + + # if not isfile(local_csv_file): + # abort(500, "Could not cache CSV file at '%s'." % local_csv_file) + # + # return send_from_directory(CACHE_DIR, filename) + + + + +@app.route("/test/auroral/") +def test_auroral_emissions(target): + tc = validate_tap_target_config(target) + target_name = tc['tap']['target_name'] + retrieved = retrieve_auroral_emissions(target_name) + + return "%d results:\n%s" % (len(retrieved), str(retrieved)) + + # API ######################################################################### @app.route("/cache/clear") diff --git a/web/static/css/home.css b/web/static/css/home.css new file mode 100644 index 0000000..ba88af8 --- /dev/null +++ b/web/static/css/home.css @@ -0,0 +1,456 @@ +.mdl-layout__drawer hr { + margin: 0.5em 0; +} + +.mdl-layout__drawer .mdl-layout-title { + line-height: 42px; + display: inline-block; +} + +.mdl-layout__drawer > details > summary { + padding-left: 15px; + cursor: pointer; + outline: none; +} + +.mdl-layout__drawer .mdl-layout-title:first-of-type { + line-height: 60px; +} + +.plots-buttons { + text-align: center; + margin: 0 auto; +} + +.plots-buttons button { + margin: 1em 1em; +} + +#time_series svg { + cursor: crosshair; +} + +#time_series .help { + position: absolute; + text-align: center; + font-size: 0.9em; + font-style: italic; + color: darkgrey; + display: none; +} + +#time_series:hover .help { + display: block; +} + +#time_series svg .brush .selection { + fill: #efa02c; + fill-opacity: 0.382; +} + +.axis path, .axis line { + fill: none; + /*{# stroke: #f4f4f4;#}*/ + shape-rendering: crispEdges; + stroke-width: 1px; +} + +path.line { + fill: none; + stroke: steelblue; + stroke-width: 1px; +} + +path.predictive-line { + fill: none; + stroke: #ff4081; + stroke-width: 2px; +} + +circle.cursor-circle { + fill: black; + stroke: rgba(20, 20, 20, 0.48); +} + +text.cursor-text { + /*{# font-family: 'Ubuntu', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif;#}*/ + /*{# font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;#}*/ + font-family: "Ubuntu Mono", 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + text-align: right; +} + +text.cursor-text-shadow { + stroke: white; + stroke-width: 5px; + opacity: 0.777 +} + +path.orbit.orbit_section { + fill: none; + stroke: steelblue; + stroke-width: 1.5px; +} + +ellipse.orbit.orbit_ellipse { + fill: none; + stroke: #a3a3a3; + stroke-width: 1px; + stroke-dasharray: 5px; +} + +#form_time_interval { + padding-left: 30px; +} + +#form_time_interval .mdl-textfield { + padding-top: 0; +} + +#started_at, #stopped_at { + width: 85%; +} + +.section-drawer { + padding-left: 3em; +} + +.targets-filters { + padding-left: 17px; +} + +.targets-filters .target { + float: left; + cursor: pointer; + position: relative; +} + +.targets-filters .target:not(.active) { + -webkit-filter: grayscale(100%); + -moz-filter: grayscale(100%); + -o-filter: grayscale(100%); + /*{# -ms-filter: grayscale(100%);#}*/ + filter: grayscale(100%); +} + +.targets-filters .target.locked { + cursor: not-allowed; +} + +.targets-filters .target.loading { + -webkit-animation-name: keyframes_rotate; + -webkit-animation-duration: 4000ms; + -webkit-animation-iteration-count: infinite; + -webkit-animation-timing-function: linear; + -moz-animation-name: keyframes_rotate; + -moz-animation-duration: 4000ms; + -moz-animation-iteration-count: infinite; + -moz-animation-timing-function: linear; + -ms-animation-name: keyframes_rotate; + -ms-animation-duration: 4000ms; + -ms-animation-iteration-count: infinite; + -ms-animation-timing-function: linear; + animation-name: keyframes_rotate; + animation-duration: 4000ms; + animation-iteration-count: infinite; + animation-timing-function: linear; +} + +.targets-filters .target .decorator { + position: absolute; + top: 0; + left: 0; + display: none; +} + +.targets-filters .target.empty .decorator.empty { + display: block; +} + +.targets-filters .target.error .decorator.error { + display: block; +} + +.targets-filters .target .decorator.loading { + top: 19px; + left: 19px; +} + +.targets-filters .target.loading .decorator.loading { + display: block; +} + +#parameters .parameter { + outline: 0; + padding-top: 7px; + padding-bottom: 7px; + +} + +#parameters .parameter.active { + background-color: #c8d3e1; +} + +.option-layer-campaign.loading { + background: #00acc1; +} + + +/* Preview Box */ + +#time_series_cursor_morebox { + margin-top: 1em; + padding-left: 7%; + padding-right: 7%; +} +#time_series_cursor_image_link:focus { + outline: none; +} +#time_series_cursor_image { + width: 100%; +} +#time_series_cursor_link { + display: block; +} +#time_series_cursor_image_extra { + text-align: center; + padding-top: 1em; +} +#time_series_cursor_image_comment { + /*text-align: center;*/ +} +#time_series_cursor_image_comment *[title]{ + text-decoration: dotted lightskyblue; +} + +/*{# CSS Spinners #}*/ + +#download_spinner { + display: none; + top: 2px; + left: 4px; + width: 14px; + height: 14px; +} + +#plots_loader { + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + height: 100%; + width: 100%; + background-color: #fff; + z-index: 1000; +} + +#plots_loader .loader-text { + width: 200px; + height: 30px; + position: absolute; + top: -240px; + left: -32px; + bottom: 0; + right: 0; + margin: auto; + text-align: center; + font-size: 1.0em; + font-style: italic; + color: darkgrey; +} + +#plots_loader .img { + width: 100px; + height: 100px; + border-radius: 100%; + position: absolute; + border: 1px solid #6978ff; + animation: keyframes_rotate 1s; + animation-iteration-count: infinite; + transition: 2s; + border-bottom: none; + border-right: none; + animation-timing-function: linear; + margin-left: -70px; + margin-top: -70px; + left: 50%; + top: 50%; +} + +#plots_loader #plots_loader_img2 { + width: 90px; + height: 90px; + left: 50.35%; + top: 50.7%; + animation-delay: .2s; +} + +#plots_loader #plots_loader_img3 { + width: 80px; + height: 80px; + left: 50.70%; + top: 51.4%; + animation-delay: .4s; +} + +#plots_loader #plots_loader_img4 { + width: 70px; + height: 70px; + left: 51.05%; + top: 52.1%; + animation-delay: .6s; +} + +#plots_loader #plots_loader_img5 { + width: 60px; + height: 60px; + left: 51.40%; + top: 52.8%; + animation-delay: .8s; +} + +@keyframes keyframes_rotate { + from { + transform: rotate(0deg); + } + 50% { + transform: rotate(180deg); + } + 100% { + transform: rotate(360deg); + } +} + +.small-loader-container { + width: 27px; + margin: 0 auto; + background: none; + pointer-events: none; +} + +.small-loader-circle-1 { + height: 27px; + width: 27px; + background: rgba(255, 238, 195, 0.72); +} + +.small-loader-circle-2 { + height: 22px; + width: 22px; + background: none; +} + +.small-loader-circle-3 { + height: 18px; + width: 18px; + background: rgba(29, 65, 255, 0.9); +} + +.small-loader-circle-4 { + height: 13px; + width: 13px; + background: none; +} + +.small-loader-circle-5 { + height: 9px; + width: 9px; + background: rgba(238, 238, 238, 0.8); +} + +.small-loader-circle-6 { + height: 4px; + width: 4px; + background: none; +} + +.small-loader-circle-7 { + height: 2px; + width: 2px; + background: rgb(110, 102, 255); +} + +.small-loader-circle-1, +.small-loader-circle-2, +.small-loader-circle-3, +.small-loader-circle-4, +.small-loader-circle-5, +.small-loader-circle-6, +.small-loader-circle-7 { + border-bottom: none; + border-radius: 50%; + -o-border-radius: 50%; + -ms-border-radius: 50%; + -webkit-border-radius: 50%; + -moz-border-radius: 50%; + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1); + -o-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1); + -ms-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1); + -webkit-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1); + -moz-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1); + animation-name: small-loader-spin; + -o-animation-name: small-loader-spin; + -ms-animation-name: small-loader-spin; + -webkit-animation-name: small-loader-spin; + -moz-animation-name: small-loader-spin; + animation-duration: 4600ms; + -o-animation-duration: 4600ms; + -ms-animation-duration: 4600ms; + -webkit-animation-duration: 4600ms; + -moz-animation-duration: 4600ms; + animation-iteration-count: infinite; + -o-animation-iteration-count: infinite; + -ms-animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; + -moz-animation-iteration-count: infinite; + animation-timing-function: linear; + -o-animation-timing-function: linear; + -ms-animation-timing-function: linear; + -webkit-animation-timing-function: linear; + -moz-animation-timing-function: linear; +} + +@keyframes small-loader-spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +@-o-keyframes small-loader-spin { + from { + -o-transform: rotate(0deg); + } + to { + -o-transform: rotate(360deg); + } +} + +@-ms-keyframes small-loader-spin { + from { + -ms-transform: rotate(0deg); + } + to { + -ms-transform: rotate(360deg); + } +} + +@-webkit-keyframes small-loader-spin { + from { + -webkit-transform: rotate(0deg); + } + to { + -webkit-transform: rotate(360deg); + } +} + +@-moz-keyframes small-loader-spin { + from { + -moz-transform: rotate(0deg); + } + to { + -moz-transform: rotate(360deg); + } +} diff --git a/web/static/js/main.js b/web/static/js/main.js index 0fabcfa..7f88042 100644 --- a/web/static/js/main.js +++ b/web/static/js/main.js @@ -83,6 +83,13 @@ this.configuration['parameters'].forEach(p => that.parameters[p['id']] = p); this.orbits = null; this.time_series = []; + + // Holds the downloaded data for each layer hash + // layer hash => data (array of dict) + this._image_preview_layers_data = {}; + // Holds the activation status for each layer hash + // layer hash => activation status (bool) + this._image_preview_layers_live = {}; } /** @@ -282,7 +289,7 @@ data = { 'hee': [] }; - configuration['parameters'].forEach(parameter => data[parameter['id']] = []); + app.configuration['parameters'].forEach(parameter => data[parameter['id']] = []); if (!csv) { reject('invalid'); } @@ -292,7 +299,7 @@ csv.forEach(d => { let dtime; dtime = timeFormat(d['time']); - configuration['parameters'].forEach(parameter => { + app.configuration['parameters'].forEach(parameter => { let id; let val; id = parameter['id']; @@ -424,12 +431,15 @@ app.time_series.forEach(ts2 => ts2.hideCursor()); return true; }; - ts.options['onMouseMove'] = t => { - let ref$; - app.time_series.forEach(ts2 => ts2.moveCursor(t)); - if ((ref$ = app.orbits) != null) { - ref$.moveToDate(t); + ts.options['onMouseMove'] = time => { + app.time_series.forEach(ts2 => + ts2.moveCursor(time) + ); + if (app.orbits != null) { + app.orbits.moveToDate(time); } + ts.updateImagePreviewFromCursor(time); + return true; }; @@ -508,6 +518,123 @@ return this; } + /////////////////////////////////////////////////////////////////////// + + hashImagePreviewLayer(target_slug, layer_slug) { + return target_slug + "_" + layer_slug; + } + + getImagePreviewData(target_slug, layer_slug) { + const app = this; // not taking any risks with `this` binding. + return new Promise((resolve, reject) => { + const h = app.hashImagePreviewLayer(target_slug, layer_slug); + if (h in this._image_preview_layers_data) { + resolve(app._image_preview_layers_data[h]); + } else { + let url_route = "saturn_auroral_catalog.csv"; // FIXME + let url = app.configuration['api']['root'] + url_route; + //url += "/" + url_route; // Extra / yields havoc -- flask? + let d3csv = d3.csv(url, csv => { + console.info("Fetched image preview CSV data.", url, csv); + console.assert(app === this); // peace of mind + // Now's a good time to do some pre-process on the data + let data = csv.map(d => { + return { + time_min: moment(d.time_min), + time_max: moment(d.time_max), + thumbnail_url: d.thumbnail_url.trim(), + external_link: d.external_link.trim() + }; + }); + app._image_preview_layers_data[h] = data; + // … also a good place to build an index for example, + // since right now we're bisecting on each cursor move, + // and that's expensive. (Binary Search) + resolve(data); + }); + } + }); + } + + isImagePreviewLayerShown(target_slug, layer_slug) { + const h = this.hashImagePreviewLayer(target_slug, layer_slug); + if (h in this._image_preview_layers_live) { + return this._image_preview_layers_live[h]; + } else { + return false; + } + } + + showImagePreviewLayer(target_slug, layer_slug) { + const app = this; + + return new Promise((resolve, reject) => { + if (this.isImagePreviewLayerShown(target_slug, layer_slug)) { + resolve(); // already shown + } + const h = this.hashImagePreviewLayer(target_slug, layer_slug); + //console.log("showImagePreviewLayer", h); + //console.log("_image_preview_layers_data", this._image_preview_layers_data); + + this._image_preview_layers_live[h] = true; + + const data_promise = this.getImagePreviewData(target_slug, layer_slug); + data_promise.then((data) => { + //console.debug("getImagePreviewData OK !", data); + let ts_left_to_process = this.time_series.length; + this.time_series.forEach(ts => { + if (ts.target.slug === target_slug) { + ts.showImagePreviewLayer(layer_slug, data).then( + () => { + //ts_left_to_process--; + //console.debug("showImagePreviewLayer promise ok!"); + }, + (err) => { + //ts_left_to_process--; + //console.error("showImagePreviewLayer promise failed.", err); + }, + ).finally(() => { + ts_left_to_process--; + //console.debug("showImagePreviewLayer left", ts_left_to_process); + if (0 === ts_left_to_process) { + //console.debug("showImagePreviewLayer resolve"); + resolve(); + } + }); + } else { + ts_left_to_process--; + } + }); + }, (err) => { + console.error("getImagePreviewData promise failed.", err); + reject(err); + }); + + }); + } + + hideImagePreviewLayer(target_slug, layer_slug) { + // perhaps make this promise-based as well? + + if ( ! this.isImagePreviewLayerShown(target_slug, layer_slug)) { + return; // already hidden + } + + const h = this.hashImagePreviewLayer(target_slug, layer_slug); + //console.log("hideImagePreviewLayer", h); + + this._image_preview_layers_live[h] = false; + + let ts; + for (let key in this.time_series) { + if ( ! this.time_series.hasOwnProperty(key)) continue; + ts = this.time_series[key]; + if (ts.target.slug !== target_slug) continue; + ts.hideImagePreviewLayer(layer_slug); + } + + } + getDomain() { if (this.current_started_at != null && this.current_stopped_at != null) { return [this.current_started_at, this.current_stopped_at]; @@ -619,6 +746,20 @@ } } this.predictiveData = predictiveData; + + this._image_preview_layers_live = {}; // layer_hash => is active? (bool) + this._image_preview_layers_rect = {}; // layer_hash => rects (array) + this._image_preview_layers_data = {}; // layer_hash => data (array) (/!. not a copy) + // datum example: + // { + // time_min: moment("2017-09-09 00:13:49.845227"), + // thumbnail_url: "http://voparis-srv.obspm.fr/vo/planeto/apis/dataset/Bastet/Saturn_-_2017_14_Feb-09_Sept/od9u21u4q_proc_small.jpg", + // time_max: moment("2017-09-09 00:58:50.044784"), + // external_link: "http://apis.obspm.fr/spip.php?page=observation&id=bas_8167" + // } + // Note that time_min and time_max are pre-processed + // to moment.js instances already during creation. + this.init(); } @@ -778,6 +919,7 @@ this.yAxisText.attr("y", 20 - this.margin.left).attr("x", 0 - height / 2); this.yAxisTextTarget.attr("y", 0 - this.margin.left).attr("x", 0 - height / 2.0); this.resizeCatalogLayers(); + this.resizeImagePreviewLayers(); if (!this.visible) { this.hide(); } @@ -891,6 +1033,7 @@ this.predictiveDataPath.attr('d', this.line); } this.resizeCatalogLayers(); + this.resizeImagePreviewLayers(); this.hideCursor(); return new Promise((resolve, reject) => { if (0 === duration) { @@ -901,6 +1044,19 @@ }); } + createLayerRect(started_at, stopped_at, color) { + // We're ignoring the parameters, since we're + // doing the resize logic in resizeXXX() + let layer_rect; // positioning is done in resize() + layer_rect = this.pathWrapper.append("rect") + .attr('y', 0) + .attr('height', this.plotHeight) // => move to resize too? + .attr('fill', color); + //won't work, possibly because of our input catcher rect. + //layer_rect.append('svg:title').text("!"); + return layer_rect; + } + createCatalogLayers() { let catalog_slug; let ref$; @@ -912,35 +1068,32 @@ let stopped_at; this.layers_rects = {}; for (catalog_slug in ref$ = this.target.config.layers) { + if ( ! (ref$.hasOwnProperty(catalog_slug))) continue; // oh, js layers = ref$[catalog_slug]; this.layers_rects[catalog_slug] = []; for (i$ = 0, len$ = layers.length; i$ < len$; ++i$) { layer = layers[i$]; started_at = moment(layer.start); stopped_at = moment(layer.stop); - this.layers_rects[catalog_slug].push(this.createCatalogLayer(started_at, stopped_at)); + this.layers_rects[catalog_slug].push(this.createCatalogLayerRect(started_at, stopped_at)); } this.hideCatalogLayer(catalog_slug); } return this; } - createCatalogLayer(started_at, stopped_at) { - let layer_rect; - layer_rect = this.pathWrapper.append("rect") - .attr('y', 0) - .attr('height', this.plotHeight) - .attr('fill', '#FFFD64C2'); - //won't work, possibly because of our input catcher rect. - //layer_rect.append('svg:title').text("!"); - return layer_rect; + createCatalogLayerRect(started_at, stopped_at) { + return this.createLayerRect(started_at, stopped_at, '#FFFD64C2'); } resizeCatalogLayers() { + let animate = true; // move to param if needed + // Perhaps make that transition part of the prototype instead? + let t = this.svg.transition().duration(750); + let catalog_slug; let ref$; let layers; - let i$; let len$; let i; let layer; @@ -948,47 +1101,352 @@ let stopped_at; let width; for (catalog_slug in ref$ = this.target.config.layers) { + if ( ! (ref$.hasOwnProperty(catalog_slug))) continue; layers = ref$[catalog_slug]; - for (i$ = 0, len$ = layers.length; i$ < len$; ++i$) { - i = i$; - layer = layers[i$]; + for (i = 0, len$ = layers.length; i < len$; ++i) { + layer = layers[i]; started_at = moment(layer.start); stopped_at = moment(layer.stop); width = Math.max(2, this.xScale(stopped_at) - this.xScale(started_at)); - this.layers_rects[catalog_slug][i].attr('x', this.xScale(started_at)).attr('width', width); + + if (animate) { + this.layers_rects[catalog_slug][i] + .transition(t) + .attr('x', this.xScale(started_at)) + .attr('width', width); + } else { + this.layers_rects[catalog_slug][i] + .attr('x', this.xScale(started_at)) + .attr('width', width); + } + } } return this; } showCatalogLayer(catalog_slug) { - let i$; let ref$; let len$; let i; let layer; - for (i$ = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i$ < len$; ++i$) { - i = i$; - layer = ref$[i$]; + for (i = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i < len$; ++i) { + layer = ref$[i]; this.layers_rects[catalog_slug][i].style("display", null); } return this; } hideCatalogLayer(catalog_slug) { - let i$; let ref$; let len$; let i; - let layer; - for (i$ = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i$ < len$; ++i$) { - i = i$; - layer = ref$[i$]; + //let layer; + for (i = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i < len$; ++i) { + //layer = ref$[i]; this.layers_rects[catalog_slug][i].style("display", "none"); } return this; } + /////////////////////////////////////////////////////////////////////// + + hasImagePreviewLayer(layer_slug) { + return layer_slug in this._image_preview_layers_data; + } + + // createImagePreviewLayers(layer_slug, data) { + // + // } + + createImagePreviewLayer(layer_slug, data) { + this._image_preview_layers_data[layer_slug] = data; + this._image_preview_layers_rect[layer_slug] = []; + this._image_preview_layers_live[layer_slug] = true; + let datum; + for (let key in data) { + if (data.hasOwnProperty(key)) { + if ("columns" === key) continue; + // console.debug("datum", key, data[key]); + datum = data[key]; + // { time_min: "2017-09-09 00:13:49.845227", thumbnail_url: "http://voparis-srv.obspm.fr/vo/planeto/apis/dataset/Bastet/Saturn_-_2017_14_Feb-09_Sept/od9u21u4q_proc_small.jpg", time_max: "2017-09-09 00:58:50.044784", external_link: "http://apis.obspm.fr/spip.php?page=observation&id=bas_8167" } + this._image_preview_layers_rect[layer_slug].push( + this.createImagePreviewLayerRect( + datum.time_min, datum.time_max + ) + ); + } + } + } + + isImagePreviewLayerLive(layer_slug) { + if (layer_slug in this._image_preview_layers_live) { + return this._image_preview_layers_live[layer_slug]; + } else { + return false; + } + } + + resizeImagePreviewLayers() { + for (let layer_slug in this._image_preview_layers_live) { + if (!(this._image_preview_layers_live.hasOwnProperty(layer_slug))) + continue; + console.debug("Resize image layer", layer_slug); + this.resizeImagePreviewLayer(layer_slug); + } + } + + resizeImagePreviewLayer(layer_slug) { + if (! this.isImagePreviewLayerLive(layer_slug)) { + return; + } + + const rects = this._image_preview_layers_rect[layer_slug]; + if (! rects) { + console.error( + "Tried to resize an image preview layer without rects.", + layer_slug, this + ); + return; + } + + const data = this._image_preview_layers_data[layer_slug]; + if (! data) { + console.error( + "Tried to resize an image preview layer without data.", + layer_slug, this + ); + return; + } + + let animate = true; + let t = this.svg.transition().duration(750); + + let width; + let started_at; + let stopped_at; + //for (let [dkey, datum] of data) { // nope + let datum; + for (let dkey in data) { + if ( ! data.hasOwnProperty(dkey)) continue; + if ("columns" === dkey) continue; + + datum = data[dkey]; + + // We assume that moment(moment(x)) == moment(x) (it's true) + // Remove these moment() casts for some more perfs since we're + // already casting to moment() in the data pre-process. + started_at = moment(datum.time_min); + stopped_at = moment(datum.time_max); + + //console.debug("Resize image layer rect", started_at, stopped_at); + + width = this.xScale(stopped_at) - this.xScale(started_at); + // Under 2 pixels wide the rects appear glitchy + width = Math.max(2, width); + + if (animate) { + rects[dkey] + .transition(t) + .attr('x', this.xScale(started_at)) + .attr('width', width); + } else { + rects[dkey] + .attr('x', this.xScale(started_at)) + .attr('width', width); + } + + } + } + + static get COLOR_IMAGE_PREVIEW_LAYER() { + return '#FF339942'; + } + + static get COLOR_IMAGE_PREVIEW_LAYER_ACTIVE() { + return '#FF3399D2'; + } + + createImagePreviewLayerRect(started_at, stopped_at) { + return this.createLayerRect( + started_at, stopped_at, TimeSeries.COLOR_IMAGE_PREVIEW_LAYER + ); + } + + highlightImagePreviewLayerRect(layer_slug, rect_key) { + const rects = this._image_preview_layers_rect[layer_slug]; + if (! rects) { + console.error( + "Tried to highlight an image preview layer without rects.", + layer_slug, this + ); + return; + } + + const rect_to_highlight = rects[rect_key]; + if (! rect_to_highlight) { + console.error( + "Tried to highlight a non-existent rect.", + layer_slug, rect_key, this + ); + return; + } + + for (let rect of rects) { + TimeSeries.setRectColor( + rect, TimeSeries.COLOR_IMAGE_PREVIEW_LAYER + ); + } + TimeSeries.setRectColor( + rect_to_highlight, TimeSeries.COLOR_IMAGE_PREVIEW_LAYER_ACTIVE + ); + + } + + static setRectColor(rect, color) { + rect.attr('fill', color); + } + + showImagePreviewLayer(layer_slug, data) { + return new Promise((resolve, reject) => { + //console.log("showImagePreviewLayer Promise…"); + + if (this._image_preview_layers_live[layer_slug]) { + console.warn("TS.showImagePreviewLayer: already shown."); + resolve(); // we're already shown + } + + if ( ! this.hasImagePreviewLayer(layer_slug)) { + this.createImagePreviewLayer(layer_slug, data); + } + + this._image_preview_layers_live[layer_slug] = true; + + this.resizeImagePreviewLayer(layer_slug); + + const rects = this._image_preview_layers_rect[layer_slug]; + if (! rects) { + console.error( + "Tried to show an image preview layer without rects.", + layer_slug, this + ); + return; + } + + for (let rect of rects) { + rect.style("display", null) + } + + resolve(); + }); + } + + hideImagePreviewLayer(layer_slug) { + // console.log( + // "TS.hideImagePreviewLayer()", + // this._image_preview_layers_live[layer_slug], + // this._image_preview_layers_live + // ); + if ( ! this._image_preview_layers_live[layer_slug]) { + console.warn("TS.hideImagePreviewLayer: already hidden."); + return; // we're already hidden + } + this._image_preview_layers_live[layer_slug] = false; + + const rects = this._image_preview_layers_rect[layer_slug]; + //console.debug("RECTS", rects); + + if (! rects) { + console.error( + "Tried to hide an image preview layer without rects.", + layer_slug, this + ); + return; + } + + + for (let rect of rects) { + rect.style("display", "none"); + } + } + + showImagePreview(image_url, external_link, comment) { + const box = jQuery('#time_series_cursor_morebox'); + const img = jQuery('#time_series_cursor_image'); + const cil = jQuery('#time_series_cursor_image_link'); + const cic = jQuery('#time_series_cursor_image_comment'); + const lnk = jQuery('#time_series_cursor_link'); + + const previous_image_url = img.attr('src'); + if (image_url === previous_image_url) { + return; + } + + img.attr('src', image_url); + lnk.attr('href', external_link); + cic.html(comment); + + // Interesting ; using the regex in the prototype yields … fails + // Race conditions are probably reunited, afaik. + //const match = this.imageRegex.exec(image_url); + // Let's compile our regex every time instead. Optimize later. + const imageRegex = /(.+?)_small([.][a-zA-Z0-9]+)$/g; + const match = imageRegex.exec(image_url); + let image_link = "#"; + if (match) { + //console.log("Matched image url!", match, image_url); + image_link = match[1] + match[2]; // remove the `_small` + } else { + console.warn("Could not find bigger image for url:", image_url); + } + cil.attr('href', image_link); + + box.removeClass('hidden'); + } + + // hideImagePreview() {} // TODO + + updateImagePreviewFromCursor(x0) { + let data = this._image_preview_layers_data; + let i; + let layer_slug; + for (layer_slug in data) { + if ( ! data.hasOwnProperty(layer_slug)) + continue; + if ( ! this.isImagePreviewLayerLive(layer_slug)) { + continue; + } + i = this.bisectIpod(data[layer_slug], x0, 1); + //console.debug("Found i ", i, x0, data[layer_slug]); + if (i == null) continue; // ignore errors and data out of range + break; + } + if (i == null) { + //console.debug("No live image preview found."); + return; + } + + let d0 = data[layer_slug][i - 1]; + let d1 = data[layer_slug][i]; + if (!d1 || !d0) { + //this.hideImagePreview(); + return; + } + let d = x0 - d0.time_min > d1.time_min - x0 ? d1 : d0; + + //console.info("Cursor", x0, d0); + + this.highlightImagePreviewLayerRect( + layer_slug, d === d0 ? i-1 : i + ); + this.showImagePreview( + d.thumbnail_url, d.external_link, + "Integration: "+d.time_min.from(d.time_max, true) + ); + } + + /////////////////////////////////////////////////////////////////////// + showCursor() { return this.focus.style("display", null); } @@ -998,40 +1456,53 @@ } moveCursor(x0) { - let i; - let d0; - let d1; - let d; - let xx; - let yy; - let dx; - let transform; - i = this.bisectDate(this.data, x0, 1); - d0 = this.data[i - 1]; - d1 = this.data[i]; + let i = this.bisectDate(this.data, x0, 1); + let d0 = this.data[i - 1]; + let d1 = this.data[i]; if (!d1 || !d0) { this.hideCursor(); return; } - d = x0 - d0.x > d1.x - x0 ? d1 : d0; - xx = this.xScale(d.x); - yy = this.yScale(d.y); + let d = x0 - d0.x > d1.x - x0 ? d1 : d0; + let xx = this.xScale(d.x); + let yy = this.yScale(d.y); const mirrored = this.plotWidth != null && xx > this.plotWidth / 2; - dx = 8; + let dx = 8; if (mirrored) { dx = -1 * dx; } - transform = `translate(${xx}, ${yy})`; - this.cursorCircle.attr("transform", transform); - this.cursorValue.attr("transform", transform).text(d.y).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx); - this.cursorValueShadow.attr("transform", transform).text(d.y).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx); - this.cursorDate.attr("transform", transform).text(this.timeFormat(d.x)).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx); - this.cursorDateShadow.attr("transform", transform).text(this.timeFormat(d.x)).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx); + const transform = `translate(${xx}, ${yy})`; + this.cursorCircle + .attr("transform", transform); + this.cursorValue + .text(d.y) + .attr("transform", transform) + .attr('text-anchor', mirrored ? 'end' : 'start') + .attr("dx", dx); + this.cursorValueShadow + .text(d.y) + .attr("transform", transform) + .attr('text-anchor', mirrored ? 'end' : 'start') + .attr("dx", dx); + this.cursorDate + .text(this.timeFormat(d.x)) + .attr("transform", transform) + .attr('text-anchor', mirrored ? 'end' : 'start') + .attr("dx", dx); + this.cursorDateShadow + .text(this.timeFormat(d.x)) + .attr("transform", transform) + .attr('text-anchor', mirrored ? 'end' : 'start') + .attr("dx", dx); this.showCursor(); + return this; } } + // Don't use the prototype for a regex ; there are race conditions + //TimeSeries.prototype.imageRegex = /(.+?)_small([.][a-zA-Z0-9]+)$/g; + TimeSeries.prototype.bisectIpod = d3.bisector(d => d.time_min).left; TimeSeries.prototype.bisectDate = d3.bisector(d => d.x).left; TimeSeries.prototype.timeFormat = d3.utcFormat("%Y-%m-%d %H:%M"); diff --git a/web/static/js/swapp.ls b/web/static/js/swapp.ls index d76488a..ba47211 100644 --- a/web/static/js/swapp.ls +++ b/web/static/js/swapp.ls @@ -12,7 +12,7 @@ -# DEPRECATED +# DEPRECATED and NOT USED ANYMORE # Use web/static/js/main.js instead. diff --git a/web/view/home.html.jinja2 b/web/view/home.html.jinja2 index 65727c7..ac94b31 100755 --- a/web/view/home.html.jinja2 +++ b/web/view/home.html.jinja2 @@ -178,7 +178,7 @@
{% for layer in config.layers.campaigns %}
@@ -197,7 +197,7 @@
{% for layer in config.layers.catalogs %}
@@ -250,7 +250,32 @@ Alert {% endif %} + +
+ +
+
@@ -267,391 +292,9 @@ {#### CSS ####################################################################} {% block styles %} - + +{# #} {% endblock %} @@ -664,10 +307,11 @@