pwaLUPMIS2/sql/create_landuse_parcels.sql
ekke ef12e4477b Offline tile cache, polygon Divide, topographic layer integrations
Major feature batch covering drawing-tool improvements, layer additions,
and offline-first capabilities. Largest changes in MapView.js (+1700),
main.js (+1500), public/sw.js (+367), and new modules under src/.

Drawing & editing toolkit
  * Polygon Divide tool — sub-button under Split, divides a polygon into
    N equal-area pieces via binary search; user picks the cutting edge
  * UPN pick phase after Split and Divide — non-picked pieces have their
    identifier fields cleared automatically
  * Improved Merge algorithm — vertex-to-edge proximity (5 m tol.) with
    hybrid lockstep extension; bold A/B labels on selected polygons
  * Persistent vertex highlights — all vertices of the selected polygon
    rendered as dots while edit mode is on, without subclassing ol-ext
  * Toast notifications for merge/split/divide outcomes
  * Shapefile import — addGeoJSONLayer now includes an image style so
    Point features render (previously invisible)

Background & overlay layers
  * DEAfrica Coastlines v0.4 (WMS) in Biophysical Environment
  * DEAfrica Slope (SRTM 30m, style_slope) — semi-transparent background
  * Contours hillshade — get_contours_hillshade.php → local SQLite cache
  * OSM_roads — get_osm_roads.php → local SQLite cache, casing-stroke
    style (black 3.5 px outer, #F0F1F0 1.5 px inner)
  * External Source dialog — green + button in LayerSwitcher lets users
    add WMS / WFS / XYZ layers at runtime
  * Generic addWMSLayer / addXYZLayer with style, opacity, zIndex,
    legendUrl, onlineOnly options
  * TileWMS replaces ImageWMS (fixes 'Width exceeds 512' WMS errors)
  * Legend panel — bottom-right, auto-shown for visible layers that
    register a legendUrl
  * Default base map setting in Settings, persisted in localStorage;
    setBaseMap() on MapView

Offline tile cache (Phase 1 + 2)
  * Service worker: per-host tile caches (osm / topo / satellite /
    carto-light / carto-dark), counter-based eviction to prevent
    iOS Safari memory-pressure reloads, GET_TILE_STATS /
    CLEAR_TILE_CACHES message API
  * pwa.js helpers: getActiveServiceWorker, onServiceWorkerControllerChange,
    getTileCacheStats, clearTileCaches, getStorageEstimate
  * Settings: Offline Map Tiles card with per-provider stats + clear
  * Phase 2 download dialog: form to pick base map, area (current view /
    district / Ghana), zoom range; live tile-count + size estimate;
    progress bar with cancel; OfflineTileDownloader class with
    concurrency + throttling

Local database management
  * osm_roads table + saveOSMRoads / getLocalOSMRoads helpers
  * CACHED_LAYER_TABLES allow-list with clearTable / clearAllCachedLayers
  * Local Database Tables card: per-row Clear button (cached layers
    only) + 'Refresh cached layers' header button with reload prompt

Build & infrastructure
  * Shpjs lazy-loaded via dynamic import (saves ~140 kB from initial JS)
  * chunkSizeWarningLimit raised to 900 kB (openlayers + sqlite3.wasm
    can't be split further)
  * Toast notification module (src/toast.js)
  * Units module (src/units.js) for metric / imperial conversions
  * PDF export module (src/pdf-export.js)

Documentation & SQL
  * Topographic_Background_Layers_for_LUPMIS2.docx — research report
  * OpenTopography_Workflow.svg/.png — ETL pipeline diagram
  * LUPMIS2_Development_Status_Report.docx — April update section
  * sql/create_landuse_parcels.sql — PostgreSQL schema for the LUSPA
    land-use parcel specification (Feb 2026, revised), with PostGIS
    geometry column and standard indices

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 10:55:30 +02:00

208 lines
10 KiB
PL/PgSQL

-- ============================================================================
-- LUPMIS — Land Use Parcels schema
-- ============================================================================
-- Source: "LAND USE INFORMATION FOR LUPMIS" (LUSPA, February 2026, revised)
-- Implements the parcel-attribute table defined by Stephen / LUSPA, with a
-- PostGIS geometry column and the indices needed for typical access patterns
-- (spatial queries, lookup by zone / district / locality, time filtering).
--
-- Conventions:
-- • Identifiers are unquoted (lowercase) — PostgreSQL folds them to lower
-- case anyway, and this avoids the need for double-quotes in queries.
-- • Source column names are PascalCase / Mixed_Case in the spec; their
-- mapping to snake_case is shown in COMMENT ON COLUMN.
-- • Geometry is stored in EPSG:4326 (WGS 84) for portability with the
-- remote API. The MultiPolygon type accommodates parcels with islands
-- or multi-part shapes.
--
-- Run as a database superuser (or a role with CREATEEXTENSION privilege)
-- in the target database.
-- ============================================================================
-- PostGIS is required for the geometry column and spatial index.
CREATE EXTENSION IF NOT EXISTS postgis;
-- Drop existing table for clean re-runs in dev. Comment out for production.
-- DROP TABLE IF EXISTS public.landuse_parcels CASCADE;
-- ---------------------------------------------------------------------------
-- Table: public.landuse_parcels
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS public.landuse_parcels (
id BIGSERIAL PRIMARY KEY,
-- Spec field 1: UPN — Unique Parcel Number (Integer, up to 10 digits).
-- 10-digit integers can exceed INTEGER's max (2,147,483,647), hence BIGINT.
upn BIGINT NOT NULL,
-- Spec field 2: Style — Colour Assign ID (Integer, 2 digits).
-- References the colour palette defined in the Revised Zoning Guidelines
-- and Planning Standards (2025). Optional FK to a lookup table.
style SMALLINT,
-- Spec field 3: Landuse — Broad land use (Text, 50).
landuse VARCHAR(50),
-- Spec field 4: Zone_Code — Zone acronym (Text, 5), e.g. "Re A".
zone_code VARCHAR(5),
-- Spec field 5: Zone_Name — Zone name (Text, 50), e.g. "Residential Zone A".
zone_name VARCHAR(50),
-- Spec field 6: Sector — Sector number of plan area (Text, 5).
sector VARCHAR(5),
-- Spec field 7: Block — Block name within the sector (Text, 3).
block VARCHAR(3),
-- Spec field 8: Parcel_No — Plot number for land registration (Text, 5).
parcel_no VARCHAR(5),
-- Spec field 9: Prop_No — Property number for street addressing (Text, 5).
prop_no VARCHAR(5),
-- Spec field 10: St_Name — Street name (Text, 18). From the Street Naming
-- and Property Addressing System (SNPAS).
st_name VARCHAR(18),
-- Spec field 11: Prop_Add — Property address (Text, 25).
prop_add VARCHAR(25),
-- Spec field 12: Fac_Name — Facility name (Text, 100).
fac_name VARCHAR(100),
-- Spec field 13: Min_Height — Minimum building height in storeys (Integer, 3).
min_height SMALLINT,
-- Spec field 14: Max_Height — Maximum building height in storeys (Integer, 3).
max_height SMALLINT,
-- Spec field 15: Eff_Date — Effective approval date by the District SPC.
eff_date DATE,
-- Spec field 16: LP_Name — Local plan name (Text, 100).
lp_name VARCHAR(100),
-- Spec field 17: Locality — Community / area name (Text, 50).
locality VARCHAR(50),
-- Spec field 18: MMDA — Metropolitan / Municipal / District Assembly
-- abbreviation (Text, 10), e.g. "LADMA".
mmda VARCHAR(10),
-- Spec field 19: Last_Update — Last update on a parcel (e.g. change of
-- use approved by SPC).
last_update DATE,
-- Spec field 20: Remarks — Additional info (Text, 200).
remarks VARCHAR(200),
-- ------------------------------------------------------------------
-- Geometry — parcel polygon in WGS 84 (EPSG:4326).
-- MultiPolygon allows parcels with islands or disjoint parts.
-- ------------------------------------------------------------------
geom geometry(MultiPolygon, 4326),
-- ------------------------------------------------------------------
-- Audit columns (not in the spec, added for change tracking)
-- ------------------------------------------------------------------
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- ------------------------------------------------------------------
-- Constraints
-- ------------------------------------------------------------------
CONSTRAINT uq_landuse_parcels_upn UNIQUE (upn),
CONSTRAINT ck_landuse_parcels_style CHECK (style IS NULL OR style >= 0),
CONSTRAINT ck_landuse_parcels_min_height CHECK (min_height IS NULL OR min_height >= 0),
CONSTRAINT ck_landuse_parcels_max_height CHECK (max_height IS NULL OR max_height >= 0),
CONSTRAINT ck_landuse_parcels_height_order
CHECK (min_height IS NULL OR max_height IS NULL OR min_height <= max_height)
);
-- ---------------------------------------------------------------------------
-- Column comments — preserve the source-document descriptions
-- ---------------------------------------------------------------------------
COMMENT ON TABLE public.landuse_parcels IS 'Land use parcels — LUSPA spec, February 2026 (revised).';
COMMENT ON COLUMN public.landuse_parcels.upn IS 'UPN — Unique Parcel Number (Integer, 10 digits).';
COMMENT ON COLUMN public.landuse_parcels.style IS 'Style — Colour Assign ID per Revised Zoning Guidelines (2025).';
COMMENT ON COLUMN public.landuse_parcels.landuse IS 'Broad land use, e.g. Residential, Commercial, Mixed.';
COMMENT ON COLUMN public.landuse_parcels.zone_code IS 'Zone code (acronym), e.g. Re A.';
COMMENT ON COLUMN public.landuse_parcels.zone_name IS 'Zone name, e.g. Residential Zone A.';
COMMENT ON COLUMN public.landuse_parcels.sector IS 'Sector number of the plan area.';
COMMENT ON COLUMN public.landuse_parcels.block IS 'Block name within the sector.';
COMMENT ON COLUMN public.landuse_parcels.parcel_no IS 'Plot number for land registration.';
COMMENT ON COLUMN public.landuse_parcels.prop_no IS 'Property number for street addressing.';
COMMENT ON COLUMN public.landuse_parcels.st_name IS 'Street name (max 18 characters, per SNPAS).';
COMMENT ON COLUMN public.landuse_parcels.prop_add IS 'Property address of parcel.';
COMMENT ON COLUMN public.landuse_parcels.fac_name IS 'Facility name of property.';
COMMENT ON COLUMN public.landuse_parcels.min_height IS 'Minimum building height (storeys).';
COMMENT ON COLUMN public.landuse_parcels.max_height IS 'Maximum building height (storeys).';
COMMENT ON COLUMN public.landuse_parcels.eff_date IS 'Effective approval date by the District Spatial Planning Committee.';
COMMENT ON COLUMN public.landuse_parcels.lp_name IS 'Local plan name.';
COMMENT ON COLUMN public.landuse_parcels.locality IS 'Name of community or area.';
COMMENT ON COLUMN public.landuse_parcels.mmda IS 'Metropolitan/Municipal/District Assembly abbreviation, e.g. LADMA.';
COMMENT ON COLUMN public.landuse_parcels.last_update IS 'Last update on a parcel (e.g. change of use approved by SPC).';
COMMENT ON COLUMN public.landuse_parcels.remarks IS 'Additional information on the parcel.';
COMMENT ON COLUMN public.landuse_parcels.geom IS 'Parcel boundary geometry (MultiPolygon, EPSG:4326).';
COMMENT ON COLUMN public.landuse_parcels.created_at IS 'Row-creation timestamp (audit).';
COMMENT ON COLUMN public.landuse_parcels.updated_at IS 'Row last-modified timestamp (audit, maintained by trigger).';
-- ---------------------------------------------------------------------------
-- Indices
-- ---------------------------------------------------------------------------
-- Spatial index — required for any ST_Intersects / ST_Within / map-bbox query.
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_geom
ON public.landuse_parcels
USING GIST (geom);
-- B-tree indices for common attribute lookups.
-- (uq_landuse_parcels_upn already creates an implicit index on upn.)
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_zone_code
ON public.landuse_parcels (zone_code);
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_mmda
ON public.landuse_parcels (mmda);
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_locality
ON public.landuse_parcels (locality);
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_lp_name
ON public.landuse_parcels (lp_name);
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_eff_date
ON public.landuse_parcels (eff_date);
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_last_update
ON public.landuse_parcels (last_update);
-- Composite index for the very common "find all parcels in MMDA X with zone Y" query.
CREATE INDEX IF NOT EXISTS idx_landuse_parcels_mmda_zone
ON public.landuse_parcels (mmda, zone_code);
-- ---------------------------------------------------------------------------
-- Trigger — keep updated_at fresh on every UPDATE
-- ---------------------------------------------------------------------------
CREATE OR REPLACE FUNCTION public.fn_landuse_parcels_set_updated_at()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
NEW.updated_at := NOW();
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_landuse_parcels_set_updated_at ON public.landuse_parcels;
CREATE TRIGGER trg_landuse_parcels_set_updated_at
BEFORE UPDATE ON public.landuse_parcels
FOR EACH ROW
EXECUTE FUNCTION public.fn_landuse_parcels_set_updated_at();
-- ============================================================================
-- End of script
-- ============================================================================