feat(parcel-sync): native PostGIS geometry support for QGIS
- Remove postgresqlExtensions/postgis from Prisma schema (PostGIS not yet installed) - Add prisma/postgis-setup.sql: trigger auto-converts GeoJSON→native geometry, GiST spatial index, QGIS-friendly views (gis_terenuri, gis_cladiri, etc.) - Add POST /api/eterra/setup-postgis endpoint (idempotent, runs all SQL setup) - Add safety-net raw SQL in sync-service: backfills geom after upsert phase - Add QGIS/PostGIS setup card in layer catalog UI with connection info - Schema comment documents the trigger-managed 'geom' column approach
This commit is contained in:
@@ -0,0 +1,115 @@
|
|||||||
|
-- =============================================================================
|
||||||
|
-- PostGIS native geometry setup for GisFeature
|
||||||
|
-- Run once via POST /api/eterra/setup-postgis (idempotent — safe to re-run)
|
||||||
|
--
|
||||||
|
-- What this does:
|
||||||
|
-- 1. Ensures PostGIS extension
|
||||||
|
-- 2. Adds native geometry column (geom) if missing
|
||||||
|
-- 3. Creates trigger to auto-convert GeoJSON → native on INSERT/UPDATE
|
||||||
|
-- 4. Backfills existing features that have JSON geometry but no native geom
|
||||||
|
-- 5. Creates GiST spatial index for fast spatial queries
|
||||||
|
-- 6. Creates QGIS-friendly views with clean column names
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- 1. Ensure PostGIS extension
|
||||||
|
CREATE EXTENSION IF NOT EXISTS postgis;
|
||||||
|
|
||||||
|
-- 2. Add native geometry column (idempotent)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'GisFeature' AND column_name = 'geom'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "GisFeature" ADD COLUMN geom geometry(Geometry, 3844);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- 3. Trigger function: auto-convert GeoJSON (geometry JSON column) → native PostGIS (geom)
|
||||||
|
CREATE OR REPLACE FUNCTION gis_feature_sync_geom()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.geometry IS NOT NULL THEN
|
||||||
|
BEGIN
|
||||||
|
NEW.geom := ST_SetSRID(ST_GeomFromGeoJSON(NEW.geometry::text), 3844);
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
-- Invalid GeoJSON → leave geom NULL rather than fail the write
|
||||||
|
NEW.geom := NULL;
|
||||||
|
END;
|
||||||
|
ELSE
|
||||||
|
NEW.geom := NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- 4. Attach trigger (drop + recreate for idempotency)
|
||||||
|
DROP TRIGGER IF EXISTS trg_gis_feature_sync_geom ON "GisFeature";
|
||||||
|
CREATE TRIGGER trg_gis_feature_sync_geom
|
||||||
|
BEFORE INSERT OR UPDATE OF geometry ON "GisFeature"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION gis_feature_sync_geom();
|
||||||
|
|
||||||
|
-- 5. Backfill: convert existing JSON geometries to native
|
||||||
|
UPDATE "GisFeature"
|
||||||
|
SET geom = ST_SetSRID(ST_GeomFromGeoJSON(geometry::text), 3844)
|
||||||
|
WHERE geometry IS NOT NULL AND geom IS NULL;
|
||||||
|
|
||||||
|
-- 6. GiST spatial index for fast bounding-box / intersection queries
|
||||||
|
CREATE INDEX IF NOT EXISTS gis_feature_geom_idx
|
||||||
|
ON "GisFeature" USING GIST (geom);
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- 7. QGIS-friendly views
|
||||||
|
-- - Clean snake_case column names
|
||||||
|
-- - Only rows with valid geometry
|
||||||
|
-- - One master view + per-category views
|
||||||
|
-- =============================================================================
|
||||||
|
|
||||||
|
-- Master view: all features
|
||||||
|
CREATE OR REPLACE VIEW gis_features AS
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
"layerId" AS layer_id,
|
||||||
|
siruta,
|
||||||
|
"objectId" AS object_id,
|
||||||
|
"inspireId" AS inspire_id,
|
||||||
|
"cadastralRef" AS cadastral_ref,
|
||||||
|
"areaValue" AS area_value,
|
||||||
|
"isActive" AS is_active,
|
||||||
|
attributes,
|
||||||
|
"projectId" AS project_id,
|
||||||
|
"createdAt" AS created_at,
|
||||||
|
"updatedAt" AS updated_at,
|
||||||
|
geom
|
||||||
|
FROM "GisFeature"
|
||||||
|
WHERE geom IS NOT NULL;
|
||||||
|
|
||||||
|
-- Terenuri (parcels)
|
||||||
|
CREATE OR REPLACE VIEW gis_terenuri AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'TERENURI%' OR layer_id LIKE 'CADGEN_LAND%';
|
||||||
|
|
||||||
|
-- Clădiri (buildings)
|
||||||
|
CREATE OR REPLACE VIEW gis_cladiri AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'CLADIRI%' OR layer_id LIKE 'CADGEN_BUILDING%';
|
||||||
|
|
||||||
|
-- Documentații (expertize, zone interes, recepții)
|
||||||
|
CREATE OR REPLACE VIEW gis_documentatii AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'EXPERTIZA%'
|
||||||
|
OR layer_id LIKE 'ZONE_INTERES%'
|
||||||
|
OR layer_id LIKE 'RECEPTII%';
|
||||||
|
|
||||||
|
-- Administrativ (limite UAT, intravilan, arii speciale)
|
||||||
|
CREATE OR REPLACE VIEW gis_administrativ AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'LIMITE%'
|
||||||
|
OR layer_id LIKE 'SPECIAL_AREAS%';
|
||||||
|
|
||||||
|
-- =============================================================================
|
||||||
|
-- Done! QGIS connection: PostgreSQL → 10.10.10.166:5432 / architools_db
|
||||||
|
-- Add layers from views: gis_terenuri, gis_cladiri, gis_documentatii, etc.
|
||||||
|
-- SRID: 3844 (Stereo70)
|
||||||
|
-- =============================================================================
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
generator client {
|
generator client {
|
||||||
provider = "prisma-client-js"
|
provider = "prisma-client-js"
|
||||||
previewFeatures = ["postgresqlExtensions"]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
extensions = [postgis]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
model KeyValueStore {
|
model KeyValueStore {
|
||||||
@@ -34,6 +32,8 @@ model GisFeature {
|
|||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
attributes Json // all raw eTerra attributes
|
attributes Json // all raw eTerra attributes
|
||||||
geometry Json? // GeoJSON geometry (Polygon/MultiPolygon)
|
geometry Json? // GeoJSON geometry (Polygon/MultiPolygon)
|
||||||
|
// NOTE: native PostGIS column 'geom' is managed via SQL trigger (see prisma/postgis-setup.sql)
|
||||||
|
// Prisma doesn't need to know about it — trigger auto-populates from geometry JSON
|
||||||
syncRunId String?
|
syncRunId String?
|
||||||
projectId String? // link to project tag
|
projectId String? // link to project tag
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
/**
|
||||||
|
* POST /api/eterra/setup-postgis
|
||||||
|
*
|
||||||
|
* One-time (idempotent) setup: adds native PostGIS geometry column,
|
||||||
|
* trigger for auto-conversion, spatial index, and QGIS-friendly views.
|
||||||
|
*
|
||||||
|
* Safe to call multiple times — all operations use IF NOT EXISTS / CREATE OR REPLACE.
|
||||||
|
*/
|
||||||
|
import { prisma } from "@/core/storage/prisma";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export async function POST() {
|
||||||
|
try {
|
||||||
|
// Execute the setup SQL statements individually
|
||||||
|
// (Prisma $executeRawUnsafe doesn't support multi-statement SQL)
|
||||||
|
|
||||||
|
// 1. Ensure PostGIS extension
|
||||||
|
await prisma.$executeRawUnsafe(`CREATE EXTENSION IF NOT EXISTS postgis`);
|
||||||
|
|
||||||
|
// 2. Add native geometry column if missing
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM information_schema.columns
|
||||||
|
WHERE table_name = 'GisFeature' AND column_name = 'geom'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "GisFeature" ADD COLUMN geom geometry(Geometry, 3844);
|
||||||
|
END IF;
|
||||||
|
END $$
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 3. Trigger function
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE OR REPLACE FUNCTION gis_feature_sync_geom()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF NEW.geometry IS NOT NULL THEN
|
||||||
|
BEGIN
|
||||||
|
NEW.geom := ST_SetSRID(ST_GeomFromGeoJSON(NEW.geometry::text), 3844);
|
||||||
|
EXCEPTION WHEN OTHERS THEN
|
||||||
|
NEW.geom := NULL;
|
||||||
|
END;
|
||||||
|
ELSE
|
||||||
|
NEW.geom := NULL;
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 4. Trigger
|
||||||
|
await prisma.$executeRawUnsafe(
|
||||||
|
`DROP TRIGGER IF EXISTS trg_gis_feature_sync_geom ON "GisFeature"`,
|
||||||
|
);
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE TRIGGER trg_gis_feature_sync_geom
|
||||||
|
BEFORE INSERT OR UPDATE OF geometry ON "GisFeature"
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION gis_feature_sync_geom()
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 5. Backfill existing features
|
||||||
|
const backfilled: { count: bigint }[] = await prisma.$queryRawUnsafe(`
|
||||||
|
WITH updated AS (
|
||||||
|
UPDATE "GisFeature"
|
||||||
|
SET geom = ST_SetSRID(ST_GeomFromGeoJSON(geometry::text), 3844)
|
||||||
|
WHERE geometry IS NOT NULL AND geom IS NULL
|
||||||
|
RETURNING id
|
||||||
|
)
|
||||||
|
SELECT count(*) FROM updated
|
||||||
|
`);
|
||||||
|
const backfilledCount = Number(backfilled[0]?.count ?? 0);
|
||||||
|
|
||||||
|
// 6. Spatial index
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE INDEX IF NOT EXISTS gis_feature_geom_idx
|
||||||
|
ON "GisFeature" USING GIST (geom)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// 7. QGIS-friendly views
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE OR REPLACE VIEW gis_features AS
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
"layerId" AS layer_id,
|
||||||
|
siruta,
|
||||||
|
"objectId" AS object_id,
|
||||||
|
"inspireId" AS inspire_id,
|
||||||
|
"cadastralRef" AS cadastral_ref,
|
||||||
|
"areaValue" AS area_value,
|
||||||
|
"isActive" AS is_active,
|
||||||
|
attributes,
|
||||||
|
"projectId" AS project_id,
|
||||||
|
"createdAt" AS created_at,
|
||||||
|
"updatedAt" AS updated_at,
|
||||||
|
geom
|
||||||
|
FROM "GisFeature"
|
||||||
|
WHERE geom IS NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE OR REPLACE VIEW gis_terenuri AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'TERENURI%' OR layer_id LIKE 'CADGEN_LAND%'
|
||||||
|
`);
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE OR REPLACE VIEW gis_cladiri AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'CLADIRI%' OR layer_id LIKE 'CADGEN_BUILDING%'
|
||||||
|
`);
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE OR REPLACE VIEW gis_documentatii AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'EXPERTIZA%'
|
||||||
|
OR layer_id LIKE 'ZONE_INTERES%'
|
||||||
|
OR layer_id LIKE 'RECEPTII%'
|
||||||
|
`);
|
||||||
|
|
||||||
|
await prisma.$executeRawUnsafe(`
|
||||||
|
CREATE OR REPLACE VIEW gis_administrativ AS
|
||||||
|
SELECT * FROM gis_features
|
||||||
|
WHERE layer_id LIKE 'LIMITE%'
|
||||||
|
OR layer_id LIKE 'SPECIAL_AREAS%'
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Count total features with native geometry
|
||||||
|
const total: { count: bigint }[] = await prisma.$queryRawUnsafe(`
|
||||||
|
SELECT count(*) FROM "GisFeature" WHERE geom IS NOT NULL
|
||||||
|
`);
|
||||||
|
const totalWithGeom = Number(total[0]?.count ?? 0);
|
||||||
|
|
||||||
|
return Response.json({
|
||||||
|
success: true,
|
||||||
|
message: "PostGIS setup complet",
|
||||||
|
details: {
|
||||||
|
backfilledFeatures: backfilledCount,
|
||||||
|
totalFeaturesWithGeom: totalWithGeom,
|
||||||
|
trigger: "gis_feature_sync_geom (BEFORE INSERT OR UPDATE OF geometry)",
|
||||||
|
index: "gis_feature_geom_idx (GiST)",
|
||||||
|
views: [
|
||||||
|
"gis_features (master)",
|
||||||
|
"gis_terenuri",
|
||||||
|
"gis_cladiri",
|
||||||
|
"gis_documentatii",
|
||||||
|
"gis_administrativ",
|
||||||
|
],
|
||||||
|
qgisConnection: {
|
||||||
|
host: "10.10.10.166",
|
||||||
|
port: 5432,
|
||||||
|
database: "architools_db",
|
||||||
|
user: "architools_user",
|
||||||
|
srid: 3844,
|
||||||
|
note: "Adaugă layere din view-uri: gis_terenuri, gis_cladiri, etc.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Eroare server";
|
||||||
|
console.error("[setup-postgis] Error:", error);
|
||||||
|
return Response.json({ success: false, error: message }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -338,6 +338,15 @@ export function ParcelSyncModule() {
|
|||||||
const [syncProgress, setSyncProgress] = useState("");
|
const [syncProgress, setSyncProgress] = useState("");
|
||||||
const [exportingLocal, setExportingLocal] = useState(false);
|
const [exportingLocal, setExportingLocal] = useState(false);
|
||||||
|
|
||||||
|
/* ── PostGIS setup ───────────────────────────────────────────── */
|
||||||
|
const [postgisRunning, setPostgisRunning] = useState(false);
|
||||||
|
const [postgisResult, setPostgisResult] = useState<{
|
||||||
|
success: boolean;
|
||||||
|
message?: string;
|
||||||
|
details?: Record<string, unknown>;
|
||||||
|
error?: string;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
/* ── Parcel search tab ──────────────────────────────────────── */
|
/* ── Parcel search tab ──────────────────────────────────────── */
|
||||||
const [searchResults, setSearchResults] = useState<ParcelDetail[]>([]);
|
const [searchResults, setSearchResults] = useState<ParcelDetail[]>([]);
|
||||||
const [searchList, setSearchList] = useState<ParcelDetail[]>([]);
|
const [searchList, setSearchList] = useState<ParcelDetail[]>([]);
|
||||||
@@ -783,6 +792,25 @@ export function ParcelSyncModule() {
|
|||||||
[siruta, exportingLocal],
|
[siruta, exportingLocal],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
|
/* PostGIS setup (one-time) */
|
||||||
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
const handleSetupPostgis = useCallback(async () => {
|
||||||
|
if (postgisRunning) return;
|
||||||
|
setPostgisRunning(true);
|
||||||
|
setPostgisResult(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/eterra/setup-postgis", { method: "POST" });
|
||||||
|
const json = await res.json();
|
||||||
|
setPostgisResult(json as typeof postgisResult);
|
||||||
|
} catch (error) {
|
||||||
|
const msg = error instanceof Error ? error.message : "Eroare setup";
|
||||||
|
setPostgisResult({ success: false, error: msg });
|
||||||
|
}
|
||||||
|
setPostgisRunning(false);
|
||||||
|
}, [postgisRunning, postgisResult]);
|
||||||
|
|
||||||
/* ════════════════════════════════════════════════════════════ */
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
/* Export individual layer */
|
/* Export individual layer */
|
||||||
/* ════════════════════════════════════════════════════════════ */
|
/* ════════════════════════════════════════════════════════════ */
|
||||||
@@ -1852,6 +1880,103 @@ export function ParcelSyncModule() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* PostGIS / QGIS setup */}
|
||||||
|
<Card>
|
||||||
|
<div className="px-4 py-3 border-b">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Database className="h-4 w-4 text-violet-500" />
|
||||||
|
<span className="text-sm font-semibold">
|
||||||
|
QGIS / PostGIS
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
disabled={postgisRunning}
|
||||||
|
onClick={() => void handleSetupPostgis()}
|
||||||
|
className="border-violet-300 text-violet-700 dark:border-violet-700 dark:text-violet-300"
|
||||||
|
>
|
||||||
|
{postgisRunning ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin mr-1.5" />
|
||||||
|
) : (
|
||||||
|
<Database className="h-3.5 w-3.5 mr-1.5" />
|
||||||
|
)}
|
||||||
|
{postgisRunning ? "Se configurează…" : "Setup PostGIS"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<CardContent className="py-3 space-y-2">
|
||||||
|
{postgisResult ? (
|
||||||
|
postgisResult.success ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
||||||
|
<span className="text-sm font-medium text-emerald-700 dark:text-emerald-400">
|
||||||
|
{postgisResult.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{postgisResult.details && (
|
||||||
|
<div className="rounded bg-muted/50 p-3 text-xs space-y-1 font-mono">
|
||||||
|
<p>
|
||||||
|
Backfill:{" "}
|
||||||
|
{String(
|
||||||
|
(
|
||||||
|
postgisResult.details as {
|
||||||
|
backfilledFeatures?: number;
|
||||||
|
}
|
||||||
|
).backfilledFeatures ?? 0,
|
||||||
|
)}{" "}
|
||||||
|
features convertite
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Total cu geometrie nativă:{" "}
|
||||||
|
{String(
|
||||||
|
(
|
||||||
|
postgisResult.details as {
|
||||||
|
totalFeaturesWithGeom?: number;
|
||||||
|
}
|
||||||
|
).totalFeaturesWithGeom ?? 0,
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
QGIS → PostgreSQL → 10.10.10.166:5432 /
|
||||||
|
architools_db
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
View-uri: gis_terenuri, gis_cladiri,
|
||||||
|
gis_documentatii, gis_administrativ
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground">SRID: 3844</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<XCircle className="h-4 w-4 text-red-500 shrink-0 mt-0.5" />
|
||||||
|
<div className="text-sm">
|
||||||
|
<p className="font-medium text-red-700 dark:text-red-400">
|
||||||
|
PostGIS nu este instalat
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground mt-1">
|
||||||
|
Instalează PostGIS pe serverul PostgreSQL:
|
||||||
|
</p>
|
||||||
|
<code className="text-xs block mt-1 bg-muted rounded px-2 py-1">
|
||||||
|
apt install postgresql-16-postgis-3
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Creează coloana nativă PostGIS, trigger auto-conversie,
|
||||||
|
index spațial GiST și view-uri QGIS-compatibile. Necesită
|
||||||
|
PostGIS instalat pe server.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -221,6 +221,17 @@ export async function syncLayer(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Populate native PostGIS geometry (safety net if trigger not installed)
|
||||||
|
try {
|
||||||
|
await prisma.$executeRaw`
|
||||||
|
UPDATE "GisFeature"
|
||||||
|
SET geom = ST_SetSRID(ST_GeomFromGeoJSON(geometry::text), 3844)
|
||||||
|
WHERE "layerId" = ${layerId} AND siruta = ${siruta}
|
||||||
|
AND geometry IS NOT NULL AND geom IS NULL`;
|
||||||
|
} catch {
|
||||||
|
// PostGIS not available yet — not critical, skip silently
|
||||||
|
}
|
||||||
|
|
||||||
// Mark removed features
|
// Mark removed features
|
||||||
if (removedObjIds.length > 0) {
|
if (removedObjIds.length > 0) {
|
||||||
push({ phase: "Marcare șterse" });
|
push({ phase: "Marcare șterse" });
|
||||||
|
|||||||
Reference in New Issue
Block a user