Skip to content

Commit

Permalink
Merge pull request #2 from beveradb/add-basic-web-ui
Browse files Browse the repository at this point in the history
Created responsive web UI to replace primitive CLI usage, added drive time and variety of robustness improvements.
  • Loading branch information
beveradb authored Oct 6, 2019
2 parents b158c83 + 1b3352d commit 9f42964
Show file tree
Hide file tree
Showing 24 changed files with 771 additions and 130 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.DS_Store
mapbox-polygon-temp.html
__pycache__
*.pyc
*.swp
.idea
2 changes: 0 additions & 2 deletions .idea/.gitignore

This file was deleted.

11 changes: 0 additions & 11 deletions .idea/home-hunting-assistance.iml

This file was deleted.

7 changes: 0 additions & 7 deletions .idea/inspectionProfiles/profiles_settings.xml

This file was deleted.

7 changes: 0 additions & 7 deletions .idea/misc.xml

This file was deleted.

8 changes: 0 additions & 8 deletions .idea/modules.xml

This file was deleted.

6 changes: 0 additions & 6 deletions .idea/vcs.xml

This file was deleted.

1 change: 1 addition & 0 deletions Procfile
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
web: python run_server.py
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ If you've ever been looking for a property to rent or buy, but:

Well, this tool is here to help solve that, with data! 😎

Latest release hosted on Heroku here: <https://home-area-helper.herokuapp.com>

This should help you generate a meaningful search area which factors in your personal preferences for:

- Maximum walking time from a desired location (optional)
Expand All @@ -17,10 +19,10 @@ This should help you generate a meaningful search area which factors in your per

It'll generate shapes for each of those filters based on the parameters you specify, then find the intersection of all of those shapes, representing the area you want to search in:

![alt text](https://raw.githubusercontent.com/beveradb/home-area-helper/master/screenshots/polygon-debugging.png "Debug Output")
![alt text](https://github.com/beveradb/home-area-helper/blob/master/screenshots/web-ui-example-1.png?raw=true "Web UI")

It'll then produce an encoded URL which is compatible with the leading property search site in the UK, Zoopla, and launch that in a web browser for your convenience:

![alt text](https://github.com/beveradb/home-area-helper/blob/master/screenshots/end-result-zoopla.png?raw=true "Zoopla Output")
![alt text](https://github.com/beveradb/home-area-helper/blob/master/screenshots/web-ui-example-3.png?raw=true "Zoopla Output")

You can then set up an alert for that search on Zoopla's site so you get an immediate notification whenever a new property comes on the market in your budget in that area!
30 changes: 0 additions & 30 deletions generate_search.py

This file was deleted.

3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ celery
fiona
descartes
pyproj
geopy
geopy
flask
49 changes: 49 additions & 0 deletions run_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env python3
import os

from flask import Flask, render_template, jsonify
from shapely.geometry import mapping

from src import target_area

app = Flask(__name__)


@app.route('/')
def index():
# This script requires you have environment variables set with your personal API keys:
# MAPBOX_ACCESS_TOKEN from https://docs.mapbox.com/help/how-mapbox-works/access-tokens/
# TRAVELTIME_APP_ID from https://docs.traveltimeplatform.com/overview/introduction
# TRAVELTIME_API_KEY from https://docs.traveltimeplatform.com/overview/introduction

return render_template(
'index.html',
MAPBOX_ACCESS_TOKEN=os.environ['MAPBOX_ACCESS_TOKEN']
)


@app.route('/target_area/<string:target>/<int:walking>/<int:pubtrans>/<int:driving>/<int:deprivation>')
def target_area_json(
target: str,
walking: int,
pubtrans: int,
driving: int,
deprivation: int
):
polygon_results = target_area.get_target_area_polygons(
target_location_address=target,
min_deprivation_score=deprivation,
max_walking_time_mins=walking,
max_public_transport_travel_time_mins=pubtrans,
max_driving_time_mins=driving
)
for key, value in polygon_results.items():
if 'polygon' in value:
polygon_results[key]['polygon'] = mapping(value['polygon'])

return jsonify(polygon_results)


if __name__ == '__main__':
port = os.environ['PORT'] if 'PORT' in os.environ else 9876
app.run(debug=True, host='0.0.0.0', port=port, use_evalex=False)
1 change: 1 addition & 0 deletions runtime.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-3.7.4
Binary file added screenshots/web-ui-example-1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/web-ui-example-2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added screenshots/web-ui-example-3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 5 additions & 7 deletions src/deprivation.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,15 @@ def get_polygon_for_least_deprived_zones_uk(minimum_deprivation_rank):
))


def get_simplified_clipped_uk_deprivation_polygon(min_deprivation_score, target_lng_lat, clip_distance_miles):

def get_simplified_clipped_uk_deprivation_polygon(min_deprivation_score, bounding_poly):
imd_filter_multi_polygon = get_polygon_for_least_deprived_zones_uk(min_deprivation_score)
# print("imdFilterMultiPolygons after deprivation filter: " + str(len(imdFilterMultiPolygon)))

imd_filter_multi_polygon = multi_polygons.filter_uk_multipoly_by_target_radius(imd_filter_multi_polygon,
target_lng_lat,
clip_distance_miles)
# print("imdFilterMultiPolygons after distance filter: " + str(len(imdFilterMultiPolygon)))
imd_filter_multi_polygon = multi_polygons.filter_uk_multipoly_by_bounding_box(imd_filter_multi_polygon,
bounding_poly)
# print("imdFilterMultiPolygons after bounds filter: " + str(len(imdFilterMultiPolygon)))

imd_filter_multi_polygon = multi_polygons.simplify(imd_filter_multi_polygon, 0.001)
imd_filter_multi_polygon = multi_polygons.simplify_multi(imd_filter_multi_polygon, 0.001)
imd_filter_combined_polygon = multi_polygons.convert_multi_to_single_with_joining_lines(imd_filter_multi_polygon)

u_kto_w_g_s84_project = partial(pyproj.transform, pyproj.Proj(init='epsg:27700'), pyproj.Proj(init='epsg:4326'))
Expand Down
14 changes: 7 additions & 7 deletions src/mapbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ def get_centre_point_lng_lat_for_address(address_string):
return target_location_geocode_feature['geometry']['coordinates']


def get_walking_isochrone_geometry(target_lng_lat, max_walking_time_mins):
walking_isochrone_url = "https://api.mapbox.com/isochrone/v1/mapbox/walking/"
walking_isochrone_url += str(target_lng_lat[0]) + "," + str(target_lng_lat[1])
walking_isochrone_url += "?contours_minutes=" + str(max_walking_time_mins)
walking_isochrone_url += "&access_token=" + os.environ['MAPBOX_ACCESS_TOKEN']
def get_isochrone_geometry(target_lng_lat, max_travel_time_mins, travel_mode):
isochrone_url = "https://api.mapbox.com/isochrone/v1/mapbox/" + travel_mode + "/"
isochrone_url += str(target_lng_lat[0]) + "," + str(target_lng_lat[1])
isochrone_url += "?contours_minutes=" + str(max_travel_time_mins)
isochrone_url += "&access_token=" + os.environ['MAPBOX_ACCESS_TOKEN']

walking_isochrone_response_object = json.load(
urllib.request.urlopen(walking_isochrone_url)
urllib.request.urlopen(isochrone_url)
)

return walking_isochrone_response_object['features'][0]['geometry']['coordinates']
Expand All @@ -45,7 +45,7 @@ def view_polygon_in_browser(single_polygon):
single_poly_rep = single_polygon.representative_point()

temp_map_plot_filename = "mapbox-polygon-temp.html"
jinja2.Template(open("src/mapbox-polygon-template.html").read()).stream(
jinja2.Template(open("templates/single-polygon.html").read()).stream(
MAPBOX_ACCESS_TOKEN=os.environ['MAPBOX_ACCESS_TOKEN'],
MAP_CENTER_POINT_COORD="[" + str(single_poly_rep.x) + "," + str(single_poly_rep.y) + "]",
MAP_LAYER_GEOJSON=[single_polygon_coords]
Expand Down
51 changes: 37 additions & 14 deletions src/multi_polygons.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ def get_bounding_circle_for_point(target_lng_lat, bounding_box_radius_miles):
def filter_uk_multipoly_by_target_radius(multi_polygon_to_filter, target_lng_lat, max_distance_limit_miles):
multi_polygon_to_filter = convert_list_to_multi_polygon(multi_polygon_to_filter)

w_g_s84to_uk_project = partial(pyproj.transform, pyproj.Proj(init='epsg:4326'), pyproj.Proj(init='epsg:27700'))
wgs84_to_uk_project = partial(pyproj.transform, pyproj.Proj(init='epsg:4326'), pyproj.Proj(init='epsg:27700'))

target_bounding_circle = get_bounding_circle_for_point(target_lng_lat, max_distance_limit_miles)
target_bounding_circle_uk_project = shapely.ops.transform(w_g_s84to_uk_project, target_bounding_circle)
target_bounding_circle_uk_project = shapely.ops.transform(wgs84_to_uk_project, target_bounding_circle)

filtered_multipolygon = []
for singlePolygon in multi_polygon_to_filter:
Expand All @@ -31,18 +31,37 @@ def filter_uk_multipoly_by_target_radius(multi_polygon_to_filter, target_lng_lat
return filtered_multipolygon


def simplify(multi_polygon_to_simplify, simplification_factor):
def filter_uk_multipoly_by_bounding_box(multi_polygon_to_filter, wgs84_bounding_polygon):
multi_polygon_to_filter = convert_list_to_multi_polygon(multi_polygon_to_filter)

wgs84_to_uk_project = partial(pyproj.transform, pyproj.Proj(init='epsg:4326'), pyproj.Proj(init='epsg:27700'))
uk_bounds_polygon = shapely.ops.transform(wgs84_to_uk_project, wgs84_bounding_polygon)

filtered_multipolygon = []
for singlePolygon in multi_polygon_to_filter:
if uk_bounds_polygon.contains(singlePolygon.centroid):
filtered_multipolygon.append(singlePolygon)

return filtered_multipolygon


def simplify_multi(multi_polygon_to_simplify, simplification_factor):
multi_polygon_to_simplify = convert_list_to_multi_polygon(multi_polygon_to_simplify)

if multi_polygon_to_simplify is Polygon:
return multi_polygon_to_simplify.simplify(simplification_factor)

simplified_multipolygon = []
for singlePolygon in multi_polygon_to_simplify:
simplified_single_polygon = singlePolygon.simplify(simplification_factor)
simplified_multipolygon.append(simplified_single_polygon)
try:
simplified_multipolygon = []
for singlePolygon in multi_polygon_to_simplify:
simplified_single_polygon = singlePolygon.simplify(simplification_factor)
simplified_multipolygon.append(simplified_single_polygon)
return simplified_multipolygon
except TypeError as te:
# Unsure why we're still getting here occasionally despite if statement above, but meh
pass

return simplified_multipolygon
return multi_polygon_to_simplify


def convert_multi_to_single_with_joining_lines(multi_polygon_to_join):
Expand Down Expand Up @@ -82,12 +101,16 @@ def convert_multi_to_single_with_joining_lines(multi_polygon_to_join):


def convert_list_to_multi_polygon(multi_polygon_list):
# If the object passed in isn't a list, assume it's already a MultiPolygon and do nothing for easier recursion
if type(multi_polygon_list) == list and len(multi_polygon_list) > 0:
if type(multi_polygon_list[0]) is not Polygon:
polygons_list = [Polygon(singlePolygonList) for singlePolygonList in multi_polygon_list]
else:
polygons_list = multi_polygon_list

multi_polygon_list = shapely.ops.unary_union(polygons_list)
# For each polygon in the list, ensure it is actually a Polygon object and buffer to remove self-intersections
refined_polygons_list = []
for single_polygon in multi_polygon_list:
if type(single_polygon) is not Polygon:
single_polygon = Polygon(single_polygon)
refined_polygons_list.append(single_polygon.buffer(0.0000001))

# Once we have a buffered list of Polygons, combine into a single Polygon or MultiPolygon if there are gaps
multi_polygon_list = shapely.ops.unary_union(refined_polygons_list)

return multi_polygon_list
Loading

0 comments on commit 9f42964

Please sign in to comment.