11 Commits

Author SHA1 Message Date
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
203ca5bc4d Permit-iframe hardening, import UX refinements, workshop deliverables
Permit-iframe hardening:
- public/embed.php — replace the 302 redirect on unauthenticated visits
  with an in-iframe HTML "Sign in to view the map" card (HTTP 401)
  whose primary button uses target="_top" to break the iframe and send
  the parent window to the SSO portal. The 302 was broken UX inside an
  iframe because the LUSPA portal refuses to be framed.
- public/embed.php + public/.htaccess — strip X-Frame-Options at the
  embed endpoint (defence in depth). Apache's <Files "embed.php">
  Header always unset X-Frame-Options + PHP's header_remove() both
  ensure the only iframe-policy header on the response is our CSP
  frame-ancestors (which already allows the permits subdomain). Fixes
  Safari's "Refused to display ... because it set 'X-Frame-Options'
  to 'SAMEORIGIN'" when the container's reverse proxy injects it.

Import UX refinements:
- Spinner overlay (index.html #import-spinner-overlay + main.js
  showImportSpinner/hideImportSpinner) shown during the file-drop →
  mapping-modal gap. Wired at the top of each handle*Import and at
  every error / early-return path; hidden by stageImport() just before
  openImportMappingModal() so it spans both the JS parse and the
  SQLocal staging insert.
- Per-feature client_uuid tagging — each imported OL feature now
  carries _externalImportId + _clientUuid set in stageImport(). These
  tags are the link that lets later edits find the matching staging
  row, and they are passed through to addExternalImportFeatures.
- Geometry-edit persistence — new public callback registry
  MapView.onFeatureModified(cb) fired from a modifyend listener on
  _modifyInteraction. main.js handler writes the new WKT (EPSG:4326)
  back to external_import_features.geometry_wkt via new helper
  updateExternalImportFeatureGeometry(clientUuid, wkt). Non-imported
  features carry no tags, so the handler is a no-op for them.
- Delete persistence — removefeature listener on each imported layer's
  source. New helper deleteExternalImportFeature(clientUuid) runs an
  atomic DELETE + decrement of external_imports.feature_count and
  broadcasts the changes so the LayerSwitcher badge can recount.
- Field-mapping dropdown — sample values + bold field names.
  New helpers sampleSourceValues(fc) in import-detect.js (picks first
  non-empty value per attribute, JSON-stringifies objects, collapses
  whitespace, truncates to 35 chars) and toBoldUnicode(s) in
  import-modal.js (ASCII letters/digits → Mathematical Alphanumeric
  Symbols block). Options now read as "𝐮𝐩𝐧 — [12345-6789]";
  HTML/CSS bold doesn't render inside <option> elements, so Unicode
  bold codepoints are the cross-browser way.

Workshop deliverables:
- LUPMIS2_Improvements_Mar_to_Jun_2026.docx — handout mirroring the
  slide deck one-to-one (160 paragraphs, branded styling).
- LUPMIS2_Workshop_Mar_to_Jun_2026.pptx — 16-slide pptxgenjs deck
  (16:9 widescreen, brand palette, hero + content + closing masters,
  embedded staged-upload diagram on slide 9).
- LUPMIS2_Staged_Upload_Flow.svg + .png — three swim-lane diagram of
  the staged-upload pipeline with a dedicated "Client QA Gate"
  callout. Hand-crafted SVG + 2400 px PNG.

save_gps_trail.php diagnosis (no code change, on the database team):
the reported "CORS" error is a missing endpoint — Apache returns 404
with no CORS headers and the browser surfaces it as access-control.
Once the endpoint is deployed the API server's global CORS handling
attaches the right headers and the GPS-trail sync will work without
client changes.

dist/ rebuilt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-23 13:16:37 +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