diff --git a/prettymaps/draw.py b/prettymaps/draw.py index bf0de08..73535bb 100644 --- a/prettymaps/draw.py +++ b/prettymaps/draw.py @@ -157,6 +157,54 @@ def plot( scale_y=None, rotation=None, ): + """ + + Draw a map from OpenStreetMap data. + + Parameters + ---------- + query : string + The address to geocode and use as the central point around which to get the geometries + backup : dict + (Optional) feed the output from a previous 'plot()' run to save time + postprocessing: function + (Optional) Apply a postprocessing step to the 'layers' dict + radius + (Optional) If not None, draw the map centered around the address with this radius (in meters) + layers: dict + Specify the name of each layer and the OpenStreetMap tags to fetch + drawing_kwargs: dict + Drawing params for each layer (matplotlib params such as 'fc', 'ec', 'fill', etc.) + osm_credit: dict + OSM Caption parameters + figsize: Tuple + (Optional) Width and Height (in inches) for the Matplotlib figure. Defaults to (10, 10) + ax: axes + Matplotlib axes + title: String + (Optional) Title for the Matplotlib figure + vsketch: Vsketch + (Optional) Vsketch object for pen plotting + x: float + (Optional) Horizontal displacement + y: float + (Optional) Vertical displacement + scale_x: float + (Optional) Horizontal scale factor + scale_y: float + (Optional) Vertical scale factor + rotation: float + (Optional) Rotation in angles (0-360) + + Returns + ------- + layers: dict + Dictionary of layers (each layer is a Shapely MultiPolygon) + + Notes + ----- + + """ # Interpret query query_mode = parse_query(query) diff --git a/prettymaps/fetch.py b/prettymaps/fetch.py index 5afb592..681ca0c 100644 --- a/prettymaps/fetch.py +++ b/prettymaps/fetch.py @@ -1,15 +1,34 @@ +from ast import Dict from functools import reduce +from tokenize import Number, String +from typing import Optional, Union, Tuple +from xmlrpc.client import Boolean import osmnx as ox from osmnx import utils_geo import numpy as np -from shapely.geometry import Point, Polygon, MultiPolygon, MultiLineString +from shapely.geometry import Point, Polygon, MultiPolygon, LineString, MultiLineString from shapely.ops import unary_union from geopandas import GeoDataFrame, read_file -# Compute circular or square boundary given point, radius and crs -def get_boundary(point, radius, crs, circle=True, dilate=0): +def get_boundary( + point: Tuple, radius: float, crs: String, circle: Boolean = True, dilate: float = 0 +) -> Polygon: + """ + Compute circular or square boundary given point, radius and crs. + + Args: + point (Tuple): GPS coordinates + radius (Number): radius in meters + crs (String): Coordinate Reference System + circle (bool, optional): Whether to use a circular (True) or square (False) boundary. Defaults to True. + dilate (int, optional): Dilate the boundary by this much, in meters. Defaults to 0. + + Returns: + Polygon: a shapely Polygon representing the boundary + """ + if circle: return ( ox.project_gdf(GeoDataFrame(geometry=[Point(point[::-1])], crs=crs)) @@ -28,8 +47,17 @@ def get_boundary(point, radius, crs, circle=True, dilate=0): ).buffer(dilate) -# Get perimeter -def get_perimeter(query, by_osmid=False, **kwargs): +def get_perimeter(query, by_osmid: Boolean = False, **kwargs) -> GeoDataFrame: + """ + Fetch perimeter given query + + Args: + query (String): Query for the perimeter to be fetched (for example, "France") + by_osmid (bool, optional): Whether to fetch perimeter by OSM Id. Defaults to False. + + Returns: + GeoDataFrame: GeoDataFrame representation of the perimeter + """ return ox.geocode_to_gdf( query, by_osmid=by_osmid, @@ -38,75 +66,36 @@ def get_perimeter(query, by_osmid=False, **kwargs): ) -# Get coastline -def get_coast(perimeter = None, point = None, radius = None, perimeter_tolerance = 0, union = True, buffer = 0, circle = True, dilate = 0, file_location = None): +def get_coast( + perimeter=None, + point=None, + radius=None, + perimeter_tolerance=0, + union=True, + buffer=0, + circle=True, + dilate=0, + file_location=None, +): + if perimeter is not None: # Boundary defined by polygon (perimeter) - bbox=perimeter.to_crs(3174) - bbox=bbox.buffer(perimeter_tolerance+dilate+buffer) - bbox=bbox.to_crs(4326) - bbox=bbox.envelope - # Load the polygons for the coastline from a file + bbox = perimeter.to_crs(3174) + bbox = bbox.buffer(perimeter_tolerance + dilate + buffer) + bbox = bbox.to_crs(4326) + bbox = bbox.envelope + # Load the polygons for the coastline from a file geometries = read_file(file_location, bbox=bbox) perimeter = unary_union(ox.project_gdf(perimeter).geometry) elif (point is not None) and (radius is not None): # Boundary defined by circle with radius 'radius' around point - north,south,west,east=utils_geo.bbox_from_point(point, dist=radius+dilate+buffer) - bbox=(west,south,east,north) + north, south, west, east = utils_geo.bbox_from_point( + point, dist=radius + dilate + buffer + ) + bbox = (west, south, east, north) # Load the polygons for the coastline from a file - geometries=read_file(file_location, bbox=bbox) - perimeter = get_boundary(point, radius, geometries.crs, circle = circle, dilate = dilate) - - # Project GDF - if len(geometries) > 0: - geometries = ox.project_gdf(geometries) - - # Intersect with perimeter - geometries = geometries.intersection(perimeter) - - if union: - geometries = unary_union(reduce(lambda x,y: x+y, [ - [x] if type(x) == Polygon else list(x) - for x in geometries if type(x) in [Polygon, MultiPolygon] - ], [])) - else: - geometries = MultiPolygon(reduce(lambda x,y: x+y, [ - [x] if type(x) == Polygon else list(x) - for x in geometries if type(x) in [Polygon, MultiPolygon] - ], [])) - - return geometries - -# Get geometries -def get_geometries( - perimeter=None, - point=None, - radius=None, - tags={}, - perimeter_tolerance=0, - union=True, - circle=True, - dilate=0, -): - - if perimeter is not None: - # Boundary defined by polygon (perimeter) - geometries = ox.geometries_from_polygon( - unary_union(perimeter.geometry).buffer(perimeter_tolerance) - if perimeter_tolerance > 0 - else unary_union(perimeter.geometry), - tags={tags: True} if type(tags) == str else tags, - ) - perimeter = unary_union(ox.project_gdf(perimeter).geometry) - - elif (point is not None) and (radius is not None): - # Boundary defined by circle with radius 'radius' around point - geometries = ox.geometries_from_point( - point, - dist=radius + dilate, - tags={tags: True} if type(tags) == str else tags, - ) + geometries = read_file(file_location, bbox=bbox) perimeter = get_boundary( point, radius, geometries.crs, circle=circle, dilate=dilate ) @@ -146,19 +135,130 @@ def get_geometries( return geometries -# Get streets +def get_geometries( + perimeter: Optional[GeoDataFrame] = None, + point: Optional[Tuple] = None, + radius: Optional[float] = None, + tags: Dict = {}, + perimeter_tolerance: float = 0, + union: Boolean = True, + circle: Boolean = True, + dilate: float = 0, +) -> Union[Polygon, MultiPolygon]: + """Get geometries + + Args: + perimeter (Optional[GeoDataFrame], optional): Perimeter from within geometries will be fetched. Defaults to None. + point (Optional[Tuple], optional): GPS coordinates. Defaults to None. + radius (Optional[Number], optional): Radius in meters. Defaults to None. + tags (Dict, optional): OpenStreetMap tags for the geometries to be fetched. Defaults to {}. + perimeter_tolerance (Number, optional): Tolerance in meters for fetching geometries that fall outside the perimeter. Defaults to 0. + union (Boolean, optional): Whether to compute the union of all geometries. Defaults to True. + circle (Boolean, optional): Whether to fetch geometries in a circular (True) or square (False) shape. Defaults to True. + dilate (Number, optional): Dilate the boundary by this much in meters. Defaults to 0. + + Returns: + [type]: [description] + """ + + if perimeter is not None: + # Boundary defined by polygon (perimeter) + geometries = ox.geometries_from_polygon( + unary_union(perimeter.geometry).buffer(perimeter_tolerance) + if perimeter_tolerance > 0 + else unary_union(perimeter.geometry), + tags={tags: True} if type(tags) == str else tags, + ) + perimeter = unary_union(ox.project_gdf(perimeter).geometry) + + elif (point is not None) and (radius is not None): + # Boundary defined by circle with radius 'radius' around point + geometries = ox.geometries_from_point( + point, + dist=radius + dilate, + tags={tags: True} if type(tags) == str else tags, + ) + perimeter = get_boundary( + point, radius, geometries.crs, circle=circle, dilate=dilate + ) + + # Project GDF + if len(geometries) > 0: + geometries = ox.project_gdf(geometries) + + # Intersect with perimeter + geometries = geometries.intersection(perimeter) + + if union: + polys = unary_union( + reduce( + lambda x, y: x + y, + [ + [x] if type(x) == Polygon else list(x) + for x in geometries + if type(x) in [Polygon, MultiPolygon] + ], + [], + ) + ) + points = unary_union([ + x for x in geometries + if isinstance(x, Point) + ]).buffer(2) + + lines = unary_union([ + x for x in geometries + if isinstance(x, LineString) + ]).buffer(3) + + geometries = unary_union([polys, points, lines]) + + else: + geometries = MultiPolygon( + reduce( + lambda x, y: x + y, + [ + [x] if type(x) == Polygon else list(x) + for x in geometries + if type(x) in [Polygon, MultiPolygon] + ], + [], + ) + ) + + return geometries + + def get_streets( - perimeter=None, - point=None, - radius=None, - layer="streets", - width=6, - custom_filter=None, - buffer=0, - retain_all=False, - circle=True, - dilate=0, -): + perimeter: Optional[GeoDataFrame] = None, + point: Optional[Tuple] = None, + radius: Optional[float] = None, + layer: String = "streets", + width: float = 6, + custom_filter: Optional[str] = None, + buffer: float = 0, + retain_all: Boolean = False, + circle: Boolean = True, + dilate: float = 0, +) -> MultiPolygon: + """ + Get streets + + Args: + perimeter (Optional[GeoDataFrame], optional): [description]. Defaults to None. + point (Optional[Tuple], optional): [description]. Defaults to None. + radius (Optional[Number], optional): [description]. Defaults to None. + layer (String, optional): [description]. Defaults to "streets". + width (Number, optional): [description]. Defaults to 6. + custom_filter (Optional[String], optional): [description]. Defaults to None. + buffer (Number, optional): [description]. Defaults to 0. + retain_all (Boolean, optional): [description]. Defaults to False. + circle (Boolean, optional): [description]. Defaults to True. + dilate (Number, optional): [description]. Defaults to 0. + + Returns: + MultiPolygon: [description] + """ if layer == "streets": layer = "highway" @@ -226,8 +326,18 @@ def get_streets( return streets -# Get any layer -def get_layer(layer, **kwargs): +def get_layer(layer: String, **kwargs) -> Union[Polygon, MultiPolygon]: + """[summary] + + Args: + layer (String): [description] + + Raises: + Exception: [description] + + Returns: + Union[Polygon, MultiPolygon]: [description] + """ # Fetch perimeter if layer == "perimeter": # If perimeter is already provided: @@ -246,10 +356,10 @@ def get_layer(layer, **kwargs): else: raise Exception("Either 'perimeter' or 'point' & 'radius' must be provided") # Fetch streets or railway - if layer in ['streets', 'railway', 'waterway']: + if layer in ["streets", "railway", "waterway"]: return get_streets(**kwargs, layer=layer) # Fetch Coastline - elif layer == 'coastline': + elif layer == "coastline": return get_coast(**kwargs) # Fetch geometries else: