-- ============================================================================= -- PostGIS native geometry setup for GisUat (UAT boundaries) -- Run once manually: PGPASSWORD='...' psql -h 10.10.10.166 -p 5432 \ -- -U architools_user -d architools_db -f prisma/gisuat-postgis-setup.sql -- -- Idempotent — safe to re-run. -- -- What this does: -- 1. Ensures PostGIS extension -- 2. Adds native geometry column (geom) if missing -- 3. Creates function to convert Esri ring JSON -> PostGIS geometry -- 4. Creates trigger to auto-convert on INSERT/UPDATE -- 5. Backfills existing rows -- 6. Creates GiST spatial index -- 7. Creates Martin/QGIS-friendly view 'gis_uats' -- -- After running both SQL scripts (postgis-setup.sql + this file), Martin -- will auto-discover these views (any table/view with a 'geom' geometry column): -- - gis_features (master: all GisFeature rows with geometry) -- - gis_terenuri (parcels from GisFeature) -- - gis_cladiri (buildings from GisFeature) -- - gis_documentatii (expertize/zone/receptii from GisFeature) -- - gis_administrativ (limite UAT/intravilan/arii speciale from GisFeature) -- - gis_uats (UAT boundaries from GisUat) <-- this script -- -- All geometries are in EPSG:3844 (Stereo70). -- ============================================================================= -- 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 = 'GisUat' AND column_name = 'geom' ) THEN ALTER TABLE "GisUat" ADD COLUMN geom geometry(Geometry, 3844); END IF; END $$; -- 3. Function: convert Esri ring JSON { rings: number[][][] } -> PostGIS geometry -- Esri rings format: each ring is an array of [x, y] coordinate pairs. -- First ring = exterior, subsequent rings = holes. -- Multiple outer rings (non-holes) would need MultiPolygon, but UAT boundaries -- from eTerra typically have a single polygon with possible holes. -- -- Strategy: build WKT POLYGON/MULTIPOLYGON from the rings array, then -- use ST_GeomFromText with SRID 3844. CREATE OR REPLACE FUNCTION gis_uat_esri_to_geom(geom_json jsonb) RETURNS geometry AS $$ DECLARE rings jsonb; ring jsonb; coord jsonb; ring_count int; coord_count int; i int; j int; wkt_ring text; wkt text; first_x double precision; first_y double precision; last_x double precision; last_y double precision; BEGIN -- Extract the rings array from the JSON rings := geom_json -> 'rings'; IF rings IS NULL OR jsonb_array_length(rings) = 0 THEN RETURN NULL; END IF; ring_count := jsonb_array_length(rings); -- Build WKT POLYGON with all rings (first = exterior, rest = holes) wkt := 'POLYGON('; FOR i IN 0 .. ring_count - 1 LOOP ring := rings -> i; coord_count := jsonb_array_length(ring); IF coord_count < 3 THEN CONTINUE; -- skip degenerate rings END IF; IF i > 0 THEN wkt := wkt || ', '; END IF; wkt_ring := '('; FOR j IN 0 .. coord_count - 1 LOOP coord := ring -> j; IF j > 0 THEN wkt_ring := wkt_ring || ', '; END IF; wkt_ring := wkt_ring || (coord ->> 0) || ' ' || (coord ->> 1); -- Track first and last coordinates to check ring closure IF j = 0 THEN first_x := (coord ->> 0)::double precision; first_y := (coord ->> 1)::double precision; END IF; IF j = coord_count - 1 THEN last_x := (coord ->> 0)::double precision; last_y := (coord ->> 1)::double precision; END IF; END LOOP; -- Close the ring if not already closed IF first_x != last_x OR first_y != last_y THEN wkt_ring := wkt_ring || ', ' || first_x::text || ' ' || first_y::text; END IF; wkt_ring := wkt_ring || ')'; wkt := wkt || wkt_ring; END LOOP; wkt := wkt || ')'; RETURN ST_GeomFromText(wkt, 3844); END; $$ LANGUAGE plpgsql IMMUTABLE; -- 4. Trigger function: auto-convert Esri JSON -> native PostGIS on INSERT/UPDATE 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); EXCEPTION WHEN OTHERS THEN -- Invalid geometry JSON -> leave geom NULL rather than fail the write NEW.geom := NULL; END; ELSE NEW.geom := NULL; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- 5. Attach trigger (drop + recreate for idempotency) DROP TRIGGER IF EXISTS trg_gis_uat_sync_geom ON "GisUat"; CREATE TRIGGER trg_gis_uat_sync_geom BEFORE INSERT OR UPDATE OF geometry ON "GisUat" FOR EACH ROW EXECUTE FUNCTION gis_uat_sync_geom(); -- 6. Backfill: convert existing Esri JSON geometries to native PostGIS UPDATE "GisUat" SET geom = gis_uat_esri_to_geom(geometry::jsonb) WHERE geometry IS NOT NULL AND geom IS NULL; -- 7. GiST spatial index for fast spatial queries CREATE INDEX IF NOT EXISTS gis_uat_geom_idx ON "GisUat" USING GIST (geom); -- 8. Martin/QGIS-friendly view (moderate simplification — 50m tolerance) CREATE OR REPLACE VIEW gis_uats AS SELECT siruta, name, county, ST_SimplifyPreserveTopology(geom, 50) AS geom FROM "GisUat" WHERE geom IS NOT NULL; -- 9. Simplified view for low zoom levels (z5-z9) — 500m tolerance CREATE OR REPLACE VIEW gis_uats_simple AS SELECT siruta, name, ST_SimplifyPreserveTopology(geom, 500) AS geom FROM "GisUat" WHERE geom IS NOT NULL; -- ============================================================================= -- Done! Martin serves these views as vector tiles: -- - gis_uats (moderate detail, z9+) -- - gis_uats_simple (coarse overview, z5-z9) -- QGIS: PostgreSQL -> 10.10.10.166:5432 / architools_db -> gis_uats view -- SRID: 3844 (Stereo70) -- =============================================================================