Commit 3bb894520030c57ede4637672ca142fb2310dc88

Authored by Antoine Goutenoir
1 parent d014ea17
Exists in master

feat: add a world map with legs of trips

flaskr/controllers/main_controller.py
1   -import traceback
  1 +import csv
  2 +import re
  3 +# from io import StringIO
  4 +from cStringIO import StringIO
2 5 from copy import deepcopy
  6 +from os import unlink
  7 +from os.path import join
3 8  
4   -import re
5 9 import geopy
  10 +import pandas
6 11 import sqlalchemy
7   -
8   -from os.path import join
9   -from os import unlink, getenv
10   -
11 12 from flask import (
12 13 Blueprint,
13 14 Response,
14 15 render_template,
15 16 flash,
16   - request,
17 17 redirect,
18 18 url_for,
19 19 abort,
20 20 send_from_directory,
21 21 )
22   -from flaskr.extensions import cache, basic_auth, mail, send_email
23   -from flaskr.forms import LoginForm, EstimateForm
24   -from flaskr.models import db, User, Estimation, StatusEnum, ScenarioEnum
25   -from flaskr.geocoder import CachedGeocoder
  22 +from pandas.compat import StringIO as PandasStringIO
  23 +from wtforms import validators
  24 +from yaml import safe_dump as yaml_dump
26 25  
  26 +from flaskr.content import content, base_url
27 27 from flaskr.core import (
28   - generate_unique_id,
29 28 get_emission_models,
30 29 increment_hit_counter,
31 30 )
32   -from flaskr.content import content, base_url
33   -
34   -from wtforms import validators
35   -
36   -from yaml import safe_dump as yaml_dump
37   -
38   -import csv
39   -# from io import StringIO
40   -from cStringIO import StringIO
41   -
42   -import pandas
43   -from pandas.compat import StringIO as PandasStringIO
  31 +from flaskr.extensions import cache, send_email
  32 +from flaskr.forms import EstimateForm
  33 +from flaskr.geocoder import CachedGeocoder
  34 +from flaskr.models import db, Estimation, StatusEnum, ScenarioEnum
44 35  
45 36 main = Blueprint('main', __name__)
46 37  
... ... @@ -708,6 +699,9 @@ def compute(): # process the queue of estimation requests
708 699 return _respond(errmsg)
709 700  
710 701  
  702 +unavailable_statuses = [StatusEnum.pending, StatusEnum.working]
  703 +
  704 +
711 705 @main.route("/estimation/<public_id>.<extension>")
712 706 def consult_estimation(public_id, extension):
713 707 try:
... ... @@ -724,8 +718,6 @@ def consult_estimation(public_id, extension):
724 718 # if format not in allowed_formats:
725 719 # abort(404)
726 720  
727   - unavailable_statuses = [StatusEnum.pending, StatusEnum.working]
728   -
729 721 if extension in ['xhtml', 'html', 'htm']:
730 722  
731 723 if estimation.status in unavailable_statuses:
... ... @@ -795,6 +787,132 @@ def consult_estimation(public_id, extension):
795 787 abort(404)
796 788  
797 789  
  790 +def get_locations(addresses):
  791 + geocoder = CachedGeocoder()
  792 +
  793 + warnings = []
  794 + addresses_count = len(addresses)
  795 + failed_addresses = []
  796 + locations = []
  797 +
  798 + for i in range(addresses_count):
  799 +
  800 + address = addresses[i].strip()
  801 + unicode_address = address.encode('utf-8')
  802 +
  803 + if not address:
  804 + continue
  805 +
  806 + if address in failed_addresses:
  807 + continue
  808 +
  809 + try:
  810 + location = geocoder.geocode(unicode_address)
  811 + except geopy.exc.GeopyError as e:
  812 + warning = u"Ignoring address `%s` " \
  813 + u"since we failed to geocode it.\n%s\n" % (
  814 + address, e,
  815 + )
  816 + warnings.append(warning)
  817 + failed_addresses.append(address)
  818 + continue
  819 +
  820 + if location is None:
  821 + warning = u"Ignoring address `%s` " \
  822 + u"since we failed to geocode it.\n" % (
  823 + address,
  824 + )
  825 + warnings.append(warning)
  826 + failed_addresses.append(address)
  827 + failed_addresses.append(address)
  828 + continue
  829 +
  830 + print("Geocoded Location:\n", repr(location.raw))
  831 + locations.append(location)
  832 +
  833 + # response += u"Location `%s` geocoded to `%s` (%f, %f).\n" % (
  834 + # location_address, location.address,
  835 + # location.latitude, location.longitude,
  836 + # )
  837 +
  838 + return locations, warnings
  839 +
  840 +
  841 +@main.route("/estimation/<public_id>/trips_to_destination_<destination_index>.csv")
  842 +def get_trips_csv(public_id, destination_index=0):
  843 + destination_index = int(destination_index)
  844 + try:
  845 + estimation = Estimation.query \
  846 + .filter_by(public_id=public_id) \
  847 + .one()
  848 + except sqlalchemy.orm.exc.NoResultFound:
  849 + return abort(404)
  850 + except Exception as e:
  851 + return abort(500)
  852 +
  853 + if estimation.status in unavailable_statuses:
  854 + abort(404)
  855 +
  856 + si = StringIO()
  857 + cw = csv.writer(si, quoting=csv.QUOTE_ALL)
  858 + cw.writerow([
  859 + u"origin_lon",
  860 + u"origin_lat",
  861 + u"destination_lon",
  862 + u"destination_lat",
  863 + ])
  864 + results = estimation.get_output_dict()
  865 +
  866 + if not 'cities' in results:
  867 + abort(500)
  868 +
  869 + cities_length = len(results['cities'])
  870 +
  871 + if 0 == cities_length:
  872 + abort(500, Response("No cities in results."))
  873 +
  874 + destination_index = min(destination_index, cities_length - 1)
  875 + destination_index = max(destination_index, 0)
  876 +
  877 + city = results['cities'][destination_index]
  878 + # >>> yaml_dump(city)
  879 + # address: Paris, Ile - de - France, Metropolitan
  880 + # France, France
  881 + # city: Paris
  882 + # country: ' France'
  883 + # distance: 1752.7481921181325
  884 + # footprint: 824.9628320703453
  885 + # plane_trips: 1
  886 + # train_trips: 0
  887 +
  888 + geocoder = CachedGeocoder()
  889 + try:
  890 + city_location = geocoder.geocode(city['address'].encode('utf-8'))
  891 + except geopy.exc.GeopyError as e:
  892 + return Response(
  893 + response=si.getvalue().strip('\r\n'),
  894 + )
  895 +
  896 + other_locations, _warnings = get_locations(estimation.origin_addresses.split("\n"))
  897 + # destination_locations = get_locations(estimation.destination_addresses.split("\n"))
  898 + for other_location in other_locations:
  899 + cw.writerow([
  900 + u"%.8f" % city_location.longitude,
  901 + u"%.8f" % city_location.latitude,
  902 + u"%.8f" % other_location.longitude,
  903 + u"%.8f" % other_location.latitude,
  904 + ])
  905 +
  906 + filename = "trips_to_destination_%d.csv" % destination_index
  907 + return Response(
  908 + response=si.getvalue().strip('\r\n'),
  909 + headers={
  910 + 'Content-type': 'text/csv',
  911 + 'Content-disposition': 'attachment; filename=%s' % filename,
  912 + },
  913 + )
  914 +
  915 +
798 916 @main.route("/scaling_laws.csv")
799 917 def get_scaling_laws_csv():
800 918 distances = content.laws_plot.distances
... ... @@ -824,8 +942,6 @@ def get_scaling_laws_csv():
824 942 @main.route("/test")
825 943 # @basic_auth.required
826 944 def dev_test():
827   - import os
828   -
829 945 # email_content = render_template(
830 946 # 'email/run_completed.html',
831 947 # # run=run,
... ...
flaskr/templates/estimation.html
... ... @@ -144,6 +144,7 @@
144 144 {{ content.estimation.lolliplot.many_to_many | markdown | safe }}
145 145 {{ render_cities(estimation_output.cities) }}
146 146 {% endif %}
  147 +
147 148 </div>
148 149  
149 150 <div class="col-md-6">
... ... @@ -185,6 +186,11 @@
185 186 {# </a>#}
186 187 {# </li>#}
187 188 </ul>
  189 +
  190 + <hr>
  191 +
  192 + <div id="d3viz_travels" class="plot-container-noborder"></div>
  193 +
188 194 </div>
189 195  
190 196 </div>
... ... @@ -206,7 +212,7 @@
206 212 {{ content.estimation.footer | markdown | safe }}
207 213 </div>
208 214  
209   -{# Buffer to drop the PNG image into to hack firefox into downloading the PNG #}
  215 +{# Buffer to drop the PNG image into to trick firefox into downloading the PNG #}
210 216 <div id="png_buffer"></div>
211 217  
212 218 {% endif %}{# not estimation.has_failed() #}
... ... @@ -220,6 +226,8 @@
220 226  
221 227 <script src="/static/js/vendor/d3.v4.js"></script>
222 228 <script src="/static/js/vendor/d3-legend.js"></script>
  229 +<script src="/static/js/vendor/d3-scale-chromatic.v1.min.js"></script>
  230 +<script src="/static/js/vendor/d3-geo-projection.v2.min.js"></script>
223 231 {#<script src="https://d3js.org/d3.v4.js"></script>#}
224 232 {#<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.6/d3-legend.js"></script>#}
225 233  
... ... @@ -580,6 +588,97 @@ jQuery(document).ready(function($){
580 588  
581 589 });
582 590  
  591 +
  592 +jQuery(document).ready(function($) {
  593 + var vizid = "#d3viz_travels";
  594 + var worldDataUrl = "/static/public/data/world.geojson";
  595 + {#var travelsDataUrl = "/static/public/data/data_connectionmap_test.csv";#}
  596 + var travelsDataUrl = "/estimation/{{ estimation.public_id }}/trips_to_destination_0.csv";
  597 +
  598 + {#var margin = {top: 40, right: 40, bottom: 150, left: 180},#}
  599 + var margin = {top: 0, right: 0, bottom: 0, left: 0};
  600 + var width = $(vizid).parent().width();
  601 + var height = width - margin.top - margin.bottom;
  602 + height *= 1.0;
  603 + {#var height = Math.max(300, 600) - margin.top - margin.bottom;#}
  604 + {#var width = Math.max(880, $(vizid).parent().width());#}
  605 + width = width - margin.left - margin.right;
  606 +
  607 + var size_ratio = 0.85;
  608 + width = 629.0 * size_ratio;
  609 + height = 604.0 * size_ratio;
  610 + {#width = 500.0;#}
  611 + {#height = 400.0;#}
  612 +
  613 + var offset_x = 0.0;
  614 + var offset_y = 0.0;
  615 +
  616 + var svg = d3.select(vizid)
  617 + .append("svg")
  618 + .attr("width", width + margin.left + margin.right)
  619 + .attr("height", height + margin.top + margin.bottom);
  620 +
  621 + var projection = d3.geoMercator()
  622 + .scale(85)
  623 + .translate([width / 2.0 + offset_x, height / 2.0 + offset_y]);
  624 +
  625 + {#var projection = d3.geoMercator()#}
  626 + {# .scale(85)#}
  627 + {# .translate([width / 2, height / 2 * 1.3]);#}
  628 +
  629 + // A path generator
  630 + var geopath = d3.geoPath().projection(projection);
  631 +
  632 + // Load world shape AND list of connection
  633 + d3.queue()
  634 + .defer(d3.json, worldDataUrl)
  635 + {#.defer(d3.json, "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/world.geojson") // World shape#}
  636 + .defer(d3.csv, travelsDataUrl)
  637 + {#.defer(d3.csv, "https://raw.githubusercontent.com/holtzy/D3-graph-gallery/master/DATA/data_connectionmap.csv") // Position of circles#}
  638 + .await(on_map_ready);
  639 +
  640 + function on_map_ready(error, dataGeo, data) {
  641 +
  642 + // Reformat the list of link. Note that columns in csv file are called long1, long2, lat1, lat2
  643 + var link = [];
  644 + data.forEach(function (row) {
  645 + source = [+row.origin_lon, +row.origin_lat];
  646 + target = [+row.destination_lon, +row.destination_lat];
  647 + topush = {type: "LineString", coordinates: [source, target]};
  648 + link.push(topush);
  649 + });
  650 +
  651 + // Draw the map
  652 + svg.append("g")
  653 + .selectAll("path")
  654 + .data(dataGeo.features)
  655 + .enter()
  656 + .append("path")
  657 + .attr("fill", "#b8b8b8")
  658 + .attr("d", geopath)
  659 + .style("stroke", "#fff")
  660 + .style("stroke-width", 0);
  661 +
  662 + // Add the path
  663 + svg.selectAll("myPath")
  664 + .data(link)
  665 + .enter()
  666 + .append("path")
  667 + .attr("d", geopath)
  668 +// .attr("d", function (d) {
  669 +// return geopath(d);
  670 + // })
  671 + .style("fill", "none")
  672 + .style("stroke", "#69b3a2")
  673 + .style("stroke-width", 2);
  674 +
  675 + }
  676 +
  677 +});
  678 +
  679 +
  680 +
  681 +
583 682 </script>
584 683  
585 684 {% endif %}
... ...