Commit a4a9ef0393ad3b8e968d48d06398edbd145adff3
1 parent
79a09c02
Exists in
master
and in
3 other branches
Cache generated CSVs, and serve them as static files so that clients can cache t…
…hem too. Implement a visits counter.
Showing
6 changed files
with
237 additions
and
62 deletions
Show diff stats
.gitignore
CHANGELOG.md
1 | 1 | ## TODO |
2 | 2 | |
3 | -- Support enabling/disabling sources | |
4 | -- Start/Stop datetime fields | |
5 | -- Cache generated CSVs locally | |
6 | -- Download raw data (as CSV) for current time interval | |
7 | -- Same via SAMP | |
3 | +- Support multiple models for each target | |
8 | 4 | - Play button to start time dimension |
9 | 5 | |
6 | +## 1.0.0 | |
7 | + | |
8 | +- [x] Cache generated CSVs on the server side | |
9 | +- [x] Cache generated CSVs on the client side | |
10 | +- [x] Visits counter | |
11 | +- [ ] Start/Stop datetime fields | |
12 | +- [ ] Zoom in on time series | |
13 | +- [ ] Loader | |
14 | +- [ ] Download raw data (as CSV) for current time interval and targets | |
15 | +- [ ] Same via SAMP | |
16 | + | |
17 | + | |
10 | 18 | ## 0.0.0 |
11 | 19 | |
12 | 20 | - Initial website skeleton |
... | ... | @@ -26,3 +34,4 @@ |
26 | 34 | - Cache remote NetCDFs locally |
27 | 35 | - Add source name on time series |
28 | 36 | - Support multiple sources |
37 | +- Support enabling/disabling sources | ... | ... |
web/run.py
... | ... | @@ -27,6 +27,7 @@ THIS_DIRECTORY = dirname(abspath(__file__)) |
27 | 27 | |
28 | 28 | |
29 | 29 | def get_path(relative_path): |
30 | + """Get an absolute path from the relative path to this script directory.""" | |
30 | 31 | return abspath(join(THIS_DIRECTORY, relative_path)) |
31 | 32 | |
32 | 33 | |
... | ... | @@ -209,60 +210,18 @@ def retrieve_data(orbiter, what, started_at, stopped_at): |
209 | 210 | return local_netc_files |
210 | 211 | |
211 | 212 | |
212 | -# ROUTING ##################################################################### | |
213 | - | |
214 | -@app.route('/favicon.ico') | |
215 | -def favicon(): | |
216 | - return send_from_directory( | |
217 | - join(app.root_path, 'static', 'img'), | |
218 | - 'favicon.ico', mimetype='image/vnd.microsoft.icon' | |
219 | - ) | |
220 | - | |
221 | - | |
222 | -@app.route("/") | |
223 | -@app.route("/home.html") | |
224 | -@app.route("/index.html") | |
225 | -def home(): | |
226 | - return render_view('home.html.jinja2', { | |
227 | - 'targets': config['targets'], | |
228 | - 'planets': [s for s in config['targets'] if s['type'] == 'planet'], | |
229 | - 'probes': [s for s in config['targets'] if s['type'] == 'probe'], | |
230 | - 'comets': [s for s in config['targets'] if s['type'] == 'comet'], | |
231 | - }) | |
232 | - | |
233 | - | |
234 | -@app.route("/<source>/data.csv") | |
235 | -def get_orbiter_csv(source): | |
236 | - """ | |
237 | - Grab data and orbit data for the specified `source`, | |
238 | - rearrange it and return it as a CSV file. | |
239 | - """ | |
240 | - # http://cdpp1.cesr.fr/BASE/DDService/getDataUrl.php?dataSet=tao_ros_sw&StartTime=2014-02-23T10:00&StopTime=2016-02-24T23:59 | |
241 | - # Process input parameters | |
242 | - source_config = get_source_config(source) | |
243 | - date_fmt = "%Y-%m-%dT%H:%M:%S" | |
244 | - started_at = request.args.get('started_at') | |
245 | - try: | |
246 | - started_at = datetime.datetime.strptime(started_at, date_fmt) | |
247 | - except: | |
248 | - abort(400, "Invalid started_at parameter : '%s'." % started_at) | |
249 | - stopped_at = request.args.get('stopped_at') | |
250 | - try: | |
251 | - stopped_at = datetime.datetime.strptime(stopped_at, date_fmt) | |
252 | - except: | |
253 | - abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at) | |
254 | - | |
213 | +def generate_csv_contents(source_config, started_at, stopped_at): | |
255 | 214 | # todo: iterate on models when there are many |
256 | 215 | try: |
257 | 216 | model_slug = source_config['models'][0]['slug'] |
258 | 217 | except: |
259 | - abort(500, "Invalid model configuration for '%s'." % source) | |
218 | + abort(500, "Invalid model configuration for '%s'." % source_config['slug']) | |
260 | 219 | |
261 | 220 | # Grab the list of netCDF files from Myriam's API |
262 | 221 | # http://cdpp.irap.omp.eu/BASE/DDService/getDataUrl.php?dataSet=jupiter_orb_all&StartTime=2014-02-23T10:00:10&StopTime=2017-02-24T23:59:00 |
263 | 222 | # http://cdpp.irap.omp.eu/BASE/DATA/TAO/JUPITER/SW/sw_2014.nc.gz |
264 | - model_files = retrieve_data(source, model_slug, started_at, stopped_at) | |
265 | - orbits_files = retrieve_data(source, source_config['orbit']['model'], started_at, stopped_at) | |
223 | + model_files = retrieve_data(source_config['slug'], model_slug, started_at, stopped_at) | |
224 | + orbits_files = retrieve_data(source_config['slug'], source_config['orbit']['model'], started_at, stopped_at) | |
266 | 225 | |
267 | 226 | si = StringIO.StringIO() |
268 | 227 | cw = csv_writer(si) |
... | ... | @@ -321,6 +280,179 @@ def get_orbiter_csv(source): |
321 | 280 | return si.getvalue() |
322 | 281 | |
323 | 282 | |
283 | +def increment_hit_counter(): | |
284 | + hit_count_path = get_path("../VISITS") | |
285 | + | |
286 | + if isfile(hit_count_path): | |
287 | + hit_count = int(open(hit_count_path).read()) | |
288 | + hit_count += 1 | |
289 | + else: | |
290 | + hit_count = 1 | |
291 | + | |
292 | + hit_counter_file = open(hit_count_path, 'w') | |
293 | + hit_counter_file.write(str(hit_count)) | |
294 | + hit_counter_file.close() | |
295 | + | |
296 | + return hit_count | |
297 | + | |
298 | + | |
299 | +# ROUTING ##################################################################### | |
300 | + | |
301 | +@app.route('/favicon.ico') | |
302 | +def favicon(): | |
303 | + return send_from_directory( | |
304 | + join(app.root_path, 'static', 'img'), | |
305 | + 'favicon.ico', mimetype='image/vnd.microsoft.icon' | |
306 | + ) | |
307 | + | |
308 | + | |
309 | +@app.route("/") | |
310 | +@app.route("/home.html") | |
311 | +@app.route("/index.html") | |
312 | +def home(): | |
313 | + return render_view('home.html.jinja2', { | |
314 | + 'targets': config['targets'], | |
315 | + 'planets': [s for s in config['targets'] if s['type'] == 'planet'], | |
316 | + 'probes': [s for s in config['targets'] if s['type'] == 'probe'], | |
317 | + 'comets': [s for s in config['targets'] if s['type'] == 'comet'], | |
318 | + 'visits': increment_hit_counter(), | |
319 | + }) | |
320 | + | |
321 | + | |
322 | +@app.route("/<source>_<started_at>_<stopped_at>.csv") | |
323 | +def get_target_csv(source, started_at, stopped_at): | |
324 | + """ | |
325 | + Grab data and orbit data for the specified `target`, | |
326 | + rearrange it and return it as a CSV file. | |
327 | + `started_at` and `stopped_at` should be UTC. | |
328 | + """ | |
329 | + # http://cdpp1.cesr.fr/BASE/DDService/getDataUrl.php?dataSet=tao_ros_sw&StartTime=2014-02-23T10:00&StopTime=2016-02-24T23:59 | |
330 | + # Process input parameters | |
331 | + source_config = get_source_config(source) | |
332 | + date_fmt = "%Y-%m-%dT%H:%M:%S" | |
333 | + try: | |
334 | + started_at = datetime.datetime.strptime(started_at, date_fmt) | |
335 | + except: | |
336 | + abort(400, "Invalid started_at parameter : '%s'." % started_at) | |
337 | + try: | |
338 | + stopped_at = datetime.datetime.strptime(stopped_at, date_fmt) | |
339 | + except: | |
340 | + abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at) | |
341 | + | |
342 | + # todo: iterate on models when there are many | |
343 | + try: | |
344 | + model_slug = source_config['models'][0]['slug'] | |
345 | + except: | |
346 | + abort(500, "Invalid model configuration for '%s'." % source) | |
347 | + | |
348 | + filename = "%s_%s_%s.csv" % (source, | |
349 | + started_at.strftime(date_fmt), | |
350 | + stopped_at.strftime(date_fmt)) | |
351 | + | |
352 | + local_csv_file = get_path("../cache/%s" % filename) | |
353 | + if not isfile(local_csv_file): | |
354 | + with open(local_csv_file, mode="w+") as f: | |
355 | + f.write(generate_csv_contents(source_config, | |
356 | + started_at=started_at, | |
357 | + stopped_at=stopped_at)) | |
358 | + | |
359 | + if not isfile(local_csv_file): | |
360 | + abort(500, "Could not cache CSV file at '%s'." % local_csv_file) | |
361 | + | |
362 | + return send_from_directory(get_path("../cache/"), filename) | |
363 | + | |
364 | + | |
365 | +# @app.route("/<source>/data.csv") | |
366 | +# def get_orbiter_csv(source): | |
367 | +# """ | |
368 | +# DEPRECATED | |
369 | +# Grab data and orbit data for the specified `source`, | |
370 | +# rearrange it and return it as a CSV file. | |
371 | +# """ | |
372 | +# # http://cdpp1.cesr.fr/BASE/DDService/getDataUrl.php?dataSet=tao_ros_sw&StartTime=2014-02-23T10:00&StopTime=2016-02-24T23:59 | |
373 | +# # Process input parameters | |
374 | +# source_config = get_source_config(source) | |
375 | +# date_fmt = "%Y-%m-%dT%H:%M:%S" | |
376 | +# started_at = request.args.get('started_at') | |
377 | +# try: | |
378 | +# started_at = datetime.datetime.strptime(started_at, date_fmt) | |
379 | +# except: | |
380 | +# abort(400, "Invalid started_at parameter : '%s'." % started_at) | |
381 | +# stopped_at = request.args.get('stopped_at') | |
382 | +# try: | |
383 | +# stopped_at = datetime.datetime.strptime(stopped_at, date_fmt) | |
384 | +# except: | |
385 | +# abort(400, "Invalid stopped_at parameter : '%s'." % stopped_at) | |
386 | +# | |
387 | +# # todo: iterate on models when there are many | |
388 | +# try: | |
389 | +# model_slug = source_config['models'][0]['slug'] | |
390 | +# except: | |
391 | +# abort(500, "Invalid model configuration for '%s'." % source) | |
392 | +# | |
393 | +# # Grab the list of netCDF files from Myriam's API | |
394 | +# # http://cdpp.irap.omp.eu/BASE/DDService/getDataUrl.php?dataSet=jupiter_orb_all&StartTime=2014-02-23T10:00:10&StopTime=2017-02-24T23:59:00 | |
395 | +# # http://cdpp.irap.omp.eu/BASE/DATA/TAO/JUPITER/SW/sw_2014.nc.gz | |
396 | +# model_files = retrieve_data(source, model_slug, started_at, stopped_at) | |
397 | +# orbits_files = retrieve_data(source, source_config['orbit']['model'], started_at, stopped_at) | |
398 | +# | |
399 | +# si = StringIO.StringIO() | |
400 | +# cw = csv_writer(si) | |
401 | +# cw.writerow(( # the order matters ! | |
402 | +# 'time', | |
403 | +# 'vrad', 'vtan', 'vlen', | |
404 | +# 'magn', 'temp', 'pdyn', 'dens', 'angl', | |
405 | +# 'xhci', 'yhci' | |
406 | +# )) | |
407 | +# | |
408 | +# precision = "%Y-%m-%dT%H" # model and orbits times are equal-ish | |
409 | +# orbits_data = {} # keys are datetime as str, values arrays of XY | |
410 | +# for orbits_file in orbits_files: | |
411 | +# cdf_handle = Dataset(orbits_file, "r", format="NETCDF4") | |
412 | +# times = cdf_handle.variables['Time'] # YYYY DOY HH MM SS .ms | |
413 | +# data_hci = cdf_handle.variables['HCI'] | |
414 | +# for time, datum_hci in zip(times, data_hci): | |
415 | +# dtime = datetime_from_list(time) | |
416 | +# if started_at <= dtime <= stopped_at: | |
417 | +# dkey = dtime.strftime(precision) | |
418 | +# orbits_data[dkey] = datum_hci | |
419 | +# all_data = {} # keys are datetime as str, values tuples of data | |
420 | +# for model_file in model_files: | |
421 | +# # Time, StartTime, StopTime, V, B, N, T, Delta_angle, P_dyn | |
422 | +# cdf_handle = Dataset(model_file, "r", format="NETCDF4") | |
423 | +# times = cdf_handle.variables['Time'] # YYYY DOY HH MM SS .ms | |
424 | +# data_v = cdf_handle.variables['V'] | |
425 | +# data_b = cdf_handle.variables['B'] | |
426 | +# data_t = cdf_handle.variables['T'] | |
427 | +# data_n = cdf_handle.variables['N'] | |
428 | +# data_p = cdf_handle.variables['P_dyn'] | |
429 | +# data_d = cdf_handle.variables['Delta_angle'] | |
430 | +# for time, datum_v, datum_b, datum_t, datum_p, datum_n, datum_d \ | |
431 | +# in zip(times, data_v, data_b, data_t, data_n, data_p, data_d): | |
432 | +# vrad = datum_v[0] | |
433 | +# vtan = datum_v[1] | |
434 | +# dtime = datetime_from_list(time) | |
435 | +# if started_at <= dtime <= stopped_at: | |
436 | +# dkey = dtime.strftime(precision) | |
437 | +# x_hci = None | |
438 | +# y_hci = None | |
439 | +# if dkey in orbits_data: | |
440 | +# x_hci = orbits_data[dkey][0] | |
441 | +# y_hci = orbits_data[dkey][1] | |
442 | +# all_data[dkey] = ( | |
443 | +# dtime.strftime("%Y-%m-%dT%H:%M:%S+00:00"), | |
444 | +# vrad, vtan, sqrt(vrad * vrad + vtan * vtan), | |
445 | +# datum_b, datum_t, datum_n, datum_p, datum_d, | |
446 | +# x_hci, y_hci | |
447 | +# ) | |
448 | +# cdf_handle.close() | |
449 | +# | |
450 | +# for dkey in sorted(all_data): | |
451 | +# cw.writerow(all_data[dkey]) | |
452 | +# | |
453 | +# return si.getvalue() | |
454 | + | |
455 | + | |
324 | 456 | # DEV TOOLS ################################################################### |
325 | 457 | |
326 | 458 | @app.route("/inspect") | ... | ... |
web/static/js/swapp.js
... | ... | @@ -24,10 +24,12 @@ |
24 | 24 | return this$.parameters[p['id']] = p; |
25 | 25 | }); |
26 | 26 | } |
27 | - SpaceWeather.prototype.buildDataUrlForSource = function(source_slug){ | |
27 | + SpaceWeather.prototype.buildDataUrlForSource = function(source_slug, started_at, stopped_at){ | |
28 | 28 | var url; |
29 | 29 | url = this.configuration['api']['data_for_interval']; |
30 | 30 | url = url.replace('<source>', source_slug); |
31 | + url = url.replace('<started_at>', started_at); | |
32 | + url = url.replace('<stopped_at>', stopped_at); | |
31 | 33 | return url; |
32 | 34 | }; |
33 | 35 | SpaceWeather.prototype.addSource = function(source){ |
... | ... | @@ -71,7 +73,7 @@ |
71 | 73 | }); |
72 | 74 | }; |
73 | 75 | SpaceWeather.prototype.init = function(){ |
74 | - var active_sources, res$, k, this$ = this; | |
76 | + var active_sources, res$, k, started_at, stopped_at, this$ = this; | |
75 | 77 | res$ = []; |
76 | 78 | for (k in this.sources) { |
77 | 79 | if (this.sources[k].config.active) { |
... | ... | @@ -80,8 +82,12 @@ |
80 | 82 | } |
81 | 83 | active_sources = res$; |
82 | 84 | this.orbits = new Orbits(this.configuration.orbits_container, this.configuration); |
85 | + started_at = moment().subtract(1, 'years').hours(0).minutes(0).seconds(0); | |
86 | + stopped_at = moment().add(7, 'days').hours(0).minutes(0).seconds(0); | |
87 | + started_at = started_at.format("YYYY-MM-DDTHH:mm:ss"); | |
88 | + stopped_at = stopped_at.format("YYYY-MM-DDTHH:mm:ss"); | |
83 | 89 | active_sources.forEach(function(source){ |
84 | - return this$.loadData(source.slug, '2016-01-01T00:00:00', '2023-01-01T00:00:00').then(function(data){ | |
90 | + return this$.loadData(source.slug, started_at, stopped_at).then(function(data){ | |
85 | 91 | console.info("Loaded CSV data for " + source.slug + "."); |
86 | 92 | this$.createTimeSeries(source, data); |
87 | 93 | return this$.orbits.initOrbiter(source.slug, source.config, data['hci']); |
... | ... | @@ -98,8 +104,8 @@ |
98 | 104 | sw = this; |
99 | 105 | promise = new Promise(function(resolve, reject){ |
100 | 106 | var url; |
101 | - url = sw.buildDataUrlForSource(source_slug); | |
102 | - return d3.csv(url + ("?started_at=" + started_at + "&stopped_at=" + stopped_at), function(csv){ | |
107 | + url = sw.buildDataUrlForSource(source_slug, started_at, stopped_at); | |
108 | + return d3.csv(url, function(csv){ | |
103 | 109 | var timeFormat, data; |
104 | 110 | timeFormat = d3.timeParse('%Y-%m-%dT%H:%M:%S%Z'); |
105 | 111 | data = { | ... | ... |
web/static/js/swapp.ls
... | ... | @@ -32,9 +32,11 @@ export class SpaceWeather |
32 | 32 | @parameters[p['id']] = p |
33 | 33 | ) |
34 | 34 | |
35 | - buildDataUrlForSource: (source_slug) -> | |
35 | + buildDataUrlForSource: (source_slug, started_at, stopped_at) -> | |
36 | 36 | url = @configuration['api']['data_for_interval'] |
37 | 37 | url = url.replace('<source>', source_slug) |
38 | + url = url.replace('<started_at>', started_at) | |
39 | + url = url.replace('<stopped_at>', stopped_at) | |
38 | 40 | url |
39 | 41 | |
40 | 42 | addSource: (source) -> |
... | ... | @@ -63,8 +65,20 @@ export class SpaceWeather |
63 | 65 | init: -> |
64 | 66 | active_sources = [ @sources[k] for k of @sources when @sources[k].config.active ] |
65 | 67 | @orbits = new Orbits(@configuration.orbits_container, @configuration) |
68 | +# day = 864e5 # milliseconds | |
69 | +# now = new Date() | |
70 | +# started_at = new Date(+now - 365 * days) | |
71 | +# stopped_at = new Date(+now + 7 * days) | |
72 | +# year = new Date().getFullYear() | |
73 | +# started_at = "#{year - 1}-01-01T00:00:00" | |
74 | +# stopped_at = "#{year + 1}-01-01T00:00:00" | |
75 | + # Set the hours to zero so that files are cached for a day at most | |
76 | + started_at = moment().subtract(1, 'years').hours(0).minutes(0).seconds(0) | |
77 | + stopped_at = moment().add(7, 'days').hours(0).minutes(0).seconds(0) | |
78 | + started_at = started_at.format("YYYY-MM-DDTHH:mm:ss") | |
79 | + stopped_at = stopped_at.format("YYYY-MM-DDTHH:mm:ss") | |
66 | 80 | active_sources.forEach((source) ~> |
67 | - @loadData(source.slug, '2016-01-01T00:00:00', '2023-01-01T00:00:00').then( | |
81 | + @loadData(source.slug, started_at, stopped_at).then( | |
68 | 82 | (data) ~> |
69 | 83 | console.info "Loaded CSV data for #{source.slug}." |
70 | 84 | @createTimeSeries(source, data) |
... | ... | @@ -78,8 +92,8 @@ export class SpaceWeather |
78 | 92 | loadData: (source_slug, started_at, stopped_at) -> |
79 | 93 | sw = this |
80 | 94 | promise = new Promise((resolve, reject) -> |
81 | - url = sw.buildDataUrlForSource(source_slug) | |
82 | - d3.csv(url+"?started_at=#{started_at}&stopped_at=#{stopped_at}", (csv) -> | |
95 | + url = sw.buildDataUrlForSource(source_slug, started_at, stopped_at) | |
96 | + d3.csv(url, (csv) -> | |
83 | 97 | timeFormat = d3.timeParse('%Y-%m-%dT%H:%M:%S%Z') |
84 | 98 | data = {'hci': []}; |
85 | 99 | configuration['parameters'].forEach((parameter) -> | ... | ... |
web/view/home.html.jinja2
... | ... | @@ -53,6 +53,16 @@ |
53 | 53 | <a class="mdl-navigation__link parameter" data-ts-slug="angl" href="#">Angle Source-Sun-Earth</a> |
54 | 54 | </nav> |
55 | 55 | |
56 | + <div class="mdl-layout-spacer"></div> | |
57 | + | |
58 | + <main class="mdl-layout__content"> | |
59 | + <div class="page-content" style="text-align:center;font-style:italic;"> | |
60 | + <p> | |
61 | + {{ visits }} visits since 2017 | |
62 | + </p> | |
63 | + </div> | |
64 | + </main> | |
65 | + | |
56 | 66 | </div> |
57 | 67 | <main id="plots_wrapper" class="mdl-layout__content"> |
58 | 68 | |
... | ... | @@ -158,6 +168,7 @@ |
158 | 168 | |
159 | 169 | {% block scripts_footer %} |
160 | 170 | <script type="application/javascript" src="{{ static('js/vendor/d3.min.js') }}"></script> |
171 | +<script type="application/javascript" src="{{ static('js/vendor/moment.min.js') }}"></script> | |
161 | 172 | <script type="application/javascript" src="{{ static('js/swapp.js') }}"></script> |
162 | 173 | <script type="application/javascript"> |
163 | 174 | |
... | ... | @@ -165,8 +176,7 @@ var configuration = { |
165 | 176 | time_series_container: '#time_series', |
166 | 177 | orbits_container: '#orbits', |
167 | 178 | api : { |
168 | - 'data_for_interval': "{{ request.url_root }}<source>/data.csv" | |
169 | - // ?started_at=<started_at>&stopped_at=<stopped_at> | |
179 | + 'data_for_interval': "{{ request.url_root }}<source>_<started_at>_<stopped_at>.csv" | |
170 | 180 | }, |
171 | 181 | sun: { |
172 | 182 | img: '{{ static('img/sun_128.png') }}' | ... | ... |