perf(geoportal): 4-level UAT simplification + intravilan layer + preserve view on basemap switch

UAT zoom-dependent views (read-only, original geom NEVER modified):
- gis_uats_z0 (z0-5): 2000m simplification — country outlines
- gis_uats_z5 (z5-8): 500m — regional overview
- gis_uats_z8 (z8-12): 50m — county/city level with labels
- gis_uats_z12 (z12+): 10m — near-original precision

New layers:
- gis_administrativ (intravilan, arii speciale) — orange dashed, no simplification
- Toggle in layer panel (off by default)

Basemap switching:
- Now preserves current center + zoom when switching between basemaps

Parcels + buildings: NO simplification (exact geometry needed)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-23 20:34:15 +02:00
parent 76c19449f3
commit 4f694d4458
4 changed files with 231 additions and 128 deletions
+56 -8
View File
@@ -1,36 +1,82 @@
# Martin v0.15 configuration — optimized tile sources for ArchiTools Geoportal # Martin v0.15 configuration — optimized tile sources for ArchiTools Geoportal
# All geometries are EPSG:3844 (Stereo70). Bounds are approximate Romania extent. # All geometries are EPSG:3844 (Stereo70). Bounds are approximate Romania extent.
# Original table data is NEVER modified — views compute simplification on-the-fly.
# Disable auto-discovery — only serve explicitly configured sources
postgres: postgres:
connection_string: ${DATABASE_URL} connection_string: ${DATABASE_URL}
default_srid: 3844 default_srid: 3844
auto_publish: false auto_publish: false
tables: tables:
gis_uats: # ── UAT boundaries: 4 zoom-dependent simplification levels ──
gis_uats_z0:
schema: public schema: public
table: gis_uats table: gis_uats_z0
geometry_column: geom geometry_column: geom
srid: 3844 srid: 3844
bounds: [20.2, 43.5, 30.0, 48.3] bounds: [20.2, 43.5, 30.0, 48.3]
minzoom: 0 minzoom: 0
maxzoom: 14 maxzoom: 5
properties: properties:
name: text name: text
siruta: text siruta: text
gis_uats_simple: gis_uats_z5:
schema: public schema: public
table: gis_uats_simple table: gis_uats_z5
geometry_column: geom geometry_column: geom
srid: 3844 srid: 3844
bounds: [20.2, 43.5, 30.0, 48.3] bounds: [20.2, 43.5, 30.0, 48.3]
minzoom: 0 minzoom: 5
maxzoom: 9 maxzoom: 8
properties: properties:
name: text name: text
siruta: text siruta: text
gis_uats_z8:
schema: public
table: gis_uats_z8
geometry_column: geom
srid: 3844
bounds: [20.2, 43.5, 30.0, 48.3]
minzoom: 8
maxzoom: 12
properties:
name: text
siruta: text
county: text
gis_uats_z12:
schema: public
table: gis_uats_z12
geometry_column: geom
srid: 3844
bounds: [20.2, 43.5, 30.0, 48.3]
minzoom: 12
maxzoom: 16
properties:
name: text
siruta: text
county: text
# ── Administrativ (intravilan, arii speciale) — NO simplification ──
gis_administrativ:
schema: public
table: gis_administrativ
geometry_column: geom
srid: 3844
bounds: [20.2, 43.5, 30.0, 48.3]
minzoom: 10
maxzoom: 16
properties:
object_id: text
siruta: text
layer_id: text
cadastral_ref: text
# ── Terenuri (parcels) — NO simplification ──
gis_terenuri: gis_terenuri:
schema: public schema: public
table: gis_terenuri table: gis_terenuri
@@ -46,6 +92,8 @@ postgres:
area_value: float8 area_value: float8
layer_id: text layer_id: text
# ── Cladiri (buildings) — NO simplification ──
gis_cladiri: gis_cladiri:
schema: public schema: public
table: gis_cladiri table: gis_cladiri
+39 -19
View File
@@ -158,29 +158,49 @@ WHERE geometry IS NOT NULL AND geom IS NULL;
CREATE INDEX IF NOT EXISTS gis_uat_geom_idx CREATE INDEX IF NOT EXISTS gis_uat_geom_idx
ON "GisUat" USING GIST (geom); ON "GisUat" USING GIST (geom);
-- 8. Martin/QGIS-friendly view (moderate simplification — 50m tolerance) -- =============================================================================
CREATE OR REPLACE VIEW gis_uats AS -- 8. Zoom-dependent views for Martin vector tiles
SELECT -- 4 levels of geometry simplification for progressive loading.
siruta, -- SAFE: these are read-only views — original geom column is NEVER modified.
name, -- =============================================================================
county,
ST_SimplifyPreserveTopology(geom, 50) AS geom
FROM "GisUat"
WHERE geom IS NOT NULL;
-- 9. Simplified view for low zoom levels (z5-z9) — 500m tolerance -- z0-5: Very coarse overview (2000m tolerance) — country-level outlines
CREATE OR REPLACE VIEW gis_uats_simple AS CREATE OR REPLACE VIEW gis_uats_z0 AS
SELECT SELECT siruta, name,
siruta, ST_SimplifyPreserveTopology(geom, 2000) AS geom
name, FROM "GisUat" WHERE geom IS NOT NULL;
-- z5-8: Coarse (500m tolerance) — regional overview
CREATE OR REPLACE VIEW gis_uats_z5 AS
SELECT siruta, name,
ST_SimplifyPreserveTopology(geom, 500) AS geom ST_SimplifyPreserveTopology(geom, 500) AS geom
FROM "GisUat" FROM "GisUat" WHERE geom IS NOT NULL;
WHERE geom IS NOT NULL;
-- z8-12: Moderate (50m tolerance) — county/city level
CREATE OR REPLACE VIEW gis_uats_z8 AS
SELECT siruta, name, county,
ST_SimplifyPreserveTopology(geom, 50) AS geom
FROM "GisUat" WHERE geom IS NOT NULL;
-- z12+: Fine (10m tolerance) — near-original precision
CREATE OR REPLACE VIEW gis_uats_z12 AS
SELECT siruta, name, county,
ST_SimplifyPreserveTopology(geom, 10) AS geom
FROM "GisUat" WHERE geom IS NOT NULL;
-- Keep the legacy gis_uats view for QGIS compatibility
CREATE OR REPLACE VIEW gis_uats AS
SELECT siruta, name, county,
ST_SimplifyPreserveTopology(geom, 50) AS geom
FROM "GisUat" WHERE geom IS NOT NULL;
-- ============================================================================= -- =============================================================================
-- Done! Martin serves these views as vector tiles: -- Done! Martin serves these views as vector tiles:
-- - gis_uats (moderate detail, z9+) -- - gis_uats_z0 (z0-5, 2000m simplification)
-- - gis_uats_simple (coarse overview, z5-z9) -- - gis_uats_z5 (z5-8, 500m)
-- QGIS: PostgreSQL -> 10.10.10.166:5432 / architools_db -> gis_uats view -- - gis_uats_z8 (z8-12, 50m)
-- - gis_uats_z12 (z12+, 10m near-original)
-- - gis_uats (legacy for QGIS, 50m)
-- Original geometry in GisUat.geom is NEVER modified.
-- SRID: 3844 (Stereo70) -- SRID: 3844 (Stereo70)
-- ============================================================================= -- =============================================================================
@@ -42,6 +42,13 @@ const LAYER_GROUPS: LayerGroupDef[] = [
color: "#3b82f6", color: "#3b82f6",
defaultVisible: true, defaultVisible: true,
}, },
{
id: "administrativ",
label: "Intravilan",
description: "Limite intravilan, arii speciale (zoom >= 10)",
color: "#ea580c",
defaultVisible: false,
},
]; ];
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
+129 -101
View File
@@ -34,19 +34,28 @@ const DEFAULT_ZOOM = 7;
/** Source/layer IDs used on the map */ /** Source/layer IDs used on the map */
const SOURCES = { const SOURCES = {
uatsSimple: "gis_uats_simple", uatsZ0: "gis_uats_z0", // z0-5: 2000m simplification
uats: "gis_uats", uatsZ5: "gis_uats_z5", // z5-8: 500m
uatsZ8: "gis_uats_z8", // z8-12: 50m
uatsZ12: "gis_uats_z12", // z12+: 10m (near-original)
terenuri: "gis_terenuri", terenuri: "gis_terenuri",
cladiri: "gis_cladiri", cladiri: "gis_cladiri",
administrativ: "gis_administrativ",
} as const; } as const;
/** Map layer IDs (prefixed to avoid collisions) */ /** Map layer IDs (prefixed to avoid collisions) */
const LAYER_IDS = { const LAYER_IDS = {
uatsSimpleFill: "layer-uats-simple-fill", uatsZ0Line: "layer-uats-z0-line",
uatsSimpleLine: "layer-uats-simple-line", uatsZ5Fill: "layer-uats-z5-fill",
uatsFill: "layer-uats-fill", uatsZ5Line: "layer-uats-z5-line",
uatsLine: "layer-uats-line", uatsZ8Fill: "layer-uats-z8-fill",
uatsLabel: "layer-uats-label", uatsZ8Line: "layer-uats-z8-line",
uatsZ8Label: "layer-uats-z8-label",
uatsZ12Fill: "layer-uats-z12-fill",
uatsZ12Line: "layer-uats-z12-line",
uatsZ12Label: "layer-uats-z12-label",
adminFill: "layer-admin-fill",
adminLine: "layer-admin-line",
terenuriFill: "layer-terenuri-fill", terenuriFill: "layer-terenuri-fill",
terenuriLine: "layer-terenuri-line", terenuriLine: "layer-terenuri-line",
cladiriFill: "layer-cladiri-fill", cladiriFill: "layer-cladiri-fill",
@@ -235,7 +244,13 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
if (!map || !map.isStyleLoaded()) return; if (!map || !map.isStyleLoaded()) return;
const mapping: Record<string, string[]> = { const mapping: Record<string, string[]> = {
uats: [LAYER_IDS.uatsSimpleFill, LAYER_IDS.uatsSimpleLine, LAYER_IDS.uatsFill, LAYER_IDS.uatsLine, LAYER_IDS.uatsLabel], uats: [
LAYER_IDS.uatsZ0Line,
LAYER_IDS.uatsZ5Fill, LAYER_IDS.uatsZ5Line,
LAYER_IDS.uatsZ8Fill, LAYER_IDS.uatsZ8Line, LAYER_IDS.uatsZ8Label,
LAYER_IDS.uatsZ12Fill, LAYER_IDS.uatsZ12Line, LAYER_IDS.uatsZ12Label,
],
administrativ: [LAYER_IDS.adminFill, LAYER_IDS.adminLine],
terenuri: [LAYER_IDS.terenuriFill, LAYER_IDS.terenuriLine], terenuri: [LAYER_IDS.terenuriFill, LAYER_IDS.terenuriLine],
cladiri: [LAYER_IDS.cladiriFill, LAYER_IDS.cladiriLine], cladiri: [LAYER_IDS.cladiriFill, LAYER_IDS.cladiriLine],
}; };
@@ -263,13 +278,18 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
useEffect(() => { useEffect(() => {
if (!containerRef.current) return; if (!containerRef.current) return;
// Preserve current view when switching basemaps
const prevMap = mapRef.current;
const currentCenter = prevMap ? prevMap.getCenter().toArray() as [number, number] : (center ?? DEFAULT_CENTER);
const currentZoom = prevMap ? prevMap.getZoom() : (zoom ?? DEFAULT_ZOOM);
const basemapDef = BASEMAPS[basemap]; const basemapDef = BASEMAPS[basemap];
const map = new maplibregl.Map({ const map = new maplibregl.Map({
container: containerRef.current, container: containerRef.current,
style: buildStyle(basemapDef), style: buildStyle(basemapDef),
center: center ?? DEFAULT_CENTER, center: currentCenter,
zoom: zoom ?? DEFAULT_ZOOM, zoom: currentZoom,
maxZoom: basemapDef.maxzoom ?? 20, maxZoom: basemapDef.maxzoom ?? 20,
}); });
@@ -281,111 +301,118 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
/* ---- Add Martin sources + layers on load ---- */ /* ---- Add Martin sources + layers on load ---- */
map.on("load", () => { map.on("load", () => {
// --- UAT boundaries (simplified, low zoom z0-z9) --- const m = resolvedMartinUrl;
map.addSource(SOURCES.uatsSimple, {
// === UAT z0-5: very coarse (2000m) — lines only ===
map.addSource(SOURCES.uatsZ0, {
type: "vector", type: "vector",
tiles: [`${resolvedMartinUrl}/${SOURCES.uatsSimple}/{z}/{x}/{y}`], tiles: [`${m}/${SOURCES.uatsZ0}/{z}/{x}/{y}`],
minzoom: 0, minzoom: 0, maxzoom: 5,
maxzoom: 9,
}); });
map.addLayer({ map.addLayer({
id: LAYER_IDS.uatsSimpleFill, id: LAYER_IDS.uatsZ0Line, type: "line",
type: "fill", source: SOURCES.uatsZ0, "source-layer": SOURCES.uatsZ0,
source: SOURCES.uatsSimple, maxzoom: 5,
"source-layer": SOURCES.uatsSimple, paint: { "line-color": "#7c3aed", "line-width": 0.3 },
maxzoom: 9,
paint: {
"fill-color": "#8b5cf6",
"fill-opacity": [
"interpolate", ["linear"], ["zoom"],
5, 0.03,
8, 0.05,
],
},
}); });
map.addLayer({ // === UAT z5-8: coarse (500m) — lines + faint fill ===
id: LAYER_IDS.uatsSimpleLine, map.addSource(SOURCES.uatsZ5, {
type: "line",
source: SOURCES.uatsSimple,
"source-layer": SOURCES.uatsSimple,
maxzoom: 9,
paint: {
"line-color": "#7c3aed",
"line-width": [
"interpolate", ["linear"], ["zoom"],
5, 0.5,
8, 1,
],
},
});
// --- UAT boundaries (detailed, high zoom z9+) ---
map.addSource(SOURCES.uats, {
type: "vector", type: "vector",
tiles: [`${resolvedMartinUrl}/${SOURCES.uats}/{z}/{x}/{y}`], tiles: [`${m}/${SOURCES.uatsZ5}/{z}/{x}/{y}`],
minzoom: 9, minzoom: 5, maxzoom: 8,
maxzoom: 16, });
map.addLayer({
id: LAYER_IDS.uatsZ5Fill, type: "fill",
source: SOURCES.uatsZ5, "source-layer": SOURCES.uatsZ5,
minzoom: 5, maxzoom: 8,
paint: { "fill-color": "#8b5cf6", "fill-opacity": 0.03 },
});
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 },
}); });
map.addLayer({ // === UAT z8-12: moderate (50m) — lines + fill + labels ===
id: LAYER_IDS.uatsFill, map.addSource(SOURCES.uatsZ8, {
type: "fill", type: "vector",
source: SOURCES.uats, tiles: [`${m}/${SOURCES.uatsZ8}/{z}/{x}/{y}`],
"source-layer": SOURCES.uats, minzoom: 8, maxzoom: 12,
minzoom: 8,
paint: {
"fill-color": "#8b5cf6",
"fill-opacity": [
"interpolate", ["linear"], ["zoom"],
8, 0.03,
12, 0.08,
],
},
}); });
map.addLayer({ map.addLayer({
id: LAYER_IDS.uatsLine, id: LAYER_IDS.uatsZ8Fill, type: "fill",
type: "line", source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8,
source: SOURCES.uats, minzoom: 8, maxzoom: 12,
"source-layer": SOURCES.uats, paint: { "fill-color": "#8b5cf6", "fill-opacity": 0.05 },
minzoom: 9,
paint: {
"line-color": "#7c3aed",
"line-width": [
"interpolate", ["linear"], ["zoom"],
5, 0.5,
8, 1,
12, 2,
],
},
}); });
map.addLayer({ map.addLayer({
id: LAYER_IDS.uatsLabel, id: LAYER_IDS.uatsZ8Line, type: "line",
type: "symbol", source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8,
source: SOURCES.uats, minzoom: 8, maxzoom: 12,
"source-layer": SOURCES.uats, paint: { "line-color": "#7c3aed", "line-width": 1 },
minzoom: 9, });
map.addLayer({
id: LAYER_IDS.uatsZ8Label, type: "symbol",
source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8,
minzoom: 9, maxzoom: 12,
layout: { layout: {
"text-field": ["coalesce", ["get", "name"], ["get", "uat_name"], ""], "text-field": ["coalesce", ["get", "name"], ""],
"text-size": [ "text-size": 10, "text-anchor": "center", "text-allow-overlap": false,
"interpolate", ["linear"], ["zoom"],
9, 10,
14, 14,
],
"text-anchor": "center",
"text-allow-overlap": false,
},
paint: {
"text-color": "#5b21b6",
"text-halo-color": "#ffffff",
"text-halo-width": 1.5,
}, },
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 },
}); });
// --- Terenuri (parcels) --- // === UAT z12+: fine (10m) — full detail ===
map.addSource(SOURCES.uatsZ12, {
type: "vector",
tiles: [`${m}/${SOURCES.uatsZ12}/{z}/{x}/{y}`],
minzoom: 12, maxzoom: 16,
});
map.addLayer({
id: LAYER_IDS.uatsZ12Fill, type: "fill",
source: SOURCES.uatsZ12, "source-layer": SOURCES.uatsZ12,
minzoom: 12,
paint: { "fill-color": "#8b5cf6", "fill-opacity": 0.08 },
});
map.addLayer({
id: LAYER_IDS.uatsZ12Line, type: "line",
source: SOURCES.uatsZ12, "source-layer": SOURCES.uatsZ12,
minzoom: 12,
paint: { "line-color": "#7c3aed", "line-width": 2 },
});
map.addLayer({
id: LAYER_IDS.uatsZ12Label, type: "symbol",
source: SOURCES.uatsZ12, "source-layer": SOURCES.uatsZ12,
minzoom: 12,
layout: {
"text-field": ["coalesce", ["get", "name"], ""],
"text-size": 13, "text-anchor": "center", "text-allow-overlap": false,
},
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 },
});
// === Administrativ (intravilan, arii speciale) ===
map.addSource(SOURCES.administrativ, {
type: "vector",
tiles: [`${m}/${SOURCES.administrativ}/{z}/{x}/{y}`],
minzoom: 10, maxzoom: 16,
});
map.addLayer({
id: LAYER_IDS.adminFill, type: "fill",
source: SOURCES.administrativ, "source-layer": SOURCES.administrativ,
minzoom: 11,
paint: { "fill-color": "#f97316", "fill-opacity": 0.06 },
});
map.addLayer({
id: LAYER_IDS.adminLine, type: "line",
source: SOURCES.administrativ, "source-layer": SOURCES.administrativ,
minzoom: 10,
paint: { "line-color": "#ea580c", "line-width": 1.2, "line-dasharray": [4, 2] },
});
// --- Terenuri (parcels) — NO simplification ---
map.addSource(SOURCES.terenuri, { map.addSource(SOURCES.terenuri, {
type: "vector", type: "vector",
tiles: [`${resolvedMartinUrl}/${SOURCES.terenuri}/{z}/{x}/{y}`], tiles: [`${resolvedMartinUrl}/${SOURCES.terenuri}/{z}/{x}/{y}`],
@@ -489,8 +516,9 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
const clickableLayers = [ const clickableLayers = [
LAYER_IDS.terenuriFill, LAYER_IDS.terenuriFill,
LAYER_IDS.cladiriFill, LAYER_IDS.cladiriFill,
LAYER_IDS.uatsFill, LAYER_IDS.uatsZ5Fill,
LAYER_IDS.uatsSimpleFill, LAYER_IDS.uatsZ8Fill,
LAYER_IDS.uatsZ12Fill,
]; ];
map.on("click", (e) => { map.on("click", (e) => {