# How Prettymaps Build Perimeters and Boundaries: Circle, Radius, and Dilate Explained

> Learn how Prettymaps builds perimeters and boundaries using circle, radius, and dilate functions. Understand the three stages of map generation and geometry manipulation for precise map outlines.

- Repository: [Marcelo de Oliveira Rosa Prates/prettymaps](https://github.com/marceloprates/prettymaps)
- Tags: deep-dive
- Published: 2026-08-20

---

**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`](https://github.com/marceloprates/prettymaps/blob/main/fetch.py) with `get_boundary()` and `get_perimeter()`.**

The perimeter and boundary system in [marceloprates/prettymaps](https://github.com/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)](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`](https://github.com/marceloprates/prettymaps/blob/main/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:

```python
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:

```python
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`](https://github.com/marceloprates/prettymaps/blob/main/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:

```python

# 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)](https://github.com/marceloprates/prettymaps/blob/main/prettymaps/draw.py#L452-L456), downstream functions access it directly:

```python
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

```python
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

```python

# 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

```python

# Expand Potsdam's boundary outward by 100 meters

osm_perim = fetch.get_perimeter(
    query="Potsdam, Germany",
    dilate=100,
)

```

### Example 4: Combined Parameters

```python
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`](https://github.com/marceloprates/prettymaps/blob/main/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`](https://github.com/marceloprates/prettymaps/blob/main/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.