Files
ArchiTools/geoportal/TILE-SERVER-EVALUATION.md
T
AI Assistant a75d0e1adc fix(geoportal): mount Martin config + upgrade v1.4 + enable building labels
Root cause: martin.yaml was never mounted in docker-compose.yml — Martin ran
in auto-discovery mode which dropped cadastral_ref from gis_cladiri tiles.

Changes:
- docker-compose: mount martin.yaml, upgrade Martin v0.15→v1.4.0, use --config
- map-viewer: add cladiriLabel layer (cadastral_ref at z16+), wire into visibility
- martin.yaml: update version comment
- geoportal/: tile server evaluation doc + 3 skill files (vector tiles, PMTiles, MapLibre perf)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 10:28:20 +02:00

13 KiB
Raw Blame History

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.


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 Mount config + 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 — docker-compose.yml updated, building labels activated.

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

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 MB1.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


Implementation Roadmap

Phase 1: Martin Fix (DONE)

Changes applied:

  • docker-compose.yml: Martin v0.15 → v1.4.0, config mounted, command changed to --config
  • martin.yaml: Comment updated to reflect v1.4
  • map-viewer.tsx: Building labels layer activated (cladiriLabel at minzoom 16)

Phase 2A: nginx Tile Cache

Impact: 10-100x faster on repeat requests, zero PostGIS load for cached tiles. Effort: ~2 hours.

Implementation Prompt

Add an nginx reverse proxy cache in front of Martin for tile serving in ArchiTools.

Context:
- Martin serves tiles at http://martin:3000 (container name: martin, port 3010 on host)
- Traefik proxies external traffic to ArchiTools at tools.beletage.ro
- Current tile URL pattern: https://tools.beletage.ro/tiles/{source}/{z}/{x}/{y}
- NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles

Requirements:
1. Create an nginx container `tile-cache` in docker-compose.yml
2. nginx config: proxy_cache for /tiles/* with:
   - Cache zone: 2GB max, keys in shared memory
   - Cache valid: 200 responses for 1 hour
   - Stale serving on error/timeout
   - Cache-Control headers passed through
   - CORS headers for tiles (Access-Control-Allow-Origin: *)
   - Gzip/brotli passthrough (Martin already compresses)
3. Route Martin traffic through tile-cache:
   - tile-cache listens on port 3010 (replace Martin's host port)
   - tile-cache proxies to http://martin:3000
4. Add cache purge endpoint or script for post-sync invalidation
5. Volume for persistent cache across container restarts

Files to modify:
- docker-compose.yml (add tile-cache service, adjust martin ports)
- Create nginx/tile-cache.conf

Do NOT change the frontend NEXT_PUBLIC_MARTIN_URL — keep the same external URL.
Build with `npx next build` to verify zero errors.

Phase 2B: PMTiles for UAT Overview Layers

Impact: Sub-10ms overview tiles, zero PostGIS load for z0-z12. Effort: ~4-6 hours.

Implementation Prompt

Implement PMTiles pre-generation for UAT overview layers in ArchiTools Geoportal.

Context:
- PostGIS at 10.10.10.166:5432, database architools_db
- Views: gis_uats_z0, gis_uats_z5, gis_uats_z8, gis_uats_z12, gis_administrativ
- All geometries EPSG:3844 (Stereo70), need reprojection to 4326 for tippecanoe
- MinIO at 10.10.10.166:9002, bucket for tiles
- Frontend: MapLibre GL JS 5.21 in src/modules/geoportal/components/map-viewer.tsx

Requirements:

1. Create `scripts/rebuild-overview-tiles.sh`:
   - Export each view with ogr2ogr: -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326
   - Generate combined PMTiles with tippecanoe:
     - --layer per view, --minimum-zoom=0, --maximum-zoom=14
     - --detect-shared-borders (critical for adjacent UAT polygons)
     - --hilbert for compression
   - Atomic upload to MinIO: upload as overview_new.pmtiles, then rename to overview.pmtiles
   - Cleanup temp files

2. Create Dockerfile for tippecanoe build container (or use ghcr.io/felt/tippecanoe)

3. Add `tippecanoe` service to docker-compose.yml (one-shot, for manual/cron runs)

4. Configure MinIO:
   - Create bucket `tiles` with public read
   - CORS: Allow GET/HEAD from tools.beletage.ro, expose Range/Content-Range headers

5. Update map-viewer.tsx:
   - npm install pmtiles
   - Register pmtiles:// protocol on MapLibre
   - Add PMTiles source for overview layers (z0-z14)
   - Keep existing Martin sources for detail layers (z14+)
   - Set zoom breakpoints: PMTiles below z14, Martin above z14

6. Add N8N webhook trigger or cron for nightly rebuild after weekend deep sync

The UAT overview layers change rarely (only when new UATs are synced).
Parcel/building layers stay on Martin for live data freshness.

Build with `npx next build` to verify zero errors.
Read CLAUDE.md for project conventions before starting.

Phase 2C: MLT Format Testing

Impact: 6x smaller tiles, 4x faster client decode. Effort: ~1 hour to test.

Implementation Prompt

Test MLT (MapLibre Tiles) format on one layer in ArchiTools Geoportal.

Context:
- Martin v1.4.0 running with config at martin.yaml
- MapLibre GL JS 5.21 in src/modules/geoportal/components/map-viewer.tsx
- Test layer: gis_terenuri (largest layer, ~250K features)

Requirements:

1. Research how Martin v1.4 serves MLT format:
   - Check if it's automatic via Accept header or needs config
   - Check Martin docs for MLT serving configuration

2. Update map-viewer.tsx to request MLT for one source (gis_terenuri):
   - Add `encoding: "mlt"` to the vector source definition if MapLibre supports it
   - Or configure via source URL parameter if Martin expects it

3. Test and measure:
   - Compare tile sizes: MVT vs MLT for same tile coordinates
   - Compare decode time in browser DevTools Network tab
   - Check that all properties (cadastral_ref, area_value, etc.) survive MLT encoding
   - Check label rendering still works

4. If MLT works correctly, apply to all Martin sources
   If issues found, document them and revert to MVT

This is experimental — keep MVT as fallback. Do not break existing functionality.
Build with `npx next build` to verify zero errors.

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.

Implementation Prompt

Evaluate mvt-rs as a replacement for Martin in ArchiTools Geoportal for multi-tenant deployment.

Context:
- Current: Martin v1.4.0 serving 9 PostGIS sources (views in EPSG:3844)
- Goal: Expose geoportal to external clients with per-layer access control
- mvt-rs repo: https://github.com/mvt-proj/mvt-rs (v0.16.2+, Rust, Salvo framework)

Requirements:

1. Deploy mvt-rs as a Docker container alongside Martin (don't replace yet)
   - Use same DATABASE_URL
   - Map to different port (e.g., 3011)

2. Configure mvt-rs with equivalent layers to martin.yaml:
   - gis_uats_z0/z5/z8/z12, gis_administrativ, gis_terenuri, gis_cladiri
   - gis_terenuri_status, gis_cladiri_status
   - All with EPSG:3844, explicit properties

3. Test:
   - Do all properties appear in MVT output? (especially cadastral_ref on gis_cladiri)
   - Performance comparison: curl timing for 10 representative tiles vs Martin
   - Admin UI: create test user, assign layer permissions
   - Cache: configure disk cache, measure cold vs warm tile times

4. Document findings:
   - Property inclusion: pass/fail per layer
   - Performance delta vs Martin
   - Admin UI capabilities and limitations
   - Missing features vs Martin (PMTiles, MLT, etc.)

5. Decision matrix: when to switch from Martin to mvt-rs

Do NOT modify the production Martin setup. This is a parallel evaluation only.

Key Technical Details

Martin v1.4.0 Config (validated compatible)

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

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

References