ee86af6183
- TILE-SERVER-EVALUATION.md: updated to reflect current architecture (PMTiles z0-z18) - MODULE-MAP.md: added PMTiles + tile-cache to Geoportal section - Monitor: timeout increased to 90 min for z18 builds, description updated - Added PROMPT-GEOPORTAL-IMPROVE.md with mega prompt for future sessions (includes MLT check, mvt-rs evaluation prompt, operational commands) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
335 lines
13 KiB
Markdown
335 lines
13 KiB
Markdown
# Tile Server Evaluation — ArchiTools Geoportal (March 2026)
|
|
|
|
## Context
|
|
|
|
ArchiTools Geoportal serves vector tiles (MVT) from PostgreSQL 16 + PostGIS 3 via Martin.
|
|
Data: ~330K GIS features (parcels, buildings, admin boundaries) in EPSG:3844 (Stereo70), growing to 1M+.
|
|
Frontend: MapLibre GL JS 5.21, Next.js 16, Docker self-hosted via Portainer CE.
|
|
|
|
---
|
|
|
|
## Problem Statement
|
|
|
|
1. Martin v0.15.0 was running in **auto-discovery mode** — the existing `martin.yaml` config was never mounted
|
|
2. Building labels (`cadastral_ref`) missing from MVT tiles despite the view exposing them
|
|
3. Performance concerns at scale (330K -> 1M+ features)
|
|
|
|
---
|
|
|
|
## Solutions Evaluated (7 options + emerging tech)
|
|
|
|
### 1. Martin (Fix + Upgrade) — WINNER
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Root cause | `martin.yaml` not mounted in docker-compose — Martin ran in auto-discovery mode |
|
|
| Fix | Bake config into custom image via Dockerfile + upgrade v0.15 -> v1.4.0 |
|
|
| Performance | Fastest tile server benchmarked (2-3x faster than #2 Tegola) |
|
|
| EPSG:3844 | Native support via `default_srid: 3844` |
|
|
| New in v1.4 | ZSTD compression, MLT format, materialized views, better logging |
|
|
|
|
**Status: IMPLEMENTED AND VERIFIED IN PRODUCTION** (2026-03-27)
|
|
|
|
### 2. pg_tileserv (CrunchyData)
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Architecture | Go binary, zero-config, delegates ST_AsMVT to PostGIS |
|
|
| Property control | Auto from schema + URL `?properties=` parameter |
|
|
| Performance | 2-3x slower than Martin (Rechsteiner benchmark) |
|
|
| EPSG:3844 | Supported (auto-reprojects via ST_Transform) |
|
|
| Killer feature | Function-based sources (full SQL tile functions) |
|
|
| Dealbreaker | View extent estimation bug (#156) affects all our views, development stagnant (last release Feb 2025) |
|
|
|
|
**Verdict: NO** — slower, buggy with views, stagnant development.
|
|
|
|
### 3. Tegola (Go-based)
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Architecture | Go, TOML config, explicit per-layer SQL |
|
|
| Performance | 2nd in benchmarks, but 2-3x slower than Martin |
|
|
| Built-in cache | File, S3/MinIO, Redis — with seed/purge CLI |
|
|
| EPSG:3844 | **NOT SUPPORTED** (only 3857/4326) — requires ST_Transform in every query |
|
|
| Killer feature | Built-in tile seeding and cache purging |
|
|
|
|
**Verdict: NO** — EPSG:3844 not supported, dealbreaker for our data.
|
|
|
|
### 4. t-rex (Rust-based)
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Status | **Abandoned/unmaintained** — no releases since 2023 |
|
|
|
|
**Verdict: NO** — dead project.
|
|
|
|
### 5. GeoJSON Direct from Next.js API
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| 330K features | ~270 MB uncompressed, 800 MB-1.4 GB browser memory |
|
|
| Browser impact | 10-30s main thread freeze, mobile crash |
|
|
| Pan/zoom | Full re-fetch on every viewport change, flickering |
|
|
| Viable range | Only at zoom 16+ with <500 features in viewport |
|
|
|
|
**Verdict: NO** — does not scale beyond ~20K features.
|
|
|
|
### 6. PMTiles (Pre-generated)
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Architecture | Single-file tile archive, HTTP Range Requests, no server needed |
|
|
| Performance | ~5ms per tile (vs 200-2000ms for Martin on low-zoom) |
|
|
| Property control | tippecanoe gives explicit include/exclude per property |
|
|
| Update strategy | Full rebuild required (~3-7 min for 330K features) |
|
|
| EPSG:3844 | Requires reprojection to 4326 via ogr2ogr before tippecanoe |
|
|
| MinIO serving | Yes — direct HTTP Range Requests with CORS |
|
|
|
|
**Verdict: YES as hybrid complement** — excellent for static UAT overview layers (z0-z12), Martin for live detail.
|
|
|
|
### 7. Emerging Solutions
|
|
|
|
| Solution | Status | Relevance |
|
|
|---|---|---|
|
|
| **mvt-rs** (Rust) | v0.16.2, active | Admin UI, auth per layer, cache — good for multi-tenant |
|
|
| **MLT format** | Stable Jan 2026 | 6x compression, 4x faster decode — Martin v1.3+ supports it |
|
|
| **BBOX** | Maturing | Similar to Tegola performance, unified raster+vector |
|
|
| **DuckDB tiles** | Early | Not PostGIS replacement, interesting for GeoParquet |
|
|
| **FlatGeobuf** | Stable | Good for <100K features, not a tile replacement |
|
|
|
|
---
|
|
|
|
## Benchmark Reference (Rechsteiner, April 2025)
|
|
|
|
| Rank | Server | Language | Relative Speed |
|
|
|---|---|---|---|
|
|
| 1 | **Martin** | Rust | 1x (fastest) |
|
|
| 2 | Tegola | Go | 2-3x slower |
|
|
| 3 | BBOX | Rust | ~same as Tegola |
|
|
| 4 | pg_tileserv | Go | ~4x slower |
|
|
| 5 | TiPg | Python | Slower |
|
|
| 6 | ldproxy | Java | 4-70x slower |
|
|
|
|
Source: [github.com/FabianRechsteiner/vector-tiles-benchmark](https://github.com/FabianRechsteiner/vector-tiles-benchmark)
|
|
|
|
---
|
|
|
|
## Implementation Roadmap
|
|
|
|
### Phase 1: Martin Fix — DONE (2026-03-27)
|
|
|
|
Changes applied:
|
|
- `martin.Dockerfile`: custom image that COPY-s `martin.yaml` into `/config/`
|
|
- `docker-compose.yml`: Martin v0.15 -> v1.4.0, build from Dockerfile, `--config` flag
|
|
- `martin.yaml`: comment updated to reflect v1.4
|
|
- `map-viewer.tsx`: building labels layer activated (`cladiriLabel` at minzoom 16)
|
|
|
|
#### Deployment Lessons Learned
|
|
|
|
1. **Docker image tag format changed at v1.0**: old tags use `v` prefix (`v0.15.0`), new tags do not (`1.4.0`). The tag `ghcr.io/maplibre/martin:v1.4.0` does NOT exist — correct is `ghcr.io/maplibre/martin:1.4.0`.
|
|
|
|
2. **Portainer CE volume mount pitfall**: volume `./martin.yaml:/config/martin.yaml:ro` fails because Portainer deploys only the docker-compose.yml content, not the full git repo. Docker silently creates an empty directory instead of failing. Solution: bake config into a custom image with a 2-line Dockerfile:
|
|
```dockerfile
|
|
FROM ghcr.io/maplibre/martin:1.4.0
|
|
COPY martin.yaml /config/martin.yaml
|
|
```
|
|
|
|
3. **Martin config format is stable**: YAML format unchanged from v0.15 to v1.4 — `postgres.tables`, `connection_string`, `auto_publish`, `properties` map all work identically. No migration needed.
|
|
|
|
4. **PostGIS view geometry type**: Martin logs `UNKNOWN GEOMETRY TYPE` for all views — this is normal for nested views (`SELECT * FROM parent_view`). Views don't register specific geometry types in `geometry_columns`. Does not affect tile generation or property inclusion.
|
|
|
|
### Phase 2A: nginx Tile Cache — DONE (2026-03-27)
|
|
|
|
**Impact**: 10-100x faster on repeat requests, zero PostGIS load for cached tiles.
|
|
|
|
Changes applied:
|
|
- `nginx/tile-cache.conf`: proxy_cache config with 2GB cache zone, 7-day TTL, stale serving
|
|
- `tile-cache.Dockerfile`: bakes nginx config into custom image (Portainer CE pattern)
|
|
- `docker-compose.yml`: `tile-cache` container, Martin no longer exposed on host
|
|
- Gzip passthrough (Martin already compresses), browser caching via Cache-Control headers
|
|
- CORS headers for cross-origin tile requests
|
|
|
|
### Phase 2B: PMTiles — DONE (2026-03-27)
|
|
|
|
**Impact**: Sub-10ms overview tiles, zero PostGIS load for z0-z18.
|
|
|
|
Changes applied:
|
|
- `scripts/rebuild-overview-tiles.sh`: ogr2ogr export (3844->4326) + tippecanoe generation
|
|
- PMTiles archive: z0-z18, ~1-2 GB, includes all terenuri, cladiri, UATs, and administrativ layers
|
|
- `map-viewer.tsx`: pmtiles:// protocol registered on MapLibre, hybrid source switching
|
|
- MinIO bucket `tiles` with public read + CORS for Range Requests
|
|
- N8N webhook trigger for rebuild (via monitor page)
|
|
- Monitor page (`/monitor`): rebuild + warm-cache actions with live status polling
|
|
|
|
### Phase 2C: MLT Format — DEFERRED
|
|
|
|
Martin v1.4 advertises MLT support, but it cannot generate MLT from PostGIS live queries.
|
|
MLT generation requires pre-built tile archives (tippecanoe does not output MLT either).
|
|
No actionable path until Martin or tippecanoe adds MLT output from PostGIS sources.
|
|
|
|
### Phase 2D: mvt-rs Evaluation — FUTURE (Multi-Tenant)
|
|
|
|
**Impact**: Built-in auth, admin UI, per-layer access control.
|
|
**Effort**: 1-2 days for evaluation + migration.
|
|
|
|
Reserved for when external client access to the geoportal is needed.
|
|
mvt-rs (v0.16.2+, Rust, Salvo framework) provides per-layer auth and admin UI.
|
|
|
|
---
|
|
|
|
## Phase 3: Current Architecture (as of 2026-03-27)
|
|
|
|
Full tile-serving pipeline in production:
|
|
|
|
```
|
|
PostGIS (EPSG:3844)
|
|
|
|
|
+--> Martin v1.4.0 (live MVT from 9 PostGIS views)
|
|
| |
|
|
| +--> tile-cache (nginx reverse proxy, 2GB disk, 7d TTL)
|
|
| |
|
|
| +--> Traefik (tools.beletage.ro/tiles)
|
|
|
|
|
+--> ogr2ogr (3844->4326) + tippecanoe (z0-z18)
|
|
|
|
|
+--> PMTiles archive (~1-2 GB)
|
|
|
|
|
+--> MinIO bucket "tiles" (HTTP Range Requests)
|
|
|
|
|
+--> MapLibre (pmtiles:// protocol)
|
|
```
|
|
|
|
**Hybrid strategy**:
|
|
- PMTiles serves pre-generated overview tiles (all zoom levels, all layers)
|
|
- Martin serves live detail tiles (real-time PostGIS data)
|
|
- nginx tile-cache sits in front of Martin to absorb repeat requests
|
|
- Rebuild triggered via N8N webhook from the `/monitor` page
|
|
|
|
---
|
|
|
|
## Operational Commands
|
|
|
|
### Rebuild PMTiles
|
|
|
|
Trigger from the Monitor page (`/monitor` -> "Rebuild PMTiles" button), which sends a webhook to N8N.
|
|
N8N runs `scripts/rebuild-overview-tiles.sh` on the server.
|
|
|
|
Manual rebuild (SSH to 10.10.10.166):
|
|
```bash
|
|
cd /path/to/architools
|
|
bash scripts/rebuild-overview-tiles.sh
|
|
```
|
|
|
|
### Warm nginx Cache
|
|
|
|
Trigger from the Monitor page (`/monitor` -> "Warm Cache" button).
|
|
Pre-loads frequently accessed tiles into the nginx disk cache.
|
|
|
|
### Purge nginx Tile Cache
|
|
|
|
```bash
|
|
docker exec tile-cache rm -rf /var/cache/nginx/tiles/*
|
|
docker exec tile-cache nginx -s reload
|
|
```
|
|
|
|
### Restart Martin (after PostGIS view changes)
|
|
|
|
```bash
|
|
docker restart martin
|
|
```
|
|
|
|
Martin caches source schema at startup — must restart after DDL changes to pick up new columns.
|
|
|
|
### Check PMTiles Status
|
|
|
|
```bash
|
|
# Check file size and last modified in MinIO
|
|
docker exec minio mc stat local/tiles/overview.pmtiles
|
|
```
|
|
|
|
---
|
|
|
|
## Key Technical Details
|
|
|
|
### Martin v1.4.0 Deployment Architecture
|
|
|
|
```
|
|
Gitea repo (martin.yaml + martin.Dockerfile)
|
|
-> Portainer CE builds custom image: FROM martin:1.4.0, COPY martin.yaml
|
|
-> Container starts with --config /config/martin.yaml
|
|
-> Reads DATABASE_URL from environment
|
|
-> Serves 9 PostGIS view sources on port 3000
|
|
-> Host maps 3010:3000
|
|
-> Traefik proxies tools.beletage.ro/tiles -> host:3010
|
|
```
|
|
|
|
**Critical**: Do NOT use volume mounts for config files in Portainer CE stacks.
|
|
Always bake configs into custom images via Dockerfile COPY.
|
|
|
|
### Martin Config (validated compatible v0.15 through v1.4)
|
|
|
|
The `martin.yaml` at project root defines 9 sources with explicit properties.
|
|
Config format unchanged from v0.15 to v1.4 — no migration needed.
|
|
|
|
Key config features used:
|
|
- `auto_publish: false` — only explicitly listed sources are served
|
|
- `default_srid: 3844` — all sources use Stereo70
|
|
- `properties:` map per source — explicit column name + PostgreSQL type
|
|
- `minzoom/maxzoom` per source — controls tile generation range
|
|
- `bounds: [20.2, 43.5, 30.0, 48.3]` — approximate Romania extent
|
|
|
|
### Docker Image Tag Convention
|
|
|
|
Martin changed tag format at v1.0:
|
|
- Pre-1.0: `ghcr.io/maplibre/martin:v0.15.0` (with `v` prefix)
|
|
- Post-1.0: `ghcr.io/maplibre/martin:1.4.0` (no `v` prefix)
|
|
- Also available: `latest`, `nightly`
|
|
|
|
### PostGIS View Chain
|
|
|
|
```
|
|
GisFeature table (Prisma) -> gis_features view -> gis_terenuri / gis_cladiri / gis_administrativ
|
|
-> gis_terenuri_status / gis_cladiri_status (with JOINs)
|
|
GisUat table -> gis_uats_z0/z5/z8/z12 (with ST_SimplifyPreserveTopology)
|
|
```
|
|
|
|
### MapLibre Layer Architecture
|
|
|
|
```
|
|
Sources (Martin): gis_uats_z0, z5, z8, z12, administrativ, terenuri, cladiri
|
|
Layers per source: fill + line + label (where applicable)
|
|
Selection: Separate highlight layers on terenuri source
|
|
Drawing: GeoJSON source for freehand/rect polygon
|
|
Building labels: cladiriLabel layer, cadastral_ref at minzoom 16
|
|
```
|
|
|
|
---
|
|
|
|
## Deployment Pitfalls (Discovered During Implementation)
|
|
|
|
1. **Portainer CE does not expose repo files to containers at runtime.** Volume mounts like `./file.conf:/etc/file.conf:ro` fail silently — Docker creates an empty directory. Always bake config files into custom images via Dockerfile COPY.
|
|
|
|
2. **Martin Docker tag format change at v1.0.** `v1.4.0` does not exist, `1.4.0` does. Always check [ghcr.io/maplibre/martin](https://github.com/maplibre/martin/pkgs/container/martin) for actual tags.
|
|
|
|
3. **Martin logs `UNKNOWN GEOMETRY TYPE` for PostGIS views.** This is normal — nested views don't register geometry types in `geometry_columns`. Does not affect functionality.
|
|
|
|
4. **Martin auto-discovery mode is unreliable for property inclusion.** Always use explicit config with `auto_publish: false` and per-source `properties:` definitions.
|
|
|
|
5. **Martin caches source schema at startup.** After PostGIS view DDL changes (e.g., adding columns to gis_features), Martin must be restarted to pick up new columns.
|
|
|
|
---
|
|
|
|
## References
|
|
|
|
- [Martin Documentation](https://maplibre.org/martin/)
|
|
- [Martin Releases](https://github.com/maplibre/martin/releases)
|
|
- [Martin Container Registry](https://github.com/maplibre/martin/pkgs/container/martin)
|
|
- [Vector Tiles Benchmark (Rechsteiner 2025)](https://github.com/FabianRechsteiner/vector-tiles-benchmark)
|
|
- [PMTiles Specification](https://github.com/protomaps/PMTiles)
|
|
- [tippecanoe (Felt)](https://github.com/felt/tippecanoe)
|
|
- [MLT Format Announcement](https://maplibre.org/news/2026-01-23-mlt-release/)
|
|
- [mvt-rs](https://github.com/mvt-proj/mvt-rs)
|
|
- [pg_tileserv](https://github.com/CrunchyData/pg_tileserv)
|
|
- [Tegola](https://github.com/go-spatial/tegola)
|
|
- [Serving Vector Tiles Fast (Spatialists)](https://spatialists.ch/posts/2025/04/05-serving-vector-tiles-fast/)
|