""" Prettymaps - A minimal Python library to draw pretty maps from OpenStreetMap Data Copyright (C) 2021 Marcelo Prates This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . """ import re import os import json import yaml #import IPython import pathlib import warnings import matplotlib import shapely.ops import numpy as np import osmnx as ox import pandas as pd import geopandas as gp import shapely.affinity from copy import deepcopy from .fetch import get_gdfs from dataclasses import dataclass from matplotlib import pyplot as plt from matplotlib.colors import hex2color from matplotlib.patches import Path, PathPatch from typing import Optional, Union, Tuple, List, Dict, Any, Iterable from shapely.geometry.base import BaseGeometry from shapely.geometry import Point, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection, box #import vsketch class Subplot: """ Class implementing a prettymaps Subplot. Attributes: - query: prettymaps.plot() query - kwargs: dictionary of prettymaps.plot() parameters """ def __init__(self, query, **kwargs): self.query = query self.kwargs = kwargs @dataclass class Plot: """ Dataclass implementing a prettymaps Plot object. Attributes: - geodataframes: A dictionary of GeoDataFrames (one for each plot layer) - fig: A matplotlib figure - ax: A matplotlib axis object - background: Background layer (shapely object) """ geodataframes: Dict[str, gp.GeoDataFrame] fig: matplotlib.figure.Figure ax: matplotlib.axes.Axes background: BaseGeometry @dataclass class Preset: """ Dataclass implementing a prettymaps Preset object. Attributes: - params: dictionary of prettymaps.plot() parameters """ params: dict ''' def _ipython_display_(self): """ Implements the _ipython_display_() function for the Preset class. 'params' will be displayed as a Markdown table with annotated hex colors """ def light_color(hexstring): rgb = np.array(hex2color(hexstring)) return rgb.mean() > .5 def annotate_colors(text): matches = re.findall( '#(?:\\d|[a-f]|[A-F]){6}|#(?:\\d|[a-f]|[A-F]){4}|#(?:\\d|[a-f]|[A-F]){3}', text) for match in matches: text = text.replace( match, f'{match}' ) return text params = pd.DataFrame(self.params) params = params.applymap(lambda x: annotate_colors( yaml.dump(x, default_flow_style=False).replace('\n', '
'))) params.iloc[1:, 2:] = '' IPython.display.display(IPython.display.Markdown(params.to_markdown())) ''' def transform_gdfs( gdfs: Dict[str, gp.GeoDataFrame], x: float = 0, y: float = 0, scale_x: float = 1, scale_y: float = 1, rotation: float = 0 ) -> Dict[str, gp.GeoDataFrame]: """ Apply geometric transformations to dictionary of GeoDataFrames Args: gdfs (Dict[str, gp.GeoDataFrame]): Dictionary of GeoDataFrames x (float, optional): x-axis translation. Defaults to 0. y (float, optional): y-axis translation. Defaults to 0. scale_x (float, optional): x-axis scale. Defaults to 1. scale_y (float, optional): y-axis scale. Defaults to 1. rotation (float, optional): rotation angle (in radians). Defaults to 0. Returns: Dict[str, gp.GeoDataFrame]: dictionary of transformed GeoDataFrames """ # Project geometries gdfs = { name: ox.project_gdf(gdf) if len(gdf) > 0 else gdf for name, gdf in gdfs.items() } # Create geometry collection from gdfs' geometries collection = GeometryCollection( [GeometryCollection(list(gdf.geometry)) for gdf in gdfs.values()] ) # Translation, scale & rotation collection = shapely.affinity.translate(collection, x, y) collection = shapely.affinity.scale( collection, scale_x, scale_y) collection = shapely.affinity.rotate(collection, rotation) # Update geometries for i, layer in enumerate(gdfs): gdfs[layer].geometry = list(collection.geoms[i].geoms) # Reproject if len(gdfs[layer]) > 0: gdfs[layer] = ox.project_gdf(gdfs[layer], to_crs="EPSG:4326") return gdfs def PolygonPatch( shape: BaseGeometry, **kwargs ) -> PathPatch: """_summary_ Args: shape (BaseGeometry): Shapely geometry kwargs: parameters for matplotlib's PathPatch constructor Returns: PathPatch: matplotlib PatchPatch created from input shapely geometry """ # Init vertices and codes lists vertices, codes = [], [] for poly in shape.geoms if isinstance(shape, Iterable) else [shape]: # Get polygon's exterior and interiors exterior = np.array(poly.exterior.xy) interiors = [np.array(interior.xy) for interior in poly.interiors] # Append to vertices and codes lists vertices += [exterior] + interiors codes += list( map( # Ring coding lambda p: [Path.MOVETO] + [Path.LINETO] * (p.shape[1] - 2) + [Path.CLOSEPOLY], [exterior] + interiors, ) ) # Generate PathPatch return PathPatch( Path(np.concatenate(vertices, 1).T, np.concatenate(codes)), **kwargs ) def plot_gdf( layer: str, gdf: gp.GeoDataFrame, ax: matplotlib.axes.Axes, mode: str = 'matplotlib', #vsk: Optional[vsketch.SketchClass] = None, vsk=None, palette: Optional[List[str]] = None, width: Optional[Union[dict, float]] = None, union: bool = False, dilate_points: Optional[float] = None, dilate_lines: Optional[float] = None, **kwargs, ) -> None: """ Plot a layer Args: layer (str): layer name gdf (gp.GeoDataFrame): GeoDataFrame ax (matplotlib.axes.Axes): matplotlib axis object mode (str): drawing mode. Options: 'matplotlib', 'vsketch'. Defaults to 'matplotlib' vsk (Optional[vsketch.SketchClass]): Vsketch object. Mandatory if mode == 'plotter' palette (Optional[List[str]], optional): Color palette. Defaults to None. width (Optional[Union[dict, float]], optional): Street widths. Either a dictionary or a float. Defaults to None. union (bool, optional): Whether to join geometries. Defaults to False. dilate_points (Optional[float], optional): Amount of dilation to be applied to point (1D) geometries. Defaults to None. dilate_lines (Optional[float], optional): Amount of dilation to be applied to line (2D) geometries. Defaults to None. Raises: Exception: _description_ """ # Get hatch and hatch_c parameter hatch_c = kwargs.pop("hatch_c") if "hatch_c" in kwargs else None # Convert GDF to shapely geometries geometries = gdf_to_shapely( layer, gdf, width, point_size=dilate_points, line_width=dilate_lines ) geometries = geometries.geoms if isinstance( geometries, Iterable) else [geometries] # Unite geometries if union: geometries = shapely.ops.unary_union(geometries) if (palette is None) and ("fc" in kwargs) and (type(kwargs["fc"]) != str): palette = kwargs.pop("fc") # Plot shapes for shape in geometries: if mode == "matplotlib": if type(shape) in [Polygon, MultiPolygon]: # Plot main shape (without silhouette) ax.add_patch( PolygonPatch( shape, lw=0, ec=hatch_c if hatch_c else kwargs["ec"] if "ec" in kwargs else None, fc=kwargs["fc"] if "fc" in kwargs else np.random.choice(palette) if palette else None, **{ k: v for k, v in kwargs.items() if k not in ["lw", "ec", "fc"] }, ), ) # Plot just silhouette ax.add_patch( PolygonPatch( shape, fill=False, **{ k: v for k, v in kwargs.items() if k not in ["hatch", "fill"] }, ) ) elif type(shape) == LineString: ax.plot( *shape.xy, c=kwargs["ec"] if "ec" in kwargs else None, **{ k: v for k, v in kwargs.items() if k in ["lw", "lt", "dashes", "zorder"] }, ) elif type(shape) == MultiLineString: for c in shape.geoms: ax.plot( *c.xy, c=kwargs["ec"] if "ec" in kwargs else None, **{ k: v for k, v in kwargs.items() if k in ["lw", "lt", "dashes", "zorder"] }, ) elif mode == "plotter": if ("draw" not in kwargs) or kwargs["draw"]: # Set stroke if "stroke" in kwargs: vsk.stroke(kwargs["stroke"]) else: vsk.stroke(1) # Set pen width if "penWidth" in kwargs: vsk.penWidth(kwargs["penWidth"]) else: vsk.penWidth(0.3) if "fill" in kwargs: vsk.fill(kwargs["fill"]) else: vsk.noFill() vsk.geometry(shape) else: raise Exception(f"Unknown mode {mode}") ########## def plot_legends(gdf, ax): for _, row in gdf.iterrows(): name = row.name x, y = np.concatenate(row.geometry.centroid.xy) ax.text(x, y, name) ########## def graph_to_shapely( gdf: gp.GeoDataFrame, width: float = 1. ) -> BaseGeometry: """ Given a GeoDataFrame containing a graph (street newtork), convert them to shapely geometries by applying dilation given by 'width' Args: gdf (gp.GeoDataFrame): input GeoDataFrame containing graph (street network) geometries width (float, optional): Line geometries will be dilated by this amount. Defaults to 1.. Returns: BaseGeometry: Shapely """ def highway_to_width(highway): if (type(highway) == str) and (highway in width): return width[highway] elif isinstance(highway, Iterable): for h in highway: if h in width: return width[h] return np.nan else: return np.nan # Annotate GeoDataFrame with the width for each highway type gdf["width"] = gdf.highway.map( highway_to_width) if type(width) == dict else width # Remove rows with inexistent width gdf.drop(gdf[gdf.width.isna()].index, inplace=True) with warnings.catch_warnings(): # Supress shapely.errors.ShapelyDeprecationWarning warnings.simplefilter( "ignore", shapely.errors.ShapelyDeprecationWarning) if not all(gdf.width.isna()): # Dilate geometries based on their width gdf.geometry.update( gdf.apply(lambda row: row.geometry.buffer(row.width), axis=1) ) return shapely.ops.unary_union(gdf.geometry) def geometries_to_shapely( gdf: gp.GeoDataFrame, point_size: Optional[float] = None, line_width: Optional[float] = None ) -> GeometryCollection: """ Convert geometries in GeoDataFrame to shapely format Args: gdf (gp.GeoDataFrame): Input GeoDataFrame point_size (Optional[float], optional): Point geometries (1D) will be dilated by this amount. Defaults to None. line_width (Optional[float], optional): Line geometries (2D) will be dilated by this amount. Defaults to None. Returns: GeometryCollection: Shapely geometries computed from GeoDataFrame geometries """ geoms = gdf.geometry.tolist() collections = [x for x in geoms if type(x) == GeometryCollection] points = [x for x in geoms if type(x) == Point] + [ y for x in collections for y in x.geoms if type(y) == Point ] lines = [x for x in geoms if type(x) in [LineString, MultiLineString]] + [ y for x in collections for y in x.geoms if type(y) in [LineString, MultiLineString] ] polys = [x for x in geoms if type(x) in [Polygon, MultiPolygon]] + [ y for x in collections for y in x.geoms if type(y) in [Polygon, MultiPolygon] ] # Convert points into circles with radius "point_size" if point_size: points = [x.buffer(point_size) for x in points] if point_size > 0 else [] if line_width: lines = [x.buffer(line_width) for x in lines] if line_width > 0 else [] return GeometryCollection(list(points) + list(lines) + list(polys)) def gdf_to_shapely( layer: str, gdf: gp.GeoDataFrame, width: Optional[Union[dict, float]] = None, point_size: Optional[float] = None, line_width: Optional[float] = None, **kwargs ) -> GeometryCollection: """ Convert a dict of GeoDataFrames to a dict of shapely geometries Args: layer (str): Layer name gdf (gp.GeoDataFrame): Input GeoDataFrame width (Optional[Union[dict, float]], optional): Street network width. Can be either a dictionary or a float. Defaults to None. point_size (Optional[float], optional): Point geometries (1D) will be dilated by this amount. Defaults to None. line_width (Optional[float], optional): Line geometries (2D) will be dilated by this amount. Defaults to None. Returns: GeometryCollection: Output GeoDataFrame """ # Project gdf try: gdf = ox.project_gdf(gdf) except: pass if layer in ["streets", "railway", "waterway"]: geometries = graph_to_shapely(gdf, width) else: geometries = geometries_to_shapely( gdf, point_size=point_size, line_width=line_width ) return geometries def override_args( layers: dict, circle: Optional[bool], dilate: Optional[Union[float, bool]] ) -> dict: """ Override arguments in layers' kwargs Args: layers (dict): prettymaps.plot() Layers parameters dict circle (Optional[bool]): prettymaps.plot() 'Circle' parameter dilate (Optional[Union[float, bool]]): prettymaps.plot() 'dilate' parameter Returns: dict: output dict """ override_args = ["circle", "dilate"] for layer in layers: for arg in override_args: if arg not in layers[layer]: layers[layer][arg] = locals()[arg] return layers def override_params( default_dict: dict, new_dict: dict ) -> dict: """ Override parameters in 'default_dict' with additional parameters from 'new_dict' Args: default_dict (dict): Default dict to be overriden with 'new_dict' parameters new_dict (dict): New dict to override 'default_dict' parameters Returns: dict: default_dict overriden with new_dict parameters """ final_dict = deepcopy(default_dict) for key in new_dict.keys(): if type(new_dict[key]) == dict: if key in final_dict: final_dict[key] = override_params( final_dict[key], new_dict[key]) else: final_dict[key] = new_dict[key] else: final_dict[key] = new_dict[key] return final_dict def create_background( gdfs: Dict[str, gp.GeoDataFrame], style: Dict[str, dict] ) -> Tuple[BaseGeometry, float, float, float, float, float, float]: """ Create a background layer given a collection of GeoDataFrames Args: gdfs (Dict[str, gp.GeoDataFrame]): Dictionary of GeoDataFrames style (Dict[str, dict]): Dictionary of matplotlib style parameters Returns: Tuple[BaseGeometry, float, float, float, float, float, float]: background geometry, bounds, width and height """ # Create background background_pad = 1.1 if "background" in style and "pad" in style["background"]: background_pad = style["background"].pop("pad") background = shapely.affinity.scale( box(* shapely.ops.unary_union(ox.project_gdf(gdfs["perimeter"]).geometry).bounds), background_pad, background_pad, ) if "background" in style and "dilate" in style["background"]: background = background.buffer(style['background'].pop("dilate")) # Get bounds xmin, ymin, xmax, ymax = background.bounds dx, dy = xmax - xmin, ymax - ymin return background, xmin, ymin, xmax, ymax, dx, dy def draw_text( params: Dict[str, dict], background: BaseGeometry ) -> None: """ Draw text with content and matplotlib style parameters specified by 'params' dictionary. params['text'] should contain the message to be drawn Args: params (Dict[str, dict]): matplotlib style parameters for drawing text. params['text'] should contain the message to be drawn. background (BaseGeometry): Background layer """ # Override default osm_credit dict with provided parameters params = override_params( dict( text="\n".join([ "data © OpenStreetMap contributors", "github.com/marceloprates/prettymaps" ]), x=0, y=1, horizontalalignment='left', verticalalignment='top', bbox=dict(boxstyle='square', fc='#fff', ec='#000'), fontfamily='Ubuntu Mono' ), params ) x, y, text = [params.pop(k) for k in ['x', 'y', 'text']] # Get background bounds xmin, ymin, xmax, ymax = background.bounds x = np.interp([x], [0, 1], [xmin, xmax])[0] y = np.interp([y], [0, 1], [ymin, ymax])[0] plt.text( x, y, text, **params ) def presets_directory(): return os.path.join(pathlib.Path(__file__).resolve().parent, 'presets') def create_preset( name: str, layers: Optional[Dict[str, dict]] = None, style: Optional[Dict[str, dict]] = None, circle: Optional[bool] = None, radius: Optional[Union[float, bool]] = None, dilate: Optional[Union[float, bool]] = None, ) -> None: """ Create a preset file and save it on the presets folder (prettymaps/presets/) under name 'name.json' Args: name (str): Preset name layers (Dict[str, dict], optional): prettymaps.plot() 'layers' parameter dict. Defaults to None. style (Dict[str, dict], optional): prettymaps.plot() 'style' parameter dict. Defaults to None. circle (Optional[bool], optional): prettymaps.plot() 'circle' parameter. Defaults to None. radius (Optional[Union[float, bool]], optional): prettymaps.plot() 'radius' parameter. Defaults to None. dilate (Optional[Union[float, bool]], optional): prettymaps.plot() 'dilate' parameter. Defaults to None. """ # if not os.path.isdir('presets'): # os.makedirs('presets') path = os.path.join(presets_directory(), f"{name}.json") with open(path, "w") as f: json.dump( { "layers": layers, "style": style, "circle": circle, "radius": radius, "dilate": dilate, }, f, ensure_ascii=False, ) def read_preset(name: str) -> Dict[str, dict]: """ Read a preset from the presets folder (prettymaps/presets/) Args: name (str): Preset name Returns: (Dict[str,dict]): parameters dictionary """ path = os.path.join(presets_directory(), f"{name}.json") with open(path, "r") as f: # Load params from JSON file params = json.load(f) return params def delete_preset(name: str) -> None: """ Delete a preset from the presets folder (prettymaps/presets/) Args: name (str): Preset name """ path = os.path.join(presets_directory(), f"{name}.json") if os.path.exists(path): os.remove(path) def override_preset( name: str, layers: Dict[str, dict] = {}, style: Dict[str, dict] = {}, circle: Optional[float] = None, radius: Optional[Union[float, bool]] = None, dilate: Optional[Union[float, bool]] = None ) -> Tuple[dict, dict, Optional[float], Optional[Union[float, bool]], Optional[Union[float, bool]]]: """ Read the preset file given by 'name' and override it with additional parameters Args: name (str): _description_ layers (Dict[str, dict], optional): _description_. Defaults to {}. style (Dict[str, dict], optional): _description_. Defaults to {}. circle (Union[float, None], optional): _description_. Defaults to None. radius (Union[float, None], optional): _description_. Defaults to None. dilate (Union[float, None], optional): _description_. Defaults to None. Returns: Tuple[dict, dict, Optional[float], Optional[Union[float, bool]], Optional[Union[float, bool]]]: Preset parameters overriden by additional provided parameters """ params = read_preset(name) # Override preset with kwargs if "layers" in params: layers = override_params(params["layers"], layers) if "style" in params: style = override_params(params["style"], style) if circle is None and "circle" in params: circle = params["circle"] if radius is None and "radius" in params: radius = params["radius"] if dilate is None and "dilate" in params: dilate = params["dilate"] # Delete layers marked as 'False' in the parameter dict for layer in [key for key in layers.keys() if layers[key] == False]: del layers[layer] # Return overriden presets return layers, style, circle, radius, dilate def manage_presets( load_preset: Optional[str], save_preset: bool, update_preset: Optional[str], layers: Dict[str, dict], style: Dict[str, dict], circle: Optional[bool], radius: Optional[Union[float, bool]], dilate: Optional[Union[float, bool]] ) -> Tuple[dict, dict, Optional[float], Optional[Union[float, bool]], Optional[Union[float, bool]]]: """_summary_ Args: load_preset (Optional[str]): Load preset named 'load_preset', if provided save_preset (Optional[str]): Save preset to file named 'save_preset', if provided update_preset (Optional[str]): Load, update and save preset named 'update_preset', if provided layers (Dict[str, dict]): prettymaps.plot() 'layers' parameter dict style (Dict[str, dict]): prettymaps.plot() 'style' parameter dict circle (Optional[bool]): prettymaps.plot() 'circle' parameter radius (Optional[Union[float, bool]]): prettymaps.plot() 'radius' parameter dilate (Optional[Union[float, bool]]): prettymaps.plot() 'dilate' parameter Returns: Tuple[dict, dict, Optional[float], Optional[Union[float, bool]], Optional[Union[float, bool]]]: Updated layers, style, circle, radius, dilate parameters """ # Update preset mode: load a preset, update it with additional parameters and update the JSON file if update_preset: load_preset = save_preset = True # Load preset (if provided) if load_preset: layers, style, circle, radius, dilate = override_preset( load_preset, layers, style, circle, radius, dilate ) # Save parameters as preset if save_preset: create_preset( save_preset, layers=layers, style=style, circle=circle, radius=radius, dilate=dilate, ) return layers, style, circle, radius, dilate def presets(): presets = [file.split('.')[0] for file in os.listdir( presets_directory()) if file.endswith('.json')] presets = sorted(presets) presets = pd.DataFrame({ 'preset': presets, 'params': list(map(read_preset, presets)) }) #print('Available presets:') # for i, preset in enumerate(presets): # print(f'{i+1}. {preset}') return pd.DataFrame(presets) def preset(name): with open(os.path.join(presets_directory(), f"{name}.json"), "r") as f: # Load params from JSON file params = json.load(f) return Preset(params) # Plot def plot( # Your query. Example: # - "Porto Alegre" # - (-30.0324999, -51.2303767) (lat/long coordinates) # - You can also provide a custom GeoDataFrame boundary as input query: Union[str, Tuple[float, float], gp.GeoDataFrame], # Which OpenStreetMap layers to plot # Example: {'building': {'tags': {'building': True}}, 'streets': {'width': 2}} # Run prettymaps.presets() for more examples layers={}, # Matplotlib params for drawing each layer style={}, # Whether to load params from preset preset='default', # Whether to save preset save_preset=None, # Whether to load and update preset with additional parameters update_preset=None, # Custom postprocessing function on layers postprocessing=None, # Circular boundary? Default: square circle=None, # Radius for circular or square boundary radius=None, # Dilate boundary by this much dilate=None, # Whether to save result save_as=None, # Figure parameters fig=None, ax=None, title=None, figsize=(12, 12), constrained_layout=True, # Credit message parameters credit={}, # Mode ('matplotlib' or 'plotter') mode='matplotlib', # Multiplot mode multiplot=False, # Whether to display matplotlib show=True, # Transform (translation, scale, rotation) parameters x=0, y=0, scale_x=1, scale_y=1, rotation=0, ): """ 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 style: 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 ----- """ # 1. Manage presets layers, style, circle, radius, dilate = manage_presets( preset, save_preset, update_preset, layers, style, circle, radius, dilate ) # 2. Init matplotlib figure if (mode == "matplotlib") and (ax is None): fig = plt.figure(figsize=figsize) ax = plt.subplot(111, aspect='equal') # 3. Override arguments in layers' kwargs dict layers = override_args(layers, circle, dilate) # 4. Fetch geodataframes gdfs = get_gdfs(query, layers, radius, dilate, -rotation) # 5. Apply transformations to GeoDataFrames (translation, scale, rotation) gdfs = transform_gdfs(gdfs, x, y, scale_x, scale_y, rotation) # 6. Apply a postprocessing function to the GeoDataFrames, if provided if postprocessing: gdfs = postprocessing(gdfs) # 7. Create background GeoDataFrame and get (x,y) bounds background, xmin, ymin, xmax, ymax, dx, dy = create_background(gdfs, style) # 8. Draw layers if mode == "plotter": # 8.1. Draw layers in plotter (vsketch) mode ''' class Sketch(vsketch.SketchClass): def draw(self, vsk: vsketch.Vsketch): vsk.size("a4", landscape=True) for layer in gdfs: if layer in layers: plot_gdf( layer, gdfs[layer], ax, width=layers[layer]["width"] if "width" in layers[layer] else None, mode=mode, vsk=vsk, **(style[layer] if layer in style else {}), ) if save_as: vsk.save(save_as) def finalize(self, vsk: vsketch.Vsketch): vsk.vpype("linemerge linesimplify reloop linesort") sketch = Sketch() sketch.display() ''' elif mode == "matplotlib": # 8.2. Draw layers in matplotlib mode for layer in gdfs: if layer in layers: plot_gdf( layer, gdfs[layer], ax, width=layers[layer]["width"] if "width" in layers[layer] else None, **(style[layer] if layer in style else {}), ) else: raise Exception(f'Unknown mode {mode}') # 9. Draw background if "background" in style: zorder = style["background"].pop( "zorder") if "zorder" in style["background"] else -1 ax.add_patch(PolygonPatch( background, **{k:v for k,v in style["background"].items() if k != 'dilate'}, zorder=zorder)) # 10. Draw credit message if (mode == "matplotlib") and (credit != False) and (not multiplot): draw_text(credit, background) # 11. Ajust figure and create PIL Image if mode == "matplotlib": # Adjust axis ax.axis("off") ax.axis("equal") ax.autoscale() # Adjust padding plt.subplots_adjust( left=0, bottom=0, right=1, top=1, wspace=0, hspace=0 ) # Save result if save_as: plt.savefig(save_as) if not show: plt.close() return Plot(gdfs, fig, ax, background) def multiplot(*subplots, figsize=None, credit={}, **kwargs): fig = plt.figure(figsize=figsize) ax = plt.subplot(111, aspect='equal') mode = "plotter" if "plotter" in kwargs and kwargs["plotter"] else "matplotlib" subplots_results = [ plot( subplot.query, ax=ax, multiplot=True, **override_params(subplot.kwargs, {k: v for k, v in kwargs.items() if k != 'load_preset' or 'load_preset' not in subplot.kwargs}) ) for subplot in subplots ] if mode == "matplotlib": ax.axis("off") ax.axis("equal") ax.autoscale() plt.subplots_adjust( left=0, bottom=0, right=1, top=1, wspace=0, hspace=0 ) if 'show' in kwargs and not kwargs['show']: plt.close() if credit != False: backgrounds = [result.background for result in subplots_results] global_background = box( *shapely.ops.unary_union(backgrounds).bounds) draw_text(credit, global_background)