feat(geoportal): nginx tile cache + PMTiles overview layers + tippecanoe pipeline

- Add nginx reverse proxy cache in front of Martin (2GB, 1h TTL, stale serving, CORS)
- Martin no longer exposes host port — all traffic routed through tile-cache on :3010
- Add PMTiles support in map-viewer.tsx (conditional: NEXT_PUBLIC_PMTILES_URL env var)
  - When set: single PMTiles source for UAT + administrativ layers (z0-z14, ~5ms/tile)
  - When empty: fallback to Martin tile sources (existing behavior, zero breaking change)
- Add tippecanoe Docker service (profiles: tools) for on-demand PMTiles generation
- Add rebuild-overview-tiles.sh: ogr2ogr export → tippecanoe → MinIO atomic upload
- Install pmtiles npm package for MapLibre protocol registration

Performance impact:
- nginx cache: 10-100x faster on repeat tile requests, zero PostGIS load on cache hit
- PMTiles: sub-10ms overview tiles, zero PostGIS load for z0-z14

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-27 20:28:49 +02:00
parent 67f3237761
commit 536b3659bb
8 changed files with 311 additions and 31 deletions
+35 -2
View File
@@ -58,6 +58,8 @@ services:
- ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-} - ILOVEPDF_PUBLIC_KEY=${ILOVEPDF_PUBLIC_KEY:-}
# Martin vector tile server (geoportal) # Martin vector tile server (geoportal)
- NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles - NEXT_PUBLIC_MARTIN_URL=https://tools.beletage.ro/tiles
# PMTiles overview tiles from MinIO (empty = use Martin for all layers)
- NEXT_PUBLIC_PMTILES_URL=${NEXT_PUBLIC_PMTILES_URL:-}
# DWG-to-DXF sidecar # DWG-to-DXF sidecar
- DWG2DXF_URL=http://dwg2dxf:5001 - DWG2DXF_URL=http://dwg2dxf:5001
# Email notifications (Brevo SMTP) # Email notifications (Brevo SMTP)
@@ -107,8 +109,39 @@ services:
dockerfile: martin.Dockerfile dockerfile: martin.Dockerfile
container_name: martin container_name: martin
restart: unless-stopped restart: unless-stopped
ports: # No host port — only accessible via tile-cache nginx proxy
- "3010:3000"
command: ["--config", "/config/martin.yaml"] command: ["--config", "/config/martin.yaml"]
environment: environment:
- DATABASE_URL=postgresql://architools_user:stictMyFon34!_gonY@10.10.10.166:5432/architools_db - DATABASE_URL=postgresql://architools_user:stictMyFon34!_gonY@10.10.10.166:5432/architools_db
tile-cache:
build:
context: .
dockerfile: tile-cache.Dockerfile
container_name: tile-cache
restart: unless-stopped
ports:
- "3010:80"
depends_on:
- martin
volumes:
- tile-cache-data:/var/cache/nginx/tiles
tippecanoe:
build:
context: .
dockerfile: tippecanoe.Dockerfile
container_name: tippecanoe
profiles: ["tools"]
environment:
- DB_HOST=10.10.10.166
- DB_PORT=5432
- DB_NAME=architools_db
- DB_USER=architools_user
- DB_PASS=stictMyFon34!_gonY
- MINIO_ENDPOINT=http://10.10.10.166:9002
- MINIO_ACCESS_KEY=admin
- MINIO_SECRET_KEY=MinioStrongPass123
volumes:
tile-cache-data:
+71
View File
@@ -0,0 +1,71 @@
# nginx tile cache for Martin vector tile server
# Proxy-cache layer: 10-100x faster on repeat requests, zero PostGIS load for cached tiles
proxy_cache_path /var/cache/nginx/tiles
levels=1:2
keys_zone=tiles:64m
max_size=2g
inactive=24h
use_temp_path=off;
server {
listen 80;
server_name _;
# Health check
location = /health {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
# Martin catalog endpoint (no cache)
location = /catalog {
proxy_pass http://martin:3000/catalog;
proxy_set_header Host $host;
}
# Tile requests — cache aggressively
location / {
proxy_pass http://martin:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Cache config
proxy_cache tiles;
proxy_cache_key "$request_uri";
proxy_cache_valid 200 1h;
proxy_cache_valid 204 1m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
# Pass cache status header (useful for debugging)
add_header X-Cache-Status $upstream_cache_status always;
# CORS headers for tile requests
add_header Access-Control-Allow-Origin "*" always;
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS" always;
add_header Access-Control-Allow-Headers "Range, If-None-Match, Accept-Encoding" always;
add_header Access-Control-Expose-Headers "Content-Range, Content-Length, ETag, X-Cache-Status" always;
# Handle preflight
if ($request_method = OPTIONS) {
add_header Access-Control-Allow-Origin "*";
add_header Access-Control-Allow-Methods "GET, HEAD, OPTIONS";
add_header Access-Control-Max-Age 86400;
add_header Content-Length 0;
return 204;
}
# Martin already compresses — pass through
proxy_set_header Accept-Encoding "";
gzip off;
# Timeouts (Martin can be slow on low-zoom tiles)
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
proxy_send_timeout 30s;
}
}
+10
View File
@@ -25,6 +25,7 @@
"next-auth": "^4.24.13", "next-auth": "^4.24.13",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nodemailer": "^7.0.13", "nodemailer": "^7.0.13",
"pmtiles": "^4.4.0",
"proj4": "^2.20.3", "proj4": "^2.20.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
@@ -10806,6 +10807,15 @@
"pathe": "^2.0.3" "pathe": "^2.0.3"
} }
}, },
"node_modules/pmtiles": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-4.4.0.tgz",
"integrity": "sha512-tCLI1C5134MR54i8izUWhse0QUtO/EC33n9yWp1N5dYLLvyc197U0fkF5gAJhq1TdWO9Tvl+9hgvFvM0fR27Zg==",
"license": "BSD-3-Clause",
"dependencies": {
"fflate": "^0.8.2"
}
},
"node_modules/pngjs": { "node_modules/pngjs": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz",
+1
View File
@@ -26,6 +26,7 @@
"next-auth": "^4.24.13", "next-auth": "^4.24.13",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"nodemailer": "^7.0.13", "nodemailer": "^7.0.13",
"pmtiles": "^4.4.0",
"proj4": "^2.20.3", "proj4": "^2.20.3",
"qrcode": "^1.5.4", "qrcode": "^1.5.4",
"radix-ui": "^1.4.3", "radix-ui": "^1.4.3",
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# rebuild-overview-tiles.sh — Export UAT overview layers from PostGIS, generate PMTiles, upload to MinIO
# Usage: ./scripts/rebuild-overview-tiles.sh
# Dependencies: ogr2ogr (GDAL), tippecanoe, mc (MinIO client)
# Run from project root or as a Docker one-shot container.
set -euo pipefail
# ── Configuration ──
DB_HOST="${DB_HOST:-10.10.10.166}"
DB_PORT="${DB_PORT:-5432}"
DB_NAME="${DB_NAME:-architools_db}"
DB_USER="${DB_USER:-architools_user}"
DB_PASS="${DB_PASS:-stictMyFon34!_gonY}"
MINIO_ALIAS="${MINIO_ALIAS:-myminio}"
MINIO_BUCKET="${MINIO_BUCKET:-tiles}"
MINIO_ENDPOINT="${MINIO_ENDPOINT:-http://10.10.10.166:9002}"
MINIO_ACCESS_KEY="${MINIO_ACCESS_KEY:-admin}"
MINIO_SECRET_KEY="${MINIO_SECRET_KEY:-MinioStrongPass123}"
TMPDIR="${TMPDIR:-/tmp/tile-rebuild}"
OUTPUT_FILE="overview.pmtiles"
PG_CONN="PG:host=${DB_HOST} port=${DB_PORT} dbname=${DB_NAME} user=${DB_USER} password=${DB_PASS}"
echo "[$(date -Iseconds)] Starting overview tile rebuild..."
# ── Setup ──
mkdir -p "$TMPDIR"
cd "$TMPDIR"
# ── Step 1: Export views from PostGIS (parallel) ──
echo "[$(date -Iseconds)] Exporting PostGIS views to FlatGeobuf..."
ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \
uats_z0.fgb "$PG_CONN" \
-sql "SELECT name, siruta, geom FROM gis_uats_z0 WHERE geom IS NOT NULL" &
ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \
uats_z5.fgb "$PG_CONN" \
-sql "SELECT name, siruta, geom FROM gis_uats_z5 WHERE geom IS NOT NULL" &
ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \
uats_z8.fgb "$PG_CONN" \
-sql "SELECT name, siruta, county, geom FROM gis_uats_z8 WHERE geom IS NOT NULL" &
ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \
uats_z12.fgb "$PG_CONN" \
-sql "SELECT name, siruta, county, geom FROM gis_uats_z12 WHERE geom IS NOT NULL" &
ogr2ogr -f FlatGeobuf -s_srs EPSG:3844 -t_srs EPSG:4326 \
administrativ.fgb "$PG_CONN" \
-sql "SELECT object_id, siruta, layer_id, cadastral_ref, geom FROM gis_administrativ WHERE geom IS NOT NULL" &
wait
echo "[$(date -Iseconds)] Export complete."
# ── Step 2: Generate PMTiles with tippecanoe ──
echo "[$(date -Iseconds)] Generating PMTiles..."
tippecanoe \
-o "$OUTPUT_FILE" \
--named-layer=gis_uats_z0:uats_z0.fgb \
--named-layer=gis_uats_z5:uats_z5.fgb \
--named-layer=gis_uats_z8:uats_z8.fgb \
--named-layer=gis_uats_z12:uats_z12.fgb \
--named-layer=gis_administrativ:administrativ.fgb \
--minimum-zoom=0 \
--maximum-zoom=14 \
--base-zoom=14 \
--drop-densest-as-needed \
--detect-shared-borders \
--hilbert \
--force
echo "[$(date -Iseconds)] PMTiles generated: $(du -h "$OUTPUT_FILE" | cut -f1)"
# ── Step 3: Upload to MinIO (atomic swap) ──
echo "[$(date -Iseconds)] Uploading to MinIO..."
# Configure MinIO client alias (idempotent)
mc alias set "$MINIO_ALIAS" "$MINIO_ENDPOINT" "$MINIO_ACCESS_KEY" "$MINIO_SECRET_KEY" --api S3v4 2>/dev/null || true
# Ensure bucket exists
mc mb --ignore-existing "${MINIO_ALIAS}/${MINIO_BUCKET}" 2>/dev/null || true
# Upload as temp file first
mc cp "$OUTPUT_FILE" "${MINIO_ALIAS}/${MINIO_BUCKET}/overview_new.pmtiles"
# Atomic rename (zero-downtime swap)
mc mv "${MINIO_ALIAS}/${MINIO_BUCKET}/overview_new.pmtiles" "${MINIO_ALIAS}/${MINIO_BUCKET}/overview.pmtiles"
echo "[$(date -Iseconds)] Upload complete."
# ── Step 4: Cleanup ──
rm -f uats_z0.fgb uats_z5.fgb uats_z8.fgb uats_z12.fgb administrativ.fgb "$OUTPUT_FILE"
echo "[$(date -Iseconds)] Rebuild finished successfully."
@@ -2,6 +2,7 @@
import { useRef, useEffect, useState, useCallback, useImperativeHandle, forwardRef } from "react"; import { useRef, useEffect, useState, useCallback, useImperativeHandle, forwardRef } from "react";
import maplibregl from "maplibre-gl"; import maplibregl from "maplibre-gl";
import { Protocol as PmtilesProtocol } from "pmtiles";
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
/* Ensure MapLibre CSS is loaded — static import fails with next/dynamic + standalone */ /* Ensure MapLibre CSS is loaded — static import fails with next/dynamic + standalone */
@@ -15,6 +16,12 @@ if (typeof document !== "undefined") {
document.head.appendChild(link); document.head.appendChild(link);
} }
} }
/* Register PMTiles protocol globally (once) for pmtiles:// source URLs */
if (typeof window !== "undefined") {
const pmtilesProto = new PmtilesProtocol();
maplibregl.addProtocol("pmtiles", pmtilesProto.tile);
}
import type { BasemapId, ClickedFeature, LayerVisibility, SelectedFeature } from "../types"; import type { BasemapId, ClickedFeature, LayerVisibility, SelectedFeature } from "../types";
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
@@ -28,6 +35,7 @@ export type SelectionType = "off" | "click" | "rect" | "freehand";
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
const DEFAULT_MARTIN_URL = process.env.NEXT_PUBLIC_MARTIN_URL || "/tiles"; const DEFAULT_MARTIN_URL = process.env.NEXT_PUBLIC_MARTIN_URL || "/tiles";
const DEFAULT_PMTILES_URL = process.env.NEXT_PUBLIC_PMTILES_URL || "";
const DEFAULT_CENTER: [number, number] = [23.8, 46.1]; const DEFAULT_CENTER: [number, number] = [23.8, 46.1];
const DEFAULT_ZOOM = 7; const DEFAULT_ZOOM = 7;
@@ -384,17 +392,56 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
} }
} }
// === UAT z0-5: very coarse — lines only === // === UAT sources: PMTiles (if configured) or Martin fallback ===
const pmtilesUrl = DEFAULT_PMTILES_URL;
const usePmtiles = !!pmtilesUrl;
if (usePmtiles) {
// Single PMTiles source contains all UAT + administrativ layers (z0-z14)
const PM_SRC = "overview-pmtiles";
map.addSource(PM_SRC, { type: "vector", url: `pmtiles://${pmtilesUrl}` });
// z0-5: lines only
map.addLayer({ id: LAYER_IDS.uatsZ0Line, type: "line", source: PM_SRC, "source-layer": SOURCES.uatsZ0, maxzoom: 5,
paint: { "line-color": "#7c3aed", "line-width": 0.3 } });
// z5-8
map.addLayer({ id: LAYER_IDS.uatsZ5Line, type: "line", source: PM_SRC, "source-layer": SOURCES.uatsZ5, minzoom: 5, maxzoom: 8,
paint: { "line-color": "#7c3aed", "line-width": 0.6 } });
// z8-12
map.addLayer({ id: LAYER_IDS.uatsZ8Line, type: "line", source: PM_SRC, "source-layer": SOURCES.uatsZ8, minzoom: 8, maxzoom: 12,
paint: { "line-color": "#7c3aed", "line-width": 1 } });
map.addLayer({ id: LAYER_IDS.uatsZ8Label, type: "symbol", source: PM_SRC, "source-layer": SOURCES.uatsZ8, minzoom: 9, maxzoom: 12,
layout: { "text-field": ["coalesce", ["get", "name"], ""], "text-font": ["Noto Sans Regular"], "text-size": 10, "text-anchor": "center", "text-allow-overlap": false },
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } });
// z12+: full detail from PMTiles
map.addLayer({ id: LAYER_IDS.uatsZ12Line, type: "line", source: PM_SRC, "source-layer": SOURCES.uatsZ12, minzoom: 12,
paint: { "line-color": "#7c3aed", "line-width": 2 } });
map.addLayer({ id: LAYER_IDS.uatsZ12Label, type: "symbol", source: PM_SRC, "source-layer": SOURCES.uatsZ12, minzoom: 12,
layout: { "text-field": ["coalesce", ["get", "name"], ""], "text-font": ["Noto Sans Regular"], "text-size": 13, "text-anchor": "center", "text-allow-overlap": false },
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } });
// Intravilan from PMTiles
map.addLayer({ id: LAYER_IDS.adminLineOuter, type: "line", source: PM_SRC, "source-layer": SOURCES.administrativ, minzoom: 13,
paint: { "line-color": "#000000", "line-width": 3 } });
map.addLayer({ id: LAYER_IDS.adminLineInner, type: "line", source: PM_SRC, "source-layer": SOURCES.administrativ, minzoom: 13,
paint: { "line-color": "#f97316", "line-width": 1.5 } });
} else {
// Fallback: Martin tile sources (existing behavior)
// z0-5: very coarse — lines only
map.addSource(SOURCES.uatsZ0, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ0}/{z}/{x}/{y}`], minzoom: 0, maxzoom: 5 }); map.addSource(SOURCES.uatsZ0, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ0}/{z}/{x}/{y}`], minzoom: 0, maxzoom: 5 });
map.addLayer({ id: LAYER_IDS.uatsZ0Line, type: "line", source: SOURCES.uatsZ0, "source-layer": SOURCES.uatsZ0, maxzoom: 5, map.addLayer({ id: LAYER_IDS.uatsZ0Line, type: "line", source: SOURCES.uatsZ0, "source-layer": SOURCES.uatsZ0, maxzoom: 5,
paint: { "line-color": "#7c3aed", "line-width": 0.3 } }); paint: { "line-color": "#7c3aed", "line-width": 0.3 } });
// === UAT z5-8: coarse === // z5-8: coarse
map.addSource(SOURCES.uatsZ5, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ5}/{z}/{x}/{y}`], minzoom: 5, maxzoom: 8 }); map.addSource(SOURCES.uatsZ5, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ5}/{z}/{x}/{y}`], minzoom: 5, maxzoom: 8 });
map.addLayer({ id: LAYER_IDS.uatsZ5Line, type: "line", source: SOURCES.uatsZ5, "source-layer": SOURCES.uatsZ5, minzoom: 5, maxzoom: 8, map.addLayer({ id: LAYER_IDS.uatsZ5Line, type: "line", source: SOURCES.uatsZ5, "source-layer": SOURCES.uatsZ5, minzoom: 5, maxzoom: 8,
paint: { "line-color": "#7c3aed", "line-width": 0.6 } }); paint: { "line-color": "#7c3aed", "line-width": 0.6 } });
// === UAT z8-12: moderate === // z8-12: moderate
map.addSource(SOURCES.uatsZ8, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ8}/{z}/{x}/{y}`], minzoom: 8, maxzoom: 12 }); map.addSource(SOURCES.uatsZ8, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ8}/{z}/{x}/{y}`], minzoom: 8, maxzoom: 12 });
map.addLayer({ id: LAYER_IDS.uatsZ8Line, type: "line", source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8, minzoom: 8, maxzoom: 12, map.addLayer({ id: LAYER_IDS.uatsZ8Line, type: "line", source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8, minzoom: 8, maxzoom: 12,
paint: { "line-color": "#7c3aed", "line-width": 1 } }); paint: { "line-color": "#7c3aed", "line-width": 1 } });
@@ -402,7 +449,7 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
layout: { "text-field": ["coalesce", ["get", "name"], ""], "text-font": ["Noto Sans Regular"], "text-size": 10, "text-anchor": "center", "text-allow-overlap": false }, layout: { "text-field": ["coalesce", ["get", "name"], ""], "text-font": ["Noto Sans Regular"], "text-size": 10, "text-anchor": "center", "text-allow-overlap": false },
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } }); paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } });
// === UAT z12+: full detail (no simplification) === // z12+: full detail (no simplification)
map.addSource(SOURCES.uatsZ12, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ12}/{z}/{x}/{y}`], minzoom: 12, maxzoom: 16 }); map.addSource(SOURCES.uatsZ12, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ12}/{z}/{x}/{y}`], minzoom: 12, maxzoom: 16 });
map.addLayer({ id: LAYER_IDS.uatsZ12Line, type: "line", source: SOURCES.uatsZ12, "source-layer": SOURCES.uatsZ12, minzoom: 12, map.addLayer({ id: LAYER_IDS.uatsZ12Line, type: "line", source: SOURCES.uatsZ12, "source-layer": SOURCES.uatsZ12, minzoom: 12,
paint: { "line-color": "#7c3aed", "line-width": 2 } }); paint: { "line-color": "#7c3aed", "line-width": 2 } });
@@ -410,14 +457,15 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
layout: { "text-field": ["coalesce", ["get", "name"], ""], "text-font": ["Noto Sans Regular"], "text-size": 13, "text-anchor": "center", "text-allow-overlap": false }, layout: { "text-field": ["coalesce", ["get", "name"], ""], "text-font": ["Noto Sans Regular"], "text-size": 13, "text-anchor": "center", "text-allow-overlap": false },
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } }); paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } });
// === Intravilan — double line (black outer + orange inner), no fill, z13+ === // Intravilan — double line (black outer + orange inner), no fill, z13+
map.addSource(SOURCES.administrativ, { type: "vector", tiles: [`${m}/${SOURCES.administrativ}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 16 }); map.addSource(SOURCES.administrativ, { type: "vector", tiles: [`${m}/${SOURCES.administrativ}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 16 });
map.addLayer({ id: LAYER_IDS.adminLineOuter, type: "line", source: SOURCES.administrativ, "source-layer": SOURCES.administrativ, minzoom: 13, map.addLayer({ id: LAYER_IDS.adminLineOuter, type: "line", source: SOURCES.administrativ, "source-layer": SOURCES.administrativ, minzoom: 13,
paint: { "line-color": "#000000", "line-width": 3 } }); paint: { "line-color": "#000000", "line-width": 3 } });
map.addLayer({ id: LAYER_IDS.adminLineInner, type: "line", source: SOURCES.administrativ, "source-layer": SOURCES.administrativ, minzoom: 13, map.addLayer({ id: LAYER_IDS.adminLineInner, type: "line", source: SOURCES.administrativ, "source-layer": SOURCES.administrativ, minzoom: 13,
paint: { "line-color": "#f97316", "line-width": 1.5 } }); paint: { "line-color": "#f97316", "line-width": 1.5 } });
}
// === Terenuri (parcels) — no simplification === // === Terenuri (parcels) — always from Martin (live data) ===
map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 18 }); map.addSource(SOURCES.terenuri, { type: "vector", tiles: [`${m}/${SOURCES.terenuri}/{z}/{x}/{y}`], minzoom: 10, maxzoom: 18 });
map.addLayer({ id: LAYER_IDS.terenuriFill, type: "fill", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13, map.addLayer({ id: LAYER_IDS.terenuriFill, type: "fill", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13,
paint: { "fill-color": "#22c55e", "fill-opacity": 0.15 } }); paint: { "fill-color": "#22c55e", "fill-opacity": 0.15 } });
+3
View File
@@ -0,0 +1,3 @@
FROM nginx:1.27-alpine
RUN mkdir -p /var/cache/nginx/tiles
COPY nginx/tile-cache.conf /etc/nginx/conf.d/default.conf
+16
View File
@@ -0,0 +1,16 @@
FROM ghcr.io/felt/tippecanoe:latest AS tippecanoe
FROM osgeo/gdal:alpine-normal-latest
# Copy tippecanoe binary from felt image
COPY --from=tippecanoe /usr/local/bin/tippecanoe /usr/local/bin/tippecanoe
COPY --from=tippecanoe /usr/local/bin/tile-join /usr/local/bin/tile-join
# Install MinIO client
RUN apk add --no-cache curl && \
curl -fsSL https://dl.min.io/client/mc/release/linux-amd64/mc -o /usr/local/bin/mc && \
chmod +x /usr/local/bin/mc
COPY scripts/rebuild-overview-tiles.sh /opt/rebuild.sh
RUN chmod +x /opt/rebuild.sh
ENTRYPOINT ["/opt/rebuild.sh"]