Files
ArchiTools/src/app/api/geoportal/boundary-check/route.ts
T
AI Assistant ba71ca3ef5 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>
2026-03-24 16:05:12 +02:00

116 lines
3.4 KiB
TypeScript

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