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>
208 lines
10 KiB
PL/PgSQL
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
|
|
-- ============================================================================
|