How Prettymaps Handles Sea and Coastline Detection and Filtering: A Complete Technical Guide
Prettymaps detects sea and coastlines by combining SRTM elevation data (pixels below sea level) with OpenStreetMap coastline geometries, then filters candidates by checking for road network intersections and minimum area thresholds.
The prettymaps library automates the creation of publication-ready maps from OpenStreetMap data. Its sea and coastline detection pipeline is particularly sophisticated, using a hybrid approach that merges raster elevation analysis with vector geometry processing to distinguish true ocean from inland water bodies. This article examines the complete implementation as found in the marceloprates/prettymaps repository.
The Six-Step Sea Detection Pipeline
Prettymaps builds the sea layer through a deterministic pipeline defined in prettymaps/fetch.py. Each step addresses a specific challenge in separating coastal waters from lakes, rivers, and other inland features.
Step 1: Elevation Data Acquisition
The process begins with SRTM (Shuttle Radar Topography Mission) data. The obtain_elevation() function at line 22 of fetch.py takes the map's area polygon and fetches corresponding elevation tiles using the elevation library.
# From prettymaps/fetch.py, line 22
def obtain_elevation(polygon, crs="EPSG:4326"):
"""
Download SRTM elevation data for the given polygon area.
Returns a reprojected raster array.
"""
# elevation library handles tile download and merging
# Output is reprojected to match OSM-derived CRS
This raster provides the first signal: any pixel below zero elevation is potentially sea.
Step 2: Sea Mask Creation from Elevation
At line 61, get_sea_mask() converts the elevation raster into a boolean mask where elevation < 0, then extracts contours using scikit-image's find_contours function.
# From prettymaps/fetch.py, line 61
def get_sea_mask(elevation_raster, threshold=0):
"""
Create boolean mask from elevation data.
Negative elevations → sea candidates.
"""
sea_mask = elevation_raster < threshold
# find_contours returns line segments at the zero-elevation boundary
contours = find_contours(sea_mask, 0.5)
# Convert pixel coordinates back to geographic polygons
return contours_to_polygons(contours, elevation_raster.transform)
This elevation-based approach catches open ocean reliably but may misclassify low-lying inland areas (e.g., Death Valley, Caspian Sea shorelines).
Step 3: OSM Coastline Download
Simultaneously, fetch_osm_data() at line 140 queries OpenStreetMap for features tagged natural=coastline or waterway=coastline using osmnx:
# From prettymaps/fetch.py, line 140
def fetch_osm_data(perimeter, tags=None):
"""
Download OSM features within perimeter.
Coastline is extracted from natural=coastline tags.
"""
coastline = ox.features_from_polygon(
perimeter,
tags={"natural": "coastline"}
)
# Returns GeoDataFrame with coastline LineString geometries
The coastline provides the critical boundary between land and sea.
Step 4: Sea Candidate Generation
The geometric core of the algorithm occurs around line 425. The bounding box polygon is differenced with a micro-buffered coastline to generate candidate sea geometries:
# From prettymaps/fetch.py, line 425
sea_candidates = bbox.difference(coastline.buffer(1e-9)).geoms
This operation creates polygons representing all areas outside the coastline—but this includes both ocean and large inland water bodies that OSM may not have tagged precisely.
Step 5: Intelligent Candidate Filtering
The filter_candidate function (lines 430-450) applies two decisive filters:
| Filter | Purpose | Implementation |
|---|---|---|
| Road network intersection | Eliminate inland lakes by checking if candidate touches drivable roads | if candidate.intersects(roads_union): return False |
| Minimum area threshold | Remove tiny islands and artifacts | if candidate.area < sea_area_threshold: return False |
The road intersection test is particularly clever: true ocean areas rarely contain drivable road networks, while inland cities—even coastal ones—have roads throughout their landmass. This heuristic correctly excludes features like the Caspian Sea when mapping nearby cities.
Step 6: Final Sea Layer Assembly
Surviving candidates are merged via unary_union and wrapped in a GeoDataFrame (lines 460-660):
# Final assembly in prettymaps/fetch.py
sea = unary_union([c for c in candidates if filter_candidate(c)])
gdf = GeoDataFrame(geometry=[sea], crs=perimeter.crs)
The coastline itself remains unmodified—it serves purely as a spatial mask.
Practical Configuration
Including and Styling the Sea Layer
Request "sea" in the layers list and customize its appearance via the style parameter:
import prettymaps as pm
perimeter = pm.get_boundary("Lisbon, Portugal", radius=5000, circle=True)
fig, ax = pm.plot(
perimeter,
layers=["sea", "water", "roads", "buildings"],
style={
"sea": {"color": "#6ab7ff", "alpha": 0.9},
"water": {"color": "#9bd9ff"},
"coastline": {"color": "#2c3e50", "linewidth": 1.5}
},
)
Tuning the Area Threshold
Control minimum sea polygon size to eliminate small artifacts or distant islands:
fig, ax = pm.plot(
perimeter,
layers=["sea"],
sea_area_threshold=0.5, # km² — increase to filter small islands
)
The sea_area_threshold parameter flows directly to filter_candidate() in fetch.py.
Disabling Sea Detection for Inland Maps
For landlocked locations, omit "sea" from layers to skip elevation processing entirely:
fig, ax = pm.plot(
perimeter,
layers=["water", "roads", "buildings"], # No sea layer
)
This significantly reduces execution time by avoiding SRTM downloads and raster processing.
Key Source Files and Functions
| File | Key Functions | Responsibility |
|---|---|---|
prettymaps/fetch.py |
obtain_elevation() (L22), get_sea_mask() (L61), fetch_osm_data() (L140), candidate filtering (L425-450) |
Complete sea/coastline detection pipeline |
prettymaps/draw.py |
Layer rendering | Matplotlib-based visualization of processed sea polygons |
prettymaps/__init__.py |
plot(), get_boundary() |
Public API entry points |
prettymaps/utils.py |
log_execution_time |
Performance instrumentation |
Summary
- Hybrid detection: Prettymaps combines SRTM elevation rasters (sea level = 0m) with OSM coastline vectors for robust sea identification
- Critical filtering: Road network intersection tests distinguish true ocean from large inland water bodies
- Configurable thresholds: The
sea_area_thresholdparameter controls minimum sea polygon size - Performance option: Omitting
"sea"from layers skips all elevation processing for faster inland maps - Implementation location: All core logic resides in
prettymaps/fetch.py, specifically lines 22-660
Frequently Asked Questions
How does prettymaps distinguish between ocean and large lakes?
Prettymaps uses a road network intersection heuristic. After generating sea candidates by differencing the coastline from the bounding box, it checks whether each candidate polygon intersects with drivable roads. True ocean areas rarely contain road networks, while inland cities—even those on large lakes—have roads throughout. Candidates touching roads are rejected as inland water.
What data source provides the elevation for sea detection?
The library uses SRTM (Shuttle Radar Topography Mission) data accessed through the elevation Python library. These 1-arc-second resolution rasters provide global coverage of terrain heights, with values below zero indicating areas potentially below sea level. The data is automatically downloaded and reprojected to match the map's coordinate reference system.
Can I adjust how aggressively prettymaps filters sea areas?
Yes. Pass the sea_area_threshold parameter to pm.plot() (in square kilometers). Higher values eliminate smaller islands and coastal features. The default behavior retains most sea polygons, but setting sea_area_threshold=0.5 or higher focuses visualization on substantial water bodies only. This parameter directly controls the area check in filter_candidate() at lines 430-450 of fetch.py.
Why might sea detection fail for some coastal locations?
Sea detection can produce artifacts in three scenarios: (1) low-lying coastal areas where elevation data shows near-zero values, (2) regions with incomplete OSM coastline data, and (3) cities built over water (e.g., Venice, Amsterdam) where road networks extend into areas the algorithm would classify as sea. The filtering heuristics handle most common cases, but manual layer exclusion remains available.
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 →