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
@@ -42,6 +42,13 @@ const LAYER_GROUPS: LayerGroupDef[] = [
color: "#3b82f6",
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 */
const SOURCES = {
uatsSimple: "gis_uats_simple",
uats: "gis_uats",
uatsZ0: "gis_uats_z0", // z0-5: 2000m simplification
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",
cladiri: "gis_cladiri",
administrativ: "gis_administrativ",
} as const;
/** Map layer IDs (prefixed to avoid collisions) */
const LAYER_IDS = {
uatsSimpleFill: "layer-uats-simple-fill",
uatsSimpleLine: "layer-uats-simple-line",
uatsFill: "layer-uats-fill",
uatsLine: "layer-uats-line",
uatsLabel: "layer-uats-label",
uatsZ0Line: "layer-uats-z0-line",
uatsZ5Fill: "layer-uats-z5-fill",
uatsZ5Line: "layer-uats-z5-line",
uatsZ8Fill: "layer-uats-z8-fill",
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",
terenuriLine: "layer-terenuri-line",
cladiriFill: "layer-cladiri-fill",
@@ -235,7 +244,13 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
if (!map || !map.isStyleLoaded()) return;
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],
cladiri: [LAYER_IDS.cladiriFill, LAYER_IDS.cladiriLine],
};
@@ -263,13 +278,18 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
useEffect(() => {
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 map = new maplibregl.Map({
container: containerRef.current,
style: buildStyle(basemapDef),
center: center ?? DEFAULT_CENTER,
zoom: zoom ?? DEFAULT_ZOOM,
center: currentCenter,
zoom: currentZoom,
maxZoom: basemapDef.maxzoom ?? 20,
});
@@ -281,111 +301,118 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
/* ---- Add Martin sources + layers on load ---- */
map.on("load", () => {
// --- UAT boundaries (simplified, low zoom z0-z9) ---
map.addSource(SOURCES.uatsSimple, {
const m = resolvedMartinUrl;
// === UAT z0-5: very coarse (2000m) — lines only ===
map.addSource(SOURCES.uatsZ0, {
type: "vector",
tiles: [`${resolvedMartinUrl}/${SOURCES.uatsSimple}/{z}/{x}/{y}`],
minzoom: 0,
maxzoom: 9,
tiles: [`${m}/${SOURCES.uatsZ0}/{z}/{x}/{y}`],
minzoom: 0, maxzoom: 5,
});
map.addLayer({
id: LAYER_IDS.uatsSimpleFill,
type: "fill",
source: SOURCES.uatsSimple,
"source-layer": SOURCES.uatsSimple,
maxzoom: 9,
paint: {
"fill-color": "#8b5cf6",
"fill-opacity": [
"interpolate", ["linear"], ["zoom"],
5, 0.03,
8, 0.05,
],
},
id: LAYER_IDS.uatsZ0Line, type: "line",
source: SOURCES.uatsZ0, "source-layer": SOURCES.uatsZ0,
maxzoom: 5,
paint: { "line-color": "#7c3aed", "line-width": 0.3 },
});
map.addLayer({
id: LAYER_IDS.uatsSimpleLine,
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, {
// === UAT z5-8: coarse (500m) — lines + faint fill ===
map.addSource(SOURCES.uatsZ5, {
type: "vector",
tiles: [`${resolvedMartinUrl}/${SOURCES.uats}/{z}/{x}/{y}`],
minzoom: 9,
maxzoom: 16,
tiles: [`${m}/${SOURCES.uatsZ5}/{z}/{x}/{y}`],
minzoom: 5, maxzoom: 8,
});
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({
id: LAYER_IDS.uatsFill,
type: "fill",
source: SOURCES.uats,
"source-layer": SOURCES.uats,
minzoom: 8,
paint: {
"fill-color": "#8b5cf6",
"fill-opacity": [
"interpolate", ["linear"], ["zoom"],
8, 0.03,
12, 0.08,
],
},
// === UAT z8-12: moderate (50m) — lines + fill + labels ===
map.addSource(SOURCES.uatsZ8, {
type: "vector",
tiles: [`${m}/${SOURCES.uatsZ8}/{z}/{x}/{y}`],
minzoom: 8, maxzoom: 12,
});
map.addLayer({
id: LAYER_IDS.uatsLine,
type: "line",
source: SOURCES.uats,
"source-layer": SOURCES.uats,
minzoom: 9,
paint: {
"line-color": "#7c3aed",
"line-width": [
"interpolate", ["linear"], ["zoom"],
5, 0.5,
8, 1,
12, 2,
],
},
id: LAYER_IDS.uatsZ8Fill, type: "fill",
source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8,
minzoom: 8, maxzoom: 12,
paint: { "fill-color": "#8b5cf6", "fill-opacity": 0.05 },
});
map.addLayer({
id: LAYER_IDS.uatsLabel,
type: "symbol",
source: SOURCES.uats,
"source-layer": SOURCES.uats,
minzoom: 9,
id: LAYER_IDS.uatsZ8Line, type: "line",
source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8,
minzoom: 8, maxzoom: 12,
paint: { "line-color": "#7c3aed", "line-width": 1 },
});
map.addLayer({
id: LAYER_IDS.uatsZ8Label, type: "symbol",
source: SOURCES.uatsZ8, "source-layer": SOURCES.uatsZ8,
minzoom: 9, maxzoom: 12,
layout: {
"text-field": ["coalesce", ["get", "name"], ["get", "uat_name"], ""],
"text-size": [
"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,
"text-field": ["coalesce", ["get", "name"], ""],
"text-size": 10, "text-anchor": "center", "text-allow-overlap": false,
},
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, {
type: "vector",
tiles: [`${resolvedMartinUrl}/${SOURCES.terenuri}/{z}/{x}/{y}`],
@@ -489,8 +516,9 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
const clickableLayers = [
LAYER_IDS.terenuriFill,
LAYER_IDS.cladiriFill,
LAYER_IDS.uatsFill,
LAYER_IDS.uatsSimpleFill,
LAYER_IDS.uatsZ5Fill,
LAYER_IDS.uatsZ8Fill,
LAYER_IDS.uatsZ12Fill,
];
map.on("click", (e) => {