diff --git a/app/lib/server_requests/init_state.dart b/app/lib/server_requests/init_state.dart index c159c477c9824a10d45043fa70d7751b25b5375e..f9ad01fe85f740a58552044e979f40777d89feda 100644 --- a/app/lib/server_requests/init_state.dart +++ b/app/lib/server_requests/init_state.dart @@ -14,7 +14,6 @@ import '../server_requests/fetch_relation.dart'; /// and the last requesting the list of all system lakes Future<void> initialiseState(bool fetchSearchOptions) async { bool serverConnection = true; - bool internetConnection = false; late Future<List<Measurement>> markerListFuture; late Future<Uint8List> relationFuture; diff --git a/server/map_handler/add_new_lake.py b/server/map_handler/add_new_lake.py index 9d47ebdde041a219ddfac5880248f7e1922871c3..2cc3996179b73eeb224b6963556ad3cbd2171808 100644 --- a/server/map_handler/add_new_lake.py +++ b/server/map_handler/add_new_lake.py @@ -25,9 +25,18 @@ lng = (distance_in_km × 360)/(40075km × cos(lat)) # Read a json file with relation data and send to response object -def cut_map(self, cursor, lake_name: str, dist_in_km=0.5): +def cut_map(self, cursor, lake_name: str, cell_size_in_km=0.5): + """ + Cuts a map into a grid based on a selected cell size + + Parameters: + self (BaseHTTPRequestHandler): A instance of a BaseHTTPRequestHandler + cursor (cursor): An Sqlite3 cursor object that points to the database + lake_name (str): The name of the lake to be cut + cell_size_in_km (float): The selected cell size in kilometers + """ try: - # Read relation from GeoJson file and extract all polygons + # Read relation from GeoJson file and extract all geometry of type Polygon geo_data = gpd.read_file(LAKE_RELATIONS_PATH + lake_name + ".geojson") polygon_data = geo_data[geo_data['geometry'].geom_type == 'Polygon'] polygons = [Polygon(polygon.exterior) for polygon in polygon_data['geometry']] @@ -35,26 +44,35 @@ def cut_map(self, cursor, lake_name: str, dist_in_km=0.5): if len(polygons) <= 1: raise Exception("Failed to convert JSON object to Shapely Polygons") + # List to store all the map tiles divided_map = [] - cell_size = dist_in_km * 10 + # Convert the cell size to degrees + cell_size = cell_size_in_km * 10 cell_width = cell_size / 111.32 + # A slightly more complicated formula is required to calculate the height to ensure that + # the height in km is equal to the width in km regardless of the latitude. cell_height = (cell_size * 360) / (40075 * cos(cell_width)) + # Process all polygons for polygon in polygons: + # Generate a grid based on the calculated cell size lines = create_grid(polygon, cell_width, cell_height) lines.append(polygon.boundary) + # Merge the grid lines into a single grid object lines = unary_union(lines) lines = linemerge(lines) lines = list(polygonize(lines)) + # Divide the polygon into tiles based on the generated grid divided_map.extend(combine_grid_with_poly(polygon, lines)) + # List to store new GeoJSON feature objects features = [] + # Create subdivisions for each map tile sub_div_id = 0 for tile in divided_map: - # Calculate tile center based on bounds, and round down to two decimals min_x, min_y, max_x, max_y = tile.bounds center = round(max_y - (max_y - min_y), 6), round(max_x - (max_x - min_x), 6) @@ -66,6 +84,7 @@ def cut_map(self, cursor, lake_name: str, dist_in_km=0.5): rounded_coordinates.append(rounded_coords) rounded_tile = Polygon(rounded_coordinates) + # Create new feature object tile_feature = { 'type': 'Feature', 'properties': { @@ -74,9 +93,11 @@ def cut_map(self, cursor, lake_name: str, dist_in_km=0.5): }, 'geometry': rounded_tile.__geo_interface__ } + # Append new feature oject to list, and increment sub_div_id for next iteration features.append(tile_feature) sub_div_id += 1 + # Create new GeoJSON object containing all the new feature objects feature_collection = { 'type': 'FeatureCollection', 'features': features, @@ -95,10 +116,11 @@ def cut_map(self, cursor, lake_name: str, dist_in_km=0.5): INSERT INTO BodyOfWater(Name) VALUES (?); ''', (lake_name,)) + # Plot the newly created map and save it to a new file plot_map(divided_map) - write_json_to_file(lake_name, feature_collection) + # Return the map to the response object self.send_response(200) self.send_header("Content-type", "application/json") self.end_headers() @@ -113,21 +135,33 @@ def cut_map(self, cursor, lake_name: str, dist_in_km=0.5): self.end_headers() -def create_grid(poly: Polygon, cell_width, cell_height): +def create_grid(poly: Polygon, cell_width: float, cell_height: float): + """ + Returns a list of vertical and horizontal LineStrings that create a grid. + + Parameters: + poly (Polygon): A Shapely Polygon representing a map or part of a map + cell_width (float): The width of the grid cells in degrees + cell_height (float): The height of the grid cells in degrees + + Returns: + grid_lines (list): List of LineString objects defining the grid + """ # Retrieve bounds of the entire polygon bounds = poly.bounds - min_x, min_y, max_x, max_y = bounds + + # List to store all created lines grid_lines = [] - # Horizontal lines + # Create new horizontal lines while within bounds y = min_y while y <= max_y: line = LineString([(min_x, y), (max_x, y)]) grid_lines.append(line) y += cell_height - # Vertical lines + # Create new vertical lines while within bounds x = min_x while x <= max_x: line = LineString([(x, min_y), (x, max_y)]) @@ -138,6 +172,17 @@ def create_grid(poly: Polygon, cell_width, cell_height): def combine_grid_with_poly(polygon, grid): + """ + Returns a list of polygons that together make up map tiles. + + Parameters: + polygon (Polygon): A polygon representing a map or part of a map + grid (list): List of LineString objects defining the grid + + Returns: + intersecting_tiles (list): List of Polygons + """ + # List to contain all the tiles intersecting_tiles = [] for line in grid: @@ -153,24 +198,32 @@ def combine_grid_with_poly(polygon, grid): return intersecting_tiles -def write_json_to_file(lake_name: str, json_data: dict): +def write_json_to_file(lake_name: str, map_data: dict): + """ + Writes a divided map to a JSON file and updates all_lake_names.json + + Parameters: + lake_name (str): Name of the lake and file to write to + map_data (dict): List of map polygons converted to a JSON dictionary + """ # Create and write divided map to new file print("Writing to file...") if not os.path.exists(LAKE_RELATIONS_PATH): raise Exception("Directory from path does not exist") with open(LAKE_RELATIONS_PATH + '/' + lake_name + '_div.json', 'w') as f: - json.dump(json_data, f) + json.dump(map_data, f) - # Update all_system_lakes + # Read all_system_lakes.json with open(LAKE_RELATIONS_PATH + 'all_lake_names.json', 'r') as file: data = json.load(file) + # Check if the lake name exists in the list if lake_name not in data: - data.append(lake_name) + data.append(lake_name) # Only append to list if it does not already exist + # Update all_lake_names.json with new lake name with open(LAKE_RELATIONS_PATH + 'all_lake_names.json', 'w') as file: - # json.dump(data, file, indent=2) json.dump(data, file, ensure_ascii=False, indent=2) @@ -178,16 +231,26 @@ def write_json_to_file(lake_name: str, json_data: dict): # subdivisions. Removing calls to plot_map will speed up the process, but it is recommended to plot the map # after each division to ensure that the map was divided as intended. def plot_map(divided_map): - tiles = [gpd.GeoDataFrame(geometry=[tile]) for tile in divided_map] + """ + Plots a divided map using matplotlib. + Parameters: + divided_map (list): List of Shapely Polygons + """ print("Plotting... This may take some time...") + # Convert Polygon objects to GeoDataFrames + tiles = [gpd.GeoDataFrame(geometry=[tile]) for tile in divided_map] + + # Configure plot settings fig, ax = plt.subplots() ax.set_aspect(1.5) # Plot each tile for tile in tiles: + # Give each tile a random color to clearly visualize the grid random_color = "#{:06x}".format(random.randint(0, 0xFFFFFF)) gpd.GeoSeries(tile.geometry).plot(ax=ax, facecolor=random_color, edgecolor='none') + # Display plot plt.show()