Updated prettymaps library, README and examples.ipynb notebook

pull/11/head
Marcelo Prates 2021-08-23 22:13:16 -03:00
rodzic dedab1066b
commit a234510ae8
21 zmienionych plików z 975019 dodań i 105 usunięć

Wyświetl plik

@ -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)

File diff suppressed because one or more lines are too long

Wyświetl plik

@ -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

Wyświetl plik

@ -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)

File diff suppressed because one or more lines are too long

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.0 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 691 KiB

79899
prints/barcelona.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 2.3 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 771 KiB

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 787 KiB

193926
prints/centro-poa.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.7 MiB

BIN
prints/erbil.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.2 MiB

157913
prints/erbil.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.9 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 765 KiB

143861
prints/heerhugowaard.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 4.3 MiB

BIN
prints/macao.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 821 KiB

114851
prints/macao.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.4 MiB

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 431 KiB

50537
prints/palmanova.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 1.8 MiB

BIN
prints/tijuca.png 100644

Plik binarny nie jest wyświetlany.

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 3.5 MiB

159285
prints/tijuca.svg 100644

Plik diff jest za duży Load Diff

Po

Szerokość:  |  Wysokość:  |  Rozmiar: 5.1 MiB