fix(geoportal): simplified info panel, preserve basemap zoom, DXF export, intravilan outline
- Feature panel: simplified (NR_CAD/NR_CF/SIRUTA/Suprafata/Proprietari), aligned top-right under basemap switcher, click empty space to close - Basemap switch: preserves zoom+center via viewStateRef + moveend listener - DXF export: use -s_srs + -t_srs (not -a_srs + -t_srs which ogr2ogr rejects) - Intravilan: double line (black outer + orange inner), z13+, no fill - Parcel labels: cadastral_ref shown at z16+ - UAT z12: original geometry (no simplification) - Removed MapLibre popup (only side panel) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,7 @@ import { useRef, useEffect, useState, useCallback, useImperativeHandle, forwardR
|
||||
import maplibregl from "maplibre-gl";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
|
||||
/* Ensure MapLibre CSS is loaded — static import fails with next/dynamic + standalone.
|
||||
Load from /public to avoid CDN/CSP issues. */
|
||||
/* Ensure MapLibre CSS is loaded — static import fails with next/dynamic + standalone */
|
||||
if (typeof document !== "undefined") {
|
||||
const LINK_ID = "maplibre-gl-css";
|
||||
if (!document.getElementById(LINK_ID)) {
|
||||
@@ -22,68 +21,53 @@ import type { BasemapId, ClickedFeature, LayerVisibility, SelectedFeature } from
|
||||
/* Constants */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Martin tile URL — relative /tiles is proxied by Next.js rewrite (dev)
|
||||
* or Traefik (production). Falls back to env var if set.
|
||||
*/
|
||||
const DEFAULT_MARTIN_URL = process.env.NEXT_PUBLIC_MARTIN_URL || "/tiles";
|
||||
|
||||
/** Default center: Romania roughly centered */
|
||||
const DEFAULT_CENTER: [number, number] = [23.8, 46.1];
|
||||
const DEFAULT_ZOOM = 7;
|
||||
|
||||
/** Source/layer IDs used on the map */
|
||||
const SOURCES = {
|
||||
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)
|
||||
uatsZ0: "gis_uats_z0",
|
||||
uatsZ5: "gis_uats_z5",
|
||||
uatsZ8: "gis_uats_z8",
|
||||
uatsZ12: "gis_uats_z12",
|
||||
terenuri: "gis_terenuri",
|
||||
cladiri: "gis_cladiri",
|
||||
administrativ: "gis_administrativ",
|
||||
} as const;
|
||||
|
||||
/** Map layer IDs (prefixed to avoid collisions) */
|
||||
const LAYER_IDS = {
|
||||
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",
|
||||
cladiriLine: "layer-cladiri-line",
|
||||
selectionFill: "layer-selection-fill",
|
||||
selectionLine: "layer-selection-line",
|
||||
uatsZ0Line: "l-uats-z0-line",
|
||||
uatsZ5Fill: "l-uats-z5-fill",
|
||||
uatsZ5Line: "l-uats-z5-line",
|
||||
uatsZ8Fill: "l-uats-z8-fill",
|
||||
uatsZ8Line: "l-uats-z8-line",
|
||||
uatsZ8Label: "l-uats-z8-label",
|
||||
uatsZ12Fill: "l-uats-z12-fill",
|
||||
uatsZ12Line: "l-uats-z12-line",
|
||||
uatsZ12Label: "l-uats-z12-label",
|
||||
adminLineOuter: "l-admin-line-outer",
|
||||
adminLineInner: "l-admin-line-inner",
|
||||
terenuriFill: "l-terenuri-fill",
|
||||
terenuriLine: "l-terenuri-line",
|
||||
terenuriLabel: "l-terenuri-label",
|
||||
cladiriFill: "l-cladiri-fill",
|
||||
cladiriLine: "l-cladiri-line",
|
||||
selectionFill: "l-selection-fill",
|
||||
selectionLine: "l-selection-line",
|
||||
} as const;
|
||||
|
||||
/** Basemap definitions — vector style URL or inline raster config */
|
||||
/* ---- Basemap definitions ---- */
|
||||
type BasemapDef =
|
||||
| { type: "style"; url: string; maxzoom?: number }
|
||||
| { type: "raster"; tiles: string[]; attribution: string; tileSize: number; maxzoom?: number };
|
||||
|
||||
const BASEMAPS: Record<BasemapId, BasemapDef> = {
|
||||
liberty: {
|
||||
type: "style",
|
||||
url: "https://tiles.openfreemap.org/styles/liberty",
|
||||
},
|
||||
dark: {
|
||||
type: "style",
|
||||
url: "https://tiles.openfreemap.org/styles/dark",
|
||||
},
|
||||
liberty: { type: "style", url: "https://tiles.openfreemap.org/styles/liberty" },
|
||||
dark: { type: "style", url: "https://tiles.openfreemap.org/styles/dark" },
|
||||
satellite: {
|
||||
type: "raster",
|
||||
tiles: [
|
||||
"https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
|
||||
],
|
||||
attribution: '© <a href="https://www.esri.com">Esri</a>, Maxar, Earthstar Geographics',
|
||||
tiles: ["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"],
|
||||
attribution: '© <a href="https://www.esri.com">Esri</a>, Maxar',
|
||||
tileSize: 256,
|
||||
},
|
||||
orto: {
|
||||
@@ -100,27 +84,14 @@ function buildStyle(def: BasemapDef): string | maplibregl.StyleSpecification {
|
||||
return {
|
||||
version: 8 as const,
|
||||
sources: {
|
||||
basemap: {
|
||||
type: "raster" as const,
|
||||
tiles: def.tiles,
|
||||
tileSize: def.tileSize,
|
||||
attribution: def.attribution,
|
||||
},
|
||||
basemap: { type: "raster" as const, tiles: def.tiles, tileSize: def.tileSize, attribution: def.attribution },
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: "basemap-tiles",
|
||||
type: "raster" as const,
|
||||
source: "basemap",
|
||||
minzoom: 0,
|
||||
maxzoom: def.maxzoom ?? 19,
|
||||
},
|
||||
],
|
||||
layers: [{ id: "basemap-tiles", type: "raster" as const, source: "basemap", minzoom: 0, maxzoom: def.maxzoom ?? 19 }],
|
||||
};
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Props */
|
||||
/* Props & Handle */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
export type MapViewerHandle = {
|
||||
@@ -139,27 +110,9 @@ type MapViewerProps = {
|
||||
selectionMode?: boolean;
|
||||
onFeatureClick?: (feature: ClickedFeature) => void;
|
||||
onSelectionChange?: (features: SelectedFeature[]) => void;
|
||||
/** External layer visibility control */
|
||||
layerVisibility?: LayerVisibility;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Helpers */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
function formatPopupContent(properties: Record<string, unknown>): string {
|
||||
const rows: string[] = [];
|
||||
for (const [key, value] of Object.entries(properties)) {
|
||||
if (value == null || value === "") continue;
|
||||
const displayKey = key.replace(/_/g, " ");
|
||||
rows.push(
|
||||
`<tr><td style="font-weight:600;padding:2px 8px 2px 0;vertical-align:top;white-space:nowrap;color:#64748b">${displayKey}</td><td style="padding:2px 0">${String(value)}</td></tr>`
|
||||
);
|
||||
}
|
||||
if (rows.length === 0) return "<p style='color:#94a3b8'>Fara atribute</p>";
|
||||
return `<table style="font-size:13px;line-height:1.4">${rows.join("")}</table>`;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Component */
|
||||
/* ------------------------------------------------------------------ */
|
||||
@@ -167,29 +120,24 @@ function formatPopupContent(properties: Record<string, unknown>): string {
|
||||
export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
function MapViewer(
|
||||
{
|
||||
center,
|
||||
zoom,
|
||||
martinUrl,
|
||||
className,
|
||||
center, zoom, martinUrl, className,
|
||||
basemap = "liberty",
|
||||
selectionMode = false,
|
||||
onFeatureClick,
|
||||
onSelectionChange,
|
||||
layerVisibility,
|
||||
onFeatureClick, onSelectionChange, layerVisibility,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<maplibregl.Map | null>(null);
|
||||
const popupRef = useRef<maplibregl.Popup | null>(null);
|
||||
const selectedRef = useRef<Map<string, SelectedFeature>>(new Map());
|
||||
const selectionModeRef = useRef(selectionMode);
|
||||
const [mapReady, setMapReady] = useState(false);
|
||||
|
||||
// Keep ref in sync
|
||||
// Persist view state across basemap switches
|
||||
const viewStateRef = useRef({ center: center ?? DEFAULT_CENTER, zoom: zoom ?? DEFAULT_ZOOM });
|
||||
|
||||
selectionModeRef.current = selectionMode;
|
||||
|
||||
// MapLibre web workers can't resolve relative URLs — need absolute
|
||||
const resolvedMartinUrl = (() => {
|
||||
const raw = martinUrl ?? DEFAULT_MARTIN_URL;
|
||||
if (raw.startsWith("http")) return raw;
|
||||
@@ -202,22 +150,14 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
const map = mapRef.current;
|
||||
if (!map) return;
|
||||
const ids = Array.from(selectedRef.current.keys());
|
||||
// Use objectId matching for the selection highlight layer
|
||||
const filter: maplibregl.FilterSpecification =
|
||||
ids.length > 0
|
||||
? ["in", ["to-string", ["get", "object_id"]], ["literal", ids]]
|
||||
: ["==", "object_id", "__NONE__"];
|
||||
|
||||
try {
|
||||
if (map.getLayer(LAYER_IDS.selectionFill)) {
|
||||
map.setFilter(LAYER_IDS.selectionFill, filter);
|
||||
}
|
||||
if (map.getLayer(LAYER_IDS.selectionLine)) {
|
||||
map.setFilter(LAYER_IDS.selectionLine, filter);
|
||||
}
|
||||
} catch {
|
||||
// layers might not exist yet
|
||||
}
|
||||
if (map.getLayer(LAYER_IDS.selectionFill)) map.setFilter(LAYER_IDS.selectionFill, filter);
|
||||
if (map.getLayer(LAYER_IDS.selectionLine)) map.setFilter(LAYER_IDS.selectionLine, filter);
|
||||
} catch { /* noop */ }
|
||||
}, []);
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
@@ -229,9 +169,7 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
/* ---- Imperative handle ---- */
|
||||
useImperativeHandle(ref, () => ({
|
||||
getMap: () => mapRef.current,
|
||||
setLayerVisibility: (vis: LayerVisibility) => {
|
||||
applyLayerVisibility(vis);
|
||||
},
|
||||
setLayerVisibility: (vis: LayerVisibility) => applyLayerVisibility(vis),
|
||||
flyTo: (c: [number, number], z?: number) => {
|
||||
mapRef.current?.flyTo({ center: c, zoom: z ?? 14, duration: 1500 });
|
||||
},
|
||||
@@ -242,7 +180,6 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
const applyLayerVisibility = useCallback((vis: LayerVisibility) => {
|
||||
const map = mapRef.current;
|
||||
if (!map || !map.isStyleLoaded()) return;
|
||||
|
||||
const mapping: Record<string, string[]> = {
|
||||
uats: [
|
||||
LAYER_IDS.uatsZ0Line,
|
||||
@@ -250,52 +187,45 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
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],
|
||||
administrativ: [LAYER_IDS.adminLineOuter, LAYER_IDS.adminLineInner],
|
||||
terenuri: [LAYER_IDS.terenuriFill, LAYER_IDS.terenuriLine, LAYER_IDS.terenuriLabel],
|
||||
cladiri: [LAYER_IDS.cladiriFill, LAYER_IDS.cladiriLine],
|
||||
};
|
||||
|
||||
for (const [group, layerIds] of Object.entries(mapping)) {
|
||||
const visible = vis[group] !== false; // default visible
|
||||
const visible = vis[group] !== false;
|
||||
for (const lid of layerIds) {
|
||||
try {
|
||||
map.setLayoutProperty(lid, "visibility", visible ? "visible" : "none");
|
||||
} catch {
|
||||
// layer might not exist yet
|
||||
}
|
||||
try { map.setLayoutProperty(lid, "visibility", visible ? "visible" : "none"); } catch { /* noop */ }
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
/* ---- Sync external visibility prop ---- */
|
||||
useEffect(() => {
|
||||
if (mapReady && layerVisibility) {
|
||||
applyLayerVisibility(layerVisibility);
|
||||
}
|
||||
if (mapReady && layerVisibility) applyLayerVisibility(layerVisibility);
|
||||
}, [mapReady, layerVisibility, applyLayerVisibility]);
|
||||
|
||||
/* ---- Map initialization (recreates on basemap change) ---- */
|
||||
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: currentCenter,
|
||||
zoom: currentZoom,
|
||||
center: viewStateRef.current.center,
|
||||
zoom: viewStateRef.current.zoom,
|
||||
maxZoom: basemapDef.maxzoom ?? 20,
|
||||
});
|
||||
|
||||
mapRef.current = map;
|
||||
|
||||
/* ---- Controls ---- */
|
||||
// Save view state on every move (for basemap switch preservation)
|
||||
map.on("moveend", () => {
|
||||
viewStateRef.current = {
|
||||
center: map.getCenter().toArray() as [number, number],
|
||||
zoom: map.getZoom(),
|
||||
};
|
||||
});
|
||||
|
||||
map.addControl(new maplibregl.NavigationControl(), "top-right");
|
||||
map.addControl(new maplibregl.ScaleControl({ unit: "metric" }), "bottom-left");
|
||||
|
||||
@@ -303,307 +233,132 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
map.on("load", () => {
|
||||
const m = resolvedMartinUrl;
|
||||
|
||||
// === UAT z0-5: very coarse (2000m) — lines only ===
|
||||
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,
|
||||
paint: { "line-color": "#7c3aed", "line-width": 0.3 },
|
||||
});
|
||||
// === UAT z0-5: very coarse — lines only ===
|
||||
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,
|
||||
paint: { "line-color": "#7c3aed", "line-width": 0.3 } });
|
||||
|
||||
// === UAT z5-8: coarse (500m) — lines + faint fill ===
|
||||
map.addSource(SOURCES.uatsZ5, {
|
||||
type: "vector",
|
||||
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 },
|
||||
});
|
||||
// === UAT z5-8: coarse ===
|
||||
map.addSource(SOURCES.uatsZ5, { type: "vector", 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 } });
|
||||
|
||||
// === 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.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.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,
|
||||
// === UAT z8-12: moderate ===
|
||||
map.addSource(SOURCES.uatsZ8, { type: "vector", tiles: [`${m}/${SOURCES.uatsZ8}/{z}/{x}/{y}`], minzoom: 8, maxzoom: 12 });
|
||||
map.addLayer({ 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.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"], ""], "text-size": 10, "text-anchor": "center", "text-allow-overlap": false },
|
||||
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 } });
|
||||
|
||||
// === UAT z12+: full detail (no simplification) ===
|
||||
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 } });
|
||||
|
||||
// === 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.addLayer({ id: LAYER_IDS.adminLineOuter, type: "line", source: SOURCES.administrativ, "source-layer": SOURCES.administrativ, minzoom: 13,
|
||||
paint: { "line-color": "#000000", "line-width": 3 } });
|
||||
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 } });
|
||||
|
||||
// === Terenuri (parcels) — no simplification ===
|
||||
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,
|
||||
paint: { "fill-color": "#22c55e", "fill-opacity": 0.15 } });
|
||||
map.addLayer({ id: LAYER_IDS.terenuriLine, type: "line", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13,
|
||||
paint: { "line-color": "#15803d", "line-width": 0.8 } });
|
||||
// Parcel cadastral number label
|
||||
map.addLayer({ id: LAYER_IDS.terenuriLabel, type: "symbol", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 16,
|
||||
layout: {
|
||||
"text-field": ["coalesce", ["get", "name"], ""],
|
||||
"text-field": ["coalesce", ["get", "cadastral_ref"], ""],
|
||||
"text-size": 10, "text-anchor": "center", "text-allow-overlap": false,
|
||||
"text-max-width": 8,
|
||||
},
|
||||
paint: { "text-color": "#5b21b6", "text-halo-color": "#fff", "text-halo-width": 1.5 },
|
||||
});
|
||||
paint: { "text-color": "#166534", "text-halo-color": "#fff", "text-halo-width": 1 } });
|
||||
|
||||
// === 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 },
|
||||
});
|
||||
// === Cladiri (buildings) — no simplification ===
|
||||
map.addSource(SOURCES.cladiri, { type: "vector", tiles: [`${m}/${SOURCES.cladiri}/{z}/{x}/{y}`], minzoom: 12, maxzoom: 18 });
|
||||
map.addLayer({ id: LAYER_IDS.cladiriFill, type: "fill", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: 14,
|
||||
paint: { "fill-color": "#3b82f6", "fill-opacity": 0.5 } });
|
||||
map.addLayer({ id: LAYER_IDS.cladiriLine, type: "line", source: SOURCES.cladiri, "source-layer": SOURCES.cladiri, minzoom: 14,
|
||||
paint: { "line-color": "#1e3a5f", "line-width": 0.6 } });
|
||||
|
||||
// === 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}`],
|
||||
minzoom: 10,
|
||||
maxzoom: 18,
|
||||
});
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: LAYER_IDS.terenuriLine,
|
||||
type: "line",
|
||||
source: SOURCES.terenuri,
|
||||
"source-layer": SOURCES.terenuri,
|
||||
minzoom: 13,
|
||||
paint: {
|
||||
"line-color": "#15803d",
|
||||
"line-width": 0.8,
|
||||
},
|
||||
});
|
||||
|
||||
// --- Cladiri (buildings) ---
|
||||
map.addSource(SOURCES.cladiri, {
|
||||
type: "vector",
|
||||
tiles: [`${resolvedMartinUrl}/${SOURCES.cladiri}/{z}/{x}/{y}`],
|
||||
minzoom: 12,
|
||||
maxzoom: 18,
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: LAYER_IDS.cladiriFill,
|
||||
type: "fill",
|
||||
source: SOURCES.cladiri,
|
||||
"source-layer": SOURCES.cladiri,
|
||||
minzoom: 14,
|
||||
paint: {
|
||||
"fill-color": "#3b82f6",
|
||||
"fill-opacity": 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: LAYER_IDS.cladiriLine,
|
||||
type: "line",
|
||||
source: SOURCES.cladiri,
|
||||
"source-layer": SOURCES.cladiri,
|
||||
minzoom: 14,
|
||||
paint: {
|
||||
"line-color": "#1e3a5f",
|
||||
"line-width": 0.6,
|
||||
},
|
||||
});
|
||||
|
||||
// --- Selection highlight layer (uses same sources) ---
|
||||
// We add a highlight layer on top for terenuri (primary selection target)
|
||||
map.addLayer({
|
||||
id: LAYER_IDS.selectionFill,
|
||||
type: "fill",
|
||||
source: SOURCES.terenuri,
|
||||
"source-layer": SOURCES.terenuri,
|
||||
minzoom: 13,
|
||||
filter: ["==", "object_id", "__NONE__"], // empty initially
|
||||
paint: {
|
||||
"fill-color": "#f59e0b",
|
||||
"fill-opacity": 0.5,
|
||||
},
|
||||
});
|
||||
|
||||
map.addLayer({
|
||||
id: LAYER_IDS.selectionLine,
|
||||
type: "line",
|
||||
source: SOURCES.terenuri,
|
||||
"source-layer": SOURCES.terenuri,
|
||||
minzoom: 13,
|
||||
// === Selection highlight ===
|
||||
map.addLayer({ id: LAYER_IDS.selectionFill, type: "fill", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13,
|
||||
filter: ["==", "object_id", "__NONE__"],
|
||||
paint: {
|
||||
"line-color": "#d97706",
|
||||
"line-width": 2.5,
|
||||
},
|
||||
});
|
||||
|
||||
// Apply initial visibility if provided
|
||||
if (layerVisibility) {
|
||||
applyLayerVisibility(layerVisibility);
|
||||
}
|
||||
paint: { "fill-color": "#f59e0b", "fill-opacity": 0.5 } });
|
||||
map.addLayer({ id: LAYER_IDS.selectionLine, type: "line", source: SOURCES.terenuri, "source-layer": SOURCES.terenuri, minzoom: 13,
|
||||
filter: ["==", "object_id", "__NONE__"],
|
||||
paint: { "line-color": "#d97706", "line-width": 2.5 } });
|
||||
|
||||
if (layerVisibility) applyLayerVisibility(layerVisibility);
|
||||
setMapReady(true);
|
||||
});
|
||||
|
||||
/* ---- Click handler ---- */
|
||||
/* ---- Click handler — NO popup, only callback ---- */
|
||||
const clickableLayers = [
|
||||
LAYER_IDS.terenuriFill,
|
||||
LAYER_IDS.cladiriFill,
|
||||
LAYER_IDS.uatsZ5Fill,
|
||||
LAYER_IDS.uatsZ8Fill,
|
||||
LAYER_IDS.uatsZ12Fill,
|
||||
LAYER_IDS.terenuriFill, LAYER_IDS.cladiriFill,
|
||||
LAYER_IDS.uatsZ5Fill, LAYER_IDS.uatsZ8Fill, LAYER_IDS.uatsZ12Fill,
|
||||
];
|
||||
|
||||
map.on("click", (e) => {
|
||||
const features = map.queryRenderedFeatures(e.point, {
|
||||
layers: clickableLayers.filter((l) => {
|
||||
try {
|
||||
return !!map.getLayer(l);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}),
|
||||
layers: clickableLayers.filter((l) => { try { return !!map.getLayer(l); } catch { return false; } }),
|
||||
});
|
||||
|
||||
// Close existing popup
|
||||
if (popupRef.current) {
|
||||
popupRef.current.remove();
|
||||
popupRef.current = null;
|
||||
if (features.length === 0) {
|
||||
onFeatureClick?.(null as unknown as ClickedFeature); // close panel
|
||||
return;
|
||||
}
|
||||
|
||||
if (features.length === 0) return;
|
||||
|
||||
const first = features[0];
|
||||
if (!first) return;
|
||||
|
||||
const props = (first.properties ?? {}) as Record<string, unknown>;
|
||||
const sourceLayer = first.sourceLayer ?? first.source ?? "";
|
||||
|
||||
// Selection mode: toggle feature in selection
|
||||
// Selection mode
|
||||
if (selectionModeRef.current && sourceLayer === SOURCES.terenuri) {
|
||||
const objectId = String(props.object_id ?? props.objectId ?? "");
|
||||
const objectId = String(props.object_id ?? "");
|
||||
if (!objectId) return;
|
||||
|
||||
if (selectedRef.current.has(objectId)) {
|
||||
selectedRef.current.delete(objectId);
|
||||
} else {
|
||||
selectedRef.current.set(objectId, {
|
||||
id: objectId,
|
||||
sourceLayer,
|
||||
properties: props,
|
||||
});
|
||||
selectedRef.current.set(objectId, { id: objectId, sourceLayer, properties: props });
|
||||
}
|
||||
updateSelectionFilter();
|
||||
onSelectionChange?.(Array.from(selectedRef.current.values()));
|
||||
return;
|
||||
}
|
||||
|
||||
// Notify parent
|
||||
if (onFeatureClick) {
|
||||
onFeatureClick({
|
||||
layerId: first.layer?.id ?? "",
|
||||
sourceLayer,
|
||||
properties: props,
|
||||
coordinates: [e.lngLat.lng, e.lngLat.lat],
|
||||
});
|
||||
}
|
||||
|
||||
// Show popup (only in non-selection mode)
|
||||
const popup = new maplibregl.Popup({
|
||||
maxWidth: "360px",
|
||||
closeButton: true,
|
||||
closeOnClick: true,
|
||||
})
|
||||
.setLngLat(e.lngLat)
|
||||
.setHTML(formatPopupContent(props))
|
||||
.addTo(map);
|
||||
|
||||
popupRef.current = popup;
|
||||
// Feature click — notify parent (no popup)
|
||||
onFeatureClick?.({
|
||||
layerId: first.layer?.id ?? "",
|
||||
sourceLayer,
|
||||
properties: props,
|
||||
coordinates: [e.lngLat.lng, e.lngLat.lat],
|
||||
});
|
||||
});
|
||||
|
||||
/* ---- Cursor change on hover ---- */
|
||||
/* ---- Cursor change ---- */
|
||||
for (const lid of clickableLayers) {
|
||||
map.on("mouseenter", lid, () => {
|
||||
map.getCanvas().style.cursor = "pointer";
|
||||
});
|
||||
map.on("mouseleave", lid, () => {
|
||||
map.getCanvas().style.cursor = "";
|
||||
});
|
||||
map.on("mouseenter", lid, () => { map.getCanvas().style.cursor = "pointer"; });
|
||||
map.on("mouseleave", lid, () => { map.getCanvas().style.cursor = ""; });
|
||||
}
|
||||
|
||||
/* ---- Cleanup ---- */
|
||||
return () => {
|
||||
if (popupRef.current) {
|
||||
popupRef.current.remove();
|
||||
popupRef.current = null;
|
||||
}
|
||||
map.remove();
|
||||
mapRef.current = null;
|
||||
setMapReady(false);
|
||||
@@ -611,16 +366,10 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [resolvedMartinUrl, basemap]);
|
||||
|
||||
/* ---- Sync center/zoom prop changes ---- */
|
||||
/* ---- Sync center/zoom prop changes (from search flyTo) ---- */
|
||||
useEffect(() => {
|
||||
if (!mapReady || !mapRef.current) return;
|
||||
if (center) {
|
||||
mapRef.current.flyTo({
|
||||
center,
|
||||
zoom: zoom ?? mapRef.current.getZoom(),
|
||||
duration: 1500,
|
||||
});
|
||||
}
|
||||
if (!mapReady || !mapRef.current || !center) return;
|
||||
mapRef.current.flyTo({ center, zoom: zoom ?? mapRef.current.getZoom(), duration: 1500 });
|
||||
}, [center, zoom, mapReady]);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user