"use client";
import { useState, useRef, useCallback, useEffect } from "react";
import dynamic from "next/dynamic";
import { Map as MapIcon, Loader2, AlertTriangle } from "lucide-react";
import { Card, CardContent } from "@/shared/components/ui/card";
import { Badge } from "@/shared/components/ui/badge";
import { BasemapSwitcher } from "@/modules/geoportal/components/basemap-switcher";
import {
SelectionToolbar,
type SelectionMode,
} from "@/modules/geoportal/components/selection-toolbar";
import { FeatureInfoPanel } from "@/modules/geoportal/components/feature-info-panel";
import type { MapViewerHandle } from "@/modules/geoportal/components/map-viewer";
import type {
BasemapId,
ClickedFeature,
LayerVisibility,
SelectedFeature,
} from "@/modules/geoportal/types";
/* MapLibre uses WebGL — must disable SSR */
const MapViewer = dynamic(
() =>
import("@/modules/geoportal/components/map-viewer").then((m) => ({
default: m.MapViewer,
})),
{
ssr: false,
loading: () => (
),
},
);
/* ------------------------------------------------------------------ */
/* Layer IDs — must match map-viewer.tsx LAYER_IDS */
/* ------------------------------------------------------------------ */
const BASE_LAYERS = [
"l-terenuri-fill",
"l-terenuri-line",
"l-terenuri-label",
"l-cladiri-fill",
"l-cladiri-line",
];
/* ------------------------------------------------------------------ */
/* Props */
/* ------------------------------------------------------------------ */
type MapTabProps = {
siruta: string;
sirutaValid: boolean;
};
/* ------------------------------------------------------------------ */
/* Typed map handle */
/* ------------------------------------------------------------------ */
type MapLike = {
getLayer(id: string): unknown;
getSource(id: string): unknown;
addSource(id: string, source: Record): void;
addLayer(layer: Record, before?: string): void;
setFilter(id: string, filter: unknown[] | null): void;
setLayoutProperty(id: string, prop: string, value: unknown): void;
setPaintProperty(id: string, prop: string, value: unknown): void;
fitBounds(
bounds: [number, number, number, number],
opts?: Record,
): void;
isStyleLoaded(): boolean;
};
function asMap(handle: MapViewerHandle | null): MapLike | null {
const m = handle?.getMap();
return m ? (m as unknown as MapLike) : null;
}
/* ------------------------------------------------------------------ */
/* Component */
/* ------------------------------------------------------------------ */
export function MapTab({ siruta, sirutaValid }: MapTabProps) {
const mapHandleRef = useRef(null);
const [basemap, setBasemap] = useState("liberty");
const [clickedFeature, setClickedFeature] =
useState(null);
const [selectionMode, setSelectionMode] = useState("off");
const [selectedFeatures, setSelectedFeatures] = useState(
[],
);
const [boundsLoading, setBoundsLoading] = useState(false);
const [mapReady, setMapReady] = useState(false);
const appliedSirutaRef = useRef("");
const boundsRef = useRef<[number, number, number, number] | null>(null);
/* Boundary check results */
const [mismatchSummary, setMismatchSummary] = useState<{
foreign: number;
edge: number;
} | null>(null);
/* Layer visibility: show terenuri + cladiri, hide admin */
const [layerVisibility] = useState({
terenuri: true,
cladiri: true,
administrativ: false,
});
/* ── Detect when map is ready ──────────────────────────────── */
useEffect(() => {
if (!sirutaValid) return;
const check = setInterval(() => {
const map = asMap(mapHandleRef.current);
if (map && map.isStyleLoaded()) {
setMapReady(true);
clearInterval(check);
}
}, 200);
return () => clearInterval(check);
}, [sirutaValid]);
/* ── Fetch UAT bounds ──────────────────────────────────────── */
const prevBoundsSirutaRef = useRef("");
useEffect(() => {
if (!sirutaValid || !siruta) return;
if (prevBoundsSirutaRef.current === siruta) return;
prevBoundsSirutaRef.current = siruta;
setBoundsLoading(true);
fetch(`/api/geoportal/uat-bounds?siruta=${siruta}`)
.then((r) => (r.ok ? r.json() : null))
.then(
(data: { bounds?: [[number, number], [number, number]] } | null) => {
if (data?.bounds) {
const [[minLng, minLat], [maxLng, maxLat]] = data.bounds;
boundsRef.current = [minLng, minLat, maxLng, maxLat];
const map = asMap(mapHandleRef.current);
if (map) {
map.fitBounds([minLng, minLat, maxLng, maxLat], {
padding: 40,
duration: 1500,
});
}
}
},
)
.catch(() => {})
.finally(() => setBoundsLoading(false));
}, [siruta, sirutaValid]);
/* ── When map becomes ready, fitBounds if we have bounds ───── */
useEffect(() => {
if (!mapReady || !boundsRef.current) return;
const map = asMap(mapHandleRef.current);
if (!map) return;
map.fitBounds(boundsRef.current, { padding: 40, duration: 1500 });
}, [mapReady]);
/* ── Apply siruta filter + enrichment overlay ──────────────── */
useEffect(() => {
if (!mapReady || !sirutaValid || !siruta) return;
if (appliedSirutaRef.current === siruta) return;
const map = asMap(mapHandleRef.current);
if (!map) return;
appliedSirutaRef.current = siruta;
const filter = ["==", ["get", "siruta"], siruta];
// Filter base layers by siruta
for (const layerId of BASE_LAYERS) {
try {
if (map.getLayer(layerId)) map.setFilter(layerId, filter);
} catch {
/* noop */
}
}
// Keep l-terenuri-fill VISIBLE but fully transparent (catches clicks!)
try {
if (map.getLayer("l-terenuri-fill"))
map.setPaintProperty("l-terenuri-fill", "fill-opacity", 0);
} catch {
/* noop */
}
const martinBase =
typeof window !== "undefined"
? `${window.location.origin}/tiles`
: "/tiles";
// ── Enrichment overlay for PARCELS ──
if (!map.getSource("gis_terenuri_status")) {
map.addSource("gis_terenuri_status", {
type: "vector",
tiles: [`${martinBase}/gis_terenuri_status/{z}/{x}/{y}`],
minzoom: 10,
maxzoom: 18,
});
// Data-driven fill: red = building no legal, green = enriched, yellow = no enrichment
map.addLayer(
{
id: "l-ps-terenuri-fill",
type: "fill",
source: "gis_terenuri_status",
"source-layer": "gis_terenuri_status",
minzoom: 13,
filter,
paint: {
"fill-color": [
"case",
// Building without legal docs: RED fill (covers building too)
[
"all",
["==", ["get", "has_building"], 1],
["==", ["get", "build_legal"], 0],
],
"#ef4444",
// Building with legal: light blue
["==", ["get", "has_building"], 1],
"#93c5fd",
// Enriched, no building: green
["==", ["get", "has_enrichment"], 1],
"#22c55e",
// No enrichment: amber/yellow
"#fbbf24",
],
"fill-opacity": [
"case",
// Stronger opacity for building issues so it shows through
[
"all",
["==", ["get", "has_building"], 1],
["==", ["get", "build_legal"], 0],
],
0.45,
0.25,
],
},
},
"l-terenuri-fill", // below the transparent click-catcher
);
// Data-driven outline: red = building no legal, blue = building legal, green = default
map.addLayer(
{
id: "l-ps-terenuri-line",
type: "line",
source: "gis_terenuri_status",
"source-layer": "gis_terenuri_status",
minzoom: 13,
filter,
paint: {
"line-color": [
"case",
[
"all",
["==", ["get", "has_building"], 1],
["==", ["get", "build_legal"], 0],
],
"#ef4444", // red: building without legal
["==", ["get", "has_building"], 1],
"#3b82f6", // blue: building with legal
"#15803d", // green: no building
],
"line-width": [
"case",
["==", ["get", "has_building"], 1],
2,
0.8,
],
},
},
"l-terenuri-fill",
);
} else {
try {
if (map.getLayer("l-ps-terenuri-fill"))
map.setFilter("l-ps-terenuri-fill", filter);
if (map.getLayer("l-ps-terenuri-line"))
map.setFilter("l-ps-terenuri-line", filter);
} catch {
/* noop */
}
}
// Buildings: keep base layer visible with siruta filter (already applied above)
}, [mapReady, siruta, sirutaValid]);
/* ── Boundary cross-check: load mismatched parcels ─────────── */
const prevCheckSirutaRef = useRef("");
useEffect(() => {
if (!mapReady || !sirutaValid || !siruta) return;
if (prevCheckSirutaRef.current === siruta) return;
prevCheckSirutaRef.current = siruta;
fetch(`/api/geoportal/boundary-check?siruta=${siruta}`)
.then((r) => (r.ok ? r.json() : null))
.then(
(
data: {
type?: string;
features?: GeoJSON.Feature[];
summary?: { foreign: number; edge: number };
} | null,
) => {
if (!data || !data.features || data.features.length === 0) {
setMismatchSummary(null);
return;
}
setMismatchSummary(data.summary ?? null);
const map = asMap(mapHandleRef.current);
if (!map) return;
// Add or update GeoJSON source for mismatched parcels
if (map.getSource("boundary-mismatch")) {
// Update data
const src = map.getSource("boundary-mismatch") as unknown as {
setData(d: unknown): void;
};
src.setData(data);
} else {
map.addSource("boundary-mismatch", {
type: "geojson",
data: data as unknown as Record,
});
// Orange dashed fill for mismatched parcels
map.addLayer({
id: "l-mismatch-fill",
type: "fill",
source: "boundary-mismatch",
paint: {
"fill-color": [
"case",
["==", ["get", "mismatch_type"], "foreign"],
"#f97316", // orange: foreign parcel in this UAT
"#a855f7", // purple: edge parcel (registered here but centroid outside)
],
"fill-opacity": 0.35,
},
});
map.addLayer({
id: "l-mismatch-line",
type: "line",
source: "boundary-mismatch",
paint: {
"line-color": [
"case",
["==", ["get", "mismatch_type"], "foreign"],
"#ea580c",
"#9333ea",
],
"line-width": 2.5,
"line-dasharray": [4, 2],
},
});
}
},
)
.catch(() => {});
}, [mapReady, siruta, sirutaValid]);
/* ── Feature click handler ─────────────────────────────────── */
const handleFeatureClick = useCallback(
(feature: ClickedFeature | null) => {
if (!feature || !feature.properties) {
setClickedFeature(null);
return;
}
setClickedFeature(feature);
},
[],
);
/* ── Selection mode handler ────────────────────────────────── */
const handleSelectionModeChange = useCallback((mode: SelectionMode) => {
if (mode === "off") {
mapHandleRef.current?.clearSelection();
setSelectedFeatures([]);
}
setSelectionMode(mode);
}, []);
/* ── Render ─────────────────────────────────────────────────── */
if (!sirutaValid) {
return (
Selectează un UAT din lista de mai sus
);
}
return (
{/* Boundary mismatch alert */}
{mismatchSummary && (mismatchSummary.foreign > 0 || mismatchSummary.edge > 0) && (
Verificare limită UAT:{" "}
{mismatchSummary.foreign > 0 && (
<>
{mismatchSummary.foreign} parcele străine
(din alt UAT dar geometric în acest UAT)
>
)}
{mismatchSummary.foreign > 0 && mismatchSummary.edge > 0 && " · "}
{mismatchSummary.edge > 0 && (
<>
{mismatchSummary.edge} parcele la limită
(înregistrate aici dar centroid în afara limitei)
>
)}
)}
{boundsLoading && (
Se încarcă zona UAT...
)}
{/* Top-right: basemap switcher + feature panel */}
{clickedFeature && selectionMode === "off" && (
setClickedFeature(null)}
/>
)}
{/* Bottom-left: selection toolbar */}
{
mapHandleRef.current?.clearSelection();
setSelectedFeatures([]);
}}
/>
{/* Bottom-right: legend */}
Fără enrichment
Cu enrichment
Cu clădire (legal)
Clădire fără acte
{mismatchSummary && (mismatchSummary.foreign > 0 || mismatchSummary.edge > 0) && (
<>
Limită UAT
Parcele străine
La limită
>
)}
);
}