feat(parcel-sync): fix click, color styling, UAT boundary cross-check
Click fix: - Keep l-terenuri-fill visible but transparent (opacity 0) so it still catches click events for FeatureInfoPanel. Enrichment overlay renders underneath. Color changes: - No enrichment: amber/yellow fill (was light green) - With enrichment: green fill - Buildings: red fill = no legal docs, blue = legal, gray = unknown - Parcel outline: red = building no legal, blue = building legal Boundary cross-check (/api/geoportal/boundary-check?siruta=X): - Finds "foreign" parcels: registered in other UATs but geometrically within this UAT boundary (orange dashed) - Finds "edge" parcels: registered here but centroid outside boundary (purple dashed) - Alert banner shows count, legend updated with mismatch indicators Martin config: added gis_cladiri_status source with build_legal property. Enrichment views: gis_cladiri_status now JOINs parent parcel's BUILD_LEGAL. Requires: docker restart martin + POST /api/geoportal/setup-enrichment-views Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* GET /api/geoportal/boundary-check?siruta=57582
|
||||
*
|
||||
* Spatial cross-check: finds parcels that geometrically fall within the
|
||||
* given UAT boundary but are registered under a DIFFERENT siruta.
|
||||
*
|
||||
* Also detects the reverse: parcels registered in this UAT whose centroid
|
||||
* falls outside its boundary (edge parcels).
|
||||
*
|
||||
* Returns GeoJSON FeatureCollection in EPSG:4326 (WGS84) for direct
|
||||
* map overlay.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
type RawRow = {
|
||||
id: string;
|
||||
siruta: string;
|
||||
object_id: number;
|
||||
cadastral_ref: string | null;
|
||||
area_value: number | null;
|
||||
layer_id: string;
|
||||
mismatch_type: string;
|
||||
geojson: string;
|
||||
};
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const siruta = req.nextUrl.searchParams.get("siruta");
|
||||
if (!siruta) {
|
||||
return NextResponse.json({ error: "siruta required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Foreign parcels: registered in OTHER UATs but geometrically overlap this UAT
|
||||
const foreign = await prisma.$queryRaw`
|
||||
SELECT
|
||||
f.id,
|
||||
f.siruta,
|
||||
f."objectId" AS object_id,
|
||||
f."cadastralRef" AS cadastral_ref,
|
||||
f."areaValue" AS area_value,
|
||||
f."layerId" AS layer_id,
|
||||
'foreign' AS mismatch_type,
|
||||
ST_AsGeoJSON(ST_Transform(f.geom, 4326)) AS geojson
|
||||
FROM "GisFeature" f
|
||||
JOIN "GisUat" u ON u.siruta = ${siruta}
|
||||
WHERE f.siruta != ${siruta}
|
||||
AND ST_Intersects(f.geom, u.geom)
|
||||
AND (f."layerId" LIKE 'TERENURI%' OR f."layerId" LIKE 'CADGEN_LAND%')
|
||||
AND f.geom IS NOT NULL
|
||||
LIMIT 500
|
||||
` as RawRow[];
|
||||
|
||||
// 2. Edge parcels: registered in this UAT but centroid falls outside boundary
|
||||
const edge = await prisma.$queryRaw`
|
||||
SELECT
|
||||
f.id,
|
||||
f.siruta,
|
||||
f."objectId" AS object_id,
|
||||
f."cadastralRef" AS cadastral_ref,
|
||||
f."areaValue" AS area_value,
|
||||
f."layerId" AS layer_id,
|
||||
'edge' AS mismatch_type,
|
||||
ST_AsGeoJSON(ST_Transform(f.geom, 4326)) AS geojson
|
||||
FROM "GisFeature" f
|
||||
JOIN "GisUat" u ON u.siruta = f.siruta AND u.siruta = ${siruta}
|
||||
WHERE NOT ST_Contains(u.geom, ST_Centroid(f.geom))
|
||||
AND (f."layerId" LIKE 'TERENURI%' OR f."layerId" LIKE 'CADGEN_LAND%')
|
||||
AND f.geom IS NOT NULL
|
||||
LIMIT 500
|
||||
` as RawRow[];
|
||||
|
||||
const allRows = [...foreign, ...edge];
|
||||
|
||||
// Build GeoJSON FeatureCollection
|
||||
const features = allRows
|
||||
.map((row) => {
|
||||
try {
|
||||
const geometry = JSON.parse(row.geojson) as GeoJSON.Geometry;
|
||||
return {
|
||||
type: "Feature" as const,
|
||||
geometry,
|
||||
properties: {
|
||||
id: row.id,
|
||||
siruta: row.siruta,
|
||||
object_id: row.object_id,
|
||||
cadastral_ref: row.cadastral_ref,
|
||||
area_value: row.area_value,
|
||||
layer_id: row.layer_id,
|
||||
mismatch_type: row.mismatch_type,
|
||||
},
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return NextResponse.json({
|
||||
type: "FeatureCollection",
|
||||
features,
|
||||
summary: {
|
||||
foreign: foreign.length,
|
||||
edge: edge.length,
|
||||
total: allRows.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : "Eroare";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,14 @@ const VIEWS = [
|
||||
f."cadastralRef" AS cadastral_ref,
|
||||
f."areaValue" AS area_value,
|
||||
f."isActive" AS is_active,
|
||||
COALESCE((p.enrichment->>'BUILD_LEGAL')::int, -1) AS build_legal,
|
||||
f.geom
|
||||
FROM "GisFeature" f
|
||||
LEFT JOIN "GisFeature" p
|
||||
ON p.siruta = f.siruta
|
||||
AND p."cadastralRef" = f."cadastralRef"
|
||||
AND (p."layerId" LIKE 'TERENURI%' OR p."layerId" LIKE 'CADGEN_LAND%')
|
||||
AND p.enrichment IS NOT NULL
|
||||
WHERE f.geom IS NOT NULL
|
||||
AND (f."layerId" LIKE 'CLADIRI%' OR f."layerId" LIKE 'CADGEN_BUILDING%')`,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user