Updated prettymaps library, README and examples.ipynb notebook
91
README.md
@ -1,32 +1,79 @@
|
||||
# prettymaps
|
||||
|
||||
A small set of Python functions to draw pretty maps from OpenStreetMap data. Based on osmnx, matplotlib and shapely libraries.
|
||||
A small set of Python functions to draw pretty maps from OpenStreetMap data. Based on osmnx, matplotlib, shapely and vsketch libraries.
|
||||
|
||||
## Install dependencies
|
||||
## Installation
|
||||
|
||||
Install dependencies with
|
||||
|
||||
`$ conda env create environment.yml`
|
||||
|
||||
## Usage
|
||||
|
||||
On Python run:
|
||||
Install with
|
||||
|
||||
```
|
||||
from draw import plot
|
||||
|
||||
plot(f'Bom Fim, Porto Alegre', palette = ['red', 'blue'], layers = ['perimeter', 'landuse', 'water', 'streets'])
|
||||
$ pip install git+https://github.com/marceloprates/prettymaps.git
|
||||
```
|
||||
|
||||
## "Circle" plots ([Jupyter Notebook](/notebooks/world-tour.ipynb)):
|
||||
![](prints/Macau.svg)
|
||||
![](prints/Palmanova.svg)
|
||||
![](prints/Erbil.svg)
|
||||
## Usage example (For more examples, see [this Jupyter Notebook](https://github.com/marceloprates/prettymaps/notebooks/examples.ipynb)):
|
||||
|
||||
# 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)
|
||||
```python
|
||||
# Init matplotlib figure
|
||||
fig, ax = plt.subplots(figsize = (12, 12), constrained_layout = True)
|
||||
|
||||
## More than one district at a time:
|
||||
![](prints/CB-R-BF.svg)
|
||||
backup = plot(
|
||||
# Address:
|
||||
'Praça Ferreira do Amaral, Macau',
|
||||
# Plot geometries in a circle of radius:
|
||||
radius = 1100,
|
||||
# Matplotlib axis
|
||||
ax = ax,
|
||||
# Which OpenStreetMap layers to plot and their parameters:
|
||||
layers = {
|
||||
# Perimeter (in this case, a circle)
|
||||
'perimeter': {},
|
||||
# Streets and their widths
|
||||
'streets': {
|
||||
'width': {
|
||||
'motorway': 5,
|
||||
'trunk': 5,
|
||||
'primary': 4.5,
|
||||
'secondary': 4,
|
||||
'tertiary': 3.5,
|
||||
'residential': 3,
|
||||
'service': 2,
|
||||
'unclassified': 2,
|
||||
'pedestrian': 2,
|
||||
'footway': 1,
|
||||
}
|
||||
},
|
||||
# Other layers:
|
||||
# Specify a name (for example, 'building') and which OpenStreetMap tags to fetch
|
||||
'building': {'tags': {'building': True, 'landuse': 'construction'}, 'union': False},
|
||||
'water': {'tags': {'natural': ['water', 'bay']}},
|
||||
'green': {'tags': {'landuse': 'grass', 'natural': ['island', 'wood'], 'leisure': 'park'}},
|
||||
'forest': {'tags': {'landuse': 'forest'}},
|
||||
'parking': {'tags': {'amenity': 'parking', 'highway': 'pedestrian', 'man_made': 'pier'}}
|
||||
},
|
||||
# drawing_kwargs:
|
||||
# Reference a name previously defined in the 'layers' argument and specify matplotlib parameters to draw it
|
||||
drawing_kwargs = {
|
||||
'background': {'fc': '#F2F4CB', 'ec': '#dadbc1', 'hatch': 'ooo...', 'zorder': -1},
|
||||
'perimeter': {'fc': '#F2F4CB', 'ec': '#dadbc1', 'lw': 0, 'hatch': 'ooo...', 'zorder': 0},
|
||||
'green': {'fc': '#D0F1BF', 'ec': '#2F3737', 'lw': 1, 'zorder': 1},
|
||||
'forest': {'fc': '#64B96A', 'ec': '#2F3737', 'lw': 1, 'zorder': 1},
|
||||
'water': {'fc': '#a1e3ff', 'ec': '#2F3737', 'hatch': 'ooo...', 'hatch_c': '#85c9e6', 'lw': 1, 'zorder': 2},
|
||||
'parking': {'fc': '#F2F4CB', 'ec': '#2F3737', 'lw': 1, 'zorder': 3},
|
||||
'streets': {'fc': '#2F3737', 'ec': '#475657', 'alpha': 1, 'lw': 0, 'zorder': 3},
|
||||
'building': {'palette': ['#FFC857', '#E9724C', '#C5283D'], 'ec': '#2F3737', 'lw': .5, 'zorder': 4},
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
![](prints/macao.png)
|
||||
|
||||
## Gallery:
|
||||
|
||||
### Barcelona:
|
||||
![](prints/barcelona.png)
|
||||
### Heerhugowaard:
|
||||
![](prints/heerhugowaard.png)
|
||||
### Barra da Tijuca:
|
||||
![](prints/tijuca.png)
|
||||
### Porto Alegre:
|
||||
![](prints/bomfim-farroupilha-cidadebaixa.png)
|
3905
notebooks/examples.ipynb
Normal file
@ -1,11 +1,9 @@
|
||||
# OpenStreetMap Networkx library to download data from OpenStretMap
|
||||
#from sympy import geometry
|
||||
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
|
||||
@ -27,37 +25,9 @@ from IPython.display import Markdown, display
|
||||
from collections.abc import Iterable
|
||||
|
||||
# Fetch
|
||||
from fetch import *
|
||||
|
||||
# Helper functions
|
||||
def get_hash(key):
|
||||
return frozenset(key.items()) if type(key) == dict else key
|
||||
|
||||
# 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)
|
||||
if type(shape) == Polygon and shape.area > 0:
|
||||
return PolygonPatch(list(zip(*shape.exterior.xy)), **kwargs)
|
||||
else:
|
||||
return None
|
||||
from .fetch import *
|
||||
|
||||
# Plot a single shape
|
||||
def plot_shape(shape, ax, vsketch = None, **kwargs):
|
||||
'''
|
||||
Plot shapely object
|
||||
@ -67,18 +37,30 @@ def plot_shape(shape, ax, vsketch = None, **kwargs):
|
||||
plot_shape(shape_, ax, vsketch = vsketch, **kwargs)
|
||||
else:
|
||||
if not shape.is_empty:
|
||||
|
||||
if vsketch is None:
|
||||
ax.add_patch(PolygonPatch(shape, **kwargs))
|
||||
else:
|
||||
if ('draw' not in kwargs) or kwargs['draw']:
|
||||
|
||||
if ('pen' in kwargs):
|
||||
vsketch.stroke(kwargs['pen'])
|
||||
|
||||
if 'stroke' in kwargs:
|
||||
vsketch.stroke(kwargs['stroke'])
|
||||
else:
|
||||
vsketch.stroke(1)
|
||||
|
||||
if 'penWidth' in kwargs:
|
||||
vsketch.penWidth(kwargs['penWidth'])
|
||||
else:
|
||||
vsketch.penWidth(0.3)
|
||||
|
||||
if 'fill' in kwargs:
|
||||
vsketch.fill(kwargs['fill'])
|
||||
else:
|
||||
vsketch.noFill()
|
||||
|
||||
vsketch.geometry(shape)
|
||||
|
||||
# Plot a collection of shapes
|
||||
def plot_shapes(shapes, ax, vsketch = None, palette = None, **kwargs):
|
||||
'''
|
||||
Plot collection of shapely objects (optionally, use a color palette)
|
||||
@ -92,11 +74,39 @@ def plot_shapes(shapes, ax, vsketch = None, palette = None, **kwargs):
|
||||
else:
|
||||
plot_shape(shape, ax, vsketch = vsketch, fc = choice(palette), **kwargs)
|
||||
|
||||
# Parse query (by coordinates, OSMId or name)
|
||||
def parse_query(query):
|
||||
if type(query) == tuple:
|
||||
return 'coordinates'
|
||||
elif False:
|
||||
return 'osmid'
|
||||
else:
|
||||
return 'address'
|
||||
|
||||
# Apply transformation (translation & scale) to layers
|
||||
def transform(layers, x, y, scale_x, scale_y, rotation):
|
||||
# Transform layers (translate & scale)
|
||||
k, v = zip(*layers.items())
|
||||
v = GeometryCollection(v)
|
||||
if (x is not None) and (y is not None):
|
||||
v = translate(v, *(np.array([x, y]) - np.concatenate(v.centroid.xy)))
|
||||
if scale_x is not None:
|
||||
v = scale(v, scale_x, 1)
|
||||
if scale_y is not None:
|
||||
v = scale(v, 1, scale_y)
|
||||
if rotation is not None:
|
||||
v = rotate(v, rotation)
|
||||
layers = dict(zip(k, v))
|
||||
return layers
|
||||
|
||||
# Plot
|
||||
def plot(
|
||||
# Address
|
||||
query,
|
||||
# Whether to use a backup for the layers
|
||||
backup = None,
|
||||
# Custom postprocessing function on layers
|
||||
postprocessing = None,
|
||||
# Radius (in case of circular plot)
|
||||
radius = None,
|
||||
# Which layers to plot
|
||||
@ -108,33 +118,36 @@ def plot(
|
||||
# Vsketch parameters
|
||||
vsketch = None,
|
||||
# Transform (translation & scale) params
|
||||
x = None, y = None, sf = None, rotation = None,
|
||||
x = None, y = None, scale_x = None, scale_y = None, rotation = None,
|
||||
):
|
||||
|
||||
# Interpret query
|
||||
if type(query) == tuple:
|
||||
query_mode = 'coordinates'
|
||||
elif False:
|
||||
query_mode = 'osmid'
|
||||
else:
|
||||
query_mode = 'address'
|
||||
query_mode = parse_query(query)
|
||||
|
||||
# Save maximum dilation for later use
|
||||
dilations = [kwargs['dilate'] for kwargs in layers.values() if 'dilate' in kwargs]
|
||||
max_dilation = max(dilations) if len(dilations) > 0 else 0
|
||||
|
||||
if backup is None:
|
||||
|
||||
#############
|
||||
### Fetch ###
|
||||
#############
|
||||
####################
|
||||
### Fetch Layers ###
|
||||
####################
|
||||
|
||||
# Use backup if provided
|
||||
if backup is not None:
|
||||
layers = backup
|
||||
# Otherwise, fetch layers
|
||||
else:
|
||||
# Define base kwargs
|
||||
if radius:
|
||||
base_kwargs = {'point': query if type(query) == tuple else ox.geocode(query), 'radius': radius}
|
||||
base_kwargs = {
|
||||
'point': query if query_mode == 'coordinates' else ox.geocode(query),
|
||||
'radius': radius
|
||||
}
|
||||
else:
|
||||
by_osmid = False
|
||||
base_kwargs = {'perimeter': get_perimeter(query, by_osmid = by_osmid)}
|
||||
base_kwargs = {
|
||||
'perimeter': get_perimeter(query, by_osmid = by_osmid)
|
||||
}
|
||||
|
||||
# Fetch layers
|
||||
layers = {
|
||||
@ -146,20 +159,18 @@ def plot(
|
||||
for layer, kwargs in layers.items()
|
||||
}
|
||||
|
||||
# Transform layers (translate & scale)
|
||||
k, v = zip(*layers.items())
|
||||
v = GeometryCollection(v)
|
||||
if (x is not None) and (y is not None):
|
||||
v = translate(v, *(np.array([x, y]) - np.concatenate(v.centroid.xy)))
|
||||
if sf is not None:
|
||||
v = scale(v, sf, sf)
|
||||
if rotation is not None:
|
||||
v = rotate(v, rotation)
|
||||
layers = dict(zip(k, v))
|
||||
# Apply transformation to layers (translate & scale)
|
||||
layers = transform(layers, x, y, scale_x, scale_y, rotation)
|
||||
|
||||
else:
|
||||
layers = backup
|
||||
# Apply postprocessing step to layers
|
||||
if postprocessing is not None:
|
||||
layers = postprocessing(layers)
|
||||
|
||||
############
|
||||
### Plot ###
|
||||
############
|
||||
|
||||
# Matplot-specific stuff (only run if vsketch mode isn't activated)
|
||||
if vsketch is None:
|
||||
# Ajust axis
|
||||
ax.axis('off')
|
||||
@ -180,10 +191,6 @@ def plot(
|
||||
ax.add_patch(PolygonPatch(geom, **drawing_kwargs['background']))
|
||||
else:
|
||||
vsketch.geometry(geom)
|
||||
|
||||
############
|
||||
### Plot ###
|
||||
############
|
||||
|
||||
# Adjust bounds
|
||||
xmin, ymin, xmax, ymax = layers['perimeter'].buffer(max_dilation).bounds
|
||||
@ -195,9 +202,12 @@ def plot(
|
||||
for layer, shapes in layers.items():
|
||||
kwargs = drawing_kwargs[layer] if layer in drawing_kwargs else {}
|
||||
if 'hatch_c' in kwargs:
|
||||
# Draw hatched shape
|
||||
plot_shapes(shapes, ax, vsketch = vsketch, lw = 0, ec = kwargs['hatch_c'], **{k:v for k,v in kwargs.items() if k not in ['lw', 'ec', 'hatch_c']})
|
||||
# Draw shape contour only
|
||||
plot_shapes(shapes, ax, vsketch = vsketch, fill = False, **{k:v for k,v in kwargs.items() if k not in ['hatch_c', 'hatch', 'fill']})
|
||||
else:
|
||||
# Draw shape normally
|
||||
plot_shapes(shapes, ax, vsketch = vsketch, **kwargs)
|
||||
|
||||
# Return perimeter
|
||||
|
@ -24,23 +24,7 @@ from descartes import PolygonPatch
|
||||
|
||||
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
|
||||
|
||||
# Compute circular or square boundary given point, radius and crs
|
||||
def get_boundary(point, radius, crs, circle = True, dilate = 0):
|
||||
if circle:
|
||||
return ox.project_gdf(
|
||||
@ -55,9 +39,11 @@ def get_boundary(point, radius, crs, circle = True, dilate = 0):
|
||||
(x-r, y-r), (x+r, y-r), (x+r, y+r), (x-r, y+r)
|
||||
]).buffer(dilate)
|
||||
|
||||
# Get perimeter
|
||||
def get_perimeter(query, by_osmid = False, **kwargs):
|
||||
return ox.geocode_to_gdf(query, by_osmid = by_osmid, **kwargs, **{x: kwargs[x] for x in ['circle', 'dilate'] if x in kwargs.keys()})
|
||||
|
||||
# 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:
|
||||
@ -93,12 +79,13 @@ def get_geometries(perimeter = None, point = None, radius = None, tags = {}, per
|
||||
|
||||
return geometries
|
||||
|
||||
# Get streets
|
||||
def get_streets(perimeter = None, point = None, radius = None, width = 6, custom_filter = None, circle = True, dilate = 0):
|
||||
|
||||
# Boundary defined by polygon (perimeter)
|
||||
if perimeter is not None:
|
||||
# Fetch streets data, project & convert to GDF
|
||||
streets = ox.graph_from_polygon(union(perimeter.geometry), custom_filter = custom_filter)
|
||||
streets = ox.graph_from_polygon(unary_union(perimeter.geometry), custom_filter = custom_filter)
|
||||
streets = ox.project_graph(streets)
|
||||
streets = ox.graph_to_gdfs(streets, nodes = False)
|
||||
# Boundary defined by polygon (perimeter)
|
||||
@ -133,10 +120,14 @@ def get_streets(perimeter = None, point = None, radius = None, width = 6, custom
|
||||
|
||||
return streets
|
||||
|
||||
# Get any layer
|
||||
def get_layer(layer, **kwargs):
|
||||
# Fetch perimeter
|
||||
if layer == 'perimeter':
|
||||
# If perimeter is already provided:
|
||||
if 'perimeter' in kwargs:
|
||||
return unary_union(ox.project_gdf(kwargs['perimeter']).geometry)
|
||||
# If point and radius are provided:
|
||||
elif 'point' in kwargs and 'radius' in kwargs:
|
||||
# Dummy request to fetch CRS
|
||||
crs = ox.graph_to_gdfs(ox.graph_from_point(kwargs['point'], dist = kwargs['radius']), nodes = False).crs
|
||||
@ -147,7 +138,9 @@ def get_layer(layer, **kwargs):
|
||||
return perimeter
|
||||
else:
|
||||
raise Exception("Either 'perimeter' or 'point' & 'radius' must be provided")
|
||||
# Fetch streets or railway
|
||||
if layer in ['streets', 'railway']:
|
||||
return get_streets(**kwargs)
|
||||
# Fetch geometries
|
||||
else:
|
||||
return get_geometries(**kwargs)
|
2997
prints/barcelona-plotter.svg
Normal file
After Width: | Height: | Size: 1.0 MiB |
BIN
prints/barcelona.png
Normal file
After Width: | Height: | Size: 691 KiB |
79899
prints/barcelona.svg
Normal file
After Width: | Height: | Size: 2.3 MiB |
BIN
prints/bomfim-farroupilha-cidadebaixa.png
Normal file
After Width: | Height: | Size: 771 KiB |
67690
prints/bomfim-farroupilha-cidadebaixa.svg
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
prints/centro-poa.png
Normal file
After Width: | Height: | Size: 787 KiB |
193926
prints/centro-poa.svg
Normal file
After Width: | Height: | Size: 5.7 MiB |
BIN
prints/erbil.png
Normal file
After Width: | Height: | Size: 1.2 MiB |
157913
prints/erbil.svg
Normal file
After Width: | Height: | Size: 4.9 MiB |
BIN
prints/heerhugowaard.png
Normal file
After Width: | Height: | Size: 765 KiB |
143861
prints/heerhugowaard.svg
Normal file
After Width: | Height: | Size: 4.3 MiB |
BIN
prints/macao.png
Normal file
After Width: | Height: | Size: 821 KiB |
114851
prints/macao.svg
Normal file
After Width: | Height: | Size: 3.4 MiB |
BIN
prints/palmanova.png
Normal file
After Width: | Height: | Size: 431 KiB |
50537
prints/palmanova.svg
Normal file
After Width: | Height: | Size: 1.8 MiB |
BIN
prints/tijuca.png
Normal file
After Width: | Height: | Size: 3.5 MiB |
159285
prints/tijuca.svg
Normal file
After Width: | Height: | Size: 5.1 MiB |