How Prettymaps Build Perimeters and Boundaries: Circle, Radius, and Dilate Explained
Prettymaps generates map perimeters in three stages: parsing the query, creating a raw boundary (circular or square) from a center point and radius, then optionally dilating the final geometry—implemented in fetch.py with get_boundary() and get_perimeter().
The perimeter and boundary system in marceloprates/prettymaps is the foundation of every map it generates. Whether you need a precise circular crop around a landmark, a rotated square framing a city block, or an expanded polygon that includes surrounding suburbs, the library handles these transformations through a consistent pipeline. This article breaks down how circle, radius, and dilate parameters work together, with direct reference to the source code implementation.
Parsing the Query: Where Perimeters Begin
Every perimeter starts with parse_query in [prettymaps/fetch.py](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/fetch.py#L87-L96). This function accepts multiple input types:
- Coordinates tuple –
(longitude, latitude) - OSM ID – OpenStreetMap identifier
- Address string – Geocodable location name
- Full polygon – Pre-defined
shapelygeometry
The parsed result feeds into the boundary creation logic, which determines whether to build a synthetic shape (circle or square) or use an OSM-derived polygon.
Creating the Raw Boundary: Circle vs. Square
When you supply a radius parameter, get_boundary() (lines 199–227 in fetch.py) constructs the initial geometry. The circle boolean flag controls which shape emerges.
Circular Boundaries (circle=True)
A true circle is generated via Shapely's .buffer() method:
if circle:
boundary.geometry = boundary.geometry.buffer(radius)
This creates a perfect circle centered on your query point with the specified radius in meters.
Square Boundaries (circle=False)
When circle=False, the code builds a square with side length 2 × radius, optionally rotated:
else: # square shape
boundary = GeoDataFrame(
geometry=[
rotate(
Polygon([(x-r, y-r), (x+r, y-r), (x+r, y+r), (x-r, y+r)]),
rotation,
)
],
crs=boundary.crs,
)
The rotation parameter (in degrees) tilts the square around its center point—useful for aligning map frames with street grids or geographic features.
Building the Final Perimeter: Scaling and Dilation
The get_perimeter() function (lines 332–372 in fetch.py) assembles the complete perimeter through these steps:
- Geometry resolution – Uses OSM data when no radius is provided via
ox.geocoder.geocode_to_gdf - Aspect ratio scaling – Applies
shapely.affinity.scaleto stretch the geometry - Dilation – Buffers the perimeter outward (or inward with negative values)
The dilation step implementation:
# Apply dilation
perimeter = ox.projection.project_gdf(perimeter)
if dilate is not None:
perimeter.geometry = perimeter.geometry.buffer(dilate)
perimeter = perimeter.to_crs(4326)
Key behavior: dilate operates in the projected coordinate system (meters), then converts back to WGS84 (EPSG:4326). This ensures consistent expansion regardless of latitude.
How the Perimeter Is Consumed Downstream
The finished perimeter—stored as gdfs["perimeter"]—becomes the clipping mask for all map layers. In [prettymaps/draw.py](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py#L452-L456), downstream functions access it directly:
perimeter = gdfs["perimeter"].geometry[0] # used for keypoint extraction, clipping, etc.
Every subsequent layer (buildings, roads, water, green space) is intersected with this geometry. This guarantees that your final map strictly honors the boundary shape you defined—whether a precise 500-meter circle around a monument or a dilated polygon capturing a city's full administrative boundary plus a 200-meter buffer zone.
Practical Code Examples
Example 1: Circular Perimeter, 500-Meter Radius
from prettymaps import fetch
# True circle around Berlin center
circle_perim = fetch.get_perimeter(
query=(13.4050, 52.5200), # lon, lat
radius=500,
circle=True,
)
Example 2: Rotated Square Perimeter
# Diamond-shaped frame, 45-degree rotation
square_perim = fetch.get_perimeter(
query="Berlin, Germany",
radius=500,
circle=False,
rotation=45,
)
Example 3: OSM Polygon with Dilation
# Expand Potsdam's boundary outward by 100 meters
osm_perim = fetch.get_perimeter(
query="Potsdam, Germany",
dilate=100,
)
Example 4: Combined Parameters
from prettymaps import draw
import matplotlib.pyplot as plt
# Circle with extra 50m breathing room
perim = fetch.get_perimeter(
query="Brandenburg Gate, Berlin",
radius=400,
circle=True,
dilate=50,
)
# Use in drawing pipeline
fig, ax = plt.subplots()
draw_keypoints = draw.draw_keypoints
draw_keypoints(keypoints={}, gdfs={"perimeter": perim}, ax=ax)
Summary
parse_queryinfetch.pynormalizes diverse input types into a coordinate referenceget_boundarycreates circular (via.buffer()) or square (viaPolygon+rotate()) geometries from radius and center pointget_perimeterfinalizes the geometry with aspect ratio scaling and optionaldilatebuffering- The perimeter lives in
gdfs["perimeter"]and clips all downstream map layers indraw.py - Dilation always occurs in projected meters before reprojection to WGS84
Frequently Asked Questions
What units does the radius parameter use?
The radius parameter always uses meters. Prettymaps projects coordinates to a UTM-based meter system before applying the buffer, ensuring accurate distances regardless of your location's latitude.
Can I use negative values for dilate?
Yes. Passing a negative dilate value shrinks the perimeter inward. This is useful for creating inset boundaries that exclude edge artifacts or noisy OSM data near administrative borders.
Why does circle=False create a square rather than a rectangle?
The square shape enforces equal side lengths of 2 × radius for visual consistency. If you need a rectangular boundary with unequal dimensions, supply a pre-built polygon via the query parameter instead of using the radius-based boundary generation.
How does rotation interact with non-square shapes?
The rotation parameter only affects square boundaries (circle=False). For circular boundaries, rotation has no visible effect due to radial symmetry. When applied to squares, rotation occurs around the center point defined by your query coordinates.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →