Map tiles from raw PBF in pure C
Carta is a map tile server written in C11 with zero graphics dependencies. No Cairo, no Mapnik, no FreeType. It reads OpenStreetMap PBF data directly, builds an R-tree spatial index, and renders PNG raster tiles, MVT vector tiles, and ASCII art tiles — all from the same codebase. Software renderer with anti-aliased lines, label placement, metatile caching, and zoom-adaptive LOD. Compiles to WASM for browser demos.
A tile server you can deploy anywhere
Standard tile pipelines are complex: PostGIS + Mapnik + Tirex for raster, or osm2pgsql + pg_tileserv for vector. Each requires PostgreSQL, dozens of dependencies, and GB of disk for a simple deployment. Want to run it in a browser? Forget it.
Carta takes a different approach: read PBF, render tiles, serve HTTP. One C binary, one PBF data file, zero external services. The same code renders tiles natively at ~75ms, or runs in the browser via WASM with embedded data. For a trucking fleet, this means map tiles served from the edge — no cloud tile provider needed.
Edge deployment. Trucks have intermittent connectivity. A Carta binary + country PBF on a vehicle-mounted computer serves map tiles offline. Same binary compiles to WASM for the web dashboard. One renderer, every platform.
From PBF bytes to pixels
PBF File (OSM data)
│
▼
┌──────────────┐ ┌──────────────┐
│ ct_pbf.c │────▶│ ct_rtree.c │ Hilbert-packed R-tree
│ Parse nodes, │ │ Spatial │ O(log n + k) queries
│ ways, rels │ │ index │
└──────────────┘ └──────┬───────┘
│
GET /tiles/14/8588/5664 │ Tile request
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ ct_tile.c │────▶│ ct_lod.c │ Level-of-detail
│ Web Mercator │ │ Zoom-based │ filtering
│ projection │ │ feature rules│
└──────────────┘ └──────┬───────┘
│
┌─────────┼─────────┐
▼ ▼ ▼
┌──────────┐ ┌───────┐ ┌───────┐
│ ct_render │ │ct_mvt │ │ct_ascii│
│ Rasterize │ │Protobuf│ │ Chars │
│ → PNG │ │encode │ │ + ANSI│
└──────────┘ └───────┘ └───────┘
Software rasterizer
Carta's rasterizer draws directly to an RGBA pixel buffer. No GPU, no OpenGL, no external graphics library. Anti-aliased lines via Xiaolin Wu's algorithm, polygon fill with even-odd rule, road casing with configurable width.
- SIMD optimization — 8× speedup for opaque polygon fill
- Pre-allocated buffers — eliminates 500+ malloc/free pairs per tile
- Thread-local render contexts — zero contention between workers
- Miniz compression level 2 — 3× faster than default PNG encoding
Label system
Three types of labels, all with collision detection on an 8×8 grid:
| Label Type | Algorithm | Features |
|---|---|---|
| Point labels | 9-anchor placement | Cities, towns, POIs. Priority: country (100) → village (50) |
| Road labels | Text along path | Per-glyph angle, follows road geometry |
| Area labels | Pole of inaccessibility | Lakes, parks. Finds interior point farthest from edges |
Metatile caching
Labels that span tile boundaries look wrong when each tile is rendered independently.
Carta's metatile system renders 2×2 tile groups with a shared
collision grid, then extracts individual tiles. Thread-safe LRU cache with
pthread_rwlock and refcounted entries. Default capacity: 4,096 metatiles.
Level of detail
Zoom-adaptive feature visibility prevents tile clutter at low zoom levels:
| Feature | Min Zoom | Feature | Min Zoom |
|---|---|---|---|
| Motorways | z5 | Buildings (>5000m²) | z13 |
| Primary roads | z8 | Buildings (all) | z15 |
| Residential roads | z14 | Large lakes | z4 |
| Service roads | z15 | Rivers | z8 |
PNG, MVT, and ASCII from the same data
PNG raster tiles
# Fetch a PNG tile (z14, downtown Budapest) curl http://localhost:8081/tiles/14/9116/5693.png -o tile.png # 512x512 pixels, ~75ms generation, <1ms cache hit # ETag support for 304 Not Modified
MVT vector tiles
# Fetch an MVT tile (Mapbox Vector Tile spec v2) curl http://localhost:8081/tiles/14/9116/5693.mvt -o tile.mvt # Layers: background, landuse, water, buildings, # roads, railways, boundaries, labels # Delta + zigzag coordinate encoding # Douglas-Peucker simplification (20-30% smaller)
ASCII art tiles
# Render as ASCII art curl "http://localhost:8081/tiles/14/9116/5693.txt?width=80&charset=braille&color=1" # Charsets: simple (10-level), extended, blocks (Unicode), braille (max detail) # Options: width, height, invert, ANSI color
TileJSON metadata
# TileJSON 3.0.0 endpoint curl http://localhost:8081/tiles.json # Returns: tilejson, tiles URL template, bounds, # center, minzoom, maxzoom, attribution
| Endpoint | Format | Content-Type | Use Case |
|---|---|---|---|
| /tiles/{z}/{x}/{y}.png | PNG raster | image/png | Web maps, offline maps, previews |
| /tiles/{z}/{x}/{y}.mvt | MVT vector | application/vnd.mapbox-vector-tile | MapLibre GL, Mapbox GL, custom renderers |
| /tiles/{z}/{x}/{y}.txt | ASCII art | text/plain | Terminal, monitoring, novelty |
| /tiles.json | TileJSON 3.0.0 | application/json | Client auto-configuration |
| /api/v1/health | JSON | application/json | Health check |
| /api/v1/stats | JSON | application/json | PBF stats, cache stats |
R-tree, LRU cache, worker pool
Spatial index
Hilbert-packed Sort-Tile-Recursive R-tree. O(log n + k) tile queries vs O(n) linear scan. Built once at startup from PBF data. Packed node structure for cache-line efficiency. Carta queries the R-tree for every tile request to find which OSM features intersect the tile bbox.
Tile cache
Per-zoom LRU cache with configurable total size (default 256MB). Separate caches
for PNG and MVT. 64-bit key packing: z(5) | x(29) | y(30). Cache hit
latency: <1ms vs ~75ms generation. ETag headers use FNV-1a 64-bit hash for
HTTP 304 responses.
Server architecture
| Component | Implementation | Configuration |
|---|---|---|
| HTTP server | Mongoose (event loop) | Port 8081 |
| Rate limiter | sh_ratelimit (token bucket) | 10 RPS, burst 100 |
| Work queue | sh_workqueue (bounded) | 256 depth, 5s timeout |
| Workers | sh_worker_pool | Auto CPU count |
| Adaptive | sh_adaptive (P50/P90/P99) | Optional, 70% target |
| CORS | sh_cors | Origin allowlist |
Codebase
Honest assessment. Carta is not Mapnik. Styling is not configurable via CartoCSS or MapboxGL JSON — road colors and widths are compiled in. Label rendering uses an embedded bitmap font, not FreeType with TTF fonts. For a fleet operations dashboard showing truck positions on a map, this is fine. For a consumer map product competing with Google Maps, you'd want Mapbox GL or MapLibre GL on top of Carta's MVT tiles.
Three ways to try Carta
1. Browser (zero install)
The API documentation page includes a live WASM demo with embedded Monaco data. Click to generate PNG, MVT, or ASCII tiles. No server needed.
2. Build from source
# Clone and build git clone https://github.com/ottofleet/otto.git cd otto && make carta # Run all 164 tests make test-carta
3. Tile server
# Build and start server make carta-api ./carta/api/carta-tile-server data/hungary-latest.osm.pbf # Fetch a PNG tile curl http://localhost:8081/tiles/14/9116/5693.png -o tile.png # Fetch an MVT tile curl http://localhost:8081/tiles/14/9116/5693.mvt -o tile.mvt # ASCII art in your terminal curl "http://localhost:8081/tiles/14/9116/5693.txt?width=80&color=1"
Common questions
Why build a tile server from scratch?
Three reasons: (1) Edge deployment — one binary + one PBF file, no PostgreSQL, no import pipeline. Put it on a truck's onboard computer for offline maps. (2) WASM demos — the same C code renders tiles in the browser. (3) Zero dependencies — no Cairo, no Mapnik, no transitive dependency chain. What you audit is what you ship.
How does Carta compare to Mapnik or MapServer?
Mapnik is far more capable at cartography: configurable styling, TTF fonts, SVG symbols, labeling heuristics refined over 15 years. Carta is simpler and self-contained. The tradeoff is deployment complexity (Mapnik needs PostGIS + dozens of libs) vs. rendering quality (Carta has fixed styling). For fleet dashboards showing vehicle positions and route overlays, Carta's quality is sufficient. For consumer map products, use Carta's MVT output with MapLibre GL for rendering.
What about vector tile rendering in the browser?
Carta's MVT tiles are fully compatible with MapLibre GL and Mapbox GL. Use Carta as a vector tile server and let the browser handle beautiful rendering with WebGL. The PNG output is for contexts where client-side rendering isn't available (email reports, server-generated PDFs, terminal dashboards).
How large are the PBF files?
Monaco: 450KB. Hungary: 400MB. Germany: 3.5GB. Planet: 72GB. Carta loads data into memory (10–15× PBF size for peak RAM), so country-scale works on a standard server. Continental-scale needs 64–128GB RAM or sharding. See the roadmap for planned continental sharding support.
What license?
AGPLv3 with a Trucking Exception. Same terms as all OTTO modules.