diff --git a/src/app/api/geoportal/optimize-views/route.ts b/src/app/api/geoportal/optimize-views/route.ts new file mode 100644 index 0000000..6f95039 --- /dev/null +++ b/src/app/api/geoportal/optimize-views/route.ts @@ -0,0 +1,116 @@ +/** + * POST /api/geoportal/optimize-views + * + * Replaces on-the-fly ST_Simplify views with materialized columns. + * This eliminates CPU-heavy geometry simplification on every Martin tile request. + * + * What it does: + * 1. Adds geom_z0/z5/z8 columns to GisUat table (pre-simplified geometry) + * 2. Backfills them from the original geom column + * 3. Creates spatial indexes on each + * 4. Replaces views to use pre-computed columns instead of on-the-fly simplification + * + * Safe to re-run (idempotent). Original geom column is NEVER modified. + */ +import { NextResponse } from "next/server"; +import { prisma } from "@/core/storage/prisma"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +const STEPS = [ + // 1. Add pre-simplified geometry columns + { + name: "Add geom_z0 column (2000m)", + sql: `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='GisUat' AND column_name='geom_z0') THEN ALTER TABLE "GisUat" ADD COLUMN geom_z0 geometry(Geometry, 3844); END IF; END $$`, + }, + { + name: "Add geom_z5 column (500m)", + sql: `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='GisUat' AND column_name='geom_z5') THEN ALTER TABLE "GisUat" ADD COLUMN geom_z5 geometry(Geometry, 3844); END IF; END $$`, + }, + { + name: "Add geom_z8 column (50m)", + sql: `DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name='GisUat' AND column_name='geom_z8') THEN ALTER TABLE "GisUat" ADD COLUMN geom_z8 geometry(Geometry, 3844); END IF; END $$`, + }, + + // 2. Backfill with pre-computed simplified geometries + { + name: "Backfill geom_z0 (2000m simplification)", + sql: `UPDATE "GisUat" SET geom_z0 = ST_SimplifyPreserveTopology(geom, 2000) WHERE geom IS NOT NULL AND geom_z0 IS NULL`, + }, + { + name: "Backfill geom_z5 (500m simplification)", + sql: `UPDATE "GisUat" SET geom_z5 = ST_SimplifyPreserveTopology(geom, 500) WHERE geom IS NOT NULL AND geom_z5 IS NULL`, + }, + { + name: "Backfill geom_z8 (50m simplification)", + sql: `UPDATE "GisUat" SET geom_z8 = ST_SimplifyPreserveTopology(geom, 50) WHERE geom IS NOT NULL AND geom_z8 IS NULL`, + }, + + // 3. Spatial indexes + { + name: "Index geom_z0", + sql: `CREATE INDEX IF NOT EXISTS gis_uat_geom_z0_idx ON "GisUat" USING GIST (geom_z0)`, + }, + { + name: "Index geom_z5", + sql: `CREATE INDEX IF NOT EXISTS gis_uat_geom_z5_idx ON "GisUat" USING GIST (geom_z5)`, + }, + { + name: "Index geom_z8", + sql: `CREATE INDEX IF NOT EXISTS gis_uat_geom_z8_idx ON "GisUat" USING GIST (geom_z8)`, + }, + + // 4. Replace views to use pre-computed columns (zero CPU on read) + { + name: "Replace gis_uats_z0 view", + sql: `CREATE OR REPLACE VIEW gis_uats_z0 AS SELECT siruta, name, geom_z0 AS geom FROM "GisUat" WHERE geom_z0 IS NOT NULL`, + }, + { + name: "Replace gis_uats_z5 view", + sql: `CREATE OR REPLACE VIEW gis_uats_z5 AS SELECT siruta, name, geom_z5 AS geom FROM "GisUat" WHERE geom_z5 IS NOT NULL`, + }, + { + name: "Replace gis_uats_z8 view", + sql: `CREATE OR REPLACE VIEW gis_uats_z8 AS SELECT siruta, name, county, geom_z8 AS geom FROM "GisUat" WHERE geom_z8 IS NOT NULL`, + }, + { + name: "Replace gis_uats_z12 view (original geom)", + sql: `CREATE OR REPLACE VIEW gis_uats_z12 AS SELECT siruta, name, county, geom FROM "GisUat" WHERE geom IS NOT NULL`, + }, + + // 5. Update trigger to also compute simplified geoms on INSERT/UPDATE + { + name: "Update trigger to pre-compute simplified geoms", + sql: `CREATE OR REPLACE FUNCTION gis_uat_sync_geom() RETURNS TRIGGER AS $$ BEGIN IF NEW.geometry IS NOT NULL THEN BEGIN NEW.geom := gis_uat_esri_to_geom(NEW.geometry::jsonb); IF NEW.geom IS NOT NULL THEN NEW.geom_z0 := ST_SimplifyPreserveTopology(NEW.geom, 2000); NEW.geom_z5 := ST_SimplifyPreserveTopology(NEW.geom, 500); NEW.geom_z8 := ST_SimplifyPreserveTopology(NEW.geom, 50); END IF; EXCEPTION WHEN OTHERS THEN NEW.geom := NULL; NEW.geom_z0 := NULL; NEW.geom_z5 := NULL; NEW.geom_z8 := NULL; END; ELSE NEW.geom := NULL; NEW.geom_z0 := NULL; NEW.geom_z5 := NULL; NEW.geom_z8 := NULL; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql`, + }, +]; + +export async function POST() { + const results: string[] = []; + try { + for (const step of STEPS) { + await prisma.$executeRawUnsafe(step.sql); + results.push(`${step.name} OK`); + } + return NextResponse.json({ status: "ok", results }); + } catch (error) { + const msg = error instanceof Error ? error.message : "Eroare"; + return NextResponse.json({ status: "error", results, error: msg }, { status: 500 }); + } +} + +/** GET — check optimization status */ +export async function GET() { + try { + const cols = await prisma.$queryRaw` + SELECT column_name FROM information_schema.columns + WHERE table_name = 'GisUat' AND column_name LIKE 'geom_z%' + ` as Array<{ column_name: string }>; + + const optimized = cols.length >= 3; + return NextResponse.json({ optimized, columns: cols.map((c) => c.column_name) }); + } catch { + return NextResponse.json({ optimized: false, columns: [] }); + } +} diff --git a/src/modules/geoportal/components/setup-banner.tsx b/src/modules/geoportal/components/setup-banner.tsx index 7e8bf80..bd20cf0 100644 --- a/src/modules/geoportal/components/setup-banner.tsx +++ b/src/modules/geoportal/components/setup-banner.tsx @@ -1,49 +1,65 @@ "use client"; import { useEffect, useState } from "react"; -import { Database, Loader2, CheckCircle2, AlertTriangle } from "lucide-react"; +import { Database, Loader2, CheckCircle2, AlertTriangle, Zap } from "lucide-react"; import { Button } from "@/shared/components/ui/button"; -type Status = "checking" | "needed" | "running" | "done" | "error" | "ready"; +type Status = "checking" | "views-needed" | "optimize-needed" | "running" | "done" | "error" | "ready"; export function SetupBanner() { const [status, setStatus] = useState("checking"); - const [missing, setMissing] = useState([]); + const [message, setMessage] = useState(""); const [error, setError] = useState(""); - // Check on mount useEffect(() => { - fetch("/api/geoportal/setup-views") - .then((r) => r.json()) - .then((d: { ready: boolean; missing: string[] }) => { - if (d.ready) setStatus("ready"); - else { setStatus("needed"); setMissing(d.missing); } - }) - .catch(() => setStatus("ready")); // don't block if check fails + (async () => { + try { + // Check views + const vr = await fetch("/api/geoportal/setup-views").then((r) => r.json()); + if (!vr.ready) { + setStatus("views-needed"); + setMessage(`${vr.missing?.length ?? 0} view-uri PostGIS lipsesc`); + return; + } + // Check optimization + const or2 = await fetch("/api/geoportal/optimize-views").then((r) => r.json()); + if (!or2.optimized) { + setStatus("optimize-needed"); + setMessage("View-urile UAT calculeaza geometria on-the-fly (CPU intensiv). Optimizeaza pentru performanta."); + return; + } + setStatus("ready"); + } catch { + setStatus("ready"); + } + })(); }, []); - const runSetup = async () => { + const runAction = async (endpoint: string) => { setStatus("running"); try { - const r = await fetch("/api/geoportal/setup-views", { method: "POST" }); + const r = await fetch(endpoint, { method: "POST" }); const d = await r.json(); - if (d.status === "ok") setStatus("done"); - else { setStatus("error"); setError(d.error ?? "Eroare necunoscuta"); } + if (d.status === "ok") { + setStatus("done"); + setMessage("Restart Martin pe server: docker restart martin"); + } else { + setStatus("error"); + setError(d.error ?? "Eroare"); + } } catch (e) { setStatus("error"); setError(e instanceof Error ? e.message : "Eroare retea"); } }; - // Don't show if ready or still checking if (status === "ready" || status === "checking") return null; - // Auto-hide after success if (status === "done") { return (
- - View-uri create cu succes. Restarteaza Martin pe server: docker restart martin + + {message}
); } @@ -51,28 +67,27 @@ export function SetupBanner() { if (status === "error") { return (
- - Eroare: {error} - + + {error}
); } + const isViews = status === "views-needed"; + return (
- - - {missing.length} view-uri PostGIS lipsesc ({missing.join(", ")}). Necesare pentru tile-uri UAT optimizate. - + {isViews + ? + : } + {message}
);