Commit 11d8685108026d6b23be92fe78cf686b0e8ae2b2

Authored by Goutte
1 parent 20fdc1a4

Add support for saturn's aurora HST campaign.

(finally! -- biggest commit of the tree)
This was much more work than expected.
Jupiter will follow.
CHANGELOG.md
... ... @@ -32,11 +32,17 @@ et prendre aussi
32 32 - [ ] Rework the images of Rosetta and Juno
33 33 - [ ] Optimize data aggregation (numpy vectorization?)
34 34 - [ ] IE compat, if you can (I can't)
35   -- [ ] Add a README to the download tarball (no tarball anymore)
36 35 - [ ] Bump D3JS to v5 (and its promises)
37 36 - [ ] Enable p67
38 37  
39 38  
  39 +## 1.8
  40 +
  41 +- [x] Auroral Emissions (Saturn)
  42 +- [ ] Auroral Emissions Fixes
  43 +- [ ] Fix the warmup on the prod server
  44 +
  45 +
40 46 ## 1.7
41 47  
42 48 - [x] Only get SWRT after OMNI
... ...
config.yml
... ... @@ -59,9 +59,11 @@ layers:
59 59 - slug: "hstjupiterobservations"
60 60 name: "HST Jupiter"
61 61 desc: "Hubble Space Telescope Jupiter Observations"
  62 + live: false
62 63 - slug: "hstsaturnobservations"
63 64 name: "HST Saturn"
64 65 desc: "Hubble Space Telescope Saturn Observations"
  66 + live: true
65 67 catalogs:
66 68 - slug: "cmecatalogs"
67 69 name: "CME Catalogs"
... ... @@ -267,6 +269,8 @@ targets:
267 269 sb:
268 270 - slug: 'tao_sat_sw'
269 271 - slug: 'tao_sat_swrt'
  272 + tap:
  273 + target_name: 'Saturn'
270 274 locked: false
271 275 default: true
272 276 - type: 'planet'
... ...
requirements.txt
... ... @@ -13,6 +13,11 @@ MarkupSafe==1.0
13 13 python-slugify==1.2.4
14 14 requests==2.19.1
15 15  
  16 +# Why not pyvo ?
  17 +# For obscure reasons within spacepy, only numpy 1.7 or 1.8 will work
  18 +# and pyvo requires astropy, that requires a more recent numpy.
  19 +#pyvo==0.9.*
  20 +
16 21  
17 22 ## SECOND LEVEL DEPS
18 23 # (comment them out if you have compatibility issues)
... ...
web/run.py
... ... @@ -10,8 +10,9 @@ import tarfile
10 10 import time
11 11 import urllib
12 12 import requests
  13 +import re # regex
13 14  
14   -from csv import writer as csv_writer
  15 +from csv import writer as csv_writer, DictWriter as csv_dict_writer
15 16 from math import sqrt, isnan
16 17 from os import environ, remove as removefile
17 18 from os.path import isfile, join, abspath, dirname
... ... @@ -356,10 +357,27 @@ def get_active_targets():
356 357 return [t for t in all_targets if not ('locked' in t and t['locked'])]
357 358  
358 359  
359   -def retrieve_auroral_emissions(target_name):
  360 +def validate_tap_target_config(target):
  361 + tc = get_target_config(target)
  362 + if 'tap' not in tc:
  363 + raise Exception("No `tap` configuration for target `%s`." % target)
  364 + if 'target_name' not in tc['tap']:
  365 + raise Exception("No `target_name` in the `tap` configuration for target `%s`." % target)
  366 + return tc
  367 +
  368 +
  369 +# Using pyvo would be best.
  370 +# def retrieve_auroral_emissions_vopy(target_name):
  371 +# api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync"
  372 +# import pyvo as vo
  373 +# service = vo.dal.TAPService(api_url)
  374 +# # โ€ฆ can't figure out how to install pyvo and spacepy alongside (forking?)
  375 +
  376 +
  377 +def retrieve_auroral_emissions(target_name, d_started_at=None, d_stopped_at=None):
360 378 """
361 379 Work In Progress.
362   - :param target_name: You should probably not let users choose this value,
  380 + :param target_name: You should probably not let users define this value,
363 381 as our sanitizing for ADQL may not be 100% safe.
364 382 Use values from YAML configuration, instead.
365 383 Below is a list of the ids we found to be existing.
... ... @@ -377,16 +395,34 @@ def retrieve_auroral_emissions(target_name):
377 395 - Saturn
378 396 :return:
379 397 """
  398 +
  399 + # Try out the form
  400 + # http://voparis-tap-planeto.obspm.fr/__system__/adql/query/form
  401 +
380 402 api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync"
381   - d_started_at = datetime.datetime.now()
382   - t_started_at = time.mktime(d_started_at.timetuple()) - 3600 * 24 * 365 * 10 # fixme
383   - # t_started_at = 1
384   - d_stopped_at = datetime.datetime.now()
  403 + if d_started_at is None:
  404 + d_started_at = datetime.datetime.now()
  405 + t_started_at = time.mktime(d_started_at.timetuple()) - 3600 * 24 * 365 * 2
  406 + # t_started_at = 1
  407 + else:
  408 + t_started_at = time.mktime(d_started_at.timetuple())
  409 +
  410 + if d_stopped_at is None:
  411 + d_stopped_at = datetime.datetime.now()
385 412 t_stopped_at = time.mktime(d_stopped_at.timetuple())
386 413  
387   - def to_jday(timestamp):
  414 + def timestamp_to_jday(timestamp):
388 415 return timestamp / 86400.0 + 2440587.5
389 416  
  417 + def jday_to_timestamp(jday):
  418 + return (jday - 2440587.5) * 86400.0
  419 +
  420 + def jday_to_datetime(jday):
  421 + return datetime.datetime.utcfromtimestamp(jday_to_timestamp(jday))
  422 +
  423 + # SELECT DISTINCT dataproduct_type FROM apis.epn_core
  424 + # > im sp sc
  425 +
390 426 query = """
391 427 SELECT
392 428 time_min,
... ... @@ -396,13 +432,13 @@ SELECT
396 432 FROM apis.epn_core
397 433 WHERE target_name='{target_name}'
398 434 AND dataproduct_type='im'
399   -AND time_min > {jday_start}
400   -AND time_min < {jday_stop}
  435 +AND time_min >= {jday_start}
  436 +AND time_min <= {jday_stop}
401 437 ORDER BY time_min, granule_gid
402 438 """.format(
403 439 target_name=target_name.replace("'", "\\'"),
404   - jday_start=to_jday(t_started_at),
405   - jday_stop=to_jday(t_stopped_at)
  440 + jday_start=timestamp_to_jday(t_started_at),
  441 + jday_stop=timestamp_to_jday(t_stopped_at)
406 442 )
407 443  
408 444 # query = """
... ... @@ -427,8 +463,8 @@ ORDER BY time_min, granule_gid
427 463 rows = []
428 464 for row in root.findall(rows_xpath, namespaces):
429 465 rows.append({
430   - 'time_min': row[0].text,
431   - 'time_max': row[1].text,
  466 + 'time_min': jday_to_datetime(float(row[0].text)),
  467 + 'time_max': jday_to_datetime(float(row[1].text)),
432 468 'thumbnail_url': row[2].text,
433 469 'external_link': row[3].text,
434 470 })
... ... @@ -1045,7 +1081,7 @@ def increment_hit_counter():
1045 1081  
1046 1082 def update_spacepy():
1047 1083 """
1048   - Importing pydcf will fail if the toolbox is not up to date.
  1084 + Importing pycdf will fail if the toolbox is not up to date.
1049 1085 """
1050 1086 try:
1051 1087 log.info("Updating spacepy's toolboxโ€ฆ")
... ... @@ -1500,6 +1536,61 @@ def download_targets_cdf(targets, inp, started_at, stopped_at):
1500 1536 return send_from_directory(CACHE_DIR, cdf_filename)
1501 1537  
1502 1538  
  1539 +@app.route("/<target>_auroral_catalog.csv")
  1540 +def download_auroral_catalog_csv(target):
  1541 + tc = validate_tap_target_config(target)
  1542 + log.debug("Requesting auroral emissions CSV for %s..." % tc['name'])
  1543 +
  1544 + filename = "%s_auroral_catalog.csv" % (target)
  1545 + local_csv_file = join(CACHE_DIR, filename)
  1546 +
  1547 +
  1548 + target_name = tc['tap']['target_name']
  1549 + emissions = retrieve_auroral_emissions(target_name)
  1550 +
  1551 + # Be careful with regexes in python 2 ; best always use the ^$
  1552 + thumbnail_url_filter = re.compile("^.*proc(?:_small)?\\.(?:jpe?g|png|webp|gif|bmp|tiff)$")
  1553 +
  1554 + # Filter the emissions
  1555 + def _keep_emission(emission):
  1556 + ok = thumbnail_url_filter.match(emission['thumbnail_url'])
  1557 + # print("ok", ok, emission['thumbnail_url'])
  1558 + return bool(ok)
  1559 +
  1560 + emissions = [e for e in emissions if _keep_emission(e)]
  1561 +
  1562 + header = ('time_min', 'time_max', 'thumbnail_url', 'external_link')
  1563 + if len(emissions):
  1564 + header = emissions[0].keys()
  1565 + si = StringIO.StringIO()
  1566 + cw = csv_dict_writer(si, fieldnames=header)
  1567 + cw.writeheader()
  1568 + # 'time_min', 'time_max', 'thumbnail_url', 'external_link'
  1569 + #cw.writerow(head)
  1570 +
  1571 + log.debug("Writing auroral emissions CSV for %s..." % tc['name'])
  1572 + cw.writerows(emissions)
  1573 +
  1574 + log.info("Generated auroral emissions CSV contents for %s." % tc['name'])
  1575 + return si.getvalue()
  1576 +
  1577 + # if not isfile(local_csv_file):
  1578 + # abort(500, "Could not cache CSV file at '%s'." % local_csv_file)
  1579 + #
  1580 + # return send_from_directory(CACHE_DIR, filename)
  1581 +
  1582 +
  1583 +
  1584 +
  1585 +@app.route("/test/auroral/<target>")
  1586 +def test_auroral_emissions(target):
  1587 + tc = validate_tap_target_config(target)
  1588 + target_name = tc['tap']['target_name']
  1589 + retrieved = retrieve_auroral_emissions(target_name)
  1590 +
  1591 + return "%d results:\n%s" % (len(retrieved), str(retrieved))
  1592 +
  1593 +
1503 1594 # API #########################################################################
1504 1595  
1505 1596 @app.route("/cache/clear")
... ...
web/static/css/home.css 0 โ†’ 100644
... ... @@ -0,0 +1,456 @@
  1 +.mdl-layout__drawer hr {
  2 + margin: 0.5em 0;
  3 +}
  4 +
  5 +.mdl-layout__drawer .mdl-layout-title {
  6 + line-height: 42px;
  7 + display: inline-block;
  8 +}
  9 +
  10 +.mdl-layout__drawer > details > summary {
  11 + padding-left: 15px;
  12 + cursor: pointer;
  13 + outline: none;
  14 +}
  15 +
  16 +.mdl-layout__drawer .mdl-layout-title:first-of-type {
  17 + line-height: 60px;
  18 +}
  19 +
  20 +.plots-buttons {
  21 + text-align: center;
  22 + margin: 0 auto;
  23 +}
  24 +
  25 +.plots-buttons button {
  26 + margin: 1em 1em;
  27 +}
  28 +
  29 +#time_series svg {
  30 + cursor: crosshair;
  31 +}
  32 +
  33 +#time_series .help {
  34 + position: absolute;
  35 + text-align: center;
  36 + font-size: 0.9em;
  37 + font-style: italic;
  38 + color: darkgrey;
  39 + display: none;
  40 +}
  41 +
  42 +#time_series:hover .help {
  43 + display: block;
  44 +}
  45 +
  46 +#time_series svg .brush .selection {
  47 + fill: #efa02c;
  48 + fill-opacity: 0.382;
  49 +}
  50 +
  51 +.axis path, .axis line {
  52 + fill: none;
  53 + /*{# stroke: #f4f4f4;#}*/
  54 + shape-rendering: crispEdges;
  55 + stroke-width: 1px;
  56 +}
  57 +
  58 +path.line {
  59 + fill: none;
  60 + stroke: steelblue;
  61 + stroke-width: 1px;
  62 +}
  63 +
  64 +path.predictive-line {
  65 + fill: none;
  66 + stroke: #ff4081;
  67 + stroke-width: 2px;
  68 +}
  69 +
  70 +circle.cursor-circle {
  71 + fill: black;
  72 + stroke: rgba(20, 20, 20, 0.48);
  73 +}
  74 +
  75 +text.cursor-text {
  76 + /*{# font-family: 'Ubuntu', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif;#}*/
  77 + /*{# font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;#}*/
  78 + font-family: "Ubuntu Mono", 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
  79 + text-align: right;
  80 +}
  81 +
  82 +text.cursor-text-shadow {
  83 + stroke: white;
  84 + stroke-width: 5px;
  85 + opacity: 0.777
  86 +}
  87 +
  88 +path.orbit.orbit_section {
  89 + fill: none;
  90 + stroke: steelblue;
  91 + stroke-width: 1.5px;
  92 +}
  93 +
  94 +ellipse.orbit.orbit_ellipse {
  95 + fill: none;
  96 + stroke: #a3a3a3;
  97 + stroke-width: 1px;
  98 + stroke-dasharray: 5px;
  99 +}
  100 +
  101 +#form_time_interval {
  102 + padding-left: 30px;
  103 +}
  104 +
  105 +#form_time_interval .mdl-textfield {
  106 + padding-top: 0;
  107 +}
  108 +
  109 +#started_at, #stopped_at {
  110 + width: 85%;
  111 +}
  112 +
  113 +.section-drawer {
  114 + padding-left: 3em;
  115 +}
  116 +
  117 +.targets-filters {
  118 + padding-left: 17px;
  119 +}
  120 +
  121 +.targets-filters .target {
  122 + float: left;
  123 + cursor: pointer;
  124 + position: relative;
  125 +}
  126 +
  127 +.targets-filters .target:not(.active) {
  128 + -webkit-filter: grayscale(100%);
  129 + -moz-filter: grayscale(100%);
  130 + -o-filter: grayscale(100%);
  131 + /*{# -ms-filter: grayscale(100%);#}*/
  132 + filter: grayscale(100%);
  133 +}
  134 +
  135 +.targets-filters .target.locked {
  136 + cursor: not-allowed;
  137 +}
  138 +
  139 +.targets-filters .target.loading {
  140 + -webkit-animation-name: keyframes_rotate;
  141 + -webkit-animation-duration: 4000ms;
  142 + -webkit-animation-iteration-count: infinite;
  143 + -webkit-animation-timing-function: linear;
  144 + -moz-animation-name: keyframes_rotate;
  145 + -moz-animation-duration: 4000ms;
  146 + -moz-animation-iteration-count: infinite;
  147 + -moz-animation-timing-function: linear;
  148 + -ms-animation-name: keyframes_rotate;
  149 + -ms-animation-duration: 4000ms;
  150 + -ms-animation-iteration-count: infinite;
  151 + -ms-animation-timing-function: linear;
  152 + animation-name: keyframes_rotate;
  153 + animation-duration: 4000ms;
  154 + animation-iteration-count: infinite;
  155 + animation-timing-function: linear;
  156 +}
  157 +
  158 +.targets-filters .target .decorator {
  159 + position: absolute;
  160 + top: 0;
  161 + left: 0;
  162 + display: none;
  163 +}
  164 +
  165 +.targets-filters .target.empty .decorator.empty {
  166 + display: block;
  167 +}
  168 +
  169 +.targets-filters .target.error .decorator.error {
  170 + display: block;
  171 +}
  172 +
  173 +.targets-filters .target .decorator.loading {
  174 + top: 19px;
  175 + left: 19px;
  176 +}
  177 +
  178 +.targets-filters .target.loading .decorator.loading {
  179 + display: block;
  180 +}
  181 +
  182 +#parameters .parameter {
  183 + outline: 0;
  184 + padding-top: 7px;
  185 + padding-bottom: 7px;
  186 +
  187 +}
  188 +
  189 +#parameters .parameter.active {
  190 + background-color: #c8d3e1;
  191 +}
  192 +
  193 +.option-layer-campaign.loading {
  194 + background: #00acc1;
  195 +}
  196 +
  197 +
  198 +/* Preview Box */
  199 +
  200 +#time_series_cursor_morebox {
  201 + margin-top: 1em;
  202 + padding-left: 7%;
  203 + padding-right: 7%;
  204 +}
  205 +#time_series_cursor_image_link:focus {
  206 + outline: none;
  207 +}
  208 +#time_series_cursor_image {
  209 + width: 100%;
  210 +}
  211 +#time_series_cursor_link {
  212 + display: block;
  213 +}
  214 +#time_series_cursor_image_extra {
  215 + text-align: center;
  216 + padding-top: 1em;
  217 +}
  218 +#time_series_cursor_image_comment {
  219 + /*text-align: center;*/
  220 +}
  221 +#time_series_cursor_image_comment *[title]{
  222 + text-decoration: dotted lightskyblue;
  223 +}
  224 +
  225 +/*{# CSS Spinners #}*/
  226 +
  227 +#download_spinner {
  228 + display: none;
  229 + top: 2px;
  230 + left: 4px;
  231 + width: 14px;
  232 + height: 14px;
  233 +}
  234 +
  235 +#plots_loader {
  236 + position: fixed;
  237 + top: 0;
  238 + left: 0;
  239 + bottom: 0;
  240 + right: 0;
  241 + height: 100%;
  242 + width: 100%;
  243 + background-color: #fff;
  244 + z-index: 1000;
  245 +}
  246 +
  247 +#plots_loader .loader-text {
  248 + width: 200px;
  249 + height: 30px;
  250 + position: absolute;
  251 + top: -240px;
  252 + left: -32px;
  253 + bottom: 0;
  254 + right: 0;
  255 + margin: auto;
  256 + text-align: center;
  257 + font-size: 1.0em;
  258 + font-style: italic;
  259 + color: darkgrey;
  260 +}
  261 +
  262 +#plots_loader .img {
  263 + width: 100px;
  264 + height: 100px;
  265 + border-radius: 100%;
  266 + position: absolute;
  267 + border: 1px solid #6978ff;
  268 + animation: keyframes_rotate 1s;
  269 + animation-iteration-count: infinite;
  270 + transition: 2s;
  271 + border-bottom: none;
  272 + border-right: none;
  273 + animation-timing-function: linear;
  274 + margin-left: -70px;
  275 + margin-top: -70px;
  276 + left: 50%;
  277 + top: 50%;
  278 +}
  279 +
  280 +#plots_loader #plots_loader_img2 {
  281 + width: 90px;
  282 + height: 90px;
  283 + left: 50.35%;
  284 + top: 50.7%;
  285 + animation-delay: .2s;
  286 +}
  287 +
  288 +#plots_loader #plots_loader_img3 {
  289 + width: 80px;
  290 + height: 80px;
  291 + left: 50.70%;
  292 + top: 51.4%;
  293 + animation-delay: .4s;
  294 +}
  295 +
  296 +#plots_loader #plots_loader_img4 {
  297 + width: 70px;
  298 + height: 70px;
  299 + left: 51.05%;
  300 + top: 52.1%;
  301 + animation-delay: .6s;
  302 +}
  303 +
  304 +#plots_loader #plots_loader_img5 {
  305 + width: 60px;
  306 + height: 60px;
  307 + left: 51.40%;
  308 + top: 52.8%;
  309 + animation-delay: .8s;
  310 +}
  311 +
  312 +@keyframes keyframes_rotate {
  313 + from {
  314 + transform: rotate(0deg);
  315 + }
  316 + 50% {
  317 + transform: rotate(180deg);
  318 + }
  319 + 100% {
  320 + transform: rotate(360deg);
  321 + }
  322 +}
  323 +
  324 +.small-loader-container {
  325 + width: 27px;
  326 + margin: 0 auto;
  327 + background: none;
  328 + pointer-events: none;
  329 +}
  330 +
  331 +.small-loader-circle-1 {
  332 + height: 27px;
  333 + width: 27px;
  334 + background: rgba(255, 238, 195, 0.72);
  335 +}
  336 +
  337 +.small-loader-circle-2 {
  338 + height: 22px;
  339 + width: 22px;
  340 + background: none;
  341 +}
  342 +
  343 +.small-loader-circle-3 {
  344 + height: 18px;
  345 + width: 18px;
  346 + background: rgba(29, 65, 255, 0.9);
  347 +}
  348 +
  349 +.small-loader-circle-4 {
  350 + height: 13px;
  351 + width: 13px;
  352 + background: none;
  353 +}
  354 +
  355 +.small-loader-circle-5 {
  356 + height: 9px;
  357 + width: 9px;
  358 + background: rgba(238, 238, 238, 0.8);
  359 +}
  360 +
  361 +.small-loader-circle-6 {
  362 + height: 4px;
  363 + width: 4px;
  364 + background: none;
  365 +}
  366 +
  367 +.small-loader-circle-7 {
  368 + height: 2px;
  369 + width: 2px;
  370 + background: rgb(110, 102, 255);
  371 +}
  372 +
  373 +.small-loader-circle-1,
  374 +.small-loader-circle-2,
  375 +.small-loader-circle-3,
  376 +.small-loader-circle-4,
  377 +.small-loader-circle-5,
  378 +.small-loader-circle-6,
  379 +.small-loader-circle-7 {
  380 + border-bottom: none;
  381 + border-radius: 50%;
  382 + -o-border-radius: 50%;
  383 + -ms-border-radius: 50%;
  384 + -webkit-border-radius: 50%;
  385 + -moz-border-radius: 50%;
  386 + box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
  387 + -o-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
  388 + -ms-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
  389 + -webkit-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
  390 + -moz-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.1);
  391 + animation-name: small-loader-spin;
  392 + -o-animation-name: small-loader-spin;
  393 + -ms-animation-name: small-loader-spin;
  394 + -webkit-animation-name: small-loader-spin;
  395 + -moz-animation-name: small-loader-spin;
  396 + animation-duration: 4600ms;
  397 + -o-animation-duration: 4600ms;
  398 + -ms-animation-duration: 4600ms;
  399 + -webkit-animation-duration: 4600ms;
  400 + -moz-animation-duration: 4600ms;
  401 + animation-iteration-count: infinite;
  402 + -o-animation-iteration-count: infinite;
  403 + -ms-animation-iteration-count: infinite;
  404 + -webkit-animation-iteration-count: infinite;
  405 + -moz-animation-iteration-count: infinite;
  406 + animation-timing-function: linear;
  407 + -o-animation-timing-function: linear;
  408 + -ms-animation-timing-function: linear;
  409 + -webkit-animation-timing-function: linear;
  410 + -moz-animation-timing-function: linear;
  411 +}
  412 +
  413 +@keyframes small-loader-spin {
  414 + from {
  415 + transform: rotate(0deg);
  416 + }
  417 + to {
  418 + transform: rotate(360deg);
  419 + }
  420 +}
  421 +
  422 +@-o-keyframes small-loader-spin {
  423 + from {
  424 + -o-transform: rotate(0deg);
  425 + }
  426 + to {
  427 + -o-transform: rotate(360deg);
  428 + }
  429 +}
  430 +
  431 +@-ms-keyframes small-loader-spin {
  432 + from {
  433 + -ms-transform: rotate(0deg);
  434 + }
  435 + to {
  436 + -ms-transform: rotate(360deg);
  437 + }
  438 +}
  439 +
  440 +@-webkit-keyframes small-loader-spin {
  441 + from {
  442 + -webkit-transform: rotate(0deg);
  443 + }
  444 + to {
  445 + -webkit-transform: rotate(360deg);
  446 + }
  447 +}
  448 +
  449 +@-moz-keyframes small-loader-spin {
  450 + from {
  451 + -moz-transform: rotate(0deg);
  452 + }
  453 + to {
  454 + -moz-transform: rotate(360deg);
  455 + }
  456 +}
... ...
web/static/js/main.js
... ... @@ -83,6 +83,13 @@
83 83 this.configuration['parameters'].forEach(p => that.parameters[p['id']] = p);
84 84 this.orbits = null;
85 85 this.time_series = [];
  86 +
  87 + // Holds the downloaded data for each layer hash
  88 + // layer hash => data (array of dict)
  89 + this._image_preview_layers_data = {};
  90 + // Holds the activation status for each layer hash
  91 + // layer hash => activation status (bool)
  92 + this._image_preview_layers_live = {};
86 93 }
87 94  
88 95 /**
... ... @@ -282,7 +289,7 @@
282 289 data = {
283 290 'hee': []
284 291 };
285   - configuration['parameters'].forEach(parameter => data[parameter['id']] = []);
  292 + app.configuration['parameters'].forEach(parameter => data[parameter['id']] = []);
286 293 if (!csv) {
287 294 reject('invalid');
288 295 }
... ... @@ -292,7 +299,7 @@
292 299 csv.forEach(d => {
293 300 let dtime;
294 301 dtime = timeFormat(d['time']);
295   - configuration['parameters'].forEach(parameter => {
  302 + app.configuration['parameters'].forEach(parameter => {
296 303 let id;
297 304 let val;
298 305 id = parameter['id'];
... ... @@ -424,12 +431,15 @@
424 431 app.time_series.forEach(ts2 => ts2.hideCursor());
425 432 return true;
426 433 };
427   - ts.options['onMouseMove'] = t => {
428   - let ref$;
429   - app.time_series.forEach(ts2 => ts2.moveCursor(t));
430   - if ((ref$ = app.orbits) != null) {
431   - ref$.moveToDate(t);
  434 + ts.options['onMouseMove'] = time => {
  435 + app.time_series.forEach(ts2 =>
  436 + ts2.moveCursor(time)
  437 + );
  438 + if (app.orbits != null) {
  439 + app.orbits.moveToDate(time);
432 440 }
  441 + ts.updateImagePreviewFromCursor(time);
  442 +
433 443 return true;
434 444 };
435 445  
... ... @@ -508,6 +518,123 @@
508 518 return this;
509 519 }
510 520  
  521 + ///////////////////////////////////////////////////////////////////////
  522 +
  523 + hashImagePreviewLayer(target_slug, layer_slug) {
  524 + return target_slug + "_" + layer_slug;
  525 + }
  526 +
  527 + getImagePreviewData(target_slug, layer_slug) {
  528 + const app = this; // not taking any risks with `this` binding.
  529 + return new Promise((resolve, reject) => {
  530 + const h = app.hashImagePreviewLayer(target_slug, layer_slug);
  531 + if (h in this._image_preview_layers_data) {
  532 + resolve(app._image_preview_layers_data[h]);
  533 + } else {
  534 + let url_route = "saturn_auroral_catalog.csv"; // FIXME
  535 + let url = app.configuration['api']['root'] + url_route;
  536 + //url += "/" + url_route; // Extra / yields havoc -- flask?
  537 + let d3csv = d3.csv(url, csv => {
  538 + console.info("Fetched image preview CSV data.", url, csv);
  539 + console.assert(app === this); // peace of mind
  540 + // Now's a good time to do some pre-process on the data
  541 + let data = csv.map(d => {
  542 + return {
  543 + time_min: moment(d.time_min),
  544 + time_max: moment(d.time_max),
  545 + thumbnail_url: d.thumbnail_url.trim(),
  546 + external_link: d.external_link.trim()
  547 + };
  548 + });
  549 + app._image_preview_layers_data[h] = data;
  550 + // โ€ฆ also a good place to build an index for example,
  551 + // since right now we're bisecting on each cursor move,
  552 + // and that's expensive. (Binary Search)
  553 + resolve(data);
  554 + });
  555 + }
  556 + });
  557 + }
  558 +
  559 + isImagePreviewLayerShown(target_slug, layer_slug) {
  560 + const h = this.hashImagePreviewLayer(target_slug, layer_slug);
  561 + if (h in this._image_preview_layers_live) {
  562 + return this._image_preview_layers_live[h];
  563 + } else {
  564 + return false;
  565 + }
  566 + }
  567 +
  568 + showImagePreviewLayer(target_slug, layer_slug) {
  569 + const app = this;
  570 +
  571 + return new Promise((resolve, reject) => {
  572 + if (this.isImagePreviewLayerShown(target_slug, layer_slug)) {
  573 + resolve(); // already shown
  574 + }
  575 + const h = this.hashImagePreviewLayer(target_slug, layer_slug);
  576 + //console.log("showImagePreviewLayer", h);
  577 + //console.log("_image_preview_layers_data", this._image_preview_layers_data);
  578 +
  579 + this._image_preview_layers_live[h] = true;
  580 +
  581 + const data_promise = this.getImagePreviewData(target_slug, layer_slug);
  582 + data_promise.then((data) => {
  583 + //console.debug("getImagePreviewData OK !", data);
  584 + let ts_left_to_process = this.time_series.length;
  585 + this.time_series.forEach(ts => {
  586 + if (ts.target.slug === target_slug) {
  587 + ts.showImagePreviewLayer(layer_slug, data).then(
  588 + () => {
  589 + //ts_left_to_process--;
  590 + //console.debug("showImagePreviewLayer promise ok!");
  591 + },
  592 + (err) => {
  593 + //ts_left_to_process--;
  594 + //console.error("showImagePreviewLayer promise failed.", err);
  595 + },
  596 + ).finally(() => {
  597 + ts_left_to_process--;
  598 + //console.debug("showImagePreviewLayer left", ts_left_to_process);
  599 + if (0 === ts_left_to_process) {
  600 + //console.debug("showImagePreviewLayer resolve");
  601 + resolve();
  602 + }
  603 + });
  604 + } else {
  605 + ts_left_to_process--;
  606 + }
  607 + });
  608 + }, (err) => {
  609 + console.error("getImagePreviewData promise failed.", err);
  610 + reject(err);
  611 + });
  612 +
  613 + });
  614 + }
  615 +
  616 + hideImagePreviewLayer(target_slug, layer_slug) {
  617 + // perhaps make this promise-based as well?
  618 +
  619 + if ( ! this.isImagePreviewLayerShown(target_slug, layer_slug)) {
  620 + return; // already hidden
  621 + }
  622 +
  623 + const h = this.hashImagePreviewLayer(target_slug, layer_slug);
  624 + //console.log("hideImagePreviewLayer", h);
  625 +
  626 + this._image_preview_layers_live[h] = false;
  627 +
  628 + let ts;
  629 + for (let key in this.time_series) {
  630 + if ( ! this.time_series.hasOwnProperty(key)) continue;
  631 + ts = this.time_series[key];
  632 + if (ts.target.slug !== target_slug) continue;
  633 + ts.hideImagePreviewLayer(layer_slug);
  634 + }
  635 +
  636 + }
  637 +
511 638 getDomain() {
512 639 if (this.current_started_at != null && this.current_stopped_at != null) {
513 640 return [this.current_started_at, this.current_stopped_at];
... ... @@ -619,6 +746,20 @@
619 746 }
620 747 }
621 748 this.predictiveData = predictiveData;
  749 +
  750 + this._image_preview_layers_live = {}; // layer_hash => is active? (bool)
  751 + this._image_preview_layers_rect = {}; // layer_hash => rects (array)
  752 + this._image_preview_layers_data = {}; // layer_hash => data (array) (/!. not a copy)
  753 + // datum example:
  754 + // {
  755 + // time_min: moment("2017-09-09 00:13:49.845227"),
  756 + // thumbnail_url: "http://voparis-srv.obspm.fr/vo/planeto/apis/dataset/Bastet/Saturn_-_2017_14_Feb-09_Sept/od9u21u4q_proc_small.jpg",
  757 + // time_max: moment("2017-09-09 00:58:50.044784"),
  758 + // external_link: "http://apis.obspm.fr/spip.php?page=observation&id=bas_8167"
  759 + // }
  760 + // Note that time_min and time_max are pre-processed
  761 + // to moment.js instances already during creation.
  762 +
622 763 this.init();
623 764 }
624 765  
... ... @@ -778,6 +919,7 @@
778 919 this.yAxisText.attr("y", 20 - this.margin.left).attr("x", 0 - height / 2);
779 920 this.yAxisTextTarget.attr("y", 0 - this.margin.left).attr("x", 0 - height / 2.0);
780 921 this.resizeCatalogLayers();
  922 + this.resizeImagePreviewLayers();
781 923 if (!this.visible) {
782 924 this.hide();
783 925 }
... ... @@ -891,6 +1033,7 @@
891 1033 this.predictiveDataPath.attr('d', this.line);
892 1034 }
893 1035 this.resizeCatalogLayers();
  1036 + this.resizeImagePreviewLayers();
894 1037 this.hideCursor();
895 1038 return new Promise((resolve, reject) => {
896 1039 if (0 === duration) {
... ... @@ -901,6 +1044,19 @@
901 1044 });
902 1045 }
903 1046  
  1047 + createLayerRect(started_at, stopped_at, color) {
  1048 + // We're ignoring the parameters, since we're
  1049 + // doing the resize logic in resizeXXX()
  1050 + let layer_rect; // positioning is done in resize()
  1051 + layer_rect = this.pathWrapper.append("rect")
  1052 + .attr('y', 0)
  1053 + .attr('height', this.plotHeight) // => move to resize too?
  1054 + .attr('fill', color);
  1055 + //won't work, possibly because of our input catcher rect.
  1056 + //layer_rect.append('svg:title').text("!");
  1057 + return layer_rect;
  1058 + }
  1059 +
904 1060 createCatalogLayers() {
905 1061 let catalog_slug;
906 1062 let ref$;
... ... @@ -912,35 +1068,32 @@
912 1068 let stopped_at;
913 1069 this.layers_rects = {};
914 1070 for (catalog_slug in ref$ = this.target.config.layers) {
  1071 + if ( ! (ref$.hasOwnProperty(catalog_slug))) continue; // oh, js
915 1072 layers = ref$[catalog_slug];
916 1073 this.layers_rects[catalog_slug] = [];
917 1074 for (i$ = 0, len$ = layers.length; i$ < len$; ++i$) {
918 1075 layer = layers[i$];
919 1076 started_at = moment(layer.start);
920 1077 stopped_at = moment(layer.stop);
921   - this.layers_rects[catalog_slug].push(this.createCatalogLayer(started_at, stopped_at));
  1078 + this.layers_rects[catalog_slug].push(this.createCatalogLayerRect(started_at, stopped_at));
922 1079 }
923 1080 this.hideCatalogLayer(catalog_slug);
924 1081 }
925 1082 return this;
926 1083 }
927 1084  
928   - createCatalogLayer(started_at, stopped_at) {
929   - let layer_rect;
930   - layer_rect = this.pathWrapper.append("rect")
931   - .attr('y', 0)
932   - .attr('height', this.plotHeight)
933   - .attr('fill', '#FFFD64C2');
934   - //won't work, possibly because of our input catcher rect.
935   - //layer_rect.append('svg:title').text("!");
936   - return layer_rect;
  1085 + createCatalogLayerRect(started_at, stopped_at) {
  1086 + return this.createLayerRect(started_at, stopped_at, '#FFFD64C2');
937 1087 }
938 1088  
939 1089 resizeCatalogLayers() {
  1090 + let animate = true; // move to param if needed
  1091 + // Perhaps make that transition part of the prototype instead?
  1092 + let t = this.svg.transition().duration(750);
  1093 +
940 1094 let catalog_slug;
941 1095 let ref$;
942 1096 let layers;
943   - let i$;
944 1097 let len$;
945 1098 let i;
946 1099 let layer;
... ... @@ -948,47 +1101,352 @@
948 1101 let stopped_at;
949 1102 let width;
950 1103 for (catalog_slug in ref$ = this.target.config.layers) {
  1104 + if ( ! (ref$.hasOwnProperty(catalog_slug))) continue;
951 1105 layers = ref$[catalog_slug];
952   - for (i$ = 0, len$ = layers.length; i$ < len$; ++i$) {
953   - i = i$;
954   - layer = layers[i$];
  1106 + for (i = 0, len$ = layers.length; i < len$; ++i) {
  1107 + layer = layers[i];
955 1108 started_at = moment(layer.start);
956 1109 stopped_at = moment(layer.stop);
957 1110 width = Math.max(2, this.xScale(stopped_at) - this.xScale(started_at));
958   - this.layers_rects[catalog_slug][i].attr('x', this.xScale(started_at)).attr('width', width);
  1111 +
  1112 + if (animate) {
  1113 + this.layers_rects[catalog_slug][i]
  1114 + .transition(t)
  1115 + .attr('x', this.xScale(started_at))
  1116 + .attr('width', width);
  1117 + } else {
  1118 + this.layers_rects[catalog_slug][i]
  1119 + .attr('x', this.xScale(started_at))
  1120 + .attr('width', width);
  1121 + }
  1122 +
959 1123 }
960 1124 }
961 1125 return this;
962 1126 }
963 1127  
964 1128 showCatalogLayer(catalog_slug) {
965   - let i$;
966 1129 let ref$;
967 1130 let len$;
968 1131 let i;
969 1132 let layer;
970   - for (i$ = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i$ < len$; ++i$) {
971   - i = i$;
972   - layer = ref$[i$];
  1133 + for (i = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i < len$; ++i) {
  1134 + layer = ref$[i];
973 1135 this.layers_rects[catalog_slug][i].style("display", null);
974 1136 }
975 1137 return this;
976 1138 }
977 1139  
978 1140 hideCatalogLayer(catalog_slug) {
979   - let i$;
980 1141 let ref$;
981 1142 let len$;
982 1143 let i;
983   - let layer;
984   - for (i$ = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i$ < len$; ++i$) {
985   - i = i$;
986   - layer = ref$[i$];
  1144 + //let layer;
  1145 + for (i = 0, len$ = (ref$ = this.target.config.layers[catalog_slug]).length; i < len$; ++i) {
  1146 + //layer = ref$[i];
987 1147 this.layers_rects[catalog_slug][i].style("display", "none");
988 1148 }
989 1149 return this;
990 1150 }
991 1151  
  1152 + ///////////////////////////////////////////////////////////////////////
  1153 +
  1154 + hasImagePreviewLayer(layer_slug) {
  1155 + return layer_slug in this._image_preview_layers_data;
  1156 + }
  1157 +
  1158 + // createImagePreviewLayers(layer_slug, data) {
  1159 + //
  1160 + // }
  1161 +
  1162 + createImagePreviewLayer(layer_slug, data) {
  1163 + this._image_preview_layers_data[layer_slug] = data;
  1164 + this._image_preview_layers_rect[layer_slug] = [];
  1165 + this._image_preview_layers_live[layer_slug] = true;
  1166 + let datum;
  1167 + for (let key in data) {
  1168 + if (data.hasOwnProperty(key)) {
  1169 + if ("columns" === key) continue;
  1170 + // console.debug("datum", key, data[key]);
  1171 + datum = data[key];
  1172 + // { 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" }
  1173 + this._image_preview_layers_rect[layer_slug].push(
  1174 + this.createImagePreviewLayerRect(
  1175 + datum.time_min, datum.time_max
  1176 + )
  1177 + );
  1178 + }
  1179 + }
  1180 + }
  1181 +
  1182 + isImagePreviewLayerLive(layer_slug) {
  1183 + if (layer_slug in this._image_preview_layers_live) {
  1184 + return this._image_preview_layers_live[layer_slug];
  1185 + } else {
  1186 + return false;
  1187 + }
  1188 + }
  1189 +
  1190 + resizeImagePreviewLayers() {
  1191 + for (let layer_slug in this._image_preview_layers_live) {
  1192 + if (!(this._image_preview_layers_live.hasOwnProperty(layer_slug)))
  1193 + continue;
  1194 + console.debug("Resize image layer", layer_slug);
  1195 + this.resizeImagePreviewLayer(layer_slug);
  1196 + }
  1197 + }
  1198 +
  1199 + resizeImagePreviewLayer(layer_slug) {
  1200 + if (! this.isImagePreviewLayerLive(layer_slug)) {
  1201 + return;
  1202 + }
  1203 +
  1204 + const rects = this._image_preview_layers_rect[layer_slug];
  1205 + if (! rects) {
  1206 + console.error(
  1207 + "Tried to resize an image preview layer without rects.",
  1208 + layer_slug, this
  1209 + );
  1210 + return;
  1211 + }
  1212 +
  1213 + const data = this._image_preview_layers_data[layer_slug];
  1214 + if (! data) {
  1215 + console.error(
  1216 + "Tried to resize an image preview layer without data.",
  1217 + layer_slug, this
  1218 + );
  1219 + return;
  1220 + }
  1221 +
  1222 + let animate = true;
  1223 + let t = this.svg.transition().duration(750);
  1224 +
  1225 + let width;
  1226 + let started_at;
  1227 + let stopped_at;
  1228 + //for (let [dkey, datum] of data) { // nope
  1229 + let datum;
  1230 + for (let dkey in data) {
  1231 + if ( ! data.hasOwnProperty(dkey)) continue;
  1232 + if ("columns" === dkey) continue;
  1233 +
  1234 + datum = data[dkey];
  1235 +
  1236 + // We assume that moment(moment(x)) == moment(x) (it's true)
  1237 + // Remove these moment() casts for some more perfs since we're
  1238 + // already casting to moment() in the data pre-process.
  1239 + started_at = moment(datum.time_min);
  1240 + stopped_at = moment(datum.time_max);
  1241 +
  1242 + //console.debug("Resize image layer rect", started_at, stopped_at);
  1243 +
  1244 + width = this.xScale(stopped_at) - this.xScale(started_at);
  1245 + // Under 2 pixels wide the rects appear glitchy
  1246 + width = Math.max(2, width);
  1247 +
  1248 + if (animate) {
  1249 + rects[dkey]
  1250 + .transition(t)
  1251 + .attr('x', this.xScale(started_at))
  1252 + .attr('width', width);
  1253 + } else {
  1254 + rects[dkey]
  1255 + .attr('x', this.xScale(started_at))
  1256 + .attr('width', width);
  1257 + }
  1258 +
  1259 + }
  1260 + }
  1261 +
  1262 + static get COLOR_IMAGE_PREVIEW_LAYER() {
  1263 + return '#FF339942';
  1264 + }
  1265 +
  1266 + static get COLOR_IMAGE_PREVIEW_LAYER_ACTIVE() {
  1267 + return '#FF3399D2';
  1268 + }
  1269 +
  1270 + createImagePreviewLayerRect(started_at, stopped_at) {
  1271 + return this.createLayerRect(
  1272 + started_at, stopped_at, TimeSeries.COLOR_IMAGE_PREVIEW_LAYER
  1273 + );
  1274 + }
  1275 +
  1276 + highlightImagePreviewLayerRect(layer_slug, rect_key) {
  1277 + const rects = this._image_preview_layers_rect[layer_slug];
  1278 + if (! rects) {
  1279 + console.error(
  1280 + "Tried to highlight an image preview layer without rects.",
  1281 + layer_slug, this
  1282 + );
  1283 + return;
  1284 + }
  1285 +
  1286 + const rect_to_highlight = rects[rect_key];
  1287 + if (! rect_to_highlight) {
  1288 + console.error(
  1289 + "Tried to highlight a non-existent rect.",
  1290 + layer_slug, rect_key, this
  1291 + );
  1292 + return;
  1293 + }
  1294 +
  1295 + for (let rect of rects) {
  1296 + TimeSeries.setRectColor(
  1297 + rect, TimeSeries.COLOR_IMAGE_PREVIEW_LAYER
  1298 + );
  1299 + }
  1300 + TimeSeries.setRectColor(
  1301 + rect_to_highlight, TimeSeries.COLOR_IMAGE_PREVIEW_LAYER_ACTIVE
  1302 + );
  1303 +
  1304 + }
  1305 +
  1306 + static setRectColor(rect, color) {
  1307 + rect.attr('fill', color);
  1308 + }
  1309 +
  1310 + showImagePreviewLayer(layer_slug, data) {
  1311 + return new Promise((resolve, reject) => {
  1312 + //console.log("showImagePreviewLayer Promiseโ€ฆ");
  1313 +
  1314 + if (this._image_preview_layers_live[layer_slug]) {
  1315 + console.warn("TS.showImagePreviewLayer: already shown.");
  1316 + resolve(); // we're already shown
  1317 + }
  1318 +
  1319 + if ( ! this.hasImagePreviewLayer(layer_slug)) {
  1320 + this.createImagePreviewLayer(layer_slug, data);
  1321 + }
  1322 +
  1323 + this._image_preview_layers_live[layer_slug] = true;
  1324 +
  1325 + this.resizeImagePreviewLayer(layer_slug);
  1326 +
  1327 + const rects = this._image_preview_layers_rect[layer_slug];
  1328 + if (! rects) {
  1329 + console.error(
  1330 + "Tried to show an image preview layer without rects.",
  1331 + layer_slug, this
  1332 + );
  1333 + return;
  1334 + }
  1335 +
  1336 + for (let rect of rects) {
  1337 + rect.style("display", null)
  1338 + }
  1339 +
  1340 + resolve();
  1341 + });
  1342 + }
  1343 +
  1344 + hideImagePreviewLayer(layer_slug) {
  1345 + // console.log(
  1346 + // "TS.hideImagePreviewLayer()",
  1347 + // this._image_preview_layers_live[layer_slug],
  1348 + // this._image_preview_layers_live
  1349 + // );
  1350 + if ( ! this._image_preview_layers_live[layer_slug]) {
  1351 + console.warn("TS.hideImagePreviewLayer: already hidden.");
  1352 + return; // we're already hidden
  1353 + }
  1354 + this._image_preview_layers_live[layer_slug] = false;
  1355 +
  1356 + const rects = this._image_preview_layers_rect[layer_slug];
  1357 + //console.debug("RECTS", rects);
  1358 +
  1359 + if (! rects) {
  1360 + console.error(
  1361 + "Tried to hide an image preview layer without rects.",
  1362 + layer_slug, this
  1363 + );
  1364 + return;
  1365 + }
  1366 +
  1367 +
  1368 + for (let rect of rects) {
  1369 + rect.style("display", "none");
  1370 + }
  1371 + }
  1372 +
  1373 + showImagePreview(image_url, external_link, comment) {
  1374 + const box = jQuery('#time_series_cursor_morebox');
  1375 + const img = jQuery('#time_series_cursor_image');
  1376 + const cil = jQuery('#time_series_cursor_image_link');
  1377 + const cic = jQuery('#time_series_cursor_image_comment');
  1378 + const lnk = jQuery('#time_series_cursor_link');
  1379 +
  1380 + const previous_image_url = img.attr('src');
  1381 + if (image_url === previous_image_url) {
  1382 + return;
  1383 + }
  1384 +
  1385 + img.attr('src', image_url);
  1386 + lnk.attr('href', external_link);
  1387 + cic.html(comment);
  1388 +
  1389 + // Interesting ; using the regex in the prototype yields โ€ฆ fails
  1390 + // Race conditions are probably reunited, afaik.
  1391 + //const match = this.imageRegex.exec(image_url);
  1392 + // Let's compile our regex every time instead. Optimize later.
  1393 + const imageRegex = /(.+?)_small([.][a-zA-Z0-9]+)$/g;
  1394 + const match = imageRegex.exec(image_url);
  1395 + let image_link = "#";
  1396 + if (match) {
  1397 + //console.log("Matched image url!", match, image_url);
  1398 + image_link = match[1] + match[2]; // remove the `_small`
  1399 + } else {
  1400 + console.warn("Could not find bigger image for url:", image_url);
  1401 + }
  1402 + cil.attr('href', image_link);
  1403 +
  1404 + box.removeClass('hidden');
  1405 + }
  1406 +
  1407 + // hideImagePreview() {} // TODO
  1408 +
  1409 + updateImagePreviewFromCursor(x0) {
  1410 + let data = this._image_preview_layers_data;
  1411 + let i;
  1412 + let layer_slug;
  1413 + for (layer_slug in data) {
  1414 + if ( ! data.hasOwnProperty(layer_slug))
  1415 + continue;
  1416 + if ( ! this.isImagePreviewLayerLive(layer_slug)) {
  1417 + continue;
  1418 + }
  1419 + i = this.bisectIpod(data[layer_slug], x0, 1);
  1420 + //console.debug("Found i ", i, x0, data[layer_slug]);
  1421 + if (i == null) continue; // ignore errors and data out of range
  1422 + break;
  1423 + }
  1424 + if (i == null) {
  1425 + //console.debug("No live image preview found.");
  1426 + return;
  1427 + }
  1428 +
  1429 + let d0 = data[layer_slug][i - 1];
  1430 + let d1 = data[layer_slug][i];
  1431 + if (!d1 || !d0) {
  1432 + //this.hideImagePreview();
  1433 + return;
  1434 + }
  1435 + let d = x0 - d0.time_min > d1.time_min - x0 ? d1 : d0;
  1436 +
  1437 + //console.info("Cursor", x0, d0);
  1438 +
  1439 + this.highlightImagePreviewLayerRect(
  1440 + layer_slug, d === d0 ? i-1 : i
  1441 + );
  1442 + this.showImagePreview(
  1443 + d.thumbnail_url, d.external_link,
  1444 + "Integration: "+d.time_min.from(d.time_max, true)
  1445 + );
  1446 + }
  1447 +
  1448 + ///////////////////////////////////////////////////////////////////////
  1449 +
992 1450 showCursor() {
993 1451 return this.focus.style("display", null);
994 1452 }
... ... @@ -998,40 +1456,53 @@
998 1456 }
999 1457  
1000 1458 moveCursor(x0) {
1001   - let i;
1002   - let d0;
1003   - let d1;
1004   - let d;
1005   - let xx;
1006   - let yy;
1007   - let dx;
1008   - let transform;
1009   - i = this.bisectDate(this.data, x0, 1);
1010   - d0 = this.data[i - 1];
1011   - d1 = this.data[i];
  1459 + let i = this.bisectDate(this.data, x0, 1);
  1460 + let d0 = this.data[i - 1];
  1461 + let d1 = this.data[i];
1012 1462 if (!d1 || !d0) {
1013 1463 this.hideCursor();
1014 1464 return;
1015 1465 }
1016   - d = x0 - d0.x > d1.x - x0 ? d1 : d0;
1017   - xx = this.xScale(d.x);
1018   - yy = this.yScale(d.y);
  1466 + let d = x0 - d0.x > d1.x - x0 ? d1 : d0;
  1467 + let xx = this.xScale(d.x);
  1468 + let yy = this.yScale(d.y);
1019 1469 const mirrored = this.plotWidth != null && xx > this.plotWidth / 2;
1020   - dx = 8;
  1470 + let dx = 8;
1021 1471 if (mirrored) {
1022 1472 dx = -1 * dx;
1023 1473 }
1024   - transform = `translate(${xx}, ${yy})`;
1025   - this.cursorCircle.attr("transform", transform);
1026   - this.cursorValue.attr("transform", transform).text(d.y).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx);
1027   - this.cursorValueShadow.attr("transform", transform).text(d.y).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx);
1028   - this.cursorDate.attr("transform", transform).text(this.timeFormat(d.x)).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx);
1029   - this.cursorDateShadow.attr("transform", transform).text(this.timeFormat(d.x)).attr('text-anchor', mirrored ? 'end' : 'start').attr("dx", dx);
  1474 + const transform = `translate(${xx}, ${yy})`;
  1475 + this.cursorCircle
  1476 + .attr("transform", transform);
  1477 + this.cursorValue
  1478 + .text(d.y)
  1479 + .attr("transform", transform)
  1480 + .attr('text-anchor', mirrored ? 'end' : 'start')
  1481 + .attr("dx", dx);
  1482 + this.cursorValueShadow
  1483 + .text(d.y)
  1484 + .attr("transform", transform)
  1485 + .attr('text-anchor', mirrored ? 'end' : 'start')
  1486 + .attr("dx", dx);
  1487 + this.cursorDate
  1488 + .text(this.timeFormat(d.x))
  1489 + .attr("transform", transform)
  1490 + .attr('text-anchor', mirrored ? 'end' : 'start')
  1491 + .attr("dx", dx);
  1492 + this.cursorDateShadow
  1493 + .text(this.timeFormat(d.x))
  1494 + .attr("transform", transform)
  1495 + .attr('text-anchor', mirrored ? 'end' : 'start')
  1496 + .attr("dx", dx);
1030 1497 this.showCursor();
  1498 +
1031 1499 return this;
1032 1500 }
1033 1501 }
1034 1502  
  1503 + // Don't use the prototype for a regex ; there are race conditions
  1504 + //TimeSeries.prototype.imageRegex = /(.+?)_small([.][a-zA-Z0-9]+)$/g;
  1505 + TimeSeries.prototype.bisectIpod = d3.bisector(d => d.time_min).left;
1035 1506 TimeSeries.prototype.bisectDate = d3.bisector(d => d.x).left;
1036 1507 TimeSeries.prototype.timeFormat = d3.utcFormat("%Y-%m-%d %H:%M");
1037 1508  
... ...
web/static/js/swapp.ls
... ... @@ -12,7 +12,7 @@
12 12  
13 13  
14 14  
15   -# DEPRECATED
  15 +# DEPRECATED and NOT USED ANYMORE
16 16 # Use web/static/js/main.js instead.
17 17  
18 18  
... ...
web/view/home.html.jinja2
... ... @@ -178,7 +178,7 @@
178 178 <section class="section-drawer">
179 179 {% for layer in config.layers.campaigns %}
180 180 <label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="option-layer-{{ layer.slug }}" title="{{ layer.desc }}">
181   - <input type="checkbox" id="option-layer-{{ layer.slug }}" class="mdl-radio__button option-layer" name="option-layer-{{ layer.slug }}" value="{{ layer.slug }}" {{ 'disabled' if not layer.data }}>
  181 + <input type="checkbox" id="option-layer-{{ layer.slug }}" class="mdl-radio__button option-layer option-layer-campaign" name="option-layer-{{ layer.slug }}" value="{{ layer.slug }}" {{ 'disabled' if not layer.live }}>
182 182 <span class="mdl-radio__label">{{ layer.name }}</span>
183 183 </label>
184 184 <br />
... ... @@ -197,7 +197,7 @@
197 197 <section class="section-drawer">
198 198 {% for layer in config.layers.catalogs %}
199 199 <label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="option-layer-{{ layer.slug }}" title="{{ layer.desc }}">
200   - <input type="checkbox" id="option-layer-{{ layer.slug }}" class="mdl-radio__button option-layer" name="option-layer-{{ layer.slug }}" value="{{ layer.slug }}" {{ 'disabled' if not layer.data }}>
  200 + <input type="checkbox" id="option-layer-{{ layer.slug }}" class="mdl-radio__button option-layer option-layer-catalog" name="option-layer-{{ layer.slug }}" value="{{ layer.slug }}" {{ 'disabled' if not layer.data }}>
201 201 <span class="mdl-radio__label">{{ layer.name }}</span>
202 202 </label>
203 203 <br />
... ... @@ -250,7 +250,32 @@
250 250 Alert
251 251 </button>
252 252 {% endif %}
  253 +
253 254 </div>
  255 + <div>
  256 + <div id="time_series_cursor_morebox" class="hidden">
  257 + <a
  258 + id="time_series_cursor_image_link"
  259 + target="_blank"
  260 + href="#">
  261 + <img
  262 + id="time_series_cursor_image"
  263 + {# src=""#}
  264 + src="http://voparis-srv.obspm.fr/vo/planeto/apis/dataset/Bastet/Saturn_-_2017_14_Feb-09_Sept/od9u09rgq_proc_small.jpg"
  265 + alt="Image Capture of Aurorae"
  266 + >
  267 + </a>
  268 + <div id="time_series_cursor_image_extra">
  269 + <a id="time_series_cursor_link" target="_blank">
  270 + View details on APIS.
  271 + </a>
  272 + <p id="time_series_cursor_image_comment">
  273 + Nothing to comment.
  274 + </p>
  275 + </div>
  276 + </div>
  277 + </div>
  278 +
254 279 </div>
255 280 <div class="mdl-cell mdl-cell--8-col mdl-cell--8-col-tablet mdl-cell--4-col-phone">
256 281 <section id="time_series">
... ... @@ -267,391 +292,9 @@
267 292 {#### CSS ####################################################################}
268 293  
269 294 {% block styles %}
270   - <style>
271   - .mdl-layout__drawer hr {
272   - margin: 0.5em 0;
273   - }
274   - .mdl-layout__drawer .mdl-layout-title {
275   - line-height: 42px;
276   - display: inline-block;
277   - }
278   - .mdl-layout__drawer > details > summary {
279   - padding-left: 15px;
280   - cursor: pointer;
281   - outline: none;
282   - }
283   - .mdl-layout__drawer .mdl-layout-title:first-of-type {
284   - line-height: 60px;
285   - }
286   -
287   - .plots-buttons {
288   - text-align: center;
289   - margin: 0 auto;
290   - }
291   - .plots-buttons button {
292   - margin: 1em 1em;
293   - }
294   -
295   - #time_series svg {
296   - cursor: crosshair;
297   - }
298   - #time_series .help {
299   - position: absolute;
300   - text-align: center;
301   - font-size: 0.9em;
302   - font-style: italic;
303   - color: darkgrey;
304   - display: none;
305   - }
306   - #time_series:hover .help {
307   - display: block;
308   - }
309   - #time_series svg .brush .selection {
310   - fill: #efa02c;
311   - fill-opacity: 0.382;
312   - }
313   - .axis path, .axis line {
314   - fill: none;
315   -{# stroke: #f4f4f4;#}
316   - shape-rendering: crispEdges;
317   - stroke-width: 1px;
318   - }
319   - path.line {
320   - fill: none;
321   - stroke: steelblue;
322   - stroke-width: 1px;
323   - }
324   - path.predictive-line {
325   - fill: none;
326   - stroke: #ff4081;
327   - stroke-width: 2px;
328   - }
329   - circle.cursor-circle {
330   - fill: black;
331   - stroke: rgba(20, 20, 20, 0.48);
332   - }
333   - text.cursor-text {
334   -{# font-family: 'Ubuntu', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif;#}
335   -{# font-family: 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;#}
336   - font-family: "Ubuntu Mono", 'Consolas', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
337   - text-align: right;
338   - }
339   - text.cursor-text-shadow {
340   - stroke: white;
341   - stroke-width: 5px;
342   - opacity: 0.777
343   - }
344   - path.orbit.orbit_section {
345   - fill: none;
346   - stroke: steelblue;
347   - stroke-width: 1.5px;
348   - }
349   - ellipse.orbit.orbit_ellipse {
350   - fill: none;
351   - stroke: #a3a3a3;
352   - stroke-width: 1px;
353   - stroke-dasharray: 5px;
354   - }
355   - #form_time_interval {
356   - padding-left: 30px;
357   - }
358   - #form_time_interval .mdl-textfield {
359   - padding-top: 0;
360   - }
361   - #started_at, #stopped_at {
362   - width: 85%;
363   - }
364   - .section-drawer {
365   - padding-left: 3em;
366   - }
367   - .targets-filters {
368   - padding-left: 17px;
369   - }
370   - .targets-filters .target {
371   - float: left;
372   - cursor: pointer;
373   - position: relative;
374   - }
375   - .targets-filters .target:not(.active) {
376   - -webkit-filter: grayscale(100%);
377   - -moz-filter: grayscale(100%);
378   - -o-filter: grayscale(100%);
379   -{# -ms-filter: grayscale(100%);#}
380   - filter: grayscale(100%);
381   - }
382   - .targets-filters .target.locked {
383   - cursor: not-allowed;
384   - }
385   - .targets-filters .target.loading {
386   - -webkit-animation-name: keyframes_rotate;
387   - -webkit-animation-duration: 4000ms;
388   - -webkit-animation-iteration-count: infinite;
389   - -webkit-animation-timing-function: linear;
390   - -moz-animation-name: keyframes_rotate;
391   - -moz-animation-duration: 4000ms;
392   - -moz-animation-iteration-count: infinite;
393   - -moz-animation-timing-function: linear;
394   - -ms-animation-name: keyframes_rotate;
395   - -ms-animation-duration: 4000ms;
396   - -ms-animation-iteration-count: infinite;
397   - -ms-animation-timing-function: linear;
398   - animation-name: keyframes_rotate;
399   - animation-duration: 4000ms;
400   - animation-iteration-count: infinite;
401   - animation-timing-function: linear;
402   - }
403   -
404   - .targets-filters .target .decorator {
405   - position: absolute;
406   - top: 0;
407   - left: 0;
408   - display: none;
409   - }
410   - .targets-filters .target.empty .decorator.empty {
411   - display: block;
412   - }
413   - .targets-filters .target.error .decorator.error {
414   - display: block;
415   - }
416   - .targets-filters .target .decorator.loading {
417   - top: 19px;
418   - left: 19px;
419   - }
420   - .targets-filters .target.loading .decorator.loading {
421   - display: block;
422   - }
423   -
424   - #parameters .parameter {
425   - outline: 0;
426   - padding-top: 7px;
427   - padding-bottom: 7px;
428   -
429   - }
430   - #parameters .parameter.active {
431   - background-color: #c8d3e1;
432   - }
433   -
434   -
435   - {# CSS Spinners #}
436   -
437   - #download_spinner {
438   - display: none;
439   - top: 2px;
440   - left: 4px;
441   - width: 14px;
442   - height: 14px;
443   - }
444   -
445   - #plots_loader {
446   - position: fixed;
447   - top: 0; left: 0; bottom: 0; right: 0;
448   - height: 100%;
449   - width: 100%;
450   - background-color: #fff;
451   - z-index: 1000;
452   - }
453   -
454   - #plots_loader .loader-text {
455   - width: 200px;
456   - height: 30px;
457   - position: absolute;
458   - top: -240px; left: -32px; bottom: 0; right: 0;
459   - margin: auto;
460   - text-align: center;
461   - font-size: 1.0em;
462   - font-style: italic;
463   - color: darkgrey;
464   - }
465   -
466   - #plots_loader .img {
467   - width: 100px;
468   - height: 100px;
469   - border-radius: 100%;
470   - position: absolute;
471   - border: 1px solid #6978ff;
472   - animation: keyframes_rotate 1s;
473   - animation-iteration-count: infinite;
474   - transition: 2s;
475   - border-bottom: none;
476   - border-right: none;
477   - animation-timing-function: linear;
478   - margin-left: -70px;
479   - margin-top: -70px;
480   - left: 50%;
481   - top: 50%;
482   - }
483   -
484   - #plots_loader #plots_loader_img2 {
485   - width: 90px;
486   - height: 90px;
487   - left: 50.35%;
488   - top: 50.7%;
489   - animation-delay: .2s;
490   - }
491   -
492   - #plots_loader #plots_loader_img3 {
493   - width: 80px;
494   - height: 80px;
495   - left: 50.70%;
496   - top: 51.4%;
497   - animation-delay: .4s;
498   - }
499   -
500   - #plots_loader #plots_loader_img4 {
501   - width: 70px;
502   - height: 70px;
503   - left: 51.05%;
504   - top: 52.1%;
505   - animation-delay: .6s;
506   - }
507   -
508   - #plots_loader #plots_loader_img5 {
509   - width: 60px;
510   - height: 60px;
511   - left: 51.40%;
512   - top: 52.8%;
513   - animation-delay: .8s;
514   - }
515   -
516   - @keyframes keyframes_rotate {
517   - from {
518   - transform: rotate(0deg);
519   - }
520   - 50% {
521   - transform: rotate(180deg);
522   - }
523   - 100% {
524   - transform: rotate(360deg);
525   - }
526   - }
527   -
528   - .small-loader-container {
529   - width: 27px;
530   - margin: 0 auto;
531   - background: none;
532   - pointer-events: none;
533   - }
534   - .small-loader-circle-1 {
535   - height: 27px;
536   - width: 27px;
537   - background: rgba(255, 238, 195, 0.72);
538   - }
539   - .small-loader-circle-2 {
540   - height: 22px;
541   - width: 22px;
542   - background: none;
543   - }
544   - .small-loader-circle-3 {
545   - height: 18px;
546   - width: 18px;
547   - background: rgba(29, 65, 255, 0.9);
548   - }
549   - .small-loader-circle-4 {
550   - height: 13px;
551   - width: 13px;
552   - background: none;
553   - }
554   - .small-loader-circle-5 {
555   - height: 9px;
556   - width: 9px;
557   - background: rgba(238, 238, 238, 0.8);
558   - }
559   - .small-loader-circle-6 {
560   - height: 4px;
561   - width: 4px;
562   - background: none;
563   - }
564   - .small-loader-circle-7 {
565   - height: 2px;
566   - width: 2px;
567   - background: rgb(110, 102, 255);
568   - }
569   - .small-loader-circle-1,
570   - .small-loader-circle-2,
571   - .small-loader-circle-3,
572   - .small-loader-circle-4,
573   - .small-loader-circle-5,
574   - .small-loader-circle-6,
575   - .small-loader-circle-7 {
576   - border-bottom: none;
577   - border-radius: 50%;
578   - -o-border-radius: 50%;
579   - -ms-border-radius: 50%;
580   - -webkit-border-radius: 50%;
581   - -moz-border-radius: 50%;
582   - box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
583   - -o-box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
584   - -ms-box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
585   - -webkit-box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
586   - -moz-box-shadow: 0px 0px 0px rgba(0,0,0,0.1);
587   - animation-name: small-loader-spin;
588   - -o-animation-name: small-loader-spin;
589   - -ms-animation-name: small-loader-spin;
590   - -webkit-animation-name: small-loader-spin;
591   - -moz-animation-name: small-loader-spin;
592   - animation-duration: 4600ms;
593   - -o-animation-duration: 4600ms;
594   - -ms-animation-duration: 4600ms;
595   - -webkit-animation-duration: 4600ms;
596   - -moz-animation-duration: 4600ms;
597   - animation-iteration-count: infinite;
598   - -o-animation-iteration-count: infinite;
599   - -ms-animation-iteration-count: infinite;
600   - -webkit-animation-iteration-count: infinite;
601   - -moz-animation-iteration-count: infinite;
602   - animation-timing-function: linear;
603   - -o-animation-timing-function: linear;
604   - -ms-animation-timing-function: linear;
605   - -webkit-animation-timing-function: linear;
606   - -moz-animation-timing-function: linear;
607   - }
608   -
609   - @keyframes small-loader-spin {
610   - from {
611   - transform: rotate(0deg);
612   - }
613   - to {
614   - transform: rotate(360deg);
615   - }
616   - }
617   -
618   - @-o-keyframes small-loader-spin {
619   - from {
620   - -o-transform: rotate(0deg);
621   - }
622   - to {
623   - -o-transform: rotate(360deg);
624   - }
625   - }
626   -
627   - @-ms-keyframes small-loader-spin {
628   - from {
629   - -ms-transform: rotate(0deg);
630   - }
631   - to {
632   - -ms-transform: rotate(360deg);
633   - }
634   - }
635   -
636   - @-webkit-keyframes small-loader-spin {
637   - from {
638   - -webkit-transform: rotate(0deg);
639   - }
640   - to {
641   - -webkit-transform: rotate(360deg);
642   - }
643   - }
644   -
645   - @-moz-keyframes small-loader-spin {
646   - from {
647   - -moz-transform: rotate(0deg);
648   - }
649   - to {
650   - -moz-transform: rotate(360deg);
651   - }
652   - }
653   -
654   - </style>
  295 + <link rel="stylesheet" type="text/css" href="{{ static('css/home.css') }}">
  296 +{# <style>#}
  297 +{# </style>#}
655 298 {% endblock %}
656 299  
657 300  
... ... @@ -664,10 +307,11 @@
664 307 <script type="application/javascript" src="{{ static('js/main.js') }}"></script>
665 308 <script type="application/javascript">
666 309  
667   -var configuration = {
  310 +var sw_configuration = {
668 311 time_series_container: '#time_series',
669 312 orbits_container: '#orbits',
670 313 api: {
  314 + 'root': "{{ request.url_root }}",
671 315 'data_for_interval': "{{ request.url_root }}<target>_{{ input_slug }}_<started_at>_<stopped_at>.csv",
672 316 'download': "{{ request.url_root }}<targets>_{{ input_slug }}_<started_at>_<stopped_at>.cdf",
673 317 'samp': "{{ request.url_root }}<targets>_{{ input_slug }}_<started_at>_<stopped_at>.cdf"
... ... @@ -724,7 +368,7 @@ var configuration = {
724 368 var sw;
725 369 jQuery().ready(function($){
726 370 // Space Weather app itself, handling data downloads and plot draws.
727   - sw = new SpaceWeather(configuration);
  371 + sw = new SpaceWeather(sw_configuration);
728 372 sw.init("{{ started_at }}Z", "{{ stopped_at }}Z");
729 373  
730 374 // User Interface (except plots' interactivity, such as mouse hovers)
... ... @@ -784,13 +428,27 @@ jQuery().ready(function($){
784 428 return false;
785 429 });
786 430  
787   - $('.option-layer').on("click", function(e){
  431 + $('.option-layer-catalog').on("click", function(e){
788 432 var catalog_slug = $(this).attr('value');
789 433 var checked = $(this).prop('checked');
790 434 if (checked) { sw.showCatalogLayer(catalog_slug); }
791 435 else { sw.hideCatalogLayer(catalog_slug); }
792 436 });
793 437  
  438 + $('#option-layer-hstsaturnobservations').on("click", function(e){
  439 + var that = $(this);
  440 + var catalog_slug = $(this).attr('value');
  441 + var checked = $(this).prop('checked');
  442 + if (checked) {
  443 + $(this).addClass('loading');
  444 + sw.showImagePreviewLayer("saturn", catalog_slug).finally( function(){
  445 + that.removeClass('loading');
  446 + });
  447 + } else {
  448 + sw.hideImagePreviewLayer("saturn", catalog_slug);
  449 + }
  450 + });
  451 +
794 452 var download_spinner = $('#download_spinner');
795 453 var waiting_for_download = false;
796 454  
... ... @@ -861,6 +519,7 @@ jQuery().ready(function($){
861 519 var connector = new samp.Connector("Sender", {
862 520 "samp.name": "Heliopropa",
863 521 "samp.description.text": "{{ config.meta.description }}",
  522 + // {{ request.url_root }} <- use this instead ?
864 523 "samp.icon.url": "http://heliopropa.irap.omp.eu/static/img/target/earth_128.png",
865 524 });
866 525 connector.onHubAvailability(onHubAvailability, 7000);
... ...