Commit 3bb894520030c57ede4637672ca142fb2310dc88
1 parent
d014ea17
Exists in
master
feat: add a world map with legs of trips
Showing
2 changed files
with
244 additions
and
29 deletions
Show diff stats
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 %} | ... | ... |