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>
182 lines
11 KiB
XML
182 lines
11 KiB
XML
<?xml version="1.0" encoding="UTF-8"?>
|
||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 960 900" font-family="Arial, Helvetica, sans-serif">
|
||
<!-- ====================================================== -->
|
||
<!-- OpenTopography ETL Workflow for LUPMIS2 -->
|
||
<!-- A one-off data pipeline: download → process → serve -->
|
||
<!-- ====================================================== -->
|
||
|
||
<defs>
|
||
<!-- Arrowhead -->
|
||
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
|
||
<path d="M0,0 L10,5 L0,10 z" fill="#4b5563"/>
|
||
</marker>
|
||
<!-- Drop shadow -->
|
||
<filter id="shadow" x="-10%" y="-10%" width="120%" height="120%">
|
||
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
|
||
<feOffset dx="1" dy="2"/>
|
||
<feComponentTransfer><feFuncA type="linear" slope="0.18"/></feComponentTransfer>
|
||
<feMerge><feMergeNode/><feMergeNode in="SourceGraphic"/></feMerge>
|
||
</filter>
|
||
</defs>
|
||
|
||
<!-- Background -->
|
||
<rect width="960" height="900" fill="#f9fafb"/>
|
||
|
||
<!-- Title -->
|
||
<text x="480" y="42" text-anchor="middle" font-size="22" font-weight="700" fill="#1e1a4b">
|
||
OpenTopography → LUPMIS2 Topographic Workflow
|
||
</text>
|
||
<text x="480" y="66" text-anchor="middle" font-size="13" fill="#6b7280">
|
||
One-off ETL pipeline: download DEM → generate products → serve to the PWA
|
||
</text>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- STAGE 1 — SOURCE (blue) -->
|
||
<!-- ====================================================== -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="330" y="100" width="300" height="80" rx="10" ry="10" fill="#0ea5e9" stroke="#0369a1" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="480" y="128" text-anchor="middle" font-size="15" font-weight="700" fill="#fff">OpenTopography API</text>
|
||
<text x="480" y="148" text-anchor="middle" font-size="12" fill="#e0f2fe">SRTM 30m / Copernicus 30m DEM</text>
|
||
<text x="480" y="166" text-anchor="middle" font-size="11" font-style="italic" fill="#bae6fd">API key required · 50 calls/24h (non-academic)</text>
|
||
|
||
<!-- Arrow 1 -->
|
||
<path d="M480,180 L480,220" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<text x="495" y="205" font-size="11" fill="#6b7280">one-off download · Ghana bbox ≈ 240,000 km²</text>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- STAGE 2 — RAW DATA (grey) -->
|
||
<!-- ====================================================== -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="340" y="230" width="280" height="70" rx="10" ry="10" fill="#fff" stroke="#6b7280" stroke-width="1.5" stroke-dasharray="4,3"/>
|
||
</g>
|
||
<text x="480" y="258" text-anchor="middle" font-size="14" font-weight="700" fill="#1f2937">DEM GeoTIFF</text>
|
||
<text x="480" y="278" text-anchor="middle" font-size="12" fill="#4b5563">Ghana elevation raster (single file)</text>
|
||
<text x="480" y="294" text-anchor="middle" font-size="10" font-style="italic" fill="#6b7280">EPSG:4326 · ≈ 1 – 3 GB</text>
|
||
|
||
<!-- Split arrows -->
|
||
<path d="M480,300 L480,340 L230,340 L230,380" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<path d="M480,340 L730,340 L730,380" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<text x="420" y="332" text-anchor="end" font-size="11" fill="#6b7280">contours path</text>
|
||
<text x="540" y="332" font-size="11" fill="#6b7280">hillshade path</text>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- STAGE 3 — PROCESSING (green) -->
|
||
<!-- ====================================================== -->
|
||
<!-- Left: gdal_contour -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="90" y="390" width="280" height="80" rx="10" ry="10" fill="#10b981" stroke="#047857" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="230" y="418" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">gdal_contour</text>
|
||
<text x="230" y="438" text-anchor="middle" font-size="11" fill="#d1fae5">extract contour polylines at fixed intervals</text>
|
||
<text x="230" y="456" text-anchor="middle" font-size="10" font-family="Menlo, monospace" fill="#ecfccb">-i 10 (10 m) or -i 20 (20 m)</text>
|
||
|
||
<!-- Right: gdaldem hillshade -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="590" y="390" width="280" height="80" rx="10" ry="10" fill="#10b981" stroke="#047857" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="730" y="418" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">gdaldem hillshade</text>
|
||
<text x="730" y="438" text-anchor="middle" font-size="11" fill="#d1fae5">render shaded relief PNG</text>
|
||
<text x="730" y="456" text-anchor="middle" font-size="10" font-family="Menlo, monospace" fill="#ecfccb">-z 2 -az 315 -alt 45</text>
|
||
|
||
<!-- Arrows 3 → 4 -->
|
||
<path d="M230,470 L230,510" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<path d="M730,470 L730,510" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- STAGE 4 — DERIVATIVES (orange) -->
|
||
<!-- ====================================================== -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="90" y="520" width="280" height="70" rx="10" ry="10" fill="#fff" stroke="#f59e0b" stroke-width="2"/>
|
||
</g>
|
||
<text x="230" y="548" text-anchor="middle" font-size="14" font-weight="700" fill="#92400e">Contour polylines</text>
|
||
<text x="230" y="568" text-anchor="middle" font-size="11" fill="#78350f">Shapefile / GeoPackage / GeoJSON</text>
|
||
<text x="230" y="584" text-anchor="middle" font-size="10" font-style="italic" fill="#b45309">vector</text>
|
||
|
||
<g filter="url(#shadow)">
|
||
<rect x="590" y="520" width="280" height="70" rx="10" ry="10" fill="#fff" stroke="#f59e0b" stroke-width="2"/>
|
||
</g>
|
||
<text x="730" y="548" text-anchor="middle" font-size="14" font-weight="700" fill="#92400e">Hillshade raster</text>
|
||
<text x="730" y="568" text-anchor="middle" font-size="11" fill="#78350f">GeoTIFF / PNG tile pyramid</text>
|
||
<text x="730" y="584" text-anchor="middle" font-size="10" font-style="italic" fill="#b45309">raster</text>
|
||
|
||
<!-- Arrows 4 → 5 -->
|
||
<path d="M230,590 L230,630" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<path d="M730,590 L730,630" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- STAGE 5 — SERVE (purple) -->
|
||
<!-- ====================================================== -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="40" y="640" width="180" height="80" rx="10" ry="10" fill="#8b5cf6" stroke="#5b21b6" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="130" y="668" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">GeoServer</text>
|
||
<text x="130" y="688" text-anchor="middle" font-size="11" fill="#ede9fe">WMS endpoint</text>
|
||
<text x="130" y="706" text-anchor="middle" font-size="10" font-style="italic" fill="#ddd6fe">on-demand rendering</text>
|
||
|
||
<g filter="url(#shadow)">
|
||
<rect x="240" y="640" width="180" height="80" rx="10" ry="10" fill="#8b5cf6" stroke="#5b21b6" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="330" y="668" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">MBTiles</text>
|
||
<text x="330" y="688" text-anchor="middle" font-size="11" fill="#ede9fe">XYZ tile server</text>
|
||
<text x="330" y="706" text-anchor="middle" font-size="10" font-style="italic" fill="#ddd6fe">pre-rendered, fast</text>
|
||
|
||
<g filter="url(#shadow)">
|
||
<rect x="640" y="640" width="180" height="80" rx="10" ry="10" fill="#8b5cf6" stroke="#5b21b6" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="730" y="668" text-anchor="middle" font-size="14" font-weight="700" fill="#fff">Tile pyramid</text>
|
||
<text x="730" y="688" text-anchor="middle" font-size="11" fill="#ede9fe">XYZ / WMTS</text>
|
||
<text x="730" y="706" text-anchor="middle" font-size="10" font-style="italic" fill="#ddd6fe">gdal2tiles.py</text>
|
||
|
||
<!-- Split arrow from contour derivatives to BOTH GeoServer and MBTiles -->
|
||
<path d="M180,610 L130,610 L130,640" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<path d="M280,610 L330,610 L330,640" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<path d="M230,590 L230,610" stroke="#4b5563" stroke-width="2" fill="none"/>
|
||
<line x1="130" y1="610" x2="330" y2="610" stroke="#4b5563" stroke-width="2"/>
|
||
|
||
<!-- Label the "OR" on the left side -->
|
||
<text x="230" y="627" text-anchor="middle" font-size="10" font-weight="700" fill="#6b7280">serve as WMS or XYZ</text>
|
||
|
||
<!-- Arrows 5 → 6 — converging to LUPMIS2 -->
|
||
<path d="M130,720 L130,770 L460,770 L460,800" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
<path d="M330,720 L330,770" stroke="#4b5563" stroke-width="2" fill="none"/>
|
||
<line x1="330" y1="770" x2="460" y2="770" stroke="#4b5563" stroke-width="2"/>
|
||
<path d="M730,720 L730,770 L500,770 L500,800" stroke="#4b5563" stroke-width="2" fill="none" marker-end="url(#arrow)"/>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- STAGE 6 — CONSUMER (brand) -->
|
||
<!-- ====================================================== -->
|
||
<g filter="url(#shadow)">
|
||
<rect x="280" y="800" width="400" height="70" rx="10" ry="10" fill="#1e1a4b" stroke="#0f0c2a" stroke-width="1.5"/>
|
||
</g>
|
||
<text x="480" y="828" text-anchor="middle" font-size="15" font-weight="700" fill="#fff">LUPMIS2 PWA</text>
|
||
<text x="480" y="848" text-anchor="middle" font-size="11" fill="#c7d2fe">OpenLayers · addWMSLayer() / addXYZLayer()</text>
|
||
<text x="480" y="863" text-anchor="middle" font-size="10" font-style="italic" fill="#a5b4fc">"Biophysical Environment" group</text>
|
||
|
||
<!-- ====================================================== -->
|
||
<!-- LEGEND -->
|
||
<!-- ====================================================== -->
|
||
<g transform="translate(20,100)">
|
||
<rect width="140" height="180" rx="6" ry="6" fill="#fff" stroke="#d1d5db" stroke-width="1"/>
|
||
<text x="70" y="20" text-anchor="middle" font-size="12" font-weight="700" fill="#1f2937">Legend</text>
|
||
<rect x="10" y="32" width="20" height="14" rx="2" fill="#0ea5e9"/>
|
||
<text x="36" y="43" font-size="10" fill="#374151">External source</text>
|
||
<rect x="10" y="54" width="20" height="14" rx="2" fill="#fff" stroke="#6b7280" stroke-dasharray="3,2"/>
|
||
<text x="36" y="65" font-size="10" fill="#374151">Raw data file</text>
|
||
<rect x="10" y="76" width="20" height="14" rx="2" fill="#10b981"/>
|
||
<text x="36" y="87" font-size="10" fill="#374151">Processing step</text>
|
||
<rect x="10" y="98" width="20" height="14" rx="2" fill="#fff" stroke="#f59e0b" stroke-width="1.5"/>
|
||
<text x="36" y="109" font-size="10" fill="#374151">Derived product</text>
|
||
<rect x="10" y="120" width="20" height="14" rx="2" fill="#8b5cf6"/>
|
||
<text x="36" y="131" font-size="10" fill="#374151">Serving layer</text>
|
||
<rect x="10" y="142" width="20" height="14" rx="2" fill="#1e1a4b"/>
|
||
<text x="36" y="153" font-size="10" fill="#374151">Consumer</text>
|
||
<text x="70" y="172" text-anchor="middle" font-size="9" font-style="italic" fill="#6b7280">Run once · serve forever</text>
|
||
</g>
|
||
|
||
<!-- Footer note -->
|
||
<text x="480" y="888" text-anchor="middle" font-size="10" font-style="italic" fill="#6b7280">
|
||
Prepared for LUSPA · April 2026 · One-off ETL job — no runtime OpenTopography API calls from the PWA
|
||
</text>
|
||
</svg>
|