11 Commits

Author SHA1 Message Date
e9bb4d81c9 Drawing toolbar: docked left rail, Bootstrap Icons throughout
Field officers reported three problems with the drawing palette: it had no
background and disappeared over light imagery, it sat centred at the top across
the area being drawn, and its icons did not match one another. All three had
distinct causes.

Layout. The bar is now a two-column rail docked to the left edge on a solid
panel, with its controls re-ordered into labelled Draw / Edit / Shape / File
bands by MapView._buildToolbarRail(). Two columns because a single file of
thirteen buttons plus labels runs past the bottom of a laptop map view; the
rail measures 75 x 363 px. Adding ol-ext's own `ol-left` class also makes
option bars (Select's Delete/Info, Split's sub-tools) fly out to the right
rather than dropping downwards.

Icons. ol-ext ships no icon font or sprite sheet — it draws each built-in tool
as CSS ::before/::after shapes from borders and box-shadows, which is why our
own Bootstrap-Icon tools looked foreign next to them. The generated shapes are
suppressed and every button gets an icon from a single TOOLBAR_ICONS map. The
glyph is injected as a <span>, not an <i>, because ol-ext styles
`.ol-transform > button i::before` and would otherwise out-specify Bootstrap's
`.bi::before` and blank the icon.

Phones. The rail becomes a horizontally scrolling strip above the dock, which
retires the two-row flex-break workaround and its `.ol-editbar-break` element.
Touch targets stay at 36 px, and option bars open upward.

Two things worth recording for anyone changing this later. The rail must not
set `overflow` on either axis: the other axis then computes to `auto` too and
clips the option bars that fly out past its right edge. And `display` in the
phone media query must stay free of `!important`, since ol-ext shows and hides
the whole bar with an inline style.

Also picks up ol-offset, an EditBar default that was never explicitly enabled
and had been sitting in the bar unlabelled; it is grouped under Shape.

Verified in a headless browser at desktop and phone widths, in light and dark
mode: icons, grouping, active-tool tint, option-bar placement, and that the bar
still hides when Draw mode is off.

Service Worker v14 -> v15.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-20 15:25:54 +02:00
da6f968725 Zonal-statistics Excel export, COG layer entry point, DE Africa ETL
Excel export (from TWG feedback)
- src/analysis/xlsx.js: a dependency-free XLSX writer — an .xlsx is a ZIP of
  XML, so this packs the required parts with a small stored-ZIP writer. Avoids
  SheetJS (stale npm package with advisories) and ExcelJS (heavy for an
  offline-first field app), and does not rely on the JSZip that only reaches us
  transitively via shp-write. Lazy-loaded as a ~5.6 kB chunk.
- After a zonal run the Analysis panel offers "Export table (Excel)", writing a
  two-sheet workbook: Results (figures as real numbers) and Parameters (zone
  and input layers with feature counts, the "Apply to" scope, membership rule,
  statistics, numeric field and export time) — so a table can be verified or
  reproduced later rather than being an unattributed set of numbers.
  The button is hidden for overlay runs and cleared when the mode changes.
- Verified against two independent readers: openpyxl loads it with zero
  warnings and correct numeric types, and LibreOffice Calc opens it as a
  spreadsheet. Also exercised end-to-end through the real zonal pipeline.

COG raster entry point
- Add External Layer gains a COG type alongside WMS/WFS/XYZ, with a URL
  pre-flight check that distinguishes a web page, a 404, a CORS block and a
  server without byte-range support — geotiff.js otherwise reports these only
  as an opaque "AggregateError: Request failed".

Digital Earth Africa ETL
- etl/deafrica_dem_to_minio.py exports a DE Africa DEM for a district as a COG
  and uploads it to the LUSPA MinIO bucket raster-objects, then verifies the
  object is anonymously readable and range-capable. Credentials come from the
  environment; the existing PHP integration hardcodes them and an earlier key
  pair reached Gitea. NOTE: not yet run against the Sandbox — start with
  --list-products to confirm the DEM product name.

Documents
- Concept note: Digital Earth Africa added as a raster source (§6.3), separating
  the live WMS route from the batch Sandbox export; corrected the in-house
  contour table's provenance to OpenTopography (gdal_contour over an SRTM 30 m /
  Copernicus 30 m DEM), and added the ToR §2.4.2 / FS §2 alignment chapter.
- TWG presentation, architecture and two integration workflow charts (SVG
  sources kept in the repo so they stay editable), and a user guide for the
  Analyse tools.

Service worker v13 -> v14. .gitignore: exclude Python bytecode from etl/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 16:28:45 +02:00
26c941b09a Analytical tools Stage 1 + drawing-tool fixes
Stage 1 of the GIS Analytical Tools concept — client-side spatial analysis:

- src/analysis/overlay.js: vector overlays (intersect / clip / difference /
  union-dissolve) on Turf.js, reprojecting to WGS84 and merging attributes
  (intersect keeps both layers', clip keeps only A's).
- src/analysis/zonal.js: vector-in-vector zonal statistics — count / sum /
  mean / min / max / total area per zone, with centroid-in-zone (default) or
  any-overlap membership.
- Bounding-box pre-filtering in both: only genuinely overlapping pairs reach
  the expensive geometry test. 43 zones x 25,004 parcels now completes in
  ~106 ms; previously it was refused as too large.
- src/analysis-modal.js + markup: Analysis panel with "Apply to" scoping —
  whole layer, current map view, selected features, or the catch of a drawn
  Circle/Area. Reached from a new "Analyse" dock button.
- MapView.addCOGLayer() for Cloud-Optimized GeoTIFF display (WebGLTile +
  GeoTIFF source, imported lazily); listVectorLayers(); getSelectedFeatures().
- Circle/Area analysis popup: one "Export" button (PDF folded into the export
  modal as a fourth format, field-rename table hidden for it) plus an
  "Analyse" button that opens the panel pre-scoped to the intersecting
  features.
- vite.config.js: code-split turf, geotiff and pako so the eager bundle is
  unchanged (~283 kB). Giving pako its own chunk also fixes a circular chunk
  between jspdf and geotiff, which share it via fast-png.

Drawing-tool fixes carried in the same working tree:

- Delete/Backspace key deletes the selection via the EditBar's Delete
  interaction (same undoable block as the button).
- Multi-select: shift-click toggles, Ctrl/Cmd-drag box-selects; helper layers
  (vertex overlay, GPS) excluded.
- Undo: split/merge/divide wrapped in undo blocks so one press reverses the
  whole operation; vertex-overlay churn no longer pollutes the undo stack;
  sources are re-scanned and the stack cleared when edit mode is entered, so
  deletes on sub-grouped layers are undoable. NOTE: the undo behaviour is not
  yet confirmed on-device.
- ol-ext TouchCursor gated to touch-only devices, so hybrid touchscreen
  laptops keep the normal cursor.

Service worker v12 -> v13.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 14:09:06 +02:00
c335a8987e Logout: perform full SSO logout via /user-logout
The /?logout=1 endpoint destroyed only the PWA's own PHP session and then
redirected to the bare landing page — which has no session and immediately
blocks access, so the user was never actually logged out of SSO.

- public/index.php: after session_destroy(), redirect to
  https://lupmis4luspa.org/user-logout (the portal's full SSO logout) instead
  of the landing page. Crucially, no longer clear sso_auth_token here —
  /user-logout needs that cookie to identify which SSO session to terminate
  (and it clears the cookie itself). The production access-guard bounce to the
  landing page is unchanged.
- main.js: drop the now-redundant best-effort client call to /sso/logout; the
  server redirect chain (/?logout=1 → /user-logout) owns the SSO logout. Offline
  guard and district-cache wipe unchanged.
- sw.js: update the v12 changelog note (still v12; not yet deployed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 12:29:45 +00:00
d8ddbbc910 Fix stale district + offline-safe logout; GPS UTM format; touch-cursor gating; SW v12
Session / district correctness
- public/index.php: add a /?logout=1 endpoint that destroys the PWA's own PHP
  session (session_destroy + expire PHPSESSID + clear sso_auth_token, then
  redirect to the SSO portal). Logout previously cleared only the SSO cookie,
  leaving the PHPSESSID session — and its frozen district_id — intact, which is
  why a reassigned user kept loading the old district across logout/login.
- SSO token is validated once per session, at login (unchanged first-login
  logic). A district transfer is now picked up on the next logout→login, which
  is correct precisely because logout finally tears the session down. No
  periodic SSO polling.
- main.js: the menu Logout button routes through /?logout=1 and wipes
  district-scoped local caches first. Logout is blocked while offline — a
  session can only be created online, so an offline logout would strand the
  user with no way back in (and would not actually reach the server).
- main.js: enforceDistrictConsistency() clears district-scoped caches when the
  session district changes between loads; the district boundary is cached under
  a per-district key (district_boundary_<id>) so one district's geometry can
  never be served for another.

GPS coordinate format
- New "GPS Coordinate Format" setting (Lat/Lon · UTM · Both) in the Settings
  panel; the navbar read-out renders the chosen format and repaints the current
  fix immediately on change. Self-contained WGS84→UTM converter in
  geo-utils.js, verified against an independent Redfearn-series implementation.

ol-ext touch cursor
- MapView gates the TouchCursor to genuine touch-only devices via matchMedia
  (any-pointer: fine / any-hover: hover); hybrid touchscreen laptops keep the
  normal cursor. Reactive to pointer-capability changes.

- Service worker v11 → v12 (new shell). docs/SSO_Session_Refresh_Proposal.md
  documents the implemented approach.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 11:58:36 +00:00
4d594f58e5 Service Worker v10 → v11
Evicts the stale shell + module caches at the next deploy so existing
clients pick up the permit-iframe auth-required card, the new
X-Frame-Options strip, and the import-UX refinements (parse spinner,
client_uuid tagging, geometry/delete persistence, sample values +
Unicode-bold field names in the mapping dropdown).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-23 13:37:23 +00:00
26d4f6235f UPN-grid layer, external imports/staged upload, GIS export, SW v10
UPN-grid layer:
- src/database.js — new upn_grid SQLocal table (id, districtid, upn_prefix,
  geometry_wkt) + saveUpnGrid / getLocalUpnGrid; cache-once-per-district.
- src/remotedb.js — getUpnGrid → get_upn_grid_per_district.php.
- main.js loadUpnGrid + upnGridToGeoJSON in the Administration group, with
  a zoom-aware style: white casing under a bolder violet dashed stroke
  (visible against parcels) and upn_prefix labels rendered only when
  resolution ≤ 7 m/px (≈ scale ≤ 1:25,000).
- main.js click handler: single click on a UPN-grid cell opens an info
  popup showing the upn_prefix.

External-dataset import → staging → upload (client-side complete):
- src/database.js — external_imports + external_import_features tables,
  plus createExternalImport / addExternalImportFeatures /
  updateExternalImport / getExternalImport / getExternalImportFeatures /
  listExternalImports / remapImportedFeatureProperties /
  deleteExternalImport. Status enum: imported/mapped/other/uploading/
  submitted/migrated/failed (aligned with the database team's staged-
  upload model — lu_parcels_upload_tmp + supervisor review).
- src/import-detect.js — pure helpers: detectTargetType(),
  autoMapFields(), applyFieldMapping(), listSourceFields() + TARGET_TYPES
  / TARGET_FIELDS registries.
- src/import-modal.js — Bootstrap mapping modal: target dropdown,
  field-rename table, three actions (Cancel / Save / Save + Upload now).
- main.js — stageImport hooked into addImportedGeoJSON (the single
  convergence point for shp/GeoJSON/KML drops); handleImportModalResult
  applies the mapping in one transaction; runUpload builds the real
  payload (district_id + api_token from remotePost, user_id_upload from
  SSO session, per-feature client_uuid/geom/props) and currently logs +
  toasts — the upload_<target>.php endpoints are not yet live.
- index.html — #importMappingModal markup.
- MapView._decorateLayerListItem — import-state chip (Upload N /
  spinner / ✓ submitted / ✓ live / N errors) dispatching
  lupmis:import-chip-click; src/styles/layerswitcher.css — chip variants.

GIS export from Area / Circle Analysis popups:
- MapView._showAnalysisPopup now accepts an exportContext (clipGeometry +
  parcelFeatures + zoneFeatures + otherByLayer) and renders an "Export
  GIS" button next to "Export PDF". Click dispatches lupmis:export-gis.
- index.html — #exportGisModal markup.
- src/export-gis-modal.js — Bootstrap modal: format toggle (GeoJSON
  default / Shapefile / KML), filename, field-rename table with SHP
  10-char DBF warning.
- src/gis-export.js — writers: GeoJSON via Blob, KML via OL KMLFormat,
  Shapefile via shp-write (with DBF-safe name sanitiser).
- Adds shp-write@0.3.2 dependency.

MapView style options:
- addGeoJSONLayer now accepts strokeDash for line-dash patterns (used by
  the UPN-grid layer and available for any future contextual overlay).

Service Worker v9 → v10 to evict the stale shell/module caches on the
next deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-19 11:02:41 +02:00
933bfcf4c0 Permit-map iframe embed, lu_parcels schema, security guards, LayerSwitcher fix
Iframe embed for the Permitting app (LUPMIS2_Reusable_Mapping_Concept §3.2):
- public/embed.php — SSO + production gate + frame-ancestors CSP +
  whitelisted URL params (mode, lon/lat/zoom, upn, basemap,
  application_code); injects window.LUPMIS_SESSION + window.LUPMIS_EMBED.
- public/.htaccess — clean /embed URL (rewrite before the SPA fallback).
- src/embed-bridge.js — postMessage protocol: out ready / parcel:select /
  parcel:cleared / error; in set:view / set:selected / clear:selected /
  set:basemap. Visual highlight via a dedicated VectorLayer; pending-UPN
  queue resolved as parcels stream in.
- main.js — reads window.LUPMIS_EMBED, gates the normal click/dblclick
  handlers in permit mode, exposes parcelsLayer to module scope, makes
  it visible and hands it to the bridge after loadParcels().
- index.html — CSS for body.embed-mode-permit hides navbar/dock/offcanvas
  and lets the map fill the iframe.
- LUPMIS2_Permit_Map_Integration.docx — integration instructions for the
  Permitting team (contract, show.blade.php changes, phasing).

Local lu_parcels structural refactor:
- src/database.js — parcels table now mirrors spatial.lu_parcels with
  explicit columns (upn, style, landuse, zone_code/name, sector, block,
  parcel_no, prop_no, st_name, prop_add, fac_name, min/max_height,
  eff_date, lp_name, locality, mmda, last_update, remarks, geom→geometry_wkt,
  created_at, updated_at, districtid) plus local-only status/fetched_at.
  Drop-and-recreate migration off `upn` presence. saveParcels wraps the
  ~25k inserts in a transaction; numeric coercion via numOrNull.
  updateParcel/insertNewParcel write individual columns.
- main.js parcelsToGeoJSON — handles GeoJSON `geom` object (API) and
  `geometry_wkt` string (local cache); skips housekeeping fields.

Production access guard + no-district overlay:
- public/index.php — on *.lupmis4luspa.org, redirect to the SSO portal
  if no session.
- src/remotedb.js resolveDistrictId — no silent fallback to '1' for an
  authenticated user; dev mode (no session at all) keeps the fallback.
- main.js — blocking overlay if the session lacks district_id; init
  aborts so no API call is made with the wrong scope.

LayerSwitcher ordering fix:
- MapView.initEditBar + MapTools — find the Overlays group by reference
  / title instead of assuming it's the last layer (the GPS layers
  add-layered on top in the constructor broke that assumption).

Service Worker v8 → v9 to evict stale shell/module caches on deploy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-01 16:20:15 +02:00
cfaceb3487 GPS trail recording, SSO auth, account menu, and mobile/UI refinements
Major:
- GPS trail recording: reusable, dependency-free engine in src/geotracker/
  (GeoTracker + geo-utils) with pluggable storage/sync adapters; LUPMIS
  wiring in src/geotracker-lupmis.js. Expandable My Location control
  (Locate Me + Record Trail), live navbar GPS readout, on-map trail/position
  rendering, gps_trails/gps_trail_points SQLocal tables, and store-and-forward
  sync via pushGpsTrail() -> save_gps_trail.php (server side documented, not
  yet built).
- SSO authentication: public/index.php entry point validates the LUSPA SSO
  cookie and injects window.LUPMIS_SESSION; remotedb district_id is now a
  session-resolved getter. Adds public/.htaccess (DirectoryIndex).
- Account menu offcanvas (navbar burger) with sign-in/out states.

UI / fixes:
- LayerSwitcher modernisation; base-map "None" option in picker + settings.
- Mobile drawing toolbar wraps to two rows below 576px and shows only in
  Draw mode; second row right-aligned and clears the Select option bar.
- Safari bottom-dock clipping fixed (app-container 100dvh -> 100svh).
- Rename public/icons -> app-icons to dodge Apache's default /icons/ alias.
- Service Worker bumped to v8 (network-first HTML, per-provider tile clear).

Docs: reusable-mapping and OSM-3D-buildings concept notes; ignore Office
lock files (~$*). Rebuilt dist/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 16:08:37 +02:00
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
3009a11b33 intial pwa LUPMIS2 commit 2026-03-04 12:59:40 +01:00