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.
@@ -32,11 +32,17 @@ et prendre aussi @@ -32,11 +32,17 @@ et prendre aussi
32 - [ ] Rework the images of Rosetta and Juno 32 - [ ] Rework the images of Rosetta and Juno
33 - [ ] Optimize data aggregation (numpy vectorization?) 33 - [ ] Optimize data aggregation (numpy vectorization?)
34 - [ ] IE compat, if you can (I can't) 34 - [ ] IE compat, if you can (I can't)
35 -- [ ] Add a README to the download tarball (no tarball anymore)  
36 - [ ] Bump D3JS to v5 (and its promises) 35 - [ ] Bump D3JS to v5 (and its promises)
37 - [ ] Enable p67 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 ## 1.7 46 ## 1.7
41 47
42 - [x] Only get SWRT after OMNI 48 - [x] Only get SWRT after OMNI
@@ -59,9 +59,11 @@ layers: @@ -59,9 +59,11 @@ layers:
59 - slug: "hstjupiterobservations" 59 - slug: "hstjupiterobservations"
60 name: "HST Jupiter" 60 name: "HST Jupiter"
61 desc: "Hubble Space Telescope Jupiter Observations" 61 desc: "Hubble Space Telescope Jupiter Observations"
  62 + live: false
62 - slug: "hstsaturnobservations" 63 - slug: "hstsaturnobservations"
63 name: "HST Saturn" 64 name: "HST Saturn"
64 desc: "Hubble Space Telescope Saturn Observations" 65 desc: "Hubble Space Telescope Saturn Observations"
  66 + live: true
65 catalogs: 67 catalogs:
66 - slug: "cmecatalogs" 68 - slug: "cmecatalogs"
67 name: "CME Catalogs" 69 name: "CME Catalogs"
@@ -267,6 +269,8 @@ targets: @@ -267,6 +269,8 @@ targets:
267 sb: 269 sb:
268 - slug: 'tao_sat_sw' 270 - slug: 'tao_sat_sw'
269 - slug: 'tao_sat_swrt' 271 - slug: 'tao_sat_swrt'
  272 + tap:
  273 + target_name: 'Saturn'
270 locked: false 274 locked: false
271 default: true 275 default: true
272 - type: 'planet' 276 - type: 'planet'
requirements.txt
@@ -13,6 +13,11 @@ MarkupSafe==1.0 @@ -13,6 +13,11 @@ MarkupSafe==1.0
13 python-slugify==1.2.4 13 python-slugify==1.2.4
14 requests==2.19.1 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 ## SECOND LEVEL DEPS 22 ## SECOND LEVEL DEPS
18 # (comment them out if you have compatibility issues) 23 # (comment them out if you have compatibility issues)
@@ -10,8 +10,9 @@ import tarfile @@ -10,8 +10,9 @@ import tarfile
10 import time 10 import time
11 import urllib 11 import urllib
12 import requests 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 from math import sqrt, isnan 16 from math import sqrt, isnan
16 from os import environ, remove as removefile 17 from os import environ, remove as removefile
17 from os.path import isfile, join, abspath, dirname 18 from os.path import isfile, join, abspath, dirname
@@ -356,10 +357,27 @@ def get_active_targets(): @@ -356,10 +357,27 @@ def get_active_targets():
356 return [t for t in all_targets if not ('locked' in t and t['locked'])] 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 Work In Progress. 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 as our sanitizing for ADQL may not be 100% safe. 381 as our sanitizing for ADQL may not be 100% safe.
364 Use values from YAML configuration, instead. 382 Use values from YAML configuration, instead.
365 Below is a list of the ids we found to be existing. 383 Below is a list of the ids we found to be existing.
@@ -377,16 +395,34 @@ def retrieve_auroral_emissions(target_name): @@ -377,16 +395,34 @@ def retrieve_auroral_emissions(target_name):
377 - Saturn 395 - Saturn
378 :return: 396 :return:
379 """ 397 """
  398 +
  399 + # Try out the form
  400 + # http://voparis-tap-planeto.obspm.fr/__system__/adql/query/form
  401 +
380 api_url = "http://voparis-tap.obspm.fr/__system__/tap/run/tap/sync" 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 t_stopped_at = time.mktime(d_stopped_at.timetuple()) 412 t_stopped_at = time.mktime(d_stopped_at.timetuple())
386 413
387 - def to_jday(timestamp): 414 + def timestamp_to_jday(timestamp):
388 return timestamp / 86400.0 + 2440587.5 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 query = """ 426 query = """
391 SELECT 427 SELECT
392 time_min, 428 time_min,
@@ -396,13 +432,13 @@ SELECT @@ -396,13 +432,13 @@ SELECT
396 FROM apis.epn_core 432 FROM apis.epn_core
397 WHERE target_name='{target_name}' 433 WHERE target_name='{target_name}'
398 AND dataproduct_type='im' 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 ORDER BY time_min, granule_gid 437 ORDER BY time_min, granule_gid
402 """.format( 438 """.format(
403 target_name=target_name.replace("'", "\\'"), 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 # query = """ 444 # query = """
@@ -427,8 +463,8 @@ ORDER BY time_min, granule_gid @@ -427,8 +463,8 @@ ORDER BY time_min, granule_gid
427 rows = [] 463 rows = []
428 for row in root.findall(rows_xpath, namespaces): 464 for row in root.findall(rows_xpath, namespaces):
429 rows.append({ 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 'thumbnail_url': row[2].text, 468 'thumbnail_url': row[2].text,
433 'external_link': row[3].text, 469 'external_link': row[3].text,
434 }) 470 })
@@ -1045,7 +1081,7 @@ def increment_hit_counter(): @@ -1045,7 +1081,7 @@ def increment_hit_counter():
1045 1081
1046 def update_spacepy(): 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 try: 1086 try:
1051 log.info("Updating spacepy's toolboxโ€ฆ") 1087 log.info("Updating spacepy's toolboxโ€ฆ")
@@ -1500,6 +1536,61 @@ def download_targets_cdf(targets, inp, started_at, stopped_at): @@ -1500,6 +1536,61 @@ def download_targets_cdf(targets, inp, started_at, stopped_at):
1500 return send_from_directory(CACHE_DIR, cdf_filename) 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 # API ######################################################################### 1594 # API #########################################################################
1504 1595
1505 @app.route("/cache/clear") 1596 @app.route("/cache/clear")
web/static/css/home.css 0 โ†’ 100644
@@ -0,0 +1,456 @@ @@ -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,6 +83,13 @@
83 this.configuration['parameters'].forEach(p => that.parameters[p['id']] = p); 83 this.configuration['parameters'].forEach(p => that.parameters[p['id']] = p);
84 this.orbits = null; 84 this.orbits = null;
85 this.time_series = []; 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,7 +289,7 @@
282 data = { 289 data = {
283 'hee': [] 290 'hee': []
284 }; 291 };
285 - configuration['parameters'].forEach(parameter => data[parameter['id']] = []); 292 + app.configuration['parameters'].forEach(parameter => data[parameter['id']] = []);
286 if (!csv) { 293 if (!csv) {
287 reject('invalid'); 294 reject('invalid');
288 } 295 }
@@ -292,7 +299,7 @@ @@ -292,7 +299,7 @@
292 csv.forEach(d => { 299 csv.forEach(d => {
293 let dtime; 300 let dtime;
294 dtime = timeFormat(d['time']); 301 dtime = timeFormat(d['time']);
295 - configuration['parameters'].forEach(parameter => { 302 + app.configuration['parameters'].forEach(parameter => {
296 let id; 303 let id;
297 let val; 304 let val;
298 id = parameter['id']; 305 id = parameter['id'];
@@ -424,12 +431,15 @@ @@ -424,12 +431,15 @@
424 app.time_series.forEach(ts2 => ts2.hideCursor()); 431 app.time_series.forEach(ts2 => ts2.hideCursor());
425 return true; 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 return true; 443 return true;
434 }; 444 };
435 445
@@ -508,6 +518,123 @@ @@ -508,6 +518,123 @@
508 return this; 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 getDomain() { 638 getDomain() {
512 if (this.current_started_at != null && this.current_stopped_at != null) { 639 if (this.current_started_at != null && this.current_stopped_at != null) {
513 return [this.current_started_at, this.current_stopped_at]; 640 return [this.current_started_at, this.current_stopped_at];
@@ -619,6 +746,20 @@ @@ -619,6 +746,20 @@
619 } 746 }
620 } 747 }
621 this.predictiveData = predictiveData; 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 this.init(); 763 this.init();
623 } 764 }
624 765
@@ -778,6 +919,7 @@ @@ -778,6 +919,7 @@
778 this.yAxisText.attr("y", 20 - this.margin.left).attr("x", 0 - height / 2); 919 this.yAxisText.attr("y", 20 - this.margin.left).attr("x", 0 - height / 2);
779 this.yAxisTextTarget.attr("y", 0 - this.margin.left).attr("x", 0 - height / 2.0); 920 this.yAxisTextTarget.attr("y", 0 - this.margin.left).attr("x", 0 - height / 2.0);
780 this.resizeCatalogLayers(); 921 this.resizeCatalogLayers();
  922 + this.resizeImagePreviewLayers();
781 if (!this.visible) { 923 if (!this.visible) {
782 this.hide(); 924 this.hide();
783 } 925 }
@@ -891,6 +1033,7 @@ @@ -891,6 +1033,7 @@
891 this.predictiveDataPath.attr('d', this.line); 1033 this.predictiveDataPath.attr('d', this.line);
892 } 1034 }
893 this.resizeCatalogLayers(); 1035 this.resizeCatalogLayers();
  1036 + this.resizeImagePreviewLayers();
894 this.hideCursor(); 1037 this.hideCursor();
895 return new Promise((resolve, reject) => { 1038 return new Promise((resolve, reject) => {
896 if (0 === duration) { 1039 if (0 === duration) {
@@ -901,6 +1044,19 @@ @@ -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 createCatalogLayers() { 1060 createCatalogLayers() {
905 let catalog_slug; 1061 let catalog_slug;
906 let ref$; 1062 let ref$;
@@ -912,35 +1068,32 @@ @@ -912,35 +1068,32 @@
912 let stopped_at; 1068 let stopped_at;
913 this.layers_rects = {}; 1069 this.layers_rects = {};
914 for (catalog_slug in ref$ = this.target.config.layers) { 1070 for (catalog_slug in ref$ = this.target.config.layers) {
  1071 + if ( ! (ref$.hasOwnProperty(catalog_slug))) continue; // oh, js
915 layers = ref$[catalog_slug]; 1072 layers = ref$[catalog_slug];
916 this.layers_rects[catalog_slug] = []; 1073 this.layers_rects[catalog_slug] = [];
917 for (i$ = 0, len$ = layers.length; i$ < len$; ++i$) { 1074 for (i$ = 0, len$ = layers.length; i$ < len$; ++i$) {
918 layer = layers[i$]; 1075 layer = layers[i$];
919 started_at = moment(layer.start); 1076 started_at = moment(layer.start);
920 stopped_at = moment(layer.stop); 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 this.hideCatalogLayer(catalog_slug); 1080 this.hideCatalogLayer(catalog_slug);
924 } 1081 }
925 return this; 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 resizeCatalogLayers() { 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 let catalog_slug; 1094 let catalog_slug;
941 let ref$; 1095 let ref$;
942 let layers; 1096 let layers;
943 - let i$;  
944 let len$; 1097 let len$;
945 let i; 1098 let i;
946 let layer; 1099 let layer;
@@ -948,47 +1101,352 @@ @@ -948,47 +1101,352 @@
948 let stopped_at; 1101 let stopped_at;
949 let width; 1102 let width;
950 for (catalog_slug in ref$ = this.target.config.layers) { 1103 for (catalog_slug in ref$ = this.target.config.layers) {
  1104 + if ( ! (ref$.hasOwnProperty(catalog_slug))) continue;
951 layers = ref$[catalog_slug]; 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 started_at = moment(layer.start); 1108 started_at = moment(layer.start);
956 stopped_at = moment(layer.stop); 1109 stopped_at = moment(layer.stop);
957 width = Math.max(2, this.xScale(stopped_at) - this.xScale(started_at)); 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 return this; 1125 return this;
962 } 1126 }
963 1127
964 showCatalogLayer(catalog_slug) { 1128 showCatalogLayer(catalog_slug) {
965 - let i$;  
966 let ref$; 1129 let ref$;
967 let len$; 1130 let len$;
968 let i; 1131 let i;
969 let layer; 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 this.layers_rects[catalog_slug][i].style("display", null); 1135 this.layers_rects[catalog_slug][i].style("display", null);
974 } 1136 }
975 return this; 1137 return this;
976 } 1138 }
977 1139
978 hideCatalogLayer(catalog_slug) { 1140 hideCatalogLayer(catalog_slug) {
979 - let i$;  
980 let ref$; 1141 let ref$;
981 let len$; 1142 let len$;
982 let i; 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 this.layers_rects[catalog_slug][i].style("display", "none"); 1147 this.layers_rects[catalog_slug][i].style("display", "none");
988 } 1148 }
989 return this; 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 showCursor() { 1450 showCursor() {
993 return this.focus.style("display", null); 1451 return this.focus.style("display", null);
994 } 1452 }
@@ -998,40 +1456,53 @@ @@ -998,40 +1456,53 @@
998 } 1456 }
999 1457
1000 moveCursor(x0) { 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 if (!d1 || !d0) { 1462 if (!d1 || !d0) {
1013 this.hideCursor(); 1463 this.hideCursor();
1014 return; 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 const mirrored = this.plotWidth != null && xx > this.plotWidth / 2; 1469 const mirrored = this.plotWidth != null && xx > this.plotWidth / 2;
1020 - dx = 8; 1470 + let dx = 8;
1021 if (mirrored) { 1471 if (mirrored) {
1022 dx = -1 * dx; 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 this.showCursor(); 1497 this.showCursor();
  1498 +
1031 return this; 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 TimeSeries.prototype.bisectDate = d3.bisector(d => d.x).left; 1506 TimeSeries.prototype.bisectDate = d3.bisector(d => d.x).left;
1036 TimeSeries.prototype.timeFormat = d3.utcFormat("%Y-%m-%d %H:%M"); 1507 TimeSeries.prototype.timeFormat = d3.utcFormat("%Y-%m-%d %H:%M");
1037 1508
web/static/js/swapp.ls
@@ -12,7 +12,7 @@ @@ -12,7 +12,7 @@
12 12
13 13
14 14
15 -# DEPRECATED 15 +# DEPRECATED and NOT USED ANYMORE
16 # Use web/static/js/main.js instead. 16 # Use web/static/js/main.js instead.
17 17
18 18
web/view/home.html.jinja2
@@ -178,7 +178,7 @@ @@ -178,7 +178,7 @@
178 <section class="section-drawer"> 178 <section class="section-drawer">
179 {% for layer in config.layers.campaigns %} 179 {% for layer in config.layers.campaigns %}
180 <label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="option-layer-{{ layer.slug }}" title="{{ layer.desc }}"> 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 <span class="mdl-radio__label">{{ layer.name }}</span> 182 <span class="mdl-radio__label">{{ layer.name }}</span>
183 </label> 183 </label>
184 <br /> 184 <br />
@@ -197,7 +197,7 @@ @@ -197,7 +197,7 @@
197 <section class="section-drawer"> 197 <section class="section-drawer">
198 {% for layer in config.layers.catalogs %} 198 {% for layer in config.layers.catalogs %}
199 <label class="mdl-radio mdl-js-radio mdl-js-ripple-effect" for="option-layer-{{ layer.slug }}" title="{{ layer.desc }}"> 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 <span class="mdl-radio__label">{{ layer.name }}</span> 201 <span class="mdl-radio__label">{{ layer.name }}</span>
202 </label> 202 </label>
203 <br /> 203 <br />
@@ -250,7 +250,32 @@ @@ -250,7 +250,32 @@
250 Alert 250 Alert
251 </button> 251 </button>
252 {% endif %} 252 {% endif %}
  253 +
253 </div> 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 </div> 279 </div>
255 <div class="mdl-cell mdl-cell--8-col mdl-cell--8-col-tablet mdl-cell--4-col-phone"> 280 <div class="mdl-cell mdl-cell--8-col mdl-cell--8-col-tablet mdl-cell--4-col-phone">
256 <section id="time_series"> 281 <section id="time_series">
@@ -267,391 +292,9 @@ @@ -267,391 +292,9 @@
267 {#### CSS ####################################################################} 292 {#### CSS ####################################################################}
268 293
269 {% block styles %} 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 {% endblock %} 298 {% endblock %}
656 299
657 300
@@ -664,10 +307,11 @@ @@ -664,10 +307,11 @@
664 <script type="application/javascript" src="{{ static('js/main.js') }}"></script> 307 <script type="application/javascript" src="{{ static('js/main.js') }}"></script>
665 <script type="application/javascript"> 308 <script type="application/javascript">
666 309
667 -var configuration = { 310 +var sw_configuration = {
668 time_series_container: '#time_series', 311 time_series_container: '#time_series',
669 orbits_container: '#orbits', 312 orbits_container: '#orbits',
670 api: { 313 api: {
  314 + 'root': "{{ request.url_root }}",
671 'data_for_interval': "{{ request.url_root }}<target>_{{ input_slug }}_<started_at>_<stopped_at>.csv", 315 'data_for_interval': "{{ request.url_root }}<target>_{{ input_slug }}_<started_at>_<stopped_at>.csv",
672 'download': "{{ request.url_root }}<targets>_{{ input_slug }}_<started_at>_<stopped_at>.cdf", 316 'download': "{{ request.url_root }}<targets>_{{ input_slug }}_<started_at>_<stopped_at>.cdf",
673 'samp': "{{ request.url_root }}<targets>_{{ input_slug }}_<started_at>_<stopped_at>.cdf" 317 'samp': "{{ request.url_root }}<targets>_{{ input_slug }}_<started_at>_<stopped_at>.cdf"
@@ -724,7 +368,7 @@ var configuration = { @@ -724,7 +368,7 @@ var configuration = {
724 var sw; 368 var sw;
725 jQuery().ready(function($){ 369 jQuery().ready(function($){
726 // Space Weather app itself, handling data downloads and plot draws. 370 // Space Weather app itself, handling data downloads and plot draws.
727 - sw = new SpaceWeather(configuration); 371 + sw = new SpaceWeather(sw_configuration);
728 sw.init("{{ started_at }}Z", "{{ stopped_at }}Z"); 372 sw.init("{{ started_at }}Z", "{{ stopped_at }}Z");
729 373
730 // User Interface (except plots' interactivity, such as mouse hovers) 374 // User Interface (except plots' interactivity, such as mouse hovers)
@@ -784,13 +428,27 @@ jQuery().ready(function($){ @@ -784,13 +428,27 @@ jQuery().ready(function($){
784 return false; 428 return false;
785 }); 429 });
786 430
787 - $('.option-layer').on("click", function(e){ 431 + $('.option-layer-catalog').on("click", function(e){
788 var catalog_slug = $(this).attr('value'); 432 var catalog_slug = $(this).attr('value');
789 var checked = $(this).prop('checked'); 433 var checked = $(this).prop('checked');
790 if (checked) { sw.showCatalogLayer(catalog_slug); } 434 if (checked) { sw.showCatalogLayer(catalog_slug); }
791 else { sw.hideCatalogLayer(catalog_slug); } 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 var download_spinner = $('#download_spinner'); 452 var download_spinner = $('#download_spinner');
795 var waiting_for_download = false; 453 var waiting_for_download = false;
796 454
@@ -861,6 +519,7 @@ jQuery().ready(function($){ @@ -861,6 +519,7 @@ jQuery().ready(function($){
861 var connector = new samp.Connector("Sender", { 519 var connector = new samp.Connector("Sender", {
862 "samp.name": "Heliopropa", 520 "samp.name": "Heliopropa",
863 "samp.description.text": "{{ config.meta.description }}", 521 "samp.description.text": "{{ config.meta.description }}",
  522 + // {{ request.url_root }} <- use this instead ?
864 "samp.icon.url": "http://heliopropa.irap.omp.eu/static/img/target/earth_128.png", 523 "samp.icon.url": "http://heliopropa.irap.omp.eu/static/img/target/earth_128.png",
865 }); 524 });
866 connector.onHubAvailability(onHubAvailability, 7000); 525 connector.onHubAvailability(onHubAvailability, 7000);