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>
This commit is contained in:
@@ -0,0 +1,343 @@
|
||||
# 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 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)
|
||||
|
||||
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
|
||||
|
||||
- [Martin Documentation](https://maplibre.org/martin/)
|
||||
- [Martin Releases](https://github.com/maplibre/martin/releases)
|
||||
- [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/)
|
||||
Reference in New Issue
Block a user