Commit a4a9ef0393ad3b8e968d48d06398edbd145adff3

Authored by Goutte
1 parent 79a09c02

Cache generated CSVs, and serve them as static files so that clients can cache t…

…hem too. Implement a visits counter.
.gitignore
1 1 # Local configuration
2 2 secret.yml
3 3  
  4 +# Local databases
  5 +cache/*
  6 +VISITS
  7 +
4 8 # Dolphin
5 9 .directory
6 10  
... ...
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') }}'
... ...