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 shapely geometry

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:

  1. Geometry resolution – Uses OSM data when no radius is provided via ox.geocoder.geocode_to_gdf
  2. Aspect ratio scaling – Applies shapely.affinity.scale to stretch the geometry
  3. 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_query in fetch.py normalizes diverse input types into a coordinate reference
  • get_boundary creates circular (via .buffer()) or square (via Polygon + rotate()) geometries from radius and center point
  • get_perimeter finalizes the geometry with aspect ratio scaling and optional dilate buffering
  • The perimeter lives in gdfs["perimeter"] and clips all downstream map layers in draw.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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →