Merge branch 'main' into main

This commit is contained in:
Marcelo de Oliveira Rosa Prates
2021-09-06 09:10:17 -03:00
committed by GitHub
5 changed files with 323 additions and 200 deletions

View File

@@ -8,8 +8,9 @@ class CurvedText(mtext.Text):
"""
A text object that follows an arbitrary curve.
"""
def __init__(self, x, y, text, axes, **kwargs):
super(CurvedText, self).__init__(x[0],y[0],' ', **kwargs)
super(CurvedText, self).__init__(x[0], y[0], " ", **kwargs)
axes.add_artist(self)
@@ -21,22 +22,21 @@ class CurvedText(mtext.Text):
##creating the text objects
self.__Characters = []
for c in text:
if c == ' ':
if c == " ":
##make this an invisible 'a':
t = mtext.Text(0,0,'a')
t = mtext.Text(0, 0, "a")
t.set_alpha(0.0)
else:
t = mtext.Text(0, 0, c, **kwargs)
# resetting unnecessary arguments
t.set_ha('center')
t.set_ha("center")
t.set_rotation(0)
t.set_zorder(self.__zorder + 1)
self.__Characters.append((c, t))
axes.add_artist(t)
##overloading some member functions, to assure correct functionality
##on update
def set_zorder(self, zorder):
@@ -75,14 +75,17 @@ class CurvedText(mtext.Text):
# points of the curve in figure coordinates:
x_fig, y_fig = (
np.array(l) for l in zip(*self.axes.transData.transform([
(i,j) for i,j in zip(self.__x,self.__y)
]))
np.array(l)
for l in zip(
*self.axes.transData.transform(
[(i, j) for i, j in zip(self.__x, self.__y)]
)
)
)
# point distances in figure coordinates
x_fig_dist = (x_fig[1:]-x_fig[:-1])
y_fig_dist = (y_fig[1:]-y_fig[:-1])
x_fig_dist = x_fig[1:] - x_fig[:-1]
y_fig_dist = y_fig[1:] - y_fig[:-1]
r_fig_dist = np.sqrt(x_fig_dist ** 2 + y_fig_dist ** 2)
# arc length in figure coordinates
@@ -92,12 +95,11 @@ class CurvedText(mtext.Text):
rads = np.arctan2((y_fig[1:] - y_fig[:-1]), (x_fig[1:] - x_fig[:-1]))
degs = np.rad2deg(rads)
rel_pos = 10
for c, t in self.__Characters:
# finding the width of c:
t.set_rotation(0)
t.set_va('center')
t.set_va("center")
bbox1 = t.get_window_extent(renderer=renderer)
w = bbox1.width
h = bbox1.height
@@ -108,7 +110,7 @@ class CurvedText(mtext.Text):
rel_pos += w
continue
elif c != ' ':
elif c != " ":
t.set_alpha(1.0)
# finding the two data points between which the horizontal
@@ -145,10 +147,12 @@ class CurvedText(mtext.Text):
# the rotation/stretch matrix
rad = rads[il]
rot_mat = np.array([
rot_mat = np.array(
[
[math.cos(rad), math.sin(rad) * aspect],
[-math.sin(rad)/aspect, math.cos(rad)]
])
[-math.sin(rad) / aspect, math.cos(rad)],
]
)
##computing the offset vector of the rotated character
drp = np.dot(dr, rot_mat)
@@ -157,8 +161,8 @@ class CurvedText(mtext.Text):
t.set_position(np.array([x, y]) + drp)
t.set_rotation(degs[il])
t.set_va('center')
t.set_ha('center')
t.set_va("center")
t.set_ha("center")
# updating rel_pos to right edge of character
rel_pos += w - used

View File

@@ -6,7 +6,7 @@ import pandas as pd
from geopandas import GeoDataFrame
import numpy as np
from numpy.random import choice
from shapely.geometry import Polygon, MultiPolygon, MultiLineString, GeometryCollection
from shapely.geometry import box, Polygon, MultiLineString, GeometryCollection
from shapely.affinity import translate, scale, rotate
from descartes import PolygonPatch
from tabulate import tabulate
@@ -19,24 +19,25 @@ from .fetch import get_perimeter, get_layer
def get_hash(key):
return frozenset(key.items()) if type(key) == dict else key
# Drawing functions
def show_palette(palette, description = ''):
'''
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
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:
@@ -44,11 +45,12 @@ def get_patch(shape, **kwargs):
else:
return None
# Plot a single shape
def plot_shape(shape, ax, vsketch=None, **kwargs):
'''
"""
Plot shapely object
'''
"""
if isinstance(shape, Iterable) and type(shape) != MultiLineString:
for shape_ in shape:
plot_shape(shape_, ax, vsketch=vsketch, **kwargs)
@@ -58,30 +60,31 @@ def plot_shape(shape, ax, vsketch = None, **kwargs):
if vsketch is None:
ax.add_patch(PolygonPatch(shape, **kwargs))
else:
if ('draw' not in kwargs) or kwargs['draw']:
if ("draw" not in kwargs) or kwargs["draw"]:
if 'stroke' in kwargs:
vsketch.stroke(kwargs['stroke'])
if "stroke" in kwargs:
vsketch.stroke(kwargs["stroke"])
else:
vsketch.stroke(1)
if 'penWidth' in kwargs:
vsketch.penWidth(kwargs['penWidth'])
if "penWidth" in kwargs:
vsketch.penWidth(kwargs["penWidth"])
else:
vsketch.penWidth(0.3)
if 'fill' in kwargs:
vsketch.fill(kwargs['fill'])
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)
'''
"""
if not isinstance(shapes, Iterable):
shapes = [shapes]
@@ -91,16 +94,18 @@ 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 isinstance(query, GeoDataFrame):
return 'polygon'
return "polygon"
elif isinstance(query, tuple):
return 'coordinates'
elif re.match('''[A-Z][0-9]+''', query):
return 'osmid'
return "coordinates"
elif re.match("""[A-Z][0-9]+""", query):
return "osmid"
else:
return 'address'
return "address"
# Apply transformation (translation & scale) to layers
def transform(layers, x, y, scale_x, scale_y, rotation):
@@ -118,9 +123,11 @@ def transform(layers, x, y, scale_x, scale_y, rotation):
layers = dict(zip(k, v))
return layers
def draw_text(ax, text, x, y, **kwargs):
ax.text(x, y, text, **kwargs)
# Plot
def plot(
# Address
@@ -132,24 +139,30 @@ def plot(
# Radius (in case of circular plot)
radius=None,
# Which layers to plot
layers = {'perimeter': {}},
layers={"perimeter": {}},
# Drawing params for each layer (matplotlib params such as 'fc', 'ec', 'fill', etc.)
drawing_kwargs={},
# OSM Caption parameters
osm_credit={},
# Figure parameters
figsize = (10, 10), ax = None, title = None,
figsize=(10, 10),
ax=None,
title=None,
# Vsketch parameters
vsketch=None,
# Transform (translation & scale) params
x = None, y = None, scale_x = None, scale_y = None, rotation = None,
x=None,
y=None,
scale_x=None,
scale_y=None,
rotation=None,
):
# Interpret query
query_mode = parse_query(query)
# Save maximum dilation for later use
dilations = [kwargs['dilate'] for kwargs in layers.values() if 'dilate' in kwargs]
dilations = [kwargs["dilate"] for kwargs in layers.values() if "dilate" in kwargs]
max_dilation = max(dilations) if len(dilations) > 0 else 0
####################
@@ -164,20 +177,20 @@ def plot(
# Define base kwargs
if radius:
base_kwargs = {
'point': query if query_mode == 'coordinates' else ox.geocode(query),
'radius': radius
"point": query if query_mode == "coordinates" else ox.geocode(query),
"radius": radius,
}
else:
base_kwargs = {
'perimeter': query if query_mode == 'polygon' else get_perimeter(query, by_osmid = query_mode == 'osmid')
"perimeter": query
if query_mode == "polygon"
else get_perimeter(query, by_osmid=query_mode == "osmid")
}
# Fetch layers
layers = {
layer: get_layer(
layer,
**base_kwargs,
**(kwargs if type(kwargs) == dict else {})
layer, **base_kwargs, **(kwargs if type(kwargs) == dict else {})
)
for layer, kwargs in layers.items()
}
@@ -196,27 +209,21 @@ def plot(
# Matplot-specific stuff (only run if vsketch mode isn't activated)
if vsketch is None:
# Ajust axis
ax.axis('off')
ax.axis('equal')
ax.axis("off")
ax.axis("equal")
ax.autoscale()
# Plot background
if 'background' in drawing_kwargs:
xmin, ymin, xmax, ymax = layers['perimeter'].bounds
geom = scale(Polygon([
(xmin, ymin),
(xmin, ymax),
(xmax, ymax),
(xmax, ymin)
]), 2, 2)
if "background" in drawing_kwargs:
geom = scale(box(*layers["perimeter"].bounds), 2, 2)
if vsketch is None:
ax.add_patch(PolygonPatch(geom, **drawing_kwargs['background']))
ax.add_patch(PolygonPatch(geom, **drawing_kwargs["background"]))
else:
vsketch.geometry(geom)
# Adjust bounds
xmin, ymin, xmax, ymax = layers['perimeter'].buffer(max_dilation).bounds
xmin, ymin, xmax, ymax = layers["perimeter"].buffer(max_dilation).bounds
dx, dy = xmax - xmin, ymax - ymin
if vsketch is None:
ax.set_xlim(xmin, xmax)
@@ -225,27 +232,58 @@ def plot(
# Draw layers
for layer, shapes in layers.items():
kwargs = drawing_kwargs[layer] if layer in drawing_kwargs else {}
if 'hatch_c' in kwargs:
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']})
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']})
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)
if ((isinstance(osm_credit, dict)) or (osm_credit is True)) and (vsketch is None):
x, y = figsize
d = .8*(x**2+y**2)**.5
d = 0.8 * (x ** 2 + y ** 2) ** 0.5
draw_text(
ax,
(osm_credit['text'] if 'text' in osm_credit else 'data © OpenStreetMap contributors\ngithub.com/marceloprates/prettymaps'),
x = xmin + (osm_credit['x']*dx if 'x' in osm_credit else 0),
y = ymax - 4*d - (osm_credit['y']*dy if 'y' in osm_credit else 0),
fontfamily = (osm_credit['fontfamily'] if 'fontfamily' in osm_credit else 'Ubuntu Mono'),
fontsize = (osm_credit['fontsize']*d if 'fontsize' in osm_credit else d),
zorder = (osm_credit['zorder'] if 'zorder' in osm_credit else len(layers)+1),
**{k:v for k,v in osm_credit.items() if k not in ['text', 'x', 'y', 'fontfamily', 'fontsize', 'zorder']}
(
osm_credit["text"]
if "text" in osm_credit
else "data © OpenStreetMap contributors\ngithub.com/marceloprates/prettymaps"
),
x=xmin + (osm_credit["x"] * dx if "x" in osm_credit else 0),
y=ymax - 4 * d - (osm_credit["y"] * dy if "y" in osm_credit else 0),
fontfamily=(
osm_credit["fontfamily"]
if "fontfamily" in osm_credit
else "Ubuntu Mono"
),
fontsize=(osm_credit["fontsize"] * d if "fontsize" in osm_credit else d),
zorder=(
osm_credit["zorder"] if "zorder" in osm_credit else len(layers) + 1
),
**{
k: v
for k, v in osm_credit.items()
if k not in ["text", "x", "y", "fontfamily", "fontsize", "zorder"]
},
)
# Return perimeter

View File

@@ -11,21 +11,32 @@ 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):
if circle:
return ox.project_gdf(
GeoDataFrame(geometry = [Point(point[::-1])], crs = crs)
).geometry[0].buffer(radius)
return (
ox.project_gdf(GeoDataFrame(geometry=[Point(point[::-1])], crs=crs))
.geometry[0]
.buffer(radius)
)
else:
x, y = np.stack(ox.project_gdf(
GeoDataFrame(geometry = [Point(point[::-1])], crs = crs)
).geometry[0].xy)
x, y = np.stack(
ox.project_gdf(GeoDataFrame(geometry=[Point(point[::-1])], crs=crs))
.geometry[0]
.xy
)
r = radius
return Polygon([
(x-r, y-r), (x+r, y-r), (x+r, y+r), (x-r, y+r)
]).buffer(dilate)
return Polygon(
[(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()})
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 coastline
def get_coast(perimeter = None, point = None, radius = None, perimeter_tolerance = 0, union = True, buffer = 0, circle = True, dilate = 0, file_location = None):
@@ -68,20 +79,37 @@ def get_coast(perimeter = None, point = None, radius = None, perimeter_tolerance
return geometries
# Get geometries
def get_geometries(perimeter = None, point = None, radius = None, tags = {}, perimeter_tolerance = 0, union = True, circle = True, dilate = 0):
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
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)
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:
@@ -91,34 +119,70 @@ def get_geometries(perimeter = None, point = None, radius = None, tags = {}, per
geometries = geometries.intersection(perimeter)
if union:
geometries = unary_union(reduce(lambda x,y: x+y, [
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]
], []))
for x in geometries
if type(x) in [Polygon, MultiPolygon]
],
[],
)
)
else:
geometries = MultiPolygon(reduce(lambda x,y: x+y, [
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]
], []))
for x in geometries
if type(x) in [Polygon, MultiPolygon]
],
[],
)
)
return geometries
# Get streets
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):
if layer == 'streets':
layer = 'highway'
# Get streets
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,
):
if layer == "streets":
layer = "highway"
# Boundary defined by polygon (perimeter)
if perimeter is not None:
# Fetch streets data, project & convert to GDF
streets = ox.graph_from_polygon(unary_union(perimeter.geometry).buffer(buffer) if buffer > 0 else unary_union(perimeter.geometry), custom_filter = custom_filter)
streets = ox.graph_from_polygon(
unary_union(perimeter.geometry).buffer(buffer)
if buffer > 0
else 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)
elif (point is not None) and (radius is not None):
# Fetch streets data, save CRS & project
streets = ox.graph_from_point(point, dist = radius+dilate+buffer, retain_all = retain_all, custom_filter = custom_filter)
streets = ox.graph_from_point(
point,
dist=radius + dilate + buffer,
retain_all=retain_all,
custom_filter=custom_filter,
)
crs = ox.graph_to_gdfs(streets, nodes=False).crs
streets = ox.project_graph(streets)
# Compute perimeter from point & CRS
@@ -130,36 +194,53 @@ def get_streets(perimeter = None, point = None, radius = None, layer = 'streets'
streets = streets[~streets.geometry.is_empty]
if type(width) == dict:
streets = unary_union([
streets = unary_union(
[
# Dilate streets of each highway type == 'highway' using width 'w'
MultiLineString(
streets[(streets[layer] == highway) & (streets.geometry.type == 'LineString')].geometry.tolist() +
list(reduce(lambda x, y: x+y, [
streets[
(streets[layer] == highway)
& (streets.geometry.type == "LineString")
].geometry.tolist()
+ list(
reduce(
lambda x, y: x + y,
[
list(lines)
for lines in streets[(streets[layer] == highway) & (streets.geometry.type == 'MultiLineString')].geometry
], []))
for lines in streets[
(streets[layer] == highway)
& (streets.geometry.type == "MultiLineString")
].geometry
],
[],
)
)
).buffer(w)
for highway, w in width.items()
])
]
)
else:
# Dilate all streets by same amount 'width'
streets = MultiLineString(streets.geometry.tolist()).buffer(width)
return streets
# Get any layer
def get_layer(layer, **kwargs):
# Fetch perimeter
if layer == 'perimeter':
if layer == "perimeter":
# If perimeter is already provided:
if 'perimeter' in kwargs:
return unary_union(ox.project_gdf(kwargs['perimeter']).geometry)
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:
elif "point" in kwargs and "radius" in kwargs:
crs = "EPSG:4326"
perimeter = get_boundary(
kwargs['point'], kwargs['radius'], crs,
**{x: kwargs[x] for x in ['circle', 'dilate'] if x in kwargs.keys()}
kwargs["point"],
kwargs["radius"],
crs,
**{x: kwargs[x] for x in ["circle", "dilate"] if x in kwargs.keys()}
)
return perimeter
else:

View File

@@ -4,18 +4,18 @@ from pathlib import Path
parent_dir = Path(__file__).resolve().parent
setup(
name='prettymaps',
version='1.0.0',
description='A simple python library to draw pretty maps from OpenStreetMap data',
name="prettymaps",
version="1.0.0",
description="A simple python library to draw pretty maps from OpenStreetMap data",
long_description=parent_dir.joinpath("README.md").read_text(),
long_description_content_type="text/markdown",
url='https://github.com/marceloprates/prettymaps',
author='Marcelo Prates',
author_email='marceloorp@gmail.com',
license='MIT License',
url="https://github.com/marceloprates/prettymaps",
author="Marcelo Prates",
author_email="marceloorp@gmail.com",
license="MIT License",
packages=find_packages(exclude=("assets", "notebooks", "prints", "script")),
install_requires=parent_dir.joinpath("requirements.txt").read_text().splitlines(),
classifiers=[
'Intended Audience :: Science/Research',
"Intended Audience :: Science/Research",
],
)