Skip to content

[QMS-1280] Show surfaces and way types of tracks from OpenStreetMap data - #1282

Merged
kiozen merged 1 commit into
Maproom:devfrom
switch87:QMS-1280
Oct 17, 2026
Merged

kiozen merged 1 commit into
Maproom:devfrom
switch87:QMS-1280

Conversation

@switch87

@switch87 switch87 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

What is the linked issue for this pull request:

QMS-#1280

What you have done:

Surfaces and way types for every track, from a per-region surface database built from an OSM extract (.osm.pbf, the same file Routino databases are built from).

  • Database: CSurfaceDbBuilder reads the extract with a small own PBF reader (CPbfReader: zlib blocks, dense nodes, ways — no new library) and writes an SQLite file with an R*Tree over chunks of up to 16 way segments; tags are stored unchanged, classification happens when reading. Belgium: 205 MB, ~40 s, ~300 MB memory. GUI: Tool → Create Surface Database (worker thread, progress, cancel, choose active databases; also linked from the Routino panel). CLI: qms_surfacedb.
  • Classification (CSurface): highway/surface/tracktype/footway → paved / semi-paved / unpaved / unknown, a detailed surface and a way type; missing tags get the usual assumption for the highway type.
  • Lookup (CSurfaceDb, CSurfaceDbSet): the way at a position is the majority of the ways passing its ~20 m cell, nearest way within 25 m as fallback.
  • Analysis (CSurfaceAnalyzer, CSurfaceTrk, CSurfaceDbManager): sample every 20 m in a background job (Qt Concurrent, own pool, own DB connection per job, stale jobs cancelled), smooth single-sample crossings, sum km per class, way type and detailed surface. Derived data like slope, not saved.
  • UI: Info tab with two stacked bars + legend and a collapsible detail list (or a hint how to build a database); Style tab colour sources "Surface*" and "Way Type*" drawn per category with legend; track point tooltip.
  • Tests: optional BUILD_TESTING (off by default) with four QtTest tests run by ctest (classification table, PBF reader/builder/lookup on a tiny extract, analyzer, and a real 28 km hike that is skipped without external data).

New build requirement: Qt6::Concurrent (qtbase); Qt6::Test only with BUILD_TESTING=ON. SQLite needs the R*Tree module — present in the system SQLite on Linux; not tested with the SQLite Qt bundles on Windows/macOS.

Tested on Linux (Gentoo, Qt 6.11.2, GCC 15): builds, all 4 tests pass, clang-format, clang-tidy and clazy (level 0/1) clean on the new code. The same feature runs as a patch on 1.21.1 in daily use. Not tested on Windows or macOS. No .ts files touched; a Dutch translation (~70 strings) is ready if wanted.

Questions where I'd like your preference:

  1. Unit tests: is a BUILD_TESTING option with QtTest welcome, or should the tests go?
  2. Test data includes the Python prototype the classification was ported from (test/data/osmsurface.py, GPL-3.0-or-later) as source of the reference tables — keep it or only the generated tables?
  3. Qt Concurrent as a new component, or rather QThread?
  4. Default database folder is Surface/ next to the first Routino path, else the user data folder — fine, or a setting of its own?
  5. Category colours are fixed in CSurface (data colours like the activity colours) — OK with rule 1 of README_CODING?
  6. qms_surfacedb next to the qmt_* tools, built under BUILD_QMAPSHACK — OK?
  7. Three extra qint8 fields per track point in CTrackData::trkpt_t — acceptable?
  8. Squashed into one commit as the parts depend on each other; happy to split differently if that eases review.

Steps to perform a simple smoke test:

  1. Download a small extract, e.g. https://download.geofabrik.de/europe/luxembourg-latest.osm.pbf
  2. Tool → Create Surface Database: choose the file, name "Luxembourg", Build; wait for "Done".
  3. Load or draw a track in Luxembourg and open it (Edit): the Info tab shows "Surfaces" and "Way types" with bars and legend; "Surfaces in detail" expands the list.
  4. Style tab → Source "Surface*": each part of the track gets the colour of its surface, the map legend shows the four classes. Same with "Way Type*".
  5. Move a track point: after a moment the bars update.
  6. Untick the database in the tool tab: the Info tab shows how to build one.
  7. Optional: configure with -DBUILD_TESTING=ON, build, run ctest.

Does the code comply to the coding rules and naming conventions Coding Guideline:

  • yes

Is every user facing string in a tr() macro?

  • yes

Did you add the ticket number and title into the changelog? Keep the numeric order in each release block.

  • yes, I didn't forget to change changelog.txt

@kiozen

kiozen commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

Huh, this is a huge one. As I am currently traveling this will take quite some time to review.Please be patient.

@switch87

Copy link
Copy Markdown
Contributor Author

Rebased on current dev.

@wthaem

wthaem commented Oct 15, 2026

Copy link
Copy Markdown
Contributor

@switch87: I compiled this PR on Windows 11 without any trouble using the existing build environment. No additional step for use of Qt Concurrent required. I tested it with a track in Luxembourg. Way type and surface type are shown as expected. Nice new feature proposal!

@switch87

Copy link
Copy Markdown
Contributor Author

@wthaem Thanks for building and testing it on Windows, good to know Qt Concurrent needs no extra step there.

@kiozen

kiozen commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

I had some time to test, too. Nice feature!

From the first smoke test: Surface needs it's own tab in the track details dialog and probably it should be done in a scroll view. Reason: For small screens the splitter with the tab widget get to large in height.

@switch87

Copy link
Copy Markdown
Contributor Author

@kiozen Thanks for testing it and for the feedback! Good point about small screens: I'll move the surface view into its own tab in the track details, inside a scroll area.

@switch87

Copy link
Copy Markdown
Contributor Author

Done in 468b5cd: the surface view now has its own "Surface" tab next to Info, inside a scroll area, so the Info tab is back to its old height. I haven't touched the .ts files; the new tab title is a plain string in the .ui.

@kiozen

kiozen commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator

There are some compile issues now 😉

In file included from /home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/CDetailsTrk.h:26,
                 from /home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/CDetailsTrk.cpp:19:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h: In member function ‘void Ui_IDetailsTrk::setupUi(QWidget*)’:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:450:44: error: no matching function for call to ‘CSurfaceWidget::CSurfaceWidget()’
  450 |         widgetSurface = new CSurfaceWidget();
      |                                            ^
In file included from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:39:
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note: candidate: ‘CSurfaceWidget::CSurfaceWidget(QWidget*)’
   39 |   explicit CSurfaceWidget(QWidget* parent);
      |            ^~~~~~~~~~~~~~
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note:   candidate expects 1 argument, 0 provided
In file included from /home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/CDetailsTrk.h:26,
                 from /home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/CGisItemTrk.cpp:34:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h: In member function ‘void Ui_IDetailsTrk::setupUi(QWidget*)’:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:450:44: error: no matching function for call to ‘CSurfaceWidget::CSurfaceWidget()’
  450 |         widgetSurface = new CSurfaceWidget();
      |                                            ^
In file included from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:39:
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note: candidate: ‘CSurfaceWidget::CSurfaceWidget(QWidget*)’
   39 |   explicit CSurfaceWidget(QWidget* parent);
      |            ^~~~~~~~~~~~~~
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note:   candidate expects 1 argument, 0 provided
In file included from /home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/CDetailsTrk.h:26,
                 from /home/oeichler/tmp/qmapshack_switch/src/qmapshack/CMainWindow.cpp:43:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h: In member function ‘void Ui_IDetailsTrk::setupUi(QWidget*)’:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:450:44: error: no matching function for call to ‘CSurfaceWidget::CSurfaceWidget()’
  450 |         widgetSurface = new CSurfaceWidget();
      |                                            ^
In file included from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:39:
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note: candidate: ‘CSurfaceWidget::CSurfaceWidget(QWidget*)’
   39 |   explicit CSurfaceWidget(QWidget* parent);
      |            ^~~~~~~~~~~~~~
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note:   candidate expects 1 argument, 0 provided
make[2]: *** [src/qmapshack/CMakeFiles/qmapshack.dir/build.make:4144: src/qmapshack/CMakeFiles/qmapshack.dir/gis/trk/CDetailsTrk.cpp.o] Fehler 1
make[2]: *** Warte auf noch nicht beendete Prozesse...
make[2]: *** [src/qmapshack/CMakeFiles/qmapshack.dir/build.make:4186: src/qmapshack/CMakeFiles/qmapshack.dir/gis/trk/CGisItemTrk.cpp.o] Fehler 1
make[2]: *** [src/qmapshack/CMakeFiles/qmapshack.dir/build.make:2590: src/qmapshack/CMakeFiles/qmapshack.dir/CMainWindow.cpp.o] Fehler 1
In file included from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/qmapshack_autogen/SGKYG652A5/../../../../../src/qmapshack/gis/trk/CDetailsTrk.h:26,
                 from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/qmapshack_autogen/SGKYG652A5/moc_CDetailsTrk.cpp:9,
                 from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/qmapshack_autogen/mocs_compilation.cpp:86:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h: In member function ‘void Ui_IDetailsTrk::setupUi(QWidget*)’:
/home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:450:44: error: no matching function for call to ‘CSurfaceWidget::CSurfaceWidget()’
  450 |         widgetSurface = new CSurfaceWidget();
      |                                            ^
In file included from /home/oeichler/tmp/qmapshack_switch/build/src/qmapshack/ui_IDetailsTrk.h:39:
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note: candidate: ‘CSurfaceWidget::CSurfaceWidget(QWidget*)’
   39 |   explicit CSurfaceWidget(QWidget* parent);
      |            ^~~~~~~~~~~~~~
/home/oeichler/tmp/qmapshack_switch/src/qmapshack/gis/trk/surface/CSurfaceWidget.h:39:12: note:   candidate expects 1 argument, 0 provided
make[2]: *** [src/qmapshack/CMakeFiles/qmapshack.dir/build.make:2562: src/qmapshack/CMakeFiles/qmapshack.dir/qmapshack_autogen/mocs_compilation.cpp.o] Fehler 1
make[1]: *** [CMakeFiles/Makefile2:756: src/qmapshack/CMakeFiles/qmapshack.dir/all] Fehler 2
make: *** [Makefile:156: all] Fehler 2

@switch87

Copy link
Copy Markdown
Contributor Author

Sorry about that, I pushed the tab change without building it. Fixed in 564c7e5: uic creates the widget inside a scroll area without a parent, so CSurfaceWidget now has a default parent of nullptr. This time I built it locally (qmapshack target links cleanly).

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Unit tests: is a BUILD_TESTING option with QtTest welcome, or should the tests go?

Good question. Somehow establishing tests was never really done in this project. And I fear they get outdated over time as they will be ignored when updating the code. My current best practice is to create test on the fly by AI when tinkering on a complex topic. And remove them.

So unless you say the test are really important for future changes I would tend to remove them.

Test data includes the Python prototype the classification was ported from (test/data/osmsurface.py, GPL-3.0-or-later) as source of the reference tables — keep it or only the generated tables?

Keep it if you keep the test.

Qt Concurrent as a new component, or rather QThread?

I think Qt Concurrent is fine.

Default database folder is Surface/ next to the first Routino path, else the user data folder — fine, or a setting of its own?

The established concept for those paths is a list of paths. The idea is to have data on multiple disks. e.g. some everyday usage data on your internal disk and huge datasets on an external disk. This is the same for Routino, maps and DEM.

Category colours are fixed in CSurface (data colours like the activity colours) — OK with rule 1 of README_CODING?

Yes that's fine. If that is really an issue later it can be changed.

qms_surfacedb next to the qmt_* tools, built under BUILD_QMAPSHACK — OK?

Yes, that's fine

Three extra qint8 fields per track point in CTrackData::trkpt_t — acceptable?

Yes, that's fine

Squashed into one commit as the parts depend on each other; happy to split differently if that eases review.

No that's fine. If we are done with the review squash it again. Or amend the commit.

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Some remarks on the UI:

  • Routino setup: align the surface button with the comboboxes.

  • Surface tab in track details: should be placed before or after Activities.

  • The legend box does not adjust to content. This looks a bit peculiar. Fix it if it is not too much of a change.

  • When a track is highlighted and the track details dialog is open and the surface coloring is selected, disabling the database has some peculiar side effects. Track color becomes white. The combobox to select the coloring has two empty entries. I know this is an edge case. Only fix it if the fix does not become a behemoth in code.

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

These are the results of an AI review:

Bugs
1. gis/trk/surface/CSurfaceTrk.cpp:134: when an outdated result arrives, slotFinished() drops it but leaves hashRunning set. Edit to B, undo to A before B finishes, then redo to B: no new job starts, and the Surface tab stays on "Analyzing…" for good.
2. gis/trk/CGisItemTrk.cpp:1900: with Surface or Way Type as the colour source, points set to -1 aren't drawn. With no database, or while an analysis is running, only the track outline shows. ql:surface also drops out of existingExtensions, so you can't reset the colour source from the combo.
3. gis/trk/surface/CSurfaceTrk.cpp:106: possible data race. update() runs in the track-loader thread while updateSurface() runs in the GUI thread, with no locking between them. On top of that, covers() reads databases while rescan() replaces it, and the CSurfaceDbManager singleton can end up owned by a loader thread.
4. gis/trk/surface/CSurfaceDbBuilder.cpp:302: the return value of db.commit() is ignored. If the commit fails, the half-written .part file still replaces the old database and the builder reports "Done".
5. gis/trk/surface/CPbfReader.cpp:234: HistoricalInformation is accepted, but versions are ignored. A .osh.pbf history file therefore imports old and deleted ways, which skews the surface lookup. LocationsOnWays is also accepted but never read.

Performance
6. gis/trk/surface/CSurfaceTrk.cpp:121: every job builds a new CSurfaceDbSet. That means reopening every database, reclassifying the tags and starting with an empty cell cache, both on each edit and for every loaded track when the database set changes. One set per pool thread would avoid that.

User-facing details
7. tool/CSurfaceDatabaseBuilder.cpp:50 and CSurfaceWidget.h:31: the text still says the surfaces are in the "Info tab", but they now have their own Surface tab.
8. gis/trk/CDetailsTrk.h:97: inserting eTabSurface at index 1 shifts the saved visibleTab, so after upgrading users reopen one tab earlier than the one they left.

I did not verify them. So take them with quite a grain of salt.

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks for passing the review on, even unverified it was worth it: most points were real. I checked each one against the code:

  1. hashRunning after undo/redo: real. An outdated result was dropped but hashRunning stayed set, so edit → undo → redo got stuck on "Analyzing…". A finished job now always clears it, and going back to a known result cancels the running job. 16f22ac
  2. -1 points not drawn / ql:surface dropping out: real, and I think it's also the white track in your edge case. Stretches without a result (no database, analysis running) are now drawn in the track's normal color. The chosen color source also stays in the list without a database, so the details dialog doesn't silently reset it with an extra undo step. 5476290
  3. Data race / singleton ownership: mostly real, though the cause was a bit different. The project loaders create tracks in the GUI thread, but the database export thread creates and destroys tracks in its own thread, and CSurfaceTrk moved itself to the GUI thread there. Now CSurfaceTrk and CSurfaceDbManager are GUI-thread only (asserted in debug), start() is called directly, and a track created outside the GUI thread doesn't analyze surfaces at all (it's never shown anyway). So covers() and rescan() always run in the same thread. 16f22ac
  4. db.commit() ignored: real. A failed commit (or transaction()) now sets the error, so the .part file is removed and the old database stays. bb761a9. No test for this one, I couldn't make SQLite fail a commit without fault injection.
  5. History / LocationsOnWays: real. .osh.pbf files are now refused with a message asking for a regular extract. LocationsOnWays is no longer accepted as a required feature, since we never read it and such files can lack the nodes. Tests with handmade header blocks. 66ddf22
  6. CSurfaceDbSet per job: real, though opening the files is cheap. The costly parts (classifying the tags, the empty cell cache) only depend on the file, so they're now shared by all objects and threads, keyed by path + mtime/size/created so a rebuilt file gets fresh data. The cell cache is bounded. I didn't go for one long-lived set per pool thread: that keeps the files open, and on Windows a rebuild then can't replace the file. Test included. b1e5da3
  7. "Info tab" text: fixed in the builder help and the widget doc. b2d2ffb
  8. visibleTab shift: real. The tab is now saved by object name, and an index saved by 1.21 or older is mapped to the new order once. I also moved the Surface tab after Activity, as you asked in the UI remarks. ceba3c7

Built (Debug, BUILD_TESTING=ON), all surface tests pass, including the Durbuy check against my Belgium database. I haven't gotten to your other UI remarks yet (Routino button alignment, legend size, the list of paths), they come next.

@switch87

Copy link
Copy Markdown
Contributor Author

Your other UI remarks:

  • Routino button alignment: the "Surface databases..." button now sits in the combobox column only, so it lines up with the comboboxes. 4b8caed
  • Surface tab position: already after Activity since ceba3c7.
  • Legend box: with Surface or Way Type as color source the legend on the map now takes the height of the list instead of the gradient's 300 px, and goes back to it for the other sources. 2a45bbe
  • List of paths: the surface databases now use a list of paths like maps, DEM and Routino, set up with the same dialog as the Routino paths (button next to "Path" in the builder tab). A new database goes into the path picked there. The single directory from earlier builds of this branch becomes the first entry of the list. 7d70921
  • Edge case (white track, empty entries): covered by 5476290 from the last round: without a database the stretches keep the track color and the chosen color source stays in the combobox, so it isn't reset anymore. Let me know if you still see the empty entries.

Built (Debug, BUILD_TESTING=ON), surface tests pass, clang-format clean.

@kiozen

kiozen commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Here another review round (AI generated):

Likely bugs

  1. Possible crash at exit (gis/trk/surface/CSurfaceTrk.cpp:31-38, :51-57): the surface jobs run on a thread pool created with new QThreadPool() that is never deleted or waited for. stop() only sets the cancel flag, so jobs inside a SQLite query can still be running while the app shuts down. Whether it actually crashes is unverified.
  2. Database replace isn't atomic (gis/trk/surface/CSurfaceDbBuilder.cpp:322-327): it removes the old file, ignores whether that worked, then renames the new one into place. If the rename fails, the old database is already gone. On Windows, if the file is open the remove fails and so does the rename, so the build ends with "Failed to write" after all the work. That Windows lock behaviour is unverified.
  3. Missing nodes are bridged (gis/trk/surface/CSurfaceDbBuilder.cpp:223-225): continue; // node missing in the extract joins the nodes on either side into a straight segment that was never in OSM. Clipped extracts would then get a wrong surface or way type along that made-up line.
  4. Gaps at bends (gis/trk/CGisItemTrk.cpp:1897): drawColorizedByCategory() draws each segment as its own line with Qt::FlatCap, while the other colour modes use RoundCap (:1860, :1962). With a thick pen that leaves wedge-shaped gaps at every bend.
  5. Database on/off is tracked by file name only (gis/trk/surface/CSurfaceDbManager.cpp:81-88): two files with the same name in different folders are switched on and off together.
  6. Status label colour sticks (tool/CSurfaceDatabaseBuilder.cpp:276, :279): markLabel is set to Ok or Error when a build finishes and never reset in slotStart(). After a failure, the next run's progress shows in the error colour.

Performance
7. A full redraw per track (gis/trk/CGisItemTrk.cpp:901-911): updateSurface() refreshes the details view, updates all visuals and triggers a complete canvas redraw. That happens for every track each time its analysis finishes and each time a database is switched on or off. With many tracks loaded that means many full redraws; I haven't measured it.

Breaks CLAUDE.md rules
8. Unused migration code (gis/trk/surface/CSurfaceDbManager.cpp:40-45): it converts a Surface/path setting, but the file doesn't exist on origin/dev. That setting was added in 31faa89 and replaced in 7d70921, both on this branch, so no user has it. The comment "the first versions had a single directory" is the kind of history note CLAUDE.md rules out.
9. Wrong doc-comment style: there are 88 added lines with /// (git diff origin/dev...HEAD | grep -c '^+.*///'). CLAUDE.md wants /** */ and /**< */.
10. Function order in .cpp files: CLAUDE.md wants the constructor and destructor first. CSurfaceDb.cpp has attachShared() before the constructor and readTags() between the constructor and destructor; CSurfaceDbManager.cpp:31 has self() before the constructor. I haven't re-read these two files myself.

And we need better icons 😄 I attached a few suggestions. But if you have better ones in mind go ahead.

surface_icons.zip
way_icons.zip

README_ICON.md tells you how to add icons.

@kiozen

kiozen commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Somehow my last comment is not visible on the web UI:

Here another review round (AI generated):

Likely bugs

  1. Possible crash at exit (gis/trk/surface/CSurfaceTrk.cpp:31-38, :51-57): the surface jobs run on a thread pool created with new QThreadPool() that is never deleted or waited for. stop() only sets the cancel flag, so jobs inside a SQLite query can still be running while the app shuts down. Whether it actually crashes is unverified.
  2. Database replace isn't atomic (gis/trk/surface/CSurfaceDbBuilder.cpp:322-327): it removes the old file, ignores whether that worked, then renames the new one into place. If the rename fails, the old database is already gone. On Windows, if the file is open the remove fails and so does the rename, so the build ends with "Failed to write" after all the work. That Windows lock behaviour is unverified.
  3. Missing nodes are bridged (gis/trk/surface/CSurfaceDbBuilder.cpp:223-225): continue; // node missing in the extract joins the nodes on either side into a straight segment that was never in OSM. Clipped extracts would then get a wrong surface or way type along that made-up line.
  4. Gaps at bends (gis/trk/CGisItemTrk.cpp:1897): drawColorizedByCategory() draws each segment as its own line with Qt::FlatCap, while the other colour modes use RoundCap (:1860, :1962). With a thick pen that leaves wedge-shaped gaps at every bend.
  5. Database on/off is tracked by file name only (gis/trk/surface/CSurfaceDbManager.cpp:81-88): two files with the same name in different folders are switched on and off together.
  6. Status label colour sticks (tool/CSurfaceDatabaseBuilder.cpp:276, :279): markLabel is set to Ok or Error when a build finishes and never reset in slotStart(). After a failure, the next run's progress shows in the error colour.

Performance
7. A full redraw per track (gis/trk/CGisItemTrk.cpp:901-911): updateSurface() refreshes the details view, updates all visuals and triggers a complete canvas redraw. That happens for every track each time its analysis finishes and each time a database is switched on or off. With many tracks loaded that means many full redraws; I haven't measured it.

Breaks CLAUDE.md rules
8. Unused migration code (gis/trk/surface/CSurfaceDbManager.cpp:40-45): it converts a Surface/path setting, but the file doesn't exist on origin/dev. That setting was added in 31faa89 and replaced in 7d70921, both on this branch, so no user has it. The comment "the first versions had a single directory" is the kind of history note CLAUDE.md rules out.
9. Wrong doc-comment style: there are 88 added lines with /// (git diff origin/dev...HEAD | grep -c '^+.*///'). CLAUDE.md wants /** */ and /**< */.
10. Function order in .cpp files: CLAUDE.md wants the constructor and destructor first. CSurfaceDb.cpp has attachShared() before the constructor and readTags() between the constructor and destructor; CSurfaceDbManager.cpp:31 has self() before the constructor. I haven't re-read these two files myself.

And we need better icons 😄 I attached a few suggestions. But if you have better ones in mind go ahead.

surface_icons.zip
way_icons.zip

README_ICON.md tells you how to add icons.

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks for the second round, and for the icons! I went through each point against the code:

  1. Thread pool never waited for: real. The pool was a leaked static. It's now a member of CSurfaceDbManager. On aboutToQuit the tracks cancel their jobs, the queue is cleared and the pool is waited for. 934e0ab
  2. Database replace not atomic: real. The old file was removed before the rename, and the result of the remove was ignored. Now the old file is moved aside to .old first, the new one is renamed into place, and if that fails the old one is moved back. If the old file can't be moved aside (e.g. locked on Windows), the build reports it and the old database stays as it was. I couldn't test the Windows lock itself, but there are tests for replacing and for a failed rename. 3f72ae3
  3. Missing nodes bridged: real. A node missing from the extract now splits the way into separate stretches instead of joining its neighbours. The fixture's way 14 now has a missing node in the middle, and the Python reference that generates the expected cells splits it the same way. I regenerated the fixture with it and added a test for the gap. df2851f, dadb167. The Durbuy check against the Belgium extract gives the same numbers as before.
  4. FlatCap: real, it's RoundCap now, like the other modes. f23f161
  5. On/off by file name: real. It now goes by the canonical path. That setting was only ever on this branch, so I didn't add a migration. 8a11cd1
  6. Status label colour sticks: real. Every message now sets its role: progress is eInfo, done is eOk, failures (and a path that can't be created) are eError. b91525c
  7. Full redraw per track: partly. triggerCompleteUpdate() only sets a flag and calls update(), so redraws that come in together were already merged, but finished analyses come in one by one. Now the map is only redrawn for tracks colored by Surface or Way Type (for any other color source nothing on the map changes), and those requests are bundled into one redraw at most every 200 ms. The details/plots of the track itself still update right away. 7a66882
  8. Unused migration / history comment: real, Surface/path never reached dev. Removed, and the comment went with it. 13def2a
  9. /// comments: converted to /** @brief */ and /**< */ in all the code this PR adds, tests included. 0cb086e
  10. Function order: real in the two files the review named. CSurfaceDb.cpp and CSurfaceDbManager.cpp now have the constructor and destructor first. I checked the other new .cpp files and they were already fine. f057413

Icons: I took two of yours. S1b (half asphalt, half gravel) is now CSrcSurface and W5 (three different ways) is CSrcWayType, following README_ICON.md (svghygiene, 32/48 PNGs, .svgt in resources.qrc). They're used for the Surface and Way Type color sources in the combobox, and the surface icon also for "Create Surface Database". I picked those two because they still read at 16 px. W2 looks a lot like a hamburger menu at that size. 9971f87

I also ran clang-format over CKnownExtension.cpp, where a newer clang-format wanted two lines wrapped differently (95daa2b).

Built (Debug, BUILD_TESTING=ON): qmapshack, qms_surfacedb and the surface tests. All tests pass, including the Durbuy check with a database built from scratch, and clang-format is clean.

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Ok, we are getting close. Another batch:

Bugs
1. Default surface path moves (CSurfaceDbManager.cpp:35): when no path has been saved, the default folder is worked out from the first Routino path on every start. Reorder your Routino paths and databases already built in the old folder are no longer found, and every track says "No surface database covers this track".
2. Surface and Way Type offered as graph sources (CKnownExtension.cpp:204): they now show up in the Graph 2/3 combo. Picking one plots the category numbers (0–3 or 0–8) as if they were measurements, and plots nothing when there is no database.
3. Cancel can be ignored for a long time (CSurfaceAnalyzer.cpp:58): cancellation is only checked once per track point, but one long leg can produce thousands of samples, each a database lookup. stop() and quitting the app then wait until that leg is done.
4. Old surface data stays when no points are visible (CSurfaceTrk.cpp:79): the "no track" case neither clears the surface fields nor stops the running job, unlike the "no database" case. Tooltips and colouring show old values.
5. Non-ASCII names collapse (CSurfaceDatabaseBuilder.cpp:145): the file-name cleanup's \w only matches ASCII letters, so 北京 and 東京 both become _.surface.sqlite and the second build overwrites the first.

Efficiency
6. Full refresh on every update (CGisItemTrk.cpp:911): each surface update refreshes all of the track's visuals. Ticking one database causes 2 full refreshes, plot rebuild included, for each loaded track. Only the details, colour legend and canvas need updating.
7. Rescan opens every database (CSurfaceDbManager.cpp:116): rescan() fully opens each file, reading and classifying all its tags on the GUI thread, just to get its metadata. Those files then stay cached for the whole session, even ones switched off or deleted. Reading only the meta table would avoid both.

Design and style
8. Copied dialog (CSurfaceDbPathSetup.cpp): a line-for-line copy of CRouterRoutinoPathSetup, even using the same UI form. Passing the title and help text as parameters would remove the duplicate.
9. Hardcoded categorical checks: whether a source is categorical is decided by comparing against the surface and way-type keys, in about 8 places across 4 files. A category list and flag on the CKnownExtension entry would replace those checks.
10. Plain int in CColorLegend.cpp (categoriesOf, paintCategories) and CMainWindow::slotCreateSurfaceDatabase: the project's CLAUDE.md asks for qint32 and qsizetype.

Let's see if these are still relevant.

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks, all ten were still relevant. Pushed as separate commits:

  1. Default path moves – confirmed. The default is now worked out once, on the first start without a saved Surface/paths, and saved right away, so reordering the Routino paths later no longer moves it. Whoever already uses the default gets the same folder as before. c127320
  2. Surface/Way Type as graph source – confirmed. CPropertyTrk::fillComboBox() skips categorical sources, so they are only offered as colour source. ab3b1ee
  3. Cancel within a long leg – confirmed. The cancel flag is now checked for every sample. New test: a 111 km leg canceled after 10 lookups stops at 10 (it was 5560 before). bf29cad
  4. Old data with no visible points – confirmed. That case now clears the surface fields and stops the job, same as "no database". aca49b5
  5. Non-ASCII names – confirmed. The cleanup moved to CSurfaceDbBuilder::fileName(), uses UseUnicodePropertiesOption and NFC normalisation. Tests for 北京, 東京, a decomposed "Liège", quotes, a path and an empty name. 41277ac
  6. Full refresh – confirmed. updateSurface() now updates only eVisualDetails | eVisualColorLegend and the canvas (the canvas only when a surface colour source is active). 02f44e7
  7. Rescan opens every database – confirmed. CSurfaceDb has a meta-only mode for the rescan, the tags are read by the jobs that need them. The rescan also drops the shared data of files that are switched off or deleted (CSurfaceDb::releaseShared()). Tests: meta-only works on a file without a tags table, and the cache release. c9b86dc
  8. Copied dialog – confirmed. CSurfaceDbPathSetup is gone. CRouterRoutinoPathSetup got a second constructor taking the title, help text and directory caption. The Routino constructor passes its old texts in the same translation context, so Routino behaves as before. 8ab745d
  9. Hardcoded categorical checks – confirmed. A CKnownExtension entry now has a categories list (colour and name per value) and isCategorical(). The legend, track drawing, toString(), min/max and the details all use that list. Only updateExtremaAndExtensions() still names the two keys, because that is where the surface analysis adds them. 475a81c
  10. int – confirmed. Changed to qint32 in CColorLegend, CMainWindow::slotCreateSurfaceDatabase(), the builder tab and the tests. slotItemChanged(..., int column) keeps Qt's signal signature. 9574c87, cec778c (clang-format)

Debug build with BUILD_TESTING=ON is clean, ctest passes and clang-format is clean on all changed files.

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

And more 😄 Looks like the agents now got pretty deep into the rabbit's den.

@JFritzle can you check the "Packaging and platform" section?

Functional bugs
1. Map colour disagrees with the Surface statistics.
   - In CGisItemTrk.cpp:1903-1912, each leg is drawn in the colour of its start point. CSurfaceAnalyzer.cpp:69-97 counts every sample along the leg (every 20 m).
   - A 600 m leg that is half asphalt and half gravel is drawn in one colour but counted 50/50. The comment "like the analysis…" is wrong.
2. The previous build's name overwrites a different region.
   - CSurfaceDatabaseBuilder.cpp:111 restores the last name, and :184 only fills in a new one when the field is empty.
   - Build Belgium, then pick the France extract: the target is still Belgium.*. The "replace?" prompt looks like a routine rebuild, and accepting it replaces Belgium with French data.
3. Missing icon.
   - CSurfaceDatabaseBuilder.cpp:65 uses ://icons/FileLoad.svgt. grep -rln FileLoad src/ finds only that line: there is no icon file and no .qrc entry.
   - The source picker button will be empty.
4. Track details open on the wrong tab.
   - IDetailsTrk.ui:411 sets currentIndex to 4 (it is 3 in dev).
   - CDetailsTrk.cpp:604-610 returns without selecting a tab when the saved tab name isn't found. The dialog then opens on the Surface tab.

Performance
5. Every track is reanalysed on the GUI thread when the set of databases changes.
- CSurfaceDbManager.cpp:150-152 emits sigChanged. Each track's CSurfaceTrk.cpp:144-148 then calls trk.updateSurface() synchronously, including tracks the changed database doesn't cover.
- With hundreds of tracks loaded this is a likely UI freeze (not measured).
6. Cancelled jobs still open every database. CSurfaceTrk.cpp:117 constructs CSurfaceDbSet set(files) before the analyzer's first cancel check.

Robustness against bad PBF input
7. Unbounded decompression.
- CPbfReader.cpp:318-326 compares the decompressed size only after qUncompress() has returned.
- A corrupt blob that claims a small size can still inflate to many GB. This assumes qUncompress grows its buffer past the size hint, which I haven't tested.
8. Empty error message. At CPbfReader.cpp:258-261, a 0-byte file makes open() fail without setting an error, so the dialog shows just "Failed: ".
9. No coordinate range check. CPbfReader.cpp:129-131 computes qint32((offset + granularity*value)/100) unchecked, so bad input can overflow or wrap.

Cleanup
10. Dead branch. CMainWindow.cpp:1455 handles the "CreateSurface" link, which nothing emits; this is the only occurrence in src/qmapshack (grep -rn).

Agent findings I haven't checked

Treat these as leads; some of the agents' line numbers were wrong.

Packaging and platform
- CLI only packaged on Linux: qms_surfacedb has only a Linux install rule. It is missing from msvc_64/copyfiles.bat, the NSIS script and the MacOSX/bundle-*.sh scripts.
- R*Tree availability: the builder creates an R*Tree table (CSurfaceDbBuilder.cpp:185, which I did confirm). Whether the SQLite bundled with Qt on Windows/macOS includes R*Tree is unknown.

Parser and database
- String indices: an index of 2³² or more is truncated to 32 bits before the range check, so it can silently pick the wrong tag.
- Read errors as end of file: a read error, or a file cut exactly at a block boundary, is treated as a normal end. The database is then built from partial data without a warning.
- Sort order: ties go to the way seen first, so an unsorted PBF gives different results than the documented "lowest OSM id".
- Antimeridian and poles: neither is handled, so the nearest-way fallback misses ways across ±180°.
- Cache race: a check-then-insert race in the shared cache can keep a switched-off database's cells cached.

Analysis and threads
- No nearby way: samples with no way nearby run two uncached SQL queries each. The 200k-cell cache is also cleared all at once when full.
- Memory: each track keeps its own copy of the point list for the analysis. This was estimated at about 300 MB for 1000 tracks of 10k points; not measured.
- Stale values while analysing: the hover info and the map show old surface values until a job finishes.
- First database scan: it runs while holding mutexItems during project load, which stalls on slow disks.
- Quitting: a cold-cache readTags can't be interrupted, so it can delay quitting.

GUI
- Builder during a build: the source and the paths can still be changed while a build runs.
- Canvas legend: it lists the categories even when the track falls back to its plain colour.
- Misleading hint: "Build one…" is shown when the covering database is merely switched off.
- Cancel shown as error: a cancel appears as a red "Failed: Canceled.".
- Area column: the text has no tr() and is always labelled "N/E", even for western or southern extracts.
- Source picker start folder: it opens in the Routino path instead of the last source folder.
- Clipped legend: long category names are cut off at the legend's 300 px cap.

@switch87

Copy link
Copy Markdown
Contributor Author

About the unit tests: I'd like to keep them in this PR. In more than ten years as a developer, especially on projects where several people work together, I've seen that projects with tests run into far fewer problems with existing code later on. So I don't remove tests myself. Once it's merged, what you do with them is of course your call.

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks, this round went deep indeed 😄 I checked every point against the code, the unchecked leads too. Each fix has a test where it can be tested outside the GUI.

Functional bugs

  1. Map colour vs. statistics: real. The result now also lists where the classification changes within a leg, and the map draws the leg in pieces at those changes, so a 600 m leg half asphalt/half gravel is drawn half/half, as it is counted. The wrong comment is gone. 6177301
  2. Previous name overwrites another region: real. A newly picked extract now always gets its own name (CSurfaceDbBuilder::nameForSource(), tested). The replace question also says what the existing file holds and what replaces it, e.g. "It holds "Belgium", built from belgium-latest.osm.pbf. Replace it with the data of france-latest.osm.pbf?". a66b42a
  3. Missing icon: real, FileLoad.svgt doesn't exist. It's PathGreen.svgt now, the icon the Routino builder uses for its source files. 009d671
  4. Wrong tab: real. currentIndex in the .ui is back to 3 as in dev, and an unknown saved name or an index out of range opens the Info tab. 02d9758

Performance

  1. Every track reanalysed when the databases change: partly. The analysis itself already ran in the pool, but every track restarted it and was redrawn, also tracks far from the changed database. The manager now gives a track the databases covering its area, with a key that changes when one of them is switched, added, removed or rebuilt. A track whose key didn't change keeps its result and does nothing. d8804cc
  2. Canceled jobs open every database: real. A job canceled before it starts returns at once, and a job only gets the files that cover its track. Test: a canceled job leaves the file cache empty. d8804cc

Bad PBF input

  1. Unbounded decompression: real, qUncompress() takes the size only as a hint and keeps growing the buffer. It now decompresses into a buffer of the announced size with GDAL's CPLZLibInflateEx() (no resize), and longer data is an error. GDAL is linked anyway, so no new dependency; qms_surfacedb and the tests now link GDAL::GDAL too. e6b03e8
  2. Empty error message: real, a 0-byte file now says "The file is empty.". e6b03e8
  3. No coordinate range check: real. Computed with qMulOverflow()/qAddOverflow(), and outside ±90/±180 degrees is an error. e6b03e8

Cleanup

  1. Dead "CreateSurface" branch: real, removed. f18d7bc

Parser and database (leads)

  • String indices: real. A value of 2³² + 1 became the valid index 1. Values too big for the field type are now an error. e6b03e8
  • Read errors as end of file: a read error is now reported (e6b03e8). A file cut exactly at a block boundary can't be told apart from a complete one: PBF has no end marker or block count. The OSM header's bounding box doesn't help either.
  • Sort order: real. Ways are now written sorted by OSM id, so an unsorted file gives the documented "lowest OSM id" tie-break. The test (two ways on the same nodes, the higher id first in the file) fails without the fix. dce3c8c
  • Antimeridian and poles: true, not handled. I left it as is: no road network extract crosses the poles, and for ±180° only the nearest-way fallback within 25 m of the line is affected (Fiji, Chukotka, Chatham Islands). Handling it would mean splitting every query box. Happy to do it if you think it's worth it.
  • Cache race: real, but small. Two jobs with a cold cache both read the tags and the second replaced the first's data. It now takes the data that's already there (c1dfaa8). A job that started just before its database was switched off can still put that file back in the cache. It's only memory, and the next rescan drops it.

Analysis and threads (leads)

  • No nearby way: partly wrong. The cell lookup is cached, "no way here" included. Only the nearest-way query is not, because it depends on the exact position, and it's one R*Tree query on a ~50 m box. Clearing the 200k-cell cache all at once is on purpose: a real LRU (QCache) would mean one allocation per cell. 200k cells is ~4000 km of track.
  • Memory: real. The track kept its point list after the job got its copy. Now only the job has it. d8804cc
  • Stale values while analysing: true, and on purpose. Points that didn't change keep their surface until the new result is there (usually well under a second), so the track doesn't flash back to its plain colour on every edit. The changes within legs are dropped at once, because they belong to the old point indices. Without a database, or with no visible points, the values are cleared right away.
  • First database scan under mutexItems: real. The manager is now created with the main window, before any project loads. 6ab602b
  • Quitting during a cold readTags(): true. A single SQLite query over the tags table can't be interrupted. It's the tag combinations, not the ways (1457 rows for Belgium), so quitting waits at most a moment. I left it.

GUI (leads)

  • Builder during a build: real. The extract and path buttons are disabled too now. c4c9ecf
  • Canvas legend with plain colour: real. The legend is hidden while the track has no surfaces. cba7ddf
  • Misleading hint: real. With a switched-off database covering the track, the Surface tab now says that. da45c1f
  • Cancel shown as error: real. It now shows "Canceled." as information. c4c9ecf
  • Area column: real. Translatable and by hemisphere ("55.98 S..21.78 S, 73.99 W..53.64 W"), tested. c4c9ecf
  • Source picker start folder: not reproducible. It opens in the folder of the last extract, which is restored from the settings. The Routino path is only the fallback when no extract was ever picked.
  • Clipped legend: on the map the box sizes itself to the names. In the track details the legend has the 300 px maximum width from dev's .ui, so a long translated name is now elided instead of cut. cba7ddf

Packaging and platform (@JFritzle, I'll leave the Windows/macOS decisions to you)

  • CLI only packaged on Linux: true. install(TARGETS qms_surfacedb ...) is the normal CMake install, but msvc_64/copyfiles.bat, CopyFilesGis.bat and the macOS bundle scripts copy every executable by name. I didn't touch them: whether to ship the CLI on Windows/macOS is your call. The GUI builder is in qmapshack itself, so nothing is missing without it. If you want it, it's one copy ...\bin\Release\qms_surfacedb.exe line per .bat, plus a File Files\qms_surfacedb.exe in QMapShack_Installer.nsi (QMS_gisinternals.nsi takes Files\*.*), and on macOS a cp next to qmt_* in bundle-qmaptool.sh or copyExtTools in bundle-qmapshack.sh. It links Qt Core/Gui/Sql and GDAL, so no new libraries. On the Linux side I added the missing man page qms_surfacedb.1, installed like the others, and listed the target in the READMEs. 36d1f39
  • R*Tree availability: Qt's bundled SQLite is built with SQLITE_ENABLE_RTREE (qtbase src/plugins/sqldrivers/sqlite/CMakeLists.txt), so the official Qt builds for Windows and macOS have it. Only a Qt built with -system-sqlite against a SQLite without R*Tree would lack it. The build then fails with SQLite's "no such module: rtree" in the status line and leaves no file behind. If you build Qt yourself, please check that one.

Tests: new tests for the leg changes, the name of a picked extract, the canceled job, overlaps(), the unsorted ways, the area text and the reader (bounded inflate, coordinate range, string indices, empty file, and all other error paths of blocks and files). Line coverage of the surface core the tests build (CPbfReader, CSurface, CSurfaceAnalyzer, CSurfaceDb, CSurfaceDbBuilder, CSurfaceDbSet) is now 92% (CSurfaceDbSet 100%, CSurfaceAnalyzer 98%, CPbfReader 97%). Most of what's left are SQLite write errors that can't be caused without fault injection. The GUI-coupled parts (CSurfaceTrk, CSurfaceDbManager, the widgets) still have no unit tests.

Built (Debug, BUILD_TESTING=ON): qmapshack, qms_surfacedb and the surface tests. ctest passes, the Durbuy check passes against a database built from scratch from the Belgium extract, and clang-format is clean.

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Now it's getting to the bottom:

Checked against the code:

1. Tracks loaded from a Garmin device never get surfaces. CSurfaceTrk.cpp:28 turns surface analysis on only if the track is created on the main thread (enabled(QThread::isMainThread())). CDeviceGarmin.cpp:122 creates its projects inside a CThread, so these tracks are shown in the workspace but stay switched off for good. The Surface tab stays empty and the Surface/Way Type color sources are never offered. MTP devices probably behave the same way, but I didn't check them. A fix would be to check the thread when update() runs instead of at construction.
2. Missing road types in CSurface.cpp:89-94. trunk counts as a busy road, but trunk_link, motorway and motorway_link are missing. They are drawn as "other" (grey) instead of busy road.
3. Failed POI queries leave holes (from the QMS-1281 commit). CPoiFilePOI::loadPoisInRow marks every square of the row as loaded before the query runs and ignores the result of query.exec(). If the query fails, POIs in that row stay missing until the file is reloaded.
4. Doc comment style. The new //< comments in CKnownExtension.h and CTrackData.h don't follow the CLAUDE.md rule, but they match the comments around them in those files, so this is minor. CPoiFilePOIQuery.h uses ///, and the new CPoiFilePOI code uses int where CLAUDE.md asks for qint32.

Not checked:

5. Slow second build pass. In CSurfaceDbBuilder.cpp:133, the pass that reads node locations also unpacks every way and relation block of the extract. Because the file is sorted with nodes first, this pass could stop at the first block that holds ways.
6. Slow redraws. drawColorizedByCategory (CGisItemTrk.cpp:1899) builds a full track point for every surface change on every redraw, only to read three small values from it.
7. UI can freeze on close. The CSurfaceDatabaseBuilder destructor waits for the build to finish, but the build checks for cancel only now and then and not during the final SQLite commit. Closing the builder tab during a large build would freeze the UI until it finishes.
8. Untranslated error messages. The errors from CSurfaceDb and CPbfReader that users see are plain English strings with no tr().
9. Unused functions. CSurfaceDbSet::covers() and isEmpty() are only called by the tests. The app uses CSurfaceDbManager::getCoverage() instead, so there are two copies of the same check that can drift apart.

I have to check with a Garmin later. I don't really think surface is needed for tracks on devices. The usual process is to copy them from the device into a project before you do anything.

Not sure if you want to address 3 as it's not part of this PR. It's just there because I compare against your dev branch which is not 100% up-to-date.

@switch87

Copy link
Copy Markdown
Contributor Author

Thanks, I went through all nine against the code.

Checked

  1. Tracks from a Garmin device: true, it's our own check. The MTP devices and the Garmin archive load in a CThread too. Checking the thread in update() isn't enough on its own, though. The CSurfaceTrk object belongs to the loader thread, and that thread ends after loading, so the finished signal of the job would never be delivered. A device track also normally never runs update() in the GUI thread, only when it gets edited. To really analyze these tracks, the object would have to move to the GUI thread once the device has loaded. Since, as you say, tracks are copied into a project first, and a copy is made in the GUI thread and gets its surfaces, I didn't change the analysis. The Surface tab now says why it's empty instead of staying blank ("Surfaces are shown for tracks in a project. Copy the track into a project to see them."). 060ecca, f107e56. If you want surfaces on device tracks after your Garmin check, I can do that as a follow-up.
  2. Missing road types: true. trunk_link, motorway and motorway_link are busy roads now. The reference classification and its table follow, and the test that said motorway is "other" now says busy road. 75ba51a
  3. POI row query: true, and it's in my QMS-1281 code, not in this PR. I opened Mapsforge POI: a failed row query leaves POIs missing until the file is reloaded #1286 for it and will pick it up once [QMS-1280] Show surfaces and way types of tracks from OpenStreetMap data #1282 is in.
  4. Doc comment style: the new //< lines in CKnownExtension.h and CTrackData.h are /**< */ now. 917bc5a. The /// and int in the POI code are part of Mapsforge POI: a failed row query leaves POIs missing until the file is reloaded #1286.

From your unchecked list

  1. Slow second build pass: true. The header's Sort.Type_then_ID (Geofabrik puts it in the optional features, which the reader now reads too) tells us that all nodes come first. So the node pass stops after the first block that holds ways or relations, and only for sorted files. For Belgium the database comes out byte for byte the same in its chunks and tags, and the build is about 8 % faster (105 s → 97 s, Debug). 7062f27
  2. Slow redraws: true. One track point is now reused for the colors of all the changes. e00c816
  3. Freeze on close: partly. Cancel is checked after every PBF block while reading and every 16384 ways while writing. With the journal off, the final commit only writes out the page cache. What can't be interrupted are the sorts of the node ids and the ways between the passes: about 0.6 s for Belgium's ~12M node references, and around 6 s for an extract with 100M (Germany size), measured with -O2. So closing the tab during a big build can still stall for a few seconds. Fixing it properly means the job no longer reporting to the widget (progress through a QPromise, the rescan done by the manager), so the destructor wouldn't have to wait. That's a redesign of the tab, so I'd rather do it as a follow-up than in this PR. Is that OK with you?
  4. Untranslated errors: true. The errors of CPbfReader and CSurfaceDb go through tr() now. The reasons a broken block gives are marked with QT_TRANSLATE_NOOP and translated where the message is put together. 199f234
  5. Unused functions: true. CSurfaceDbSet::covers() and isEmpty() are gone, so CSurfaceDbManager::getCoverage() with CSurfaceDb::overlaps() is the only check left. Their test now checks the set itself (a missing file is left out, an empty set finds nothing), and the Durbuy test uses overlaps(). 3781318

Tests: new or extended tests for the road types (unit test plus the reference table), the node pass in sorted, required-sorted and unsorted files (block count included), the optional header features, and the translated errors (a test translator that marks every text, including the reason inside a "Broken PBF file" message). Each one fails without its fix. Line coverage of the surface core (CPbfReader, CSurface, CSurfaceAnalyzer, CSurfaceDb, CSurfaceDbBuilder, CSurfaceDbSet) is still 92%: CSurfaceDbSet 100%, CSurfaceAnalyzer 98%, CPbfReader 97%, CSurfaceDb 95%, CSurfaceDbBuilder 93%. CSurface.cpp shows 72% only because gcov counts the lines of its static tables as not run, although the tests use every table.

Built (Debug, BUILD_TESTING=ON) at every commit: qmapshack, qms_surfacedb and the surface tests. ctest passes, including the Durbuy check against a database built from scratch from the Belgium extract, and clang-format is clean.

@wthaem

wthaem commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@switch87, @kiozen : The above mentioned files msvc_64/copyfiles.bat, CopyFilesGis.bat are outdated and no more maintained. Currently, I don't know an official QMS install and deploy process for Windows delivered together with the source code distribution.

@kiozen

kiozen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

As expected this works perfectly with tracks on a Garmin, too.

@wthaem I assume as you already tested the patch that SQLite on Windows has no problems with the R*tree. Right?

@switch87 I think we are done with the review. Cleanup the commit history and what ever else you want to clean up and I can merge it.

@wthaem

wthaem commented Oct 16, 2026

Copy link
Copy Markdown
Contributor

@kiozen: Rtree is used to show POIs of versions 2 and 4 and most likely with the surface commit I used for a first test. I didn't see any issues arising from Rtree handling using these features.

CSurface maps a way's highway, surface, tracktype and footway tags to a
surface class (paved, semi-paved, unpaved, unknown), a detailed surface
and a way type, filling missing tags with the usual assumption for the
highway type. CSurfaceDb reads a surface database (SQLite, R*Tree over
chunks of up to 16 way segments) and finds the way at a position as the
majority of the ways sampled every ~10 m in its ~20 m cell, with the
nearest way within a given distance as fallback. CSurfaceDbSet uses
several databases together, switched on and off by their full path.

CSurfaceDbBuilder writes a surface database from an .osm.pbf extract,
the same file Routino databases are built from. It reads the file twice
(highways, then the locations of their nodes) with CPbfReader, a small
reader for zlib compressed PBF blocks, dense nodes and ways, so no new
library is needed. Belgium (700 MB) takes ~40 s and ~300 MB of memory.
qms_surfacedb builds a database from the command line, with a man page.

Whenever a track's points change, CSurfaceTrk samples it every 20 m in
a background job (CSurfaceAnalyzer), looks up the way at each sample in
the active surface databases (CSurfaceDbManager) and sums the lengths
per surface class, way type and detailed surface. A single sample that
differs from two equal neighbours is taken for a crossing and smoothed.
The result is derived data like slope and is not saved; an unchanged
track reuses it at once, and a database change reanalyzes only the
tracks its area covers.

The Surface tab of the track details, placed after Activity and
remembered by its name so that a new tab never shifts it, shows two
stacked bars with a legend (surface classes and way types, length and
share) and a collapsible list of the detailed surfaces, or how to build
a database when none covers the track. The track point tooltip names
surface and way type, and the tab says so when the database covering
the track is switched off or a device track was never analyzed.

The map draws each leg in the colors of its samples, so it shows what
the Surface tab counts; a stretch without a classification keeps the
track's ordinary color.

Tool -> Create Surface Database opens a tab to build databases in a
worker thread (inputs locked while building, cancel is no error) and to
choose the active ones; the Routino router panel links to it.

"Surface*" and "Way Type*" become color sources of a track. They are
categories, so they are drawn with a fixed color per category instead
of the gradient, and the legends on the map and in the Style tab list
the categories.
@switch87

Copy link
Copy Markdown
Contributor Author

@kiozen Done. The branch is now one commit on top of current dev:

[QMS-1280] Show surfaces and way types of tracks from OpenStreetMap data

The tree is unchanged — git diff f107e563..901bd7a2 is empty — so what you and @wthaem reviewed and tested is exactly what this push contains: same build, same behaviour, only the history is flat.

The commit message was brought up to date with the design the review changed: the surfaces live in their own Surface tab after Activity (not below the comment in the Info tab) and are remembered by their name, a database change reanalyzes only the tracks its area covers, the map draws each leg in the colors of its samples, qms_surfacedb has a man page, and the Builder tab locks its inputs while building.

Nothing else turned up to clean: no debug output in the diff, and the only new qWarning() calls are the two that report a database that cannot be read and a failed build.

@kiozen
kiozen merged commit e2a673f into Maproom:dev Oct 17, 2026
@kiozen

kiozen commented Oct 17, 2026

Copy link
Copy Markdown
Collaborator

Thx!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants