From 4b32d72f8f007a64cd2f2249948c0fafbddd097f Mon Sep 17 00:00:00 2001 From: Koa Wells Date: Fri, 15 Dec 2023 15:16:40 -0500 Subject: [PATCH 1/6] Add initial test for rendering buildings --- map_view_simple_example.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/map_view_simple_example.py b/map_view_simple_example.py index fabc09f..b3a098a 100644 --- a/map_view_simple_example.py +++ b/map_view_simple_example.py @@ -1,5 +1,21 @@ import tkinter import tkintermapview +import json +import copy + +with open('./data/collinear_clean.geojson', 'r') as city_file: + city = json.load(city_file) + buildings = city['features'] + +building_polygons = [] + +for building in buildings: + building_polygon = [] + if building['geometry']['type'] == 'Polygon': + for coordinate in building['geometry']['coordinates'][0]: + building_polygon.append((coordinate[1], coordinate[0])) + building_polygons.append({'id': building['id'], + 'polygon': copy.deepcopy(building_polygon)}) # create tkinter window root_tk = tkinter.Tk() @@ -15,13 +31,24 @@ map_widget.set_tile_server("https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z # map_widget.set_tile_server("https://mt0.google.com/vt/lyrs=s&hl=en&x={x}&y={y}&z={z}&s=Ga", max_zoom=22) # google satellite # set current position and zoom -map_widget.set_position(45.497059, -73.578451, marker=False) # Berlin, Germany +map_widget.set_position(45.497059, -73.578451, marker=False) # Montreal, Quebec map_widget.set_zoom(11) # set current position with address # map_widget.set_address("Berlin Germany", marker=False) def marker_click(marker): - print(f"marker clicked - text: {marker.text} position: {marker.position}") + print(f"marker clicked - text: {marker.text} position: {marker.position}") -root_tk.mainloop() +building_polygon_markers = [] +count = 0 +for building_polygon in building_polygons: + if count > 1000: + break + building_polygon_markers.append(map_widget.set_polygon(building_polygon['polygon'], + fill_color='black', + outline_color='black', + name=building_polygon['id'])) + count+=1 + +root_tk.mainloop() \ No newline at end of file From d824a8638f393d3edfd42cedd5a0a69ba9c54578 Mon Sep 17 00:00:00 2001 From: Koa Wells Date: Mon, 18 Dec 2023 16:21:57 -0500 Subject: [PATCH 2/6] Add side bar with button and textbox --- map_view_simple_example.py | 62 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/map_view_simple_example.py b/map_view_simple_example.py index b3a098a..5189523 100644 --- a/map_view_simple_example.py +++ b/map_view_simple_example.py @@ -1,54 +1,52 @@ import tkinter import tkintermapview -import json -import copy -with open('./data/collinear_clean.geojson', 'r') as city_file: - city = json.load(city_file) - buildings = city['features'] +select_buildings = False +selected_coordinates = [] -building_polygons = [] +def marker_click(marker): + print(f"marker clicked - text: {marker.text} position: {marker.position}") -for building in buildings: - building_polygon = [] - if building['geometry']['type'] == 'Polygon': - for coordinate in building['geometry']['coordinates'][0]: - building_polygon.append((coordinate[1], coordinate[0])) - building_polygons.append({'id': building['id'], - 'polygon': copy.deepcopy(building_polygon)}) +def on_click(coordinates_tuple): + if select_buildings: + print(coordinates_tuple) + selected_coordinates.append(coordinates_tuple) + +def activate_selection_box(): + select_buildings = True # create tkinter window root_tk = tkinter.Tk() root_tk.geometry(f"{1000}x{700}") root_tk.title("Building Selection Tool") +# create left frame +leftframe = tkinter.Frame(root_tk) +leftframe.pack(side=tkinter.LEFT) + +# create right frame +rightframe = tkinter.Frame(root_tk,width=80) +rightframe.pack(side=tkinter.RIGHT) + +# create button for activating building selection +selection_box_button = tkinter.Button(leftframe, text="Create Selection Box", command=activate_selection_box) +selection_box_button.pack(side=tkinter.TOP) + +# create text box for viewing selecting coordinates +text_width = tkinter.Text(leftframe,height=50,width=30) +text_width.pack(side=tkinter.BOTTOM,padx=50) + # create map widget -map_widget = tkintermapview.TkinterMapView(root_tk, width=1000, height=700, corner_radius=0) +map_widget = tkintermapview.TkinterMapView(rightframe, width=1000, height=700, corner_radius=0) map_widget.pack(fill="both", expand=True) # set other tile server (standard is OpenStreetMap) -map_widget.set_tile_server("https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}&s=Ga", max_zoom=22) # google normal -# map_widget.set_tile_server("https://mt0.google.com/vt/lyrs=s&hl=en&x={x}&y={y}&z={z}&s=Ga", max_zoom=22) # google satellite +map_widget.set_tile_server("https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}&s=Ga", max_zoom=14) # google normal # set current position and zoom map_widget.set_position(45.497059, -73.578451, marker=False) # Montreal, Quebec map_widget.set_zoom(11) -# set current position with address -# map_widget.set_address("Berlin Germany", marker=False) - -def marker_click(marker): - print(f"marker clicked - text: {marker.text} position: {marker.position}") - -building_polygon_markers = [] -count = 0 -for building_polygon in building_polygons: - if count > 1000: - break - building_polygon_markers.append(map_widget.set_polygon(building_polygon['polygon'], - fill_color='black', - outline_color='black', - name=building_polygon['id'])) - count+=1 +map_widget.add_left_click_map_command(on_click) root_tk.mainloop() \ No newline at end of file From e3e59c24dee52df2611a894781bb503c21c6aa4e Mon Sep 17 00:00:00 2001 From: Koa Wells Date: Mon, 18 Dec 2023 19:09:41 -0500 Subject: [PATCH 3/6] Object-orientify the building selection tool, add sample main, add helper functions/variables, and output directory --- building_selection_tool.py | 77 ++++++++++++++++++++++++++++++++++++++ data/.gitignore | 2 +- helpers.py | 24 ++++++++++++ main.py | 37 +++++------------- output_files/.gitignore | 2 + 5 files changed, 114 insertions(+), 28 deletions(-) create mode 100644 building_selection_tool.py create mode 100644 helpers.py create mode 100644 output_files/.gitignore diff --git a/building_selection_tool.py b/building_selection_tool.py new file mode 100644 index 0000000..dcfaeb9 --- /dev/null +++ b/building_selection_tool.py @@ -0,0 +1,77 @@ +""" +Building Selection Tool +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2023 Concordia CERC group +Project Coder Koa Wells kekoa.wells@concordia.ca +""" + +import json +from pathlib import Path +from shapely import Polygon +from shapely import Point + +import helpers +class BuildingSelectionTool: + """ + BuildingSelectionTool class + """ + def __init__(self, geojson_file_path): + geojson_file = Path(geojson_file_path).resolve() + with open(geojson_file, 'r') as file: + self.city = json.load(file) + self.buildings = self.city['features'] + + self.selected_buildings = [] + + def select_by_polygon(self, selection_polygon_coordinates): + """ + Get all buildings within a specified polygon and write the output to output_file_path + :param selection_polygon: list of coordinates forming the selection polygon + :param output_file_path: path and file name for output file containing selected buildings and their metadata + :return: None + """ + selection_polygon = Polygon(selection_polygon_coordinates) + for building in self.buildings: + coordinates = building['geometry']['coordinates'][0] + building_polygon = Polygon(coordinates) + centroid = Point(building_polygon.centroid) + + if centroid.within(selection_polygon): + self.selected_buildings.append(building) + + def select_by_coordinate(self, coordinate): + """ + :param coordinate: longitude, latitude coordinate of selected + :return: + """ + pass + + def select_by_address(self, address): + """ + + :param address: + :return: + """ + coordinates = helpers.get_gps_coordinate_by_address(address) + longitude = coordinates[1] + latitude = coordinates[0] + + point = Point([longitude, latitude]) + + for building in self.buildings: + building_coordinates = building['geometry']['coordinates'][0] + building_polygon = Polygon(building_coordinates) + if point.within(building_polygon): + self.selected_buildings.append(building) + + def save_selected_buildings(self, output_file_path): + """ + + :return: + """ + output_file = Path(output_file_path).resolve() + output_region = {"type": "FeatureCollection", + "features": self.selected_buildings} + + with open(output_file, 'w') as file: + file.write(json.dumps(output_region, indent=2)) diff --git a/data/.gitignore b/data/.gitignore index c96a04f..cf57c3b 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,2 +1,2 @@ -* +.gitignore !.gitignore \ No newline at end of file diff --git a/helpers.py b/helpers.py new file mode 100644 index 0000000..7de1af1 --- /dev/null +++ b/helpers.py @@ -0,0 +1,24 @@ +""" +Helpers +""" +import requests + +#OpenCage Geocoding API key +API_KEY = '744ad0d2d58e49d1ac57d6fb04fe5d82' + +def get_gps_coordinate_by_address(address): + base_url = "https://api.opencagedata.com/geocode/v1/json" + params = { + 'q': address, + 'key': API_KEY, + } + + response = requests.get(base_url, params=params) + data = response.json() + + if response.status_code == 200 and data['status']['code'] == 200: + location = data['results'][0]['geometry'] + return location['lat'], location['lng'] + else: + print(f"Error: {data['status']['message']}") + return None, None \ No newline at end of file diff --git a/main.py b/main.py index 0e23d69..48331ab 100644 --- a/main.py +++ b/main.py @@ -1,34 +1,17 @@ -import json -from shapely import Polygon -from shapely import Point -from pathlib import Path +from building_selection_tool import BuildingSelectionTool -# Make sure to enter your points in the clockwise direction -# and in the longitude, latitude format -selection_box = Polygon([[-73.543833, 45.575932] , +selection_tool = BuildingSelectionTool('./data/collinear_clean.geojson') + +sample_polygon_coordinates = [[-73.543833, 45.575932], [-73.541834, 45.575245], [-73.542275, 45.574652], [-73.544235, 45.575329], - [-73.543833, 45.575932]]) + [-73.543833, 45.575932]] -geojson_file = Path('./data/collinear_clean.geojson').resolve() -output_file = Path('./output_buildings.geojson').resolve() -buildings_in_region = [] +selection_tool.select_by_polygon(sample_polygon_coordinates) -with open(geojson_file, 'r') as file: - city = json.load(file) - buildings = city['features'] +sample_address = "2155 Rue Guy, Montreal, Quebec" +selection_tool.select_by_address(sample_address) -for building in buildings: - coordinates = building['geometry']['coordinates'][0] - building_polygon = Polygon(coordinates) - centroid = Point(building_polygon.centroid) - - if centroid.within(selection_box): - buildings_in_region.append(building) - -output_region = {"type": "FeatureCollection", - "features": buildings_in_region} - -with open(output_file, 'w') as file: - file.write(json.dumps(output_region, indent=2)) +output_file_path = "./output_files/sample_output.geojson" +selection_tool.save_selected_buildings(output_file_path) diff --git a/output_files/.gitignore b/output_files/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/output_files/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file From 26093f7bb22549c94c88f51558595b59a8a1af64 Mon Sep 17 00:00:00 2001 From: Koa Wells Date: Mon, 18 Dec 2023 19:12:35 -0500 Subject: [PATCH 4/6] Remove unecessary files and add requirements.txt --- building_region_selector.py | 52 ------------------------------------- map_view_simple_example.py | 52 ------------------------------------- requirements.txt | 4 +++ 3 files changed, 4 insertions(+), 104 deletions(-) delete mode 100644 building_region_selector.py delete mode 100644 map_view_simple_example.py create mode 100644 requirements.txt diff --git a/building_region_selector.py b/building_region_selector.py deleted file mode 100644 index 82a7c1c..0000000 --- a/building_region_selector.py +++ /dev/null @@ -1,52 +0,0 @@ -import json - -from pathlib import Path -from shapely.geometry import Polygon - -""" -BuildingSelectionTool is a tool that allows for a subset of buildings to be selected from a larger geojson dataset. -SPDX - License - Identifier: LGPL - 3.0 - or -later -Copyright © 2023 Concordia CERC group -Project Coder Koa Wells kekoa.wells@concordia.ca -""" - -class BuildingSelectionTool: - def __init__(self, city_file, output_file): - self._city_file = city_file - self._city = json.load(city_file) - self._buildings = self._city['features'] - self._polygon_points = [] - - def _assign_buildings_to_regions(self): - for building in self._buildings: - if building['geometry']['type'] == 'Polygon': - building_centroid = Polygon(building["geometry"]["coordinates"][0]).centroid - elif building['geometry']['type'] == 'MultiPolygon': - # use the centroid of the first polygon inside of the multipolygon - building_centroid = Polygon(building["geometry"]["coordinates"][0][0]).centroid - - building['properties']['centroid'] = [building_centroid.x, building_centroid.y] - building['properties']['district_property'] = [] - - target_regions = [] - region_assigned = False - - target_region = self._regions[len(self._regions) / 2] - while not region_assigned: - if building_centroid.within(Polygon(target_region['geometry']['coordinates'])): - region_assigned = True - break - - target_region_centroid = Polygon(target_region['geometry']['coordinates']).centroid - - if building_centroid.x <= target_region_centroid.x: - if building_centroid.y >= target_region_centroid.y: - regions = regions[0:len(regions) / 2] - elif building_centroid.y < target_region_centroid.y: - regions = regions[len(regions) / 2:len(regions) - 1] - elif building_centroid.x > target_region_centroid.x: - if building_centroid.y >= target_region_centroid.y: - regions = regions[0:len(regions) / 2] - elif building_centroid.y < target_region_centroid.y: - regions = regions[len(regions) / 2:len(regions) - 1] - diff --git a/map_view_simple_example.py b/map_view_simple_example.py deleted file mode 100644 index 5189523..0000000 --- a/map_view_simple_example.py +++ /dev/null @@ -1,52 +0,0 @@ -import tkinter -import tkintermapview - -select_buildings = False -selected_coordinates = [] - -def marker_click(marker): - print(f"marker clicked - text: {marker.text} position: {marker.position}") - -def on_click(coordinates_tuple): - if select_buildings: - print(coordinates_tuple) - selected_coordinates.append(coordinates_tuple) - -def activate_selection_box(): - select_buildings = True - -# create tkinter window -root_tk = tkinter.Tk() -root_tk.geometry(f"{1000}x{700}") -root_tk.title("Building Selection Tool") - -# create left frame -leftframe = tkinter.Frame(root_tk) -leftframe.pack(side=tkinter.LEFT) - -# create right frame -rightframe = tkinter.Frame(root_tk,width=80) -rightframe.pack(side=tkinter.RIGHT) - -# create button for activating building selection -selection_box_button = tkinter.Button(leftframe, text="Create Selection Box", command=activate_selection_box) -selection_box_button.pack(side=tkinter.TOP) - -# create text box for viewing selecting coordinates -text_width = tkinter.Text(leftframe,height=50,width=30) -text_width.pack(side=tkinter.BOTTOM,padx=50) - -# create map widget -map_widget = tkintermapview.TkinterMapView(rightframe, width=1000, height=700, corner_radius=0) -map_widget.pack(fill="both", expand=True) - -# set other tile server (standard is OpenStreetMap) -map_widget.set_tile_server("https://mt0.google.com/vt/lyrs=m&hl=en&x={x}&y={y}&z={z}&s=Ga", max_zoom=14) # google normal - -# set current position and zoom -map_widget.set_position(45.497059, -73.578451, marker=False) # Montreal, Quebec -map_widget.set_zoom(11) - -map_widget.add_left_click_map_command(on_click) - -root_tk.mainloop() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3078b41 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +json +shapely +pathlib +requests \ No newline at end of file From 43e5666a9b32a5f1590a777df9e3af7517c4b9c9 Mon Sep 17 00:00:00 2001 From: Koa Wells Date: Mon, 8 Jan 2024 18:13:23 -0500 Subject: [PATCH 5/6] Add select by coordinate function and update code comments --- building_selection_tool.py | 25 +++++++++++++++++-------- main.py | 7 +++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/building_selection_tool.py b/building_selection_tool.py index dcfaeb9..b7b05da 100644 --- a/building_selection_tool.py +++ b/building_selection_tool.py @@ -26,8 +26,7 @@ class BuildingSelectionTool: def select_by_polygon(self, selection_polygon_coordinates): """ Get all buildings within a specified polygon and write the output to output_file_path - :param selection_polygon: list of coordinates forming the selection polygon - :param output_file_path: path and file name for output file containing selected buildings and their metadata + :param selection_polygon_coordinates: list of coordinates forming the selection polygon :return: None """ selection_polygon = Polygon(selection_polygon_coordinates) @@ -42,15 +41,24 @@ class BuildingSelectionTool: def select_by_coordinate(self, coordinate): """ :param coordinate: longitude, latitude coordinate of selected - :return: + :return: None """ - pass + longitude = coordinate[0] + latitude = coordinate[1] + + point = Point([longitude, latitude]) + + for building in self.buildings: + building_coordinates = building['geometry']['coordinates'][0] + building_polygon = Polygon(building_coordinates) + if point.within(building_polygon): + self.selected_buildings.append(building) def select_by_address(self, address): """ - + Select a building by inputting an address :param address: - :return: + :return: None """ coordinates = helpers.get_gps_coordinate_by_address(address) longitude = coordinates[1] @@ -66,8 +74,9 @@ class BuildingSelectionTool: def save_selected_buildings(self, output_file_path): """ - - :return: + Save all selected buildings to an output file. + :param output_file_path: destination file path including file name and extension + :return: None """ output_file = Path(output_file_path).resolve() output_region = {"type": "FeatureCollection", diff --git a/main.py b/main.py index 48331ab..58cec0d 100644 --- a/main.py +++ b/main.py @@ -2,6 +2,7 @@ from building_selection_tool import BuildingSelectionTool selection_tool = BuildingSelectionTool('./data/collinear_clean.geojson') +# select all buildings within a polygon sample_polygon_coordinates = [[-73.543833, 45.575932], [-73.541834, 45.575245], [-73.542275, 45.574652], @@ -10,8 +11,14 @@ sample_polygon_coordinates = [[-73.543833, 45.575932], selection_tool.select_by_polygon(sample_polygon_coordinates) +# select building by address sample_address = "2155 Rue Guy, Montreal, Quebec" selection_tool.select_by_address(sample_address) +# select building by [longitude, latitude] coordinate +sample_coordinate = [-73.5790796, 45.4972906] +selection_tool.select_by_coordinate(sample_coordinate) + +# save selected buildings in geojson format to specified output_file_path output_file_path = "./output_files/sample_output.geojson" selection_tool.save_selected_buildings(output_file_path) From f381936abb9cc74800a50531a254cb787709c74c Mon Sep 17 00:00:00 2001 From: Koa Wells Date: Mon, 8 Jan 2024 18:17:27 -0500 Subject: [PATCH 6/6] Add gitignore in data folder --- data/.gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/.gitignore b/data/.gitignore index cf57c3b..c96a04f 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,2 +1,2 @@ -.gitignore +* !.gitignore \ No newline at end of file