#108: Finland peaks — DEM-derived spot heights from MML dem25m #205
No reviewers
Labels
No labels
blocked
claude-wip
map-report
map:is
map:se
needs-review
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
art/ismap!205
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "issue-108-fi-peaks"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #108
Finland: peaks — DEM-derived spot heights from MML dem25m
The source decision
DEM-derived spot heights from MML's
dem25mnational elevation model. The issueasked for a source decision before a layer; the decision is forced by the contract
and the data:
korkeuspiste(52210) is empty in the 2025 delivery (measured on disk: 0rows in
MTK-muut), so the surveyed spot-height class is unavailable.paikannimicannot fill the peaks contract. Two independentdead ends: the register has no elevation column (verified in the staging
schema — R120 requires a valid, non-NULL elevation, and the style renders the
"NNN m" label from it), and the shared contract has no name column by design
(
conf/shared/contracts/19-peaks.yaml), so the register's names could not becarried anyway;
wireSharedLayersenforces the contract's attribute list exactly.Iceland (
haedarpunktar) and Sweden (hojdpunkt): geom + elevation, no name,elevation-thinned at coarse zoom — and it is national data. This is the issue's
own recommended default ("peaks = local maxima above a threshold").
So Finland's peaks are the local maxima of MML's 25 m elevation model (the
dem25mCOG delivery on Paituli: 84 tiles, 802 MB, EPSG:2393, 16-bit values indecimetres). The named-fell register remains what
placesalready uses for names;it is not a peaks source.
The mechanism:
importer/dem.goThe importer reads data files natively — no ogr2ogr, no gdal service — and had no
raster reader.
dem.gois its first: it parses the tiled 16-bit LZW COGs(TIFF IFD + geokeys + a faithful TIFF-LZW "early change" decoder, which Go's
stdlib
compress/lzwdoes not implement), decodes the grid, and finds local maxima.Seam handling is the part that had to be right. Consecutive tiles overlap by
exactly one cell (80 025 m extent on an 80 000 m grid), so a naive per-tile scan
would both miss peaks near seams and invent false ones where a higher neighbour
sits just across the seam. The reader decodes each tile with a
radius-cell halofrom its eight neighbours, and a peak is emitted only for cells in the tile's own
extent (the first 3200 cells in each axis) — every cell belongs to exactly one
tile, so no peak is duplicated or lost. The naive per-tile prototype over-counted
by ~24%; the halo version is the honest 26 296.
Parameters (measured over the whole country):
radius: 20cells (500 mde-clustering — one summit marker per ridge, not one per 25 m cell) and
min_elevation: 200m (the "above the lowland" floor — below it, Finland's flatlake plateau produces drumlins, not peaks). Result: 26 296 peaks ≥ 200 m at z14,
7 991 ≥ 300 m at z13, 885 ≥ 500 m at z11 — the same order as Sweden's 33.8k
surveyed spot heights. The coarse z11 floor (≥500 m) matches Iceland's and
Sweden's (R31's cadence).
Config
conf/fi/downloads.yaml— 84 dem25m tile entries (one URL per path, themechanism's shape; the comment records the R5 size).
conf/fi/staging.yaml—fi_dem_peaksstaging:dem_peaks: {radius: 20, min_elevation: 200}on a glob,source_srid: 2393,srs: EPSG:3857.conf/fi/16-peaks.yaml— the layer:extends: peaks, readsfi_dem_peaks,zoom filters 300/500, and three acceptance tests (count pin + no NULLs, Halti's
summit cell ≥ 1300 m, the R31 z11 floor).
importer/fetch.go—neededFilesnow glob-matches load globs againstdownloads, so a scoped
--layer peaksrun fetches the 84 tiles.and why
namestays out for every country.docs/country-finland.md(catalogue line, source table, known-gaps item),docs/architecture.md(dem.go in the source map),docs/workstream-a.md.Verification against the live database
make build,make test— green (plus a newTestLZWDecodeTiffround-trip andTestReadDEMPeaksagainst a synthetic COG;TestNeededFilesGlobMatchesDownloadscovers the fetch change).
importer config-check conf/fi— OK (16 layers, 19 shared contracts, 57 tests).Import:
importer import conf/fi/ --layer peaks— 84 tiles fetched throughthe importer's own download path (fetchmeta sidecars written, re-run is a no-op),
staging built in ~104 s, layer swapped live. Per-zoom row counts:
Partitions at every zoom (map_peaks_11_fi … map_peaks_14_fi), all with gist
indexes, all SRID 3857 (matching is/se).
Decoded tile over Halti (14/9160/3757):
scripts/mvt-decode.py→ layerpeaks, 1 feature,elevation=1310,country=fi.Halti's summit cell reads 1310 m (the 25 m DEM's value for the summit cell; the
official height is 1324 m).
Real-browser render over CDP (
scripts/cdp-map-check.mjs): Halti shows 1peak at 1310 m; Saana (Kilpisjärvi) shows 1 peak at 1020 m; Helsinki shows 0
peaks at z11 — the negative control (the 200 m floor correctly leaves the flat
south marker-free) and the positive controls (the fells present) both hold.
Acceptance tally (per-layer + shared-contract runs, pasted from the run):
make acceptanceruns green except a pre-existing Poland buildings test thattimes out at 10 s against the 5.6 M-row table even on a clean run — unrelated to
this change.
Notes / caveats
a different population from a cartographer's spot-height survey, which is why the
contract keeps the door open to a later named-fell layer (a separate shape, not a
fake partition — the R13 sibling-contract idiom). Nothing here prevents that;
the register is still the
placessource.(a 25 m cell averages the summit), and the layer's job is to carry the DEM's
reading, not correct it.
staging build — the halo scheme needs every tile's header up front. Fine on this
machine; noted here rather than silently streamed.
View command line instructions
Manual merge helper
Use this merge commit message when completing the merge manually.
Checkout
From your project repository, check out a new branch and test the changes.Merge
Merge the changes and update on Forgejo.Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.