diff --git a/dataset-info/europe/eurostat-cities-2019/City statistics (urb).pdf b/dataset-info/europe/eurostat-cities-2019/City statistics (urb).pdf new file mode 100644 index 0000000..06d15bd Binary files /dev/null and b/dataset-info/europe/eurostat-cities-2019/City statistics (urb).pdf differ diff --git a/dataset-info/europe/eurostat-cities-2019/KS-01-16-948-EN-N.pdf b/dataset-info/europe/eurostat-cities-2019/KS-01-16-948-EN-N.pdf new file mode 100644 index 0000000..35f19c8 Binary files /dev/null and b/dataset-info/europe/eurostat-cities-2019/KS-01-16-948-EN-N.pdf differ diff --git a/requirements.txt b/requirements.txt index 15dc0ec..722e98f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,4 +20,6 @@ backoff flask_sslify pyunpack patool -googlemaps \ No newline at end of file +googlemaps +pandas +xlrd diff --git a/run_server.py b/run_server.py index e6bb5cc..dd56d66 100755 --- a/run_server.py +++ b/run_server.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import json import logging import os @@ -10,7 +11,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__) @@ -22,8 +23,10 @@ logging.getLogger().setLevel(logging.DEBUG if app_debug else logging.INFO) # Download dataset files and pre-seeded API call / compute cache to reduce slug size -preload_files('https://github.com/beveradb/home-area-helper/releases/download/v0.6/', [ +preload_files('https://github.com/beveradb/home-area-helper/releases/download/v0.7/', [ {'dir': 'datasets/uk/', 'file': 'uk-wgs84-imd-shapefiles.zip'}, + {'dir': 'datasets/europe/', 'file': 'eurostat-cities-2019.zip'}, + {'dir': 'caches/', 'file': 'api_cache.sqlite.zip'}, {'dir': 'caches/', 'file': 'requests_cache.sqlite.zip'}, {'dir': 'caches/', 'file': 'static_cache.sqlite'}, ]) @@ -32,6 +35,10 @@ requests_cache = requests_cache.core.CachedSession( cache_name='caches/requests_cache', backend="sqlite", allowable_methods=('GET', 'POST')) +# Set up disk caching for API calls which are made through a 3rd party library rather than the requests library +api_cache = ucache.SqliteCache( + filename='caches/api_cache.sqlite', cache_size=5000, timeout=32000000, compression=True) + # Set up disk caching for complex computations which should not need to change - static_cache = ucache.SqliteCache( filename='caches/static_cache.sqlite', cache_size=5000, timeout=32000000, compression=True) @@ -43,23 +50,62 @@ @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('/eurostat_countries') +def eurostat_country_codes(): + from src import target_cities + results = json.dumps(target_cities.get_eurostat_countries()) + return Response(results, mimetype='application/json') + + +@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('/eurostat_testing/', methods=['GET']) +def eurostat_testing(country): + logging.log(logging.INFO, "Target eurostat received: " + str(country)) + + from src import target_cities + results = target_cities.get_country_cities_combined_data(country) + + 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/google_maps.py b/src/google_maps.py index 5b00ed0..36be92b 100755 --- a/src/google_maps.py +++ b/src/google_maps.py @@ -3,18 +3,19 @@ import googlemaps -from run_server import transient_cache from src.utils import timeit @timeit -@transient_cache.cached() def get_centre_point_lng_lat_for_address(address_string): gmaps = googlemaps.Client(key=os.environ['GMAPS_API_KEY']) geocode_result = gmaps.geocode(address_string) - return [ - geocode_result[0]['geometry']['location']['lng'], - geocode_result[0]['geometry']['location']['lat'] - ] + if len(geocode_result) > 0: + return [ + geocode_result[0]['geometry']['location']['lng'], + geocode_result[0]['geometry']['location']['lat'] + ] + else: + return None 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..f97d56f --- /dev/null +++ b/src/target_cities.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +import json +import logging + +import pandas as pd +from shapely.geometry import mapping + +from run_server import transient_cache, static_cache +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 + + +@transient_cache.cached() +def get_target_cities(params: dict): + target_cities_result = [] + + country_code = str(params['countryCodeInput']) + target_cities = get_country_cities_combined_data(country_code) + + for target_city in target_cities: + # Filter by arbitrary user-given params + if params['minPopulationInput']: + if int(target_city['Population']["Population on the 1st of January, total"]) < int( + params['minPopulationInput']): + continue + + city_center_coords = google_maps.get_centre_point_lng_lat_for_address( + target_city['Name'] + ', ' + country_code + ) + + if city_center_coords is not None: + city_polygon = get_bounding_circle_for_point(city_center_coords, 2) + + target_cities_result.append({ + 'label': target_city['Name'], + 'coords': city_center_coords, + 'polygon': city_polygon, + 'data': target_city + }) + + return target_cities_result + + +@static_cache.cached() +def get_eurostat_countries(): + return [ + {"code": "AT", "label": "Austria"}, + {"code": "BE", "label": "Belgium"}, + {"code": "BG", "label": "Bulgaria"}, + {"code": "CH", "label": "Switzerland"}, + {"code": "CY", "label": "Cyprus"}, + {"code": "CZ", "label": "Czech Republic"}, + {"code": "DE", "label": "Germany"}, + {"code": "DK", "label": "Denmark"}, + {"code": "EE", "label": "Estonia"}, + {"code": "EL", "label": "Greece"}, + {"code": "ES", "label": "Spain"}, + {"code": "FI", "label": "Finland"}, + {"code": "FR", "label": "France"}, + {"code": "HR", "label": "Croatia"}, + {"code": "HU", "label": "Hungary"}, + {"code": "IE", "label": "Ireland"}, + {"code": "IS", "label": "Iceland"}, + {"code": "IT", "label": "Italy"}, + {"code": "LT", "label": "Lithuania"}, + {"code": "LU", "label": "Luxembourg"}, + {"code": "LV", "label": "Latvia"}, + {"code": "MT", "label": "Malta"}, + {"code": "NL", "label": "Netherlands"}, + {"code": "NO", "label": "Norway"}, + {"code": "PL", "label": "Poland"}, + {"code": "PT", "label": "Portugal"}, + {"code": "RO", "label": "Romania"}, + {"code": "SE", "label": "Sweden"}, + {"code": "SI", "label": "Slovenia"}, + {"code": "SK", "label": "Slovakia"}, + {"code": "TR", "label": "Turkey"}, + {"code": "UK", "label": "United Kingdom"} + ] + + +@static_cache.cached() +def load_eurostat_metadata(): + eurostat_data_dir = 'datasets/europe/eurostat-cities-2019/' + + eurostat_meta_df = { + 'Indicator list': pd.read_excel(eurostat_data_dir + 'urb_esms_an1.xlsx'), + 'Validation rules': pd.read_excel(eurostat_data_dir + 'urb_esms_an2.xlsx'), + 'Variable list': pd.read_excel(eurostat_data_dir + 'urb_esms_an3.xls'), + 'List of cities': pd.read_excel(eurostat_data_dir + 'urb_esms_an4.xls'), + 'Perception Indicators': pd.read_csv(eurostat_data_dir + 'urb_percep_indicators.tsv', sep='\t', header=None, + names=['Code', 'Label']), + } + + # Columns: + # List of cities: ["CODE", "NAME"] + # Indicator list: ["CODE", "LABEL", "indicator calculation nominator", "indicator calculation denominator"] + # Validation rules: ["Rule name", "New Rule Name",…] + # Variable list: ["Domain", "Code", "Label", "To be collected by NSI included in Annex A of Grants 2014/2015",…] + + return eurostat_meta_df + + +@static_cache.cached() +def load_eurostat_data(): + eurostat_data_dir = 'datasets/europe/eurostat-cities-2019/' + + eurostat_df = { + 'Economy and finance': + pd.read_csv(eurostat_data_dir + 'urb_cecfi.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Environment': + pd.read_csv(eurostat_data_dir + 'urb_cenv.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Fertility and mortality': + pd.read_csv(eurostat_data_dir + 'urb_cfermor.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Education': + pd.read_csv(eurostat_data_dir + 'urb_ceduc.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Living conditions': + pd.read_csv(eurostat_data_dir + 'urb_clivcon.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Labour market': + pd.read_csv(eurostat_data_dir + 'urb_clma.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Population': + pd.read_csv(eurostat_data_dir + 'urb_cpop1.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Culture and tourism': + pd.read_csv(eurostat_data_dir + 'urb_ctour.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Transport': + pd.read_csv(eurostat_data_dir + 'urb_ctran.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + 'Perception survey': + pd.read_csv(eurostat_data_dir + 'urb_percep.tsv.gz', sep='\t', header=0, + compression='gzip', error_bad_lines=False), + } + + # Columns: + # Culture and tourism: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 " ...] + # Economy and finance: ["indic_ur,cities\time", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 ", "2012 "...] + # Education: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 ", "2012 " ...] + # Environment: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 ", "2012 " ...] + # Fertility and mortality: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 ", "2014 " ...] + # Labour market: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 ", ...] + # Living conditions: ["indic_ur,cities\time", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 ", "2012 " ...] + # Perception survey: ["indic_ur,unit,cities\time", "2015 ", "2012 ", "2009 ", "2006 ", "2004 "] + # Population: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 " ...] + # Transport: ["indic_ur,cities\time", "2019 ", "2018 ", "2017 ", "2016 ", "2015 ", "2014 ", "2013 ", "2012 " ...] + + return eurostat_df + + +@static_cache.cached() +def get_country_cities_combined_data(country): + eurostat_meta_df = load_eurostat_metadata() + eurostat_df = load_eurostat_data() + + # Example CODE value for a UK city: UK007C1 + city_code_regex = '^' + country + '[0-9]+C[0-9]+$' + + cities_filtered_df = eurostat_meta_df['List of cities'][ + eurostat_meta_df['List of cities']['CODE'].str.contains(city_code_regex) + ] + + indicators_map = eurostat_meta_df['Indicator list'].set_index('CODE').to_dict(orient='index') + variables_map = eurostat_meta_df['Variable list'].set_index('Code').to_dict(orient='index') + perception_map = eurostat_meta_df['Perception Indicators'].set_index('Code').to_dict(orient='index') + + result_cities_list = [] + + for cities_idx, cities_row in cities_filtered_df.iterrows(): + city_code = cities_row['CODE'] + + single_city_data = { + 'Code': city_code, + 'Name': cities_row['NAME'] + } + + city_data_regex = city_code + '$' + + for category_index, category_name in enumerate(eurostat_df): + category_df = eurostat_df[category_name] + first_col_name = category_df.columns[0] + city_data_df = category_df[ + category_df[first_col_name].str.contains(city_data_regex) + ] + single_city_data[category_name] = {} + + for city_data_idx, city_data_row in city_data_df.iterrows(): + city_data_indicator = city_data_row[first_col_name] + city_data_indicator = city_data_indicator.split(',')[0] + + if city_data_indicator in variables_map.keys(): + city_data_indicator = variables_map[city_data_indicator]['Label'] + elif city_data_indicator in indicators_map.keys(): + city_data_indicator = indicators_map[city_data_indicator]['LABEL'] + elif city_data_indicator in perception_map.keys(): + city_data_indicator = perception_map[city_data_indicator]['Label'] + + for city_data_col_idx, city_data_year in enumerate(category_df.columns): + if city_data_col_idx == 0: + continue + + city_data_value = city_data_row[city_data_year] + if city_data_value != ': ': + single_city_data[category_name][city_data_indicator] = city_data_value + break + + result_cities_list.append(single_city_data) + + # Result object shape: + # [ + # { + # "Code": "UK002C1", + # "Name": "Birmingham", + # "Economy and finance": { + # "All companies": "34565 d" + # }, + # "Population": { + # "Population on the 1st of January, total": "515855 " + # } + # ... + # } + # ] + + return result_cities_list + + +@timeit +@transient_cache.cached() +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'], + 'polygon': mapping(target_city['polygon']), + 'data': target_city['data'] + } + }) + 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'] + } + + 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..f6a4d74 --- /dev/null +++ b/static/target_cities.js @@ -0,0 +1,131 @@ +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; + }).find('input').keypress(function (e) { + if (e.which === 13) { + $('#findTargetCitiesForm').submit(); + return false; + } + }); + + $("#clearSearchButton").click(function (e) { + clear_current_search(); + return false; + }); + + $("#copyToClipboardButton").click(function (e) { + let cityList = ""; + let countryCode = $('#countryCodeInput').val(); + + window.currentlyPlottedData['targets_results'].forEach(function (single_target, target_index) { + cityList += single_target['target']['label'] + ", " + countryCode + "\n"; + }); + + navigator.clipboard.writeText(cityList).then(function () { + alert("Copied city list to clipboard!"); + }); + + return false; + }); + + $.getJSON("/eurostat_countries", function (data) { + $.each(data, function (index, data) { + let selectedVal = ''; + if (data['code'] === "UK") selectedVal = " selected='selected'"; + $('#countryCodeInput').append(''); + }); + }).fail(function (jqxhr, status, error) { + console.error('jQuery getJSON error', status, error) + } + ); +}); + +function check_form_validity() { + return true; +} + +function toggle_loading_state() { + $("#findButton").toggle(); + $('#findButtonLoading').toggle(); +} + +function build_input_params_array() { + let allInputs = {}; + + $('#findTargetCitiesForm input, #findTargetCitiesForm select').each(function () { + let singleInput = $(this); + allInputs[singleInput.attr('id')] = singleInput.val(); + }); + + return allInputs; +} + +function get_results_url() { + return "/target_cities"; +} + +function after_plot_callback() { + $('#searchActionButtons').show(); +} + +function plot_results(api_call_data) { + window.currentlyPlottedData = api_call_data; + let current_colour = 1; + + 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) + ": "; + + for (let key in target_results) { + if (!target_results.hasOwnProperty(key)) continue; + + let single_result = target_results[key]; + + if (single_result.hasOwnProperty('polygon')) { + plot_polygon( + key + "-" + target_index, + target_prefix + single_result['label'], + single_result['polygon'], + window.hah_layer_colours[current_colour], 0.5, true + ); + + current_colour++; + if (current_colour >= window.hah_layer_colours.length) { + current_colour = 0; + } + } + } + + 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']); + } +} + +function clear_current_search() { + clear_map(window.mainMap.map); + $('#searchActionButtons').hide(); +} 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!
+