Added code, notebooks, prints, README, requirements.txt

This commit is contained in:
Marcelo Prates
2021-03-05 11:06:57 -03:00
parent 4cdf462c1c
commit 49fe89fae1
12 changed files with 394425 additions and 0 deletions

View File

@@ -1,2 +1,32 @@
# prettymaps
A small set of Python functions to draw pretty maps from OpenStreetMap data. Based on osmnx, matplotlib and shapely libraries.
## Install dependencies
Install dependencies with
`$ pip install -r requirements.txt`
## Usage
On Python run:
```
from draw import plot
plot(f'Bom Fim, Porto Alegre', palette = ['red', 'blue'], layers = ['perimeter', 'landuse', 'water', 'streets'])
```
## "Circle" plots ([Jupyter Notebook](/notebooks/world-tour.ipynb)):
![](prints/Macau.svg)
![](prints/Palmanova.svg)
![](prints/Erbil.svg)
# Plotting districts ([Jupyter Notebook](/notebooks/porto-alegre.ipynb)):
![](prints/Centro%20Histórico%20-%20Porto%20Alegre.svg)
![](prints/Bom%20Fim%20-%20Porto%20Alegre.svg)
![](prints/Cidade%20Baixa%20-%20Porto%20Alegre.svg)
## More than one district at a time:
![](prints/CB-R-BF.svg)

182
code/draw.py Normal file
View File

@@ -0,0 +1,182 @@
# OpenStreetMap Networkx library to download data from OpenStretMap
import osmnx as ox
# Matplotlib-related stuff, for drawing
from matplotlib.path import Path
from matplotlib import pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import PathPatch
# CV2 & Scipy & Numpy & Pandas
import numpy as np
from numpy.random import choice
# Shapely
from shapely.geometry import *
from shapely.affinity import *
# Geopandas
from geopandas import GeoDataFrame
# etc
import pandas as pd
from functools import reduce
from tabulate import tabulate
from IPython.display import Markdown
from collections.abc import Iterable
# Fetch
from fetch import *
# Drawing functions
def show_palette(palette, description = ''):
'''
Helper to display palette in Markdown
'''
colorboxes = [
f'![](https://placehold.it/30x30/{c[1:]}/{c[1:]}?text=)'
for c in palette
]
display(Markdown((description)))
display(Markdown(tabulate(pd.DataFrame(colorboxes), showindex = False)))
def get_patch(shape, **kwargs):
'''
Convert shapely object to matplotlib patch
'''
if type(shape) == Path:
return patches.PathPatch(shape, **kwargs)
elif type(shape) == Polygon and shape.area > 0:
return patches.Polygon(list(zip(*shape.exterior.xy)), **kwargs)
else:
return None
def plot_shape(shape, ax, **kwargs):
'''
Plot shapely object
'''
if isinstance(shape, Iterable):
for shape_ in shape:
plot_shape(shape_, ax, **kwargs)
else:
ax.add_patch(get_patch(shape, **kwargs))
def plot_shapes(shapes, ax, palette = None, **kwargs):
'''
Plot collection of shapely objects (optionally, use a color palette)
'''
if not isinstance(shapes, Iterable):
shapes = [shapes]
for shape in shapes:
if palette is None:
plot_shape(shape, ax, **kwargs)
else:
plot_shape(shape, ax, fc = choice(palette), **kwargs)
def plot_streets(streets, ax, color = '#f5da9f', background_color = 'white', **kwargs):
'''
Plot shapely Polygon (or MultiPolygon) representing streets using matplotlib PathPatches
'''
for s in streets if isinstance(streets, Iterable) else [streets]:
if s is not None:
ax.add_patch(get_patch(pathify(s), facecolor = color, edgecolor = 'black', **kwargs))
def plot(
# Address
query,
# Figure parameters
figsize = (10, 10),
ax = None,
title = None,
# Whether to plot a circle centered around the address; circle params
circle = False,
radius = 1000,
streets_radius = 1000,
# Street params
dilate_streets = 5,
draw_streets = True,
# Color params
background_color = 'white',
background_alpha = 1.,
palette = None,
perimeter_lw = 1,
perimeter_ec = 'black',
water_ec = 'black',
land_ec = 'black',
buildings_ec = 'black',
# Which layers to plot
layers = ['perimeter', 'landuse', 'water', 'building', 'streets'],
# Layer ordering params
zorder_perimeter = None,
zorder_landuse = None,
zorder_water = None,
zorder_streets = None,
zorder_building = None,
# Whether to fetch data using OSM Id
by_osmid = False
):
#############
### Fetch ###
#############
# Geocode central point
if not by_osmid:
point = ox.geocode(query)
# Fetch perimeter
perimeter = get_perimeter(query, by_osmid = by_osmid) if not circle else None
# Fetch buildings, land, water, streets
layers_dict = {}
for layer in layers:
if layer == 'perimeter':
pass
elif layer == 'streets':
layers_dict[layer], _ = get_streets(
**({'point': point, 'radius': streets_radius} if circle else {'perimeter': perimeter}),
dilate = dilate_streets
)
else:
layers_dict[layer], perimeter_ = get_footprints(
**({'point': point, 'radius': radius} if circle else {'perimeter': perimeter}),
footprint = layer
)
# Project perimeter
if 'perimeter' in layers:
layers_dict['perimeter'] = perimeter_ if circle else union(ox.project_gdf(perimeter).geometry)
############
### Plot ###
############
if ax is None:
# if ax is none, create figure
fig, ax = plt.subplots(figsize = figsize)
# Ajust axis
ax.axis('off')
ax.axis('equal')
ax.autoscale()
# Setup parameters for drawing layers
layer_kwargs = {
'perimeter': {'lw': perimeter_lw, 'ec': perimeter_ec, 'fc': background_color, 'alpha': background_alpha, 'zorder': zorder_perimeter},
'landuse': {'ec': land_ec, 'fc': '#53bd53', 'zorder': zorder_landuse},
'water': {'ec': water_ec, 'fc': '#a1e3ff', 'zorder': zorder_water},
'streets': {'fc': '#f5da9f', 'zorder': zorder_streets},
'building': {'ec': buildings_ec, 'palette': palette, 'zorder': zorder_building},
}
# Draw layers
for layer in ['perimeter', 'landuse', 'water', 'streets', 'building']:
if layer in layers_dict:
plot_shapes(layers_dict[layer], ax, **layer_kwargs[layer])
# Return perimeter
return layers_dict['perimeter']

106
code/fetch.py Normal file
View File

@@ -0,0 +1,106 @@
# OpenStreetMap Networkx library to download data from OpenStretMap
import osmnx as ox
# CV2 & Scipy & Numpy & Pandas
import numpy as np
# Shapely
from shapely.geometry import *
from shapely.affinity import *
# Geopandas
from geopandas import GeoDataFrame
# Matplotlib
from matplotlib.path import Path
# etc
from collections.abc import Iterable
from functools import reduce
# Helper functions to fetch data from OSM
def ring_coding(ob):
codes = np.ones(len(ob.coords), dtype = Path.code_type) * Path.LINETO
codes[0] = Path.MOVETO
return codes
def pathify(polygon):
vertices = np.concatenate([np.asarray(polygon.exterior)] + [np.asarray(r) for r in polygon.interiors])
codes = np.concatenate([ring_coding(polygon.exterior)] + [ring_coding(r) for r in polygon.interiors])
return Path(vertices, codes)
def union(geometry):
geometry = np.concatenate([[x] if type(x) == Polygon else x for x in geometry if type(x) in [Polygon, MultiPolygon]])
geometry = reduce(lambda x, y: x.union(y), geometry[1:], geometry[0])
return geometry
def get_perimeter(query, by_osmid = False):
return ox.geocode_to_gdf(query, by_osmid = by_osmid)
def get_footprints(perimeter = None, point = None, radius = None, footprint = 'building'):
if perimeter is not None:
# Boundary defined by polygon (perimeter)
footprints = ox.geometries_from_polygon(union(perimeter.geometry), tags = {footprint: True} if type(footprint) == str else footprint)
perimeter = 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
footprints = ox.geometries_from_point(point, dist = radius, tags = {footprint: True} if type(footprint) == str else footprint)
perimeter = GeoDataFrame(geometry=[Point(point[::-1])], crs = footprints.crs)
perimeter = ox.project_gdf(perimeter).geometry[0].buffer(radius)
if len(footprints) > 0:
footprints = ox.project_gdf(footprints)
footprints = [
[x] if type(x) == Polygon else x
for x in footprints.geometry if type(x) in [Polygon, MultiPolygon]
]
footprints = list(np.concatenate(footprints)) if len(footprints) > 0 else []
footprints = [pathify(x) for x in footprints if x.within(perimeter)]
return footprints, perimeter
def get_streets(perimeter = None, point = None, radius = None, dilate = 6, custom_filter = None):
if perimeter is not None:
# Boundary defined by polygon (perimeter)
streets = ox.graph_from_polygon(union(perimeter.geometry), custom_filter = custom_filter)
streets = ox.project_graph(streets)
streets = ox.graph_to_gdfs(streets, nodes = False)
#streets = ox.project_gdf(streets)
streets = MultiLineString(list(streets.geometry)).buffer(dilate)
elif (point is not None) and (radius is not None):
# Boundary defined by polygon (perimeter)
streets = ox.graph_from_point(point, dist = radius, custom_filter = custom_filter)
crs = ox.graph_to_gdfs(streets, nodes = False).crs
streets = ox.project_graph(streets)
perimeter = GeoDataFrame(geometry=[Point(point[::-1])], crs = crs)
perimeter = ox.project_gdf(perimeter).geometry[0].buffer(radius)
streets = ox.graph_to_gdfs(streets, nodes = False)
streets = MultiLineString(list(
filter(
# Filter lines with at least 2 points
lambda line: len(line) >= 2,
# Iterate over lines in geometry
map(
# Filter points within perimeter
lambda line: list(filter(lambda xy: Point(xy).within(perimeter), zip(*line.xy))),
streets.geometry
)
)
)).buffer(dilate) # Dilate lines
if not isinstance(streets, Iterable):
streets = [streets]
streets = list(map(pathify, streets))
return streets, perimeter

File diff suppressed because one or more lines are too long

262
notebooks/world-tour.ipynb Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 621 KiB

69734
prints/CB-R-BF.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1.9 MiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.1 MiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 1021 KiB

74329
prints/Erbil.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.1 MiB

89121
prints/Macau.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 2.4 MiB

27294
prints/Palmanova.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 735 KiB