Commit 11d8685108026d6b23be92fe78cf686b0e8ae2b2
1 parent
20fdc1a4
Exists in
master
and in
2 other branches
Add support for saturn's aurora HST campaign.
(finally! -- biggest commit of the tree) This was much more work than expected. Jupiter will follow.
Showing
8 changed files
with
1151 additions
and
459 deletions
Show diff stats
CHANGELOG.md
@@ -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 |
config.yml
@@ -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) |
web/run.py
@@ -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") |
@@ -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
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); |