From eadea7ba1cd490891164075181c424b41c19c5fb Mon Sep 17 00:00:00 2001 From: Andrew Beveridge Date: Sat, 23 Nov 2019 17:54:01 +0000 Subject: [PATCH 1/5] Added foundations of new page to identify target cities, split reusable code into shared files --- run_server.py | 30 ++- src/target_area.py | 2 + src/target_cities.py | 58 ++++++ static/{main.css => shared.css} | 0 static/shared.js | 157 ++++++++++++++ static/{main.js => target_area.js} | 191 +++--------------- static/target_cities.js | 65 ++++++ .../{single-polygon.html => polygon.html} | 0 templates/{index.html => target_area.html} | 18 +- templates/target_cities.html | 106 ++++++++++ 10 files changed, 451 insertions(+), 176 deletions(-) create mode 100644 src/target_cities.py rename static/{main.css => shared.css} (100%) create mode 100644 static/shared.js rename static/{main.js => target_area.js} (84%) create mode 100644 static/target_cities.js rename templates/{single-polygon.html => polygon.html} (100%) rename templates/{index.html => target_area.html} (97%) create mode 100644 templates/target_cities.html diff --git a/run_server.py b/run_server.py index e6bb5cc..ccc2060 100755 --- a/run_server.py +++ b/run_server.py @@ -10,7 +10,7 @@ from flask import Flask, render_template, Response, request from flask_sslify import SSLify -from src import target_area, utils +from src import utils from src.utils import preload_files app = Flask(__name__) @@ -43,23 +43,45 @@ @app.route('/') -def index(): +def target_area_request(): # This script requires you define environment variables 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', + 'target_area.html', MAPBOX_ACCESS_TOKEN=os.environ['MAPBOX_ACCESS_TOKEN'] ) +@app.route('/target-cities') +def target_cities_request(): + return render_template( + 'target_cities.html', + MAPBOX_ACCESS_TOKEN=os.environ['MAPBOX_ACCESS_TOKEN'] + ) + + +@app.route('/target_cities', methods=['POST']) +def target_cities_json(): + req_data = request.get_json() + logging.log(logging.INFO, "Target cities request received: " + str(req_data)) + + from src import target_cities + results = target_cities.get_target_cities_data_json(req_data) + + utils.log_method_timings() + + return Response(results, mimetype='application/json') + + @app.route('/target_area', methods=['POST']) def target_area_json(): req_data = request.get_json() - logging.log(logging.INFO, "Request received: " + str(req_data)) + logging.log(logging.INFO, "Target area request received: " + str(req_data)) + from src import target_area results = target_area.get_target_areas_polygons_json(req_data) utils.log_method_timings() diff --git a/src/target_area.py b/src/target_area.py index 845b9ce..20cc1a7 100644 --- a/src/target_area.py +++ b/src/target_area.py @@ -85,6 +85,8 @@ def get_target_area_polygons( result_polygons_length = 1 if not hasattr(result_polygons, 'geoms') else len(result_polygons.geoms) logging.info("Total result_polygons after transport min area filter: " + str(result_polygons_length)) else: + if fallback_radius_miles == 0: + fallback_radius_miles = 1 result_intersection = get_bounding_circle_for_point(target_lng_lat, fallback_radius_miles) if result_polygons_length > 0: diff --git a/src/target_cities.py b/src/target_cities.py new file mode 100644 index 0000000..09003e8 --- /dev/null +++ b/src/target_cities.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import json +import logging + +from shapely.geometry import mapping + +from src import google_maps +from src.multi_polygons import get_bounding_circle_for_point, join_multi_to_single_poly +from src.utils import timeit + + +def get_target_cities(params: dict): + city_center_coords = google_maps.get_centre_point_lng_lat_for_address( + str(params['countryInput']) + ) + + city_polygon = get_bounding_circle_for_point(city_center_coords, 2) + + return [ + { + 'label': str(params['countryInput']), + 'coords': city_center_coords, + 'polygon': city_polygon + } + ] + + +@timeit +def get_target_cities_data_json(params: dict): + response_object = { + 'targets_results': [], + 'result_intersection': None + } + + target_cities_polygons = [] + target_cities = get_target_cities(params) + + for target_city in target_cities: + response_object['targets_results'].append({ + 'target': { + 'label': target_city['label'], + 'coords': target_city['coords'] + } + }) + target_cities_polygons.append(target_city['polygon']) + + if target_cities_polygons: + logging.debug(target_cities_polygons) + joined_cities = join_multi_to_single_poly(target_cities_polygons) + + response_object['results_combined'] = { + 'label': 'All Cities Combined', + 'bounds': joined_cities.bounds, + 'centroid': mapping(joined_cities.centroid)['coordinates'], + 'polygon': mapping(joined_cities) + } + + return json.dumps(response_object) diff --git a/static/main.css b/static/shared.css similarity index 100% rename from static/main.css rename to static/shared.css diff --git a/static/shared.js b/static/shared.js new file mode 100644 index 0000000..7f64574 --- /dev/null +++ b/static/shared.js @@ -0,0 +1,157 @@ +window.mainMap = { + 'map': null, + 'currentMarkers': [], + 'currentLayers': [] +}; + +// Accessible, distinct colours from https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ +window.hah_layer_colours = ["#e6194B", "#4363d8", "#f58231", "#f032e6", "#469990", "#9A6324", "#800000", "#000075"]; + +function show_iframe_error_modal(error_message_html) { + $('#messageModalTitle').text("Server Error"); + let errorFrame = $(""); + errorFrame.attr('srcdoc', error_message_html); + $('#messageModalBody').empty().append(errorFrame); + $('#messageModal').modal(); +} + +function show_html_modal(title, message) { + $('#messageModalTitle').text(title); + $('#messageModalBody').html(message); + $('#messageModal').modal(); +} + +function plot_marker(label, coords) { + let popup = new mapboxgl.Popup({offset: 25}) + .setText(label); + + let targetMarker = new mapboxgl.Marker() + .setLngLat(coords) + .setPopup(popup) + .addTo(map); + + window.mainMap.currentMarkers.push(targetMarker); +} + +function plot_polygon(id, label, polygon, color, opacity = 0.3, visible = true) { + window.mainMap.currentLayers.push(id); + + let menuItem = $( + "" + label + "" + ); + + if (visible) menuItem.addClass('active'); + + menuItem.click(function (e) { + let visibility = window.mainMap.map.getLayoutProperty(id, 'visibility'); + + if (visibility === 'visible') { + window.mainMap.map.setLayoutProperty(id, 'visibility', 'none'); + $(this).removeClass('active'); + } else { + $(this).addClass('active'); + window.mainMap.map.setLayoutProperty(id, 'visibility', 'visible'); + } + return false; + }); + + $("#map-filter-menu").append(menuItem); + + window.mainMap.map.addLayer({ + 'id': id, + 'type': 'fill', + 'source': { + 'type': 'geojson', + 'data': { + 'type': 'Feature', + 'geometry': polygon + } + }, + 'layout': { + 'visibility': (visible ? 'visible' : 'none') + }, + 'paint': { + 'fill-color': color, + 'fill-opacity': opacity + }, + 'metadata': { + 'home-area-helper': true + } + }); +} + +function clear_map() { + $('#map-filter-menu').empty().hide(); + + if (window.mainMap.currentMarkers !== null) { + for (let i = window.mainMap.currentMarkers.length - 1; i >= 0; i--) { + window.mainMap.currentMarkers[i].remove(); + } + } + + if (window.mainMap.currentLayers !== null && window.mainMap.map) { + let hhaLayers = window.mainMap.map.getStyle().layers.filter(function (el) { + return (el['metadata'] && el['metadata']['home-area-helper']); + }); + + for (let i = hhaLayers.length - 1; i >= 0; i--) { + window.mainMap.map.removeLayer(hhaLayers[i]['id']); + window.mainMap.map.removeSource(hhaLayers[i]['id']); + } + } +} + +function validate_and_submit_request() { + if (check_form_validity() === false) { + return; + } + + toggle_loading_state(); + + request_and_plot_results( + get_results_url(), + build_input_params_array(), + function () { + toggle_loading_state(); + + if (typeof after_plot_callback !== "undefined") { + after_plot_callback(); + } + + // For UX on mobile devices where map starts off screen + $('#map').get(0).scrollIntoView(); + }, + function (jqXHR) { + show_iframe_error_modal(jqXHR.responseText); + toggle_loading_state(); + } + ); +} + +function request_and_plot_results( + requestURL, + inputParams, + successCallback, + errorCallback +) { + clear_map(window.mainMap.map); + + $.ajax({ + url: requestURL, + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(inputParams), + success: function (data) { + plot_results(data); + + if (successCallback) { + successCallback(); + } + }, + error: function (jqXHR, textStatus) { + if (errorCallback) { + errorCallback(jqXHR, textStatus); + } + } + }); +} diff --git a/static/main.js b/static/target_area.js similarity index 84% rename from static/main.js rename to static/target_area.js index ece3786..e7e47df 100644 --- a/static/main.js +++ b/static/target_area.js @@ -1,9 +1,3 @@ -window.mainMap = { - 'map': null, - 'currentMarkers': [], - 'currentLayers': [] -}; - function map_loaded(map) { window.mainMap.map = map; @@ -76,6 +70,10 @@ $(function () { }); }); +function get_results_url() { + return "/target_area"; +} + function check_and_load_search_from_url_hash() { if (window.location.hash) { let search_to_load = window.location.hash.split('#')[1]; @@ -288,7 +286,7 @@ function load_last_property_filters() { } function save_current_search() { - if (check_targets_validity() === false) { + if (check_form_validity() === false) { return; } @@ -343,12 +341,17 @@ function load_saved_search(search_object) { if (target_search['education']) new_target_card.find(".educationRankInput").val(target_search['education']); if (target_search['services']) new_target_card.find(".servicesRankInput").val(target_search['services']); if (target_search['environment']) new_target_card.find(".environmentRankInput").val(target_search['environment']); - if (target_search['fallbackradius'] > 0) new_target_card.find(".fallbackRadiusInput").val(target_search['fallbackradius']); if (target_search['maxradius'] > 0) new_target_card.find(".maxRadiusInput").val(target_search['maxradius']); if (target_search['minarea'] > 0) new_target_card.find(".minAreaRadiusInput").val(target_search['minarea']); if (target_search['simplify'] > 0) new_target_card.find(".simplifyFactorInput").val(target_search['simplify']); if (target_search['buffer'] > 0) new_target_card.find(".bufferFactorInput").val(target_search['buffer']); + if (target_search['fallbackradius'] > 0) { + new_target_card.find(".fallbackRadiusInput").val(target_search['fallbackradius']); + } else { + new_target_card.find(".fallbackRadiusInput").val(1); + } + new_target_card.find(".targetAddressInput").val(target_search['target']).focus(); }); @@ -356,7 +359,7 @@ function load_saved_search(search_object) { } function get_current_search() { - let current_search_targets = build_targets_array(); + let current_search_targets = build_input_params_array(); return { saved_date: new Date().toISOString(), targets: current_search_targets @@ -381,32 +384,6 @@ function clear_current_search() { $("#propertyButton").hide(); } -function validate_and_submit_request() { - if (check_targets_validity() === false) { - return; - } - - toggle_loading_buttons(); - - request_and_plot_areas( - build_targets_array(), - function () { - window.location.hash = encodeURIComponent(JSON.stringify(get_current_search())); - toggle_loading_buttons(); - - $('#searchActionButtons').show(); - $("#propertyButton").show(); - - // For UX on mobile devices where map starts off screen - $('#map').get(0).scrollIntoView(); - }, - function (jqXHR) { - show_iframe_error_modal(jqXHR.responseText); - toggle_loading_buttons(); - } - ); -} - function get_target_button_text(targetKey, $targetCard) { let targetArray = get_single_target_array($targetCard); @@ -495,21 +472,7 @@ function add_new_target_to_accordion(showTargetCard) { return newTargetCard; } -function show_iframe_error_modal(error_message_html) { - $('#messageModalTitle').text("Server Error"); - let errorFrame = $(""); - errorFrame.attr('srcdoc', error_message_html); - $('#messageModalBody').empty().append(errorFrame); - $('#messageModal').modal(); -} - -function show_html_modal(title, message) { - $('#messageModalTitle').text(title); - $('#messageModalBody').html(message); - $('#messageModal').modal(); -} - -function toggle_loading_buttons() { +function toggle_loading_state() { $("#generateButton").toggle(); $('#generateButtonLoading').toggle(); $("#propertyButton").hide(); @@ -540,7 +503,7 @@ function get_single_target_array(single_card) { }; } -function build_targets_array() { +function build_input_params_array() { let allTargets = []; $('#targetsAccordion div.targetCard').each(function () { @@ -566,7 +529,7 @@ function build_targets_array() { return allTargets; } -function check_targets_validity() { +function check_form_validity() { let no_invalid_targets = true; let at_least_one_valid_target = false; @@ -742,46 +705,22 @@ function build_polyline_for_url() { "type": "Feature", "geometry": { "type": "LineString", - "coordinates": window.currentAllTargetsData['result_intersection']['polygon']['coordinates'][0] + "coordinates": window.currentlyPlottedData['result_intersection']['polygon']['coordinates'][0] }, "properties": {} }, 5 ) } -function request_and_plot_areas( - allTargetsData, - successCallback, - errorCallback -) { - clear_map(window.mainMap.map); - - let targetAreaURL = "/target_area"; - - $.ajax({ - url: targetAreaURL, - type: 'POST', - contentType: 'application/json', - data: JSON.stringify(allTargetsData), - success: function (data) { - window.currentAllTargetsData = data; - plot_results(data); - - if (successCallback) { - successCallback(); - } - }, - error: function (jqXHR, textStatus) { - if (errorCallback) { - errorCallback(jqXHR, textStatus); - } - } - }); +function after_plot_callback() { + window.location.hash = encodeURIComponent(JSON.stringify(get_current_search())); + $('#searchActionButtons').show(); + $("#propertyButton").show(); } function plot_results(api_call_data) { - // Accessible, distinct colours from https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ - let layer_colours = ["#e6194B", "#4363d8", "#f58231", "#f032e6", "#469990", "#9A6324", "#800000", "#000075"]; + window.currentlyPlottedData = api_call_data; + let result_green = "#3cb44b"; let current_colour = 1; @@ -800,11 +739,11 @@ function plot_results(api_call_data) { key + "-" + target_index, target_prefix + single_result['label'], single_result['polygon'], - layer_colours[current_colour], 0.5, false + window.hah_layer_colours[current_colour], 0.5, false ); current_colour++; - if (current_colour >= layer_colours.length) { + if (current_colour >= window.hah_layer_colours.length) { current_colour = 0; } } @@ -824,86 +763,6 @@ function plot_results(api_call_data) { result_intersection['polygon'], result_green, 0.7, true ); - map.fitBounds(result_intersection['bounds']); + window.mainMap.map.fitBounds(result_intersection['bounds']); } } - -function plot_marker(label, coords) { - let popup = new mapboxgl.Popup({offset: 25}) - .setText(label); - - let targetMarker = new mapboxgl.Marker() - .setLngLat(coords) - .setPopup(popup) - .addTo(map); - - window.mainMap.currentMarkers.push(targetMarker); -} - -function plot_polygon(id, label, polygon, color, opacity = 0.3, visible = true) { - window.mainMap.currentLayers.push(id); - - let menuItem = $( - "" + label + "" - ); - - if (visible) menuItem.addClass('active'); - - menuItem.click(function (e) { - let visibility = window.mainMap.map.getLayoutProperty(id, 'visibility'); - - if (visibility === 'visible') { - window.mainMap.map.setLayoutProperty(id, 'visibility', 'none'); - $(this).removeClass('active'); - } else { - $(this).addClass('active'); - window.mainMap.map.setLayoutProperty(id, 'visibility', 'visible'); - } - return false; - }); - - $("#map-filter-menu").append(menuItem); - - window.mainMap.map.addLayer({ - 'id': id, - 'type': 'fill', - 'source': { - 'type': 'geojson', - 'data': { - 'type': 'Feature', - 'geometry': polygon - } - }, - 'layout': { - 'visibility': (visible ? 'visible' : 'none') - }, - 'paint': { - 'fill-color': color, - 'fill-opacity': opacity - }, - 'metadata': { - 'home-area-helper': true - } - }); -} - -function clear_map() { - $('#map-filter-menu').empty().hide(); - - if (window.mainMap.currentMarkers !== null) { - for (let i = window.mainMap.currentMarkers.length - 1; i >= 0; i--) { - window.mainMap.currentMarkers[i].remove(); - } - } - - if (window.mainMap.currentLayers !== null && window.mainMap.map) { - let hhaLayers = window.mainMap.map.getStyle().layers.filter(function (el) { - return (el['metadata'] && el['metadata']['home-area-helper']); - }); - - for (let i = hhaLayers.length - 1; i >= 0; i--) { - window.mainMap.map.removeLayer(hhaLayers[i]['id']); - window.mainMap.map.removeSource(hhaLayers[i]['id']); - } - } -} \ No newline at end of file diff --git a/static/target_cities.js b/static/target_cities.js new file mode 100644 index 0000000..415b322 --- /dev/null +++ b/static/target_cities.js @@ -0,0 +1,65 @@ +function map_loaded(map) { + window.mainMap.map = map; +} + +$(function () { + + $("#findButton").click(function (e) { + $('#findTargetCitiesForm').submit(); + return false; + }); + + $('#findTargetCitiesForm').submit(function (e) { + e.stopPropagation(); + e.preventDefault(); + + validate_and_submit_request(); + return false; + }); +}); + +function check_form_validity() { + return true; +} + +function toggle_loading_state() { + $("#findButton").toggle(); + $('#findButtonLoading').toggle(); +} + +function build_input_params_array() { + let allInputs = {}; + + $('#findTargetCitiesForm input').each(function () { + let singleInput = $(this); + allInputs[singleInput.attr('id')] = singleInput.val(); + }); + + return allInputs; +} + +function get_results_url() { + return "/target_cities"; +} + +function plot_results(api_call_data) { + window.currentlyPlottedData = api_call_data; + + let all_targets_results = api_call_data['targets_results']; + let results_combined = api_call_data['results_combined']; + + all_targets_results.forEach(function (target_results, target_index) { + let target_prefix = "#" + (target_index + 1) + ": "; + + plot_marker( + target_prefix + target_results['target']['label'] + target_results['target']['coords'], + target_results['target']['coords'] + ); + }); + + $('#map-filter-menu').show(); + + if (results_combined) { + window.mainMap.map.fitBounds(results_combined['bounds']); + } +} diff --git a/templates/single-polygon.html b/templates/polygon.html similarity index 100% rename from templates/single-polygon.html rename to templates/polygon.html diff --git a/templates/index.html b/templates/target_area.html similarity index 97% rename from templates/index.html rename to templates/target_area.html index 4191aab..8993ae5 100644 --- a/templates/index.html +++ b/templates/target_area.html @@ -2,13 +2,13 @@ - Property Search Area Tool + Home Area Helper - + - + +
@@ -30,7 +31,9 @@

Home Area Helper

Build property search area alerts based on target destination travel time and area deprivation.

+ Unsure where to target? Try our Target Cities tool!
+