"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: () => (

Se incarca harta...

), }, ); /* ------------------------------------------------------------------ */ /* 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ă
)}
); }