Qt/C++ desktop app for planning and analysing GPS tracks, routes and waypoints. Loads map tiles, vector maps and DEM data; supports online and offline routing engines.
- Language: C++20
- GUI / framework: Qt 6.8+
- Key libs: GDAL, PROJ 8+, Routino
- Build: CMake 3.20+, Ninja; Debug build in
build/, binaries inbuild/bin/ - Bundled 3rdparty: Garmin FIT SDK
- Minimum GDAL: 3.10
src/
qmapshack/ main app (21 subsystems)
canvas/ map rendering
gis/ tracks, routes, waypoints, DB, GPX, routing (gis/rte/router/)
mouse/ mouse interaction; line editing in mouse/line/
map/, dem/, poi/, grid/, plot/, realtime/, device/, tool/, helpers/, widgets/
qmaptool/ map creation tool
qmt_map2jnx/ utility
qmt_rgb2pct/ utility
common/ shared code
- Never build. No
cmake --build,ninjaormake— not even to verify an edit. Make the change, runclang-format, report, stop. If a compile is genuinely needed to be sure, say so. For reference, the user runscmake --build build --target qmapshack -j$(nproc). - Never commit or push without being asked for that specific change.
- No
Co-Authored-Bylines in commit messages. - For "what were we working on", read
git status && git difffirst — the live diff is ground truth, notes are not. - Store project learnings in this file, not in the auto-memory system. Append under the matching
section or add a new
###. Keep it to forward-usable facts; git holds the history. - Multi-session plans and specs go in the tracked
.notes/directory, named<topic>-plan.md, with a pointer from## Open workhere. Never invent another location. - This checkout (
~/projects/qmapshack, remotekiozen/qmapshack) is where work happens.~/projects/qmapshack_mastertracksMaproom/qmapshackand is for reference only — never branch or edit there.
Target-scoped CMake. Nothing is set at directory scope except the MSVC options below.
- Never touch the translation block's gating.
UPDATE_TRANSLATIONSoff means no lupdate target exists at all, so no build can rewrite the tracked.tsfiles; the catalogs are refreshed only when translation work is deliberately finished. Qt's always-presentupdate_translationstarget is not a substitute. The/localeresource in theelsebranch stays too: every translator loads.qmfrom a filesystem path, so nothing reads it, butqt_add_resources(<app> ...)is what makes the.qmfiles inputs of the application target. Drop it and--target qmapshackstops producing the catalogs the packaging scripts copy out of<build>/src/<app>/. qms_optionsis the INTERFACE target carrying the project's warning set. First-party targets link itPRIVATE; bundled 3rdparty must not. Add flags throughqms_add_flag_if_supported().target_link_librariesis keyword form everywhere. Plain and keyword signatures cannot be mixed on one target, so a new call must sayPRIVATE.- Dependencies are imported targets:
GDAL::GDAL,PROJ::proj,JPEG::JPEG,ROUTINO::ROUTINO.ROUTINO_XML_PATHstays a plain variable — qmapshack passes it as a define. - Defines are per target:
HELPPATHon qmapshack and qmaptool,ROUTINO_XML_PATHandHAVE_DBUSon qmapshack. Global on purpose:_CRT_SECURE_NO_WARNINGS,/MPand/utf-8under MSVC, which the bundled FIT SDK needs, and-march=native. - Install paths come from
GNUInstallDirson UNIX and stay relative to the application directory on Windows, whereCAppSetupWinresolvesHELPPATHagainst it. Six cache variables survive so a packager can override them.HTML_INSTALL_DIRisshare/doc/HTML, notCMAKE_INSTALL_DOCDIR— changing it moves the installed help. CMAKE_RUNTIME_OUTPUT_DIRECTORYis set, the four per-config variables are deliberately not. They are the only thing a multi-config generator consults for its$<CONFIG>suffix, so pinning them collapses every configuration into onebin, which leaves a.pdbfrom the previous configuration beside a mismatched.exeand makes the build report the other config's binary as up to date.msvc_64/CopyFilesGis.batreadsbin\Release\and wants the suffix. Single-config generators resolve the per-config variable to the same path as the plain one, so Linux and the macOS release path (build-QMS.shusesUnix Makefiles; its Xcode branch is marked BROKEN and exits before bundling) are flat whatever the build type.ConfigureChecks.cmakeprobes through the C++ compiler. The project enables no C language, socheck_include_file/check_symbol_existshard-fail; use the_cxxvariants.CMAKE_AUTOUICis OFF — the.uifiles are listed explicitly and go throughqt_wrap_ui.qt_standard_project_setup()sits directly afterfind_package(Qt6 ...)because it flips AUTOMOC/AUTOUIC and appends toCMAKE_INSTALL_RPATH.Qt6::Core5Compatis required, not legacy.QTextCodecdecodes the Garmin codepages (CGarminTyp.cpp,IGarminStrTbl.cpp) thatQStringDecodercannot.CMAKE_CXX_EXTENSIONSis left ON — the UNIX warning set passes-fms-extensions.CMakePresets.jsonholds three usable Linux presets; the Windows and macOS entries are untested stubs and the platform blocks inCMakeLists.txtstill drive those builds. The developer-facing howto isREADME_PRESETS.md, linked from the README's Linux build section.
IAppSetup::prepareTranslators(appPath, appPrefix, qtPath) installs both catalogs, and the
application catalog is the gate: no <appPrefix><locale>.qm means the GUI is English including
Qt's own strings. Skipping qtbase_<locale>.qm is not enough for that - a desktop environment
installs a Qt catalog for the system locale itself, from the platform plugin, before the application
touches a translator (measured on KDE: a bare QApplication with no translator installed answers
translate("QPlatformTheme", "Cancel") with Abbrechen, and QT_QPA_PLATFORM=offscreen turns it
back into Cancel). So the untranslated branch installs CSourceTextTranslator, which answers every
lookup with the source text: translators are asked newest first and a non-null answer ends the
lookup, so it wins over the desktop's. QCoreApplication::translate() applies replacePercentN() to
the result afterwards, so %n still works. It exists twice, once per app. qmt_rgb2pct keeps its
own unguarded two-liner - it is a command line tool.
Not every string in the window goes through those catalogs. A desktop integration translates its own
contributions with catalogs of its own, picked by LANGUAGE and loaded while the platform plugin
comes up - on KDE the standard button labels come from KStandardGuiItem in
/usr/share/locale/<lang>/LC_MESSAGES/kwidgetsaddons6_qt.qm, which no Qt or QMapShack catalog covers,
so --locale it alone leaves them German on a German desktop (measured). IAppSetup::exportLocaleEnv()
therefore runs as the first statement of main(), before QApplication, and puts a --locale value
into LANGUAGE. It has to parse argv by hand - CCommandProcessor needs an application instance,
which is already too late.
Running from the build tree never finds the application catalog: applicationDirPath is
<build>/bin, so the path resolves to <build>/share/<app>/translations while the .qm files are
built into <build>/src/<app>/. A build-tree run is therefore always English.
QTranslator::load()'s return value is no existence test. It strips _<suffix> and .<suffix>
off the name until something loads, so load("qmapshack_pl", dir) succeeds with the installed,
untranslated qmapshack.qm (measured, Qt 6.10.2). Test with QFileInfo::exists() on the exact
name, and pass the full <prefix><locale>.qm to load().
All C++ is formatted with clang-format. After editing any .cpp or .h:
clang-format -i <file> [<file> ...]Style is .clang-format in the project root (Google base, 120 columns). Always accept its output —
never revert or hand-tune it, and keep reformat hunks it makes on unrelated pre-existing drift.
- Every control-flow block requires braces, including single-statement bodies.
- Doc comments are
/** */doxygen blocks (@brief,@param,@return); inline member docs are/**< */; plain//for non-doxygen annotations. - Comments state the fact needed to understand the code — one terse line. Explain a non-obvious invariant, workaround or gotcha; never narrate what the code already says, and never the ticket/bug/PR history behind it.
- Pass
QStringand complex objects (QVector,QImage, …) byconst&unless the function mutates them. PlainT&is for genuine out-parameters only. - Prefer static
QFileInfo::exists(path)overQFileInfo(path).exists()for a bare existence check. - Prefer Qt fixed-width typedefs:
qint32/quint32,qreal,qsizetypefor Qt container sizes. Exception: match an external API's own types (GDAL'sint*out-param,size_tinReadRaster()). - Constructor and destructor come first in a
.cpp, in that order; every other member function follows both.
src/qmapshack/mouse/line/.
IMouseEditLine – owns the point list (SGisLine), undo/redo history,
│ routing mode buttons, and the active ILineOp
├── CMouseEditTrk – edits a track (IGisItemTrk)
├── CMouseEditRte – edits a route (IGisItemRte)
└── CMouseEditArea – edits an area overlay (IGisItemOvl)
ILineOp – base for one interactive editing operation
├── CLineOpAddPoint – insert new points / extend the line
├── CLineOpMovePoint – drag an existing point to a new position
├── CLineOpDeletePoint – remove a point
└── CLineOpSelectRange – select a range of points for bulk operations
IMouseEditLine delegates all mouse events to the active ILineOp. Switching the toolbar button
deletes the current op and creates a new one.
CRouterSetup::calcRoute() shows a CProgressDialog running a nested QEventLoop. The full Qt
event pipeline stays live while it spins, so during routing:
mouseMove()fires and driftspoints[idxFocus].coordto the cursor.- A right-click reaches
abortStep()→restoreFromHistory(), which reallocatespointsand invalidates every saved index or pointer into it. - A left-click can re-enter
leftClick().
Any ILineOp that triggers routing must go through runRoutingAndPin(coord) and check its
return value. It:
- Pins
points[idxFocus].coord = coordbefore routing. - Sets
isRouting = true— subclasses test this at the top ofleftClick()to block re-entry. - Calls
slotTimeoutRouting()→finalizeOperation()→tryRouting(). - Re-validates
idxFocusagainstpoints.size()after the loop returns (an abort leaves both unknown). - Restores
points[idxFocus].coord = coord, undoing any drift. - Returns
falseif aborted — the caller must return immediately.
isDragging |
focusIsEndpoint |
Meaning |
|---|---|---|
| false | false | cursor near a mid-segment — click inserts a point there |
| false | true | cursor near the first or last point — click extends the line |
| true | either | a new point is attached to the cursor — click drops it |
With vector or track routing active, updateLeadLines() finds the map/track polyline nearest the
active point → leadLineCoord1/2 (geographic) and leadLinePixel1/2 (screen).
GPS_Math_SubPolyline() then extracts the segment lying between the two adjacent line points →
subLineCoord1/2 / subLinePixel1/2. The sub-line is what becomes the routed sub-segment on drop.
IRouter is the abstract base:
CRouterRoutino— offline routing from Routino databases.CRouterBRouter— BRouter, either a local process (CRouterBRouterLocal) or the online HTTP API.
CRouterSetup::self() is a singleton owning the active router and exposing calcRoute(). It emits
sigHasFastRouting(bool) when the router capability changes; IMouseEditLine listens and
enables/disables the auto-routing button.
Only local BRouter supports fast (on-the-fly) routing. Online BRouter and Routino need the whole
route at once via calcRoute(const IGisItem::key_t&).
helpers/CSmoothingSpline.{h,cpp}, a penalized regression spline over equidistant nodes. Used only
by CGisItemTrk::interp, which feeds the "Interpolate elevation" filter and CPlotProfile's
preview curve.
mcounts nodes, not basis functions: m − 1 spans, m + 2 coefficients.- The penalty is
D2'D2, the second difference of the coefficients. The exact curvature Gram matrix∫B''_i B''_jfits measurably worse — do not swap it in. lambdais normalized againsttrace(B'B)andnumSpans^3, so it is independent of the point count, the node count and the units of x and y. The tuned value iskElevationSmoothinginCGisItemTrk.cpp; doubling it roughly doubles the smoothing.- Only points with
ele != NOINTenter the fit, appended in order — indexing the input arrays byidxVisibleleaves holes for points without elevation. - Below ~80 nodes the basis, not the penalty, limits the curve. That is where the quality setting changes the result.
Profiling (Tracy) showed label decoding (strtbl.get) was the single largest slice of
loadVisibleData — an mmap (via CFileExt::data()) plus a codepage decode per labelled object,
re-done every frame even though label content is immutable. To fix that, IGarminStrTbl::get() is a
non-virtual cache wrapper over a bounded LRU QCache<quint64, QStringList> (key (type << 32) | offset); on a miss it delegates to the protected pure-virtual decode() (the per-coding work,
implemented by CGarminStrTbl6/8/Utf8). Do not re-merge get/decode or make get virtual again —
the split exists so the cache lives in exactly one place. The cap (labelCacheMaxEntries) bounds
memory when panning huge gmapsupp files; a hit needs no mmap at all, which also shrinks the
per-subfile CFileExt::free() munmap loop. The cache is touched only from the draw thread
(get() ← loadSubDiv ← loadVisibleData ← draw, all serialized), so it needs no lock.
Tracy zones: strtbl.get = wrapper (hits+misses), strtbl.decode = misses only — compare their call
counts to read the hit rate.
gis/CWksItemDelegate.{h,cpp}— workspace treegis/CDBItemDelegate.{h,cpp}— database treemap/CMapItemDelegate.{h,cpp}— map-item tree
Every getRectangles*() uses CRowBuilder (helpers/CRowBuilder.{h,cpp}) to carve a row into
icon, button and text zones — no magic-number arithmetic in the delegates.
Tuning parameters live in helpers/CDraw.h: kCellPad (outer inset on all four sides of
opt.rect) and kInnerGap (gap between icon, text column and each button).
CRowBuilder row(opt.rect, kCellPad, kInnerGap);
const QRect rectIcon = row.takeLeft(row.height()); // square icon
row.markStatusColumn(); // snapshot width for status line
const QRect rectButton = row.takeButton(fmName.height()); // name-height square button
const QRect rectName = row.nameSlice(fmName.height());
const QRect rectStatus = row.fullStatusSlice(fmStatus.height());takeLeft(w)/takeRight(w)— carve a full-height rect, advance bykInnerGaptakeButton(iconSize)— square button sized soCDraw::drawToolButtonrenders its icon at exactlyiconSize × iconSize(compensates the button's internal icon inset)markStatusColumn()— snapshot the remaining rect before buttons are carvednameSlice(h)/statusSlice(h)— top / bottom strip of the button-narrowed centre areafullStatusSlice(h)— bottom strip of the pre-button column, so the status line runs under the buttonsrowHeight(cellPad, nameH, statusH)— matchingsizeHintheight
Button height convention: CWksItemDelegate and CDBItemDelegate use
takeButton(fmName.height()) (name row only) with fullStatusSlice; CMapItemDelegate uses
takeRight(row.height()) (full row height) with statusSlice.
animations_t is defined after getAnimations() in the private section, so the
struct animations_t; forward declaration above getAnimations() is required. Do not remove it.
IWksItem::icon and IDBItem::icon are QIcon, and getIcon() returns const QIcon&, so
delegates render at device resolution and non-GIS tree icons stay crisp on HiDPI. GIS items
(wpt/trk/rte/area) stay raster — paintItem pulls the pixmap out of the QIcon and stretches it.
Two delegate paths cannot assume the icon fills the cell and both branch on actualSize:
paintDevice (MTP subclasses overwrite the SVG with a raster read off the device) and
paintGeoSearch (CGeoSearch::setIcon() composes a raster when "accumulative results" is on).
Never collapse either to a bare QIcon::paint and never add a stretch back —
actualSize(rect) == rect answers "can this icon fill the cell", and QIcon downscales rasters
fine. paintProject / paintGeoSearchError are genuinely SVG and paint direct.
IDBItem::icon holds folder icons only. The DB blob raster lives on CDBItem as its own
QPixmap displayIcon (getDisplayIcon()), which CDBItemDelegate::paintItem stretches directly.
Do not re-add a setIcon(const QPixmap&) overload on IDBItem.
CUiTheme (src/common/theme/) is the only source of status colours for rich text, label
stylesheets and setTextColor(). Roles Neutral/Ok/Warn/Error/Info/Code, each a fixed light/dark
pair. Tune there, never per site.
| The widget… | Use |
|---|---|
| is permanently a status message, only shown/hidden | markLabel(label, role) |
| is a value that is sometimes a status | span() / spanBold(), so the colour travels with the string |
| fills a background (table cell, banner) | css() |
| draws text on its own background | cssForeground() / foreground() |
| is a row or cell in an item view | setForeground() / setBackground() |
Set both colours or neither — a hardcoded background inherits the palette's text colour and
inverts on dark. Set nothing on a plain item-view row so it keeps the palette. Grey out with
QPalette::Disabled, QPalette::Text, never Qt::gray. Never bake a colour into a tr() string or
a .ui <string>; the translation carries it.
Two measured facts (Qt 6.10.2) set the whole design:
QEvent::PaletteChangereaches every widget, at every nesting depth, exactly once. So a widget that holds scheme-derived content rebuilds it in its ownchangeEvent()and needs nothing central to drive it. (ApplicationPaletteChangeis the one that arrives only on the application object — that difference is what makesCThemeRefreshera filter, not achangeEvent.)- A themed colour is baked in when the content is set — into a
QTextCharFormat, a style sheet, aQPalette, a rendered pixmap — and the source is dropped.QTextDocument::toHtml()returns the baked colour, not the markup it came from, sosetHtml(toHtml())repairs nothing. Only re-running whatever produced the content works.
So the rule is Qt's own, and it costs one override in the class that owns the content:
void CFilterSpeed::changeEvent(QEvent* e) {
QWidget::changeEvent(e);
if (CUiTheme::isPaletteChange(e)) {
updateUi(); // whatever already regenerates this widget's content
}
}- Regenerate, do not patch. The producer is normally a method that already exists
(
updateData(),buildHelpText(),renderThemedContent()), and re-running it fixes the palette colours (links) and the themed markup (CUiTheme::span) in one pass. That is why no rich-text widget subclass is needed: every browser in the tree has an owner that regenerates it. - Do not run something re-entrant from the handler.
CDetailsPrjrestarts its timer becauseslotSetupGui()drives a nested event loop;CGridPlacercheckspointsis populated first. - A handler that answers with
setPalette()/setFont()needs a re-entrancy guard — both re-deliver the event to the same widget (measured: oneupdateStyle()emits twoFontChanges).CLineEdit/CTinySpinBoxuseapplyingStyle. - Do not hand a themed string to something that stores it. Pass the role and resolve at render
time:
CCanvas::reportStatus(key, role, msg)keeps the role unresolved instatusMessages. - A themed colour that is only returned (
getInfo(), a tooltip string) is fine — a tooltip is built fresh each time, and the widget that displays stored markup is what regenerates. CThemeRefreshercovers the two cases aQLabelcannot fix for itself: amarkLabel()role (the style sheet holds the resolved colour, so the role is recorded on the label and applied again) and a baked anchor colour (the label's own text is re-applied). Both are automatic, so amarkLabel()caller needs nochangeEvent().installThemeRefresh()belongs inmain.cpp, besideCQmsStyle::install(), once per application — qmaptool needs it too. Do not extend the sweep further; anything else belongs in the owning widget'schangeEvent().
Everything below is why a given surface needs rebuilding at all:
- Never cache a themed colour in a member. Resolve in the apply path. A cached one is stuck on the scheme it was built under and looks correct until the user switches.
- A brush put on an item view's item is resolved once and outlives a scheme switch, so the view
must rebuild the affected rows (
CTableTrk). Plain rows around them switch on their own, which makes the mismatch easy to miss. - Every map layer paints into a buffer rebuilt only on demand, so nothing themed on the canvas
follows a switch by itself.
CCanvas::changeEventforcesslotTriggerCompleteUpdate(eRedrawAll)— per canvas, not the statictriggerCompleteUpdate(), which reaches only the visible one. - A
QTextBrowsercannot be repaired generically.setHtml(toHtml())re-parses the baked colour and changes nothing; the original markup has to be supplied again. Subclassing does not help either —QTextEdit::setHtml()is not virtual, so an override would only shadow it. setPalette()freezes only the roles in the palette's resolve mask. Build a freshQPaletteand set just the role you own; every other role keeps following the application palette.setPalette(QPalette())clears the override. A copy ofpalette()carriesresolveMask 0and is a harmless no-op — the freeze comes from thesetColor(), not the copy.QFontbehaves the same — express an underline or weight as a bareQFontwith only that attribute set.- A
.ui<palette>override lands before the constructor body runs and freezes the same way.alpha="0"onBase/Windowis the transparent-field idiom and is fine; an opaque colour in any group is not. setPalette()replaces the widget's own override, it does not merge into it — including the onesetupUi()applied. So a widget that sets its own palette cannot take any role from a.ui<palette>:CLineEditowns the transparentBase/Windowitself, and the<palette>blocks onlineNamein the fourIDetails*.uiare inert leftovers.setTextColor()is deliberately outside the rule. It colours the text appended after it, which is the shell-transcript idiom: those lines keep the scheme they were written in on purpose (CShell,IToolShell, and the tool/BRouter output browsers).
CQmsStyle (src/common/theme/) is a QProxyStyle installed by CQmsStyle::install() from both
main.cpp. It marks the checked state of toggle tool buttons (PE_PanelButtonTool + State_On)
and checkable menu items (CE_MenuItem), which every style draws too faintly to read on a dark
palette.
- Paint a themed cue in a style, never in a style sheet. A style resolves at paint time, so it
follows the palette, cannot go stale and needs no opt-in. An app style sheet using
palette()resolves once, re-polishes every widget on re-apply, and that re-polish echoes another palette event. install()re-creates the active style by name —setStyle()deletes the style it replaces, so the running one cannot be the base. That is what preserves-style/QT_STYLE_OVERRIDE.- Paint the menu tint before delegating to the base, or it dims the text it marks;
CE_MenuItemfills the row only when selected. Checked rows also go bold, so the cue is not colour alone. - Resolve a cue's colour through
cueColorGroup(), never the palette's current group. The current group turnsInactivewhile the window is not the active one, and KDE's[ColorEffects:Inactive] ChangeSelectionColormutesHighlightto near the button face there (measured#1b4155on#292c30), so a cue drawn with the one-argumentcolor()overload disappears whenever the app loses focus. A cue marks state, not focus, soActiveis pinned and only the disabled dimming is kept. - Never fill a checked button with
Highlight—inkdrops to ~1.3:1 on it. A border over the untouched face keeps ~4.9:1. - Menus resist style sheets: Qt ignores
QMenu::item:checked, andQMenu::indicatorneeds animage:, which for a.svgtbypassesCSvgtIconEngineand renders black.
- An info bubble on the canvas is chrome, not map surface — it follows the palette. Background
CDraw::bubbleBackground()(QPalette::Window), rich textCDraw::drawBubbleText().CDraw::bubble()applies the background itself and takes no colour;CDraw::infoPanel()is the pointer-less panel (doc + frame + text in one call, leaving the painter translated).QTextDocument::drawContents()ignores the painter's pen and takes plain text fromQPalette::Text, so it cannot be used here. Anchors takeQPalette::LinkatsetHtml()time and aPaintContextcannot override it — a bubble that is not palette-backed needs ana { color: }default style sheet set before the markup is parsed. - An
IScrOptoverlay draws its sheet withCDraw::bubbleBackground(), resolved indraw()— the table and.svgttoolbar icons on it follow the palette, so a white sheet strands them. CDraw::text()haloes in white. That is for text over map tiles, where the halo is what makes any colour readable; on a solid themed bubble it glows. Paint there with a plaindrawText()andCMainWindow::self().getMapFont()(CGisItemTrk::drawLimitLabels).- Printing:
const CUiTheme::CForceLight paperColours(printable)inCDetailsPrj::draw(), beside the two palette branches it completes. It does not touchQPalette, so palette colours still need their own paper branch. QTextBrowserapplies a<link>ed stylesheet (loadResource,StyleSheetResource), soCHelpBrowser::loadResource()appends a themedcode, prerule to whatever the packaged help ships. That CSS lives outside this repo;Role::Code's light arm matches itscodebackground, so light mode renders unchanged.paletteIsDark()isinlineinCUiTheme.hsoCSvgtIconEngine::roleColor()can share it with nothing to link (the plugin needs onlytarget_include_directories(svgticonengine PRIVATE ..)). It is for that plugin and forCUiTheme::isDark()alone — app code branches onCUiTheme::isDark(), which is the same test plus theCForceLightoverride. Never write a third copy of the threshold; it drifts silently.
IPlot's sheet is white in every theme.eModeNormalandeModeSimplebothfillRect(rect(), Qt::white); waypoint icons are painted straight onto it and are unthemable raster symbols authored for a light ground. A themed panel around a white plot is intended (CScrOptRangeTool).CShell/IToolShellcolour each line as it is appended, so lines already in the log keep the scheme they were written in.- The halo under
CWksItemDelegate's progress bar andCIconGrid's tiles are white by design. CIconGrid,CScrOptUnclutter,CPrintDialog, the qmaptool overlays andCMapIMGare self-consistent light surfaces.CDetailsOvlArea's white brush-style swatches are the neutral ground a pattern preview wants.- A hardcoded colour is a defect only once its contrast fails against both arms. A mid-grey such
as
Qt::darkGrayclears#efefefand#353535alike, and white is a legitimate neutral ground for a swatch or preview tile. Work out both grounds before filing one.
plasma-apply-colorscheme BreezeDark with the app running, and QT_QPA_PLATFORMTHEME unset — on
qt5ct the Qt 6 platform theme never loads and no scheme change reaches the app.
Good probe: a DEM property panel with "Enable color shading" on and the grades combo at its last
entry, where the slope spins go read-write and underlined in Role::eInfo. The panel is cached on
the DEM (IDem::getSetup()), so it holds a stale scheme until the DEM is unloaded.
convertRad2Px() / convertPx2Rad() work in logical viewport pixels (built from center and
scale*zoomFactor) so they match Qt mouse/widget coordinates. The draw buffers are device
pixels: bufWidth/bufHeight = viewWidth/viewHeight * pixelRatio + 2*BUFFER_BORDER, and draw()
divides the scale by pixelRatio.
Never compare a convertRad2Px() result against bufWidth/bufHeight — they agree only at
pixelRatio == 1. Use viewWidth/viewHeight for viewport-fit tests.
Test HiDPI paths on a normal screen with QT_SCALE_FACTOR=2 build/bin/qmapshack, adding
QT_SCALE_FACTOR_ROUNDING_POLICY=PassThrough for fractional factors like 1.5.
A draw thread holds a reference to its buffer with the mutex unlocked, so the buffers cannot be
rebuilt while it runs and a resize has to be deferred. CCanvas::slotUpdateDrawContextViewport() is
the only path that applies size and pixel ratio, and three rules keep it honest:
- Never pass a size captured earlier. It reads
size()/devicePixelRatio()at apply time. A retry runs long after the event that scheduled it, and a queued resize event for an intermediate geometry is delivered after the synchronously sent event for the final one — the Windows fullscreen → maximized sequence does exactly that. - All or nothing (
canResize()beforeresize()). A partially applied resize leaves the layers with different viewports, which shows as map content no longer matching the GIS overlay. paintEvent()reconciles. A context left behind would never be noticed otherwise: a canvas shown again at an unchanged geometry gets no resize event, so the divergence would survive panning, zooming and view switches until the user drags a splitter.
Retries come from timerViewport and from each context's finished. print() is the one caller
that changes the size to something other than the canvas' own, so it must waitForDrawContexts()
before and after — its second draw() pass restarts the threads.
Developer howto: README_ICON.md.
A QPixmap is one raster frozen at the dpr it was built at: it cannot serve a larger request and
cannot follow a window to another screen, whatever the source format.
- Anything taking a
QIcon(buttons, actions, tree/list items, a Designer<iconset>): reference:/icons/Foo.svgtand let the paint path ask for the size. - Static icon in a dialog:
QSvgWidget+CSvgtIcon::load(). Qt ships no widget that displays aQIcon, anduicbakes a<pixmap>intoQPixmap(path)before any widget sees the path. - Rich text
<img>:CSvgtIcon::htmlImageSrc(), always with bothwidthandheight— without them there is no HiDPI path. - Canvas rasters (waypoints, POI, cache) are data, not icons.
- Never
statica paint-path icon — it pins the colour scheme live at first paint and hides the icon from a path-shaped grep. There are none in the tree; keep it that way.
cmake/IconGate.cmake fails the build on regex-detectable violations. It deliberately cannot see
QIcon(pixmapVariable) or a .pixmap(w,h) missing its dpr.
Left alone deliberately — do not "finish" these: the three .pixmap(w,h) calls with no dpr in
CSelectCopyAction/CInvalidTrk; IGridPlacer.ui's line_3px_* black PNGs; CGeoSearchWeb's
service icons, where the stored path is user data and defaultIcon equality is the "is this
user-added" test — converting needs a settings migration plus an isUserDefined flag first, or
"Restore default list" erases the user's own services.
.gitattributes pins *.svg, src/icons/svg.sha256 and src/icons/svghygiene to text eol=lf
because cmake/IconHygiene.cmake compares file(SHA256) of each working-tree .svg against
src/icons/svg.sha256. One CR changes the hash, and the build reports every icon as edited and
demands inkscape + python3.
A blob committed with CRs cannot be cleaned by checkout/restore. eol=lf normalises on the
way in, never on the way out, so the CRs land in the working tree while diff normalises the file
back — those paths report as modified forever, and any merge that touches them refuses to start
before doing anything. Only a commit carrying LF blobs fixes it.
When the local branch is a strict ancestor of the incoming one and the only dirt is that CR churn,
the merge is a fast-forward blocked by nothing real: git reset --hard <remote>/<branch>.
The same trap runs the other way for *.bat/*.cmd (text eol=crlf): a blob committed with CRLF
before the attribute existed reports permanently modified, because git normalises the working-tree
copy to LF and compares against the CRLF blob. git checkout and git stash — including
rebase --autostash, which leaves a half-built .git/rebase-merge/ behind when it fails — cannot
clear it. To rebase without committing anything, put <path> -text in .git/info/attributes
(local, untracked, beats the tracked .gitattributes), rebase, delete it, then
rm <path> && git checkout -- <path> so the file is re-materialised with the attribute's endings.
git ls-files --eol '*.bat' lists the offenders: i/crlf is a blob still awaiting
git add --renormalize. msvc_64/build_routino.bat is one.
No icon PNG is ever pruned and no dead qrc entry is removed. Two populations store a path, not a picture, so a pruned PNG is a blank icon in somebody's existing file:
| population | stored in |
|---|---|
history_event_t::icon |
.qms, .gpx, DB data column |
getInfo() HTML <img src> |
DB comment column |
History icons are PNG on disk, .svgt in memory: displayIconPath() resolves PNG→.svgt on
load, savedIconPath() converts back on save, and neither marks the item changed. A saved file
stays readable by a build without the icon engine, where a .svgt path renders blank.
The comment column keeps its PNG paths — it exists for full-text search and is never rendered.
- Draw structural line-art in
ink. Qt greys a disabled icon by lightness only, so an icon drawn only inlead/paperlooks identical enabled and disabled on dark. Keepleadfor a secondary outline. (RatingStarEmpty/UnFocusstay grey because grey is their meaning.) - Never give a shape's stroke the same role as its fill. It renders invisible and every tool passes, because each colour is individually valid. Only a render shows it.
- The letter or shape carries the meaning; colour is never the only cue —
SQLite/MySQLuse a bold initial on the cylinder face plus a brand-colour cap. - Negation uses the set's own mark: a red
#ff5555disc with apaperslash, asNotPossible. - Bake lettering to paths (
inkscape --actions "select-all;object-to-path"), then strip the group's inlinestyleback tofill:currentColor— the conversion resolves the class'scolorinline, and an inline value shadows the themed class. Never ship live<text>. - A family (
Act*,Add*,Mime*) shares stroke weight, corner radius and optical size.
Qt has no SVG recolouring API, so CSvgtIconEngine rewrites the SVG text and loads via
QSvgRenderer(QByteArray). Qt's renderer does resolve currentColor — only the setter is missing.
currentColorwith nocolorset renders black (Qt and inkscape); a duplicatecolor=renders nothing; lowercasecurrentcoloris black (QTBUG-46947); a<style>class beats a rootcolor=, so the KDE/Breeze idiom does not mix with ours.QSvgRendererignores a class-suppliedfill:.recolored()inlines the resolved fill as a presentation attribute; without it 19 icons render black.- Qt ignores
markerUnits="strokeWidth", sosvghygienebakes markers into geometry. Setstroke:nonewhereverstroke-widthis 0 first, or the shape becomes a filled block. QIcon(":/x.svg")needs bothimageformats/qsvgandiconengines/qsvgicondeployed; all three platforms are confirmed OK.- Qt 6.10.0 has a
currentColorregression, fixed in 6.10.1 (QTBUG-141102).
Dark ink is #9999ff: it must stay legible on paper #353535 at 4.5:1 while staying clear of
lead #e0e0e0 (120 icons paint both) and mark #66aaff (19 icons paint both). That rules out
the azure family — a "more vibrant blue" means a more saturated navy, not a different blue.
src/icons/waypoints/ is named by the GPX <sym> vocabulary shared with Garmin and its exact look
is a frozen contract, so it stays PNG on the canvas: SVG would hand rendering to whichever Qt
the user has, and a Qt antialiasing change could silently restyle accepted iconography. External
user icons are PNG/BMP forever (CWptIconManager), so the raster path must exist anyway. Dark
theming does not apply — they sit on map tiles, not the UI palette.
A waypoint symbol used as UI chrome (menu action, tool button) is under none of that and uses
the SVG. Only FlagBlue.svg and PinBlue.svg are registered; add others as UI needs them.
Gate any waypoint change with src/icons/tools/wptdiff.py --size 96 — it must report
visible (>8) == 0.
icon_t::focus is absolute pixels of the loaded raster, hardcoded against 32. Any resolution
change breaks every anchor until focus is stored relative (0..1) — a prerequisite for touching
waypoint resolution at all. getWptIconScaledByName holds focus = focus * scale;
getWptIconByName does not. focus is serialized but overwritten on every load via
deriveSecondaryData(), so a relative-focus change needs no migration.
.qms/DB persist the symbol name (wpt.sym) and the colour (trk.color/area.color),
never a rendered icon. The icon is re-derived on load: waypoint via getWptIconByName(sym),
track/area by loading Track.png/Area.png as a shape mask and filling it with the data colour.
The rendered pixmaps in the DB items.icon BLOB and .qms history_event_t.icon are output
caches. Serialization does not constrain the source format.
So only the waypoint symbol is a genuinely frozen raster. Tracks and areas can be SVG: their PNG
is only a silhouette mask (createMaskFromColor), so an SVG rendered at the target size gives a
crisp mask and the same data colour. QIcon will not upscale a raster, so such a change must render
the SVG at size rather than wrap the old 32px PNG.
QDockWidget::setFeatures() disables toggleViewAction() unless DockWidgetClosable is in the set,
so a docker missing that flag has a permanently greyed-out Window menu entry — and a floating one
loses its close button too. Every docker in both apps must keep DockWidgetClosable.
Qt enforces minimumSizeHint() on a window with a layout, so anything that inflates it decides the
smallest the main window can be. The track details page once pinned it to 1648x717 — unusable on a
1366x768 screen — through four separate places that read a preferred width as a required one.
All four are easy to write again:
- A
QLabelwithoutwordWrapreports its whole unwrapped string as a minimum. WithwordWrapit reports its widest word — 436 → 52 for one filter label. Qt 6 turnshasHeightForWidth()on by itself there; thesetSizePolicy()line usually recommended alongside is measured to change nothing. Give any label holding a sentencewordWrap, especially one whose text is set at runtime or translated:labelInfo's minimum used to move with the interface language. QSplittersizes a child throughqSmartMinSize(), which takesqMax(sizeHint, minimumSizeHint)unless the child's size policy carries aShrinkFlag.MinimumExpandingandMinimumhave none, so the child's preferred width becomes a hard floor — 222 px of the details page. UseExpandingorPreferredfor a splitter child that may shrink.- A
QTreeWidgetrow is as high as the item's size hint, which does not follow the width. A wrapped label in an item widget is clipped as the tree narrows; recompute the item size hints fromheightForWidth()on resize, asCDetailsTrk::updateFilterRowHeights()does. - A combo box beside a field frame adds both widths. Stacking it above cost the speed filters nothing and saved 190 px.
Measure before believing any of this of a given widget: walk the tree printing minimumSizeHint(),
and remember that a sizeHint() is what the widget wants.
Never pass img.bits() as the destination buffer when the width isn't guaranteed to be a
multiple of 4. Qt may pad QImage::bytesPerLine() beyond the pixel width for 1-byte-per-pixel
formats, but a GDAL read with no explicit line spacing assumes a tightly packed buffer
(bytesPerLine == width), silently skewing every row once they diverge.
Read into a flat QVector<quint8> and build the image with the explicit-stride constructor:
QImage(buf.constData(), w, h, w, QImage::Format_Indexed8) — as CDemVRT and CMapVRT::draw() do.
Multi-byte formats (Format_ARGB32) are unaffected since width * 4 is always a multiple of 4.
GDALAutoCreateWarpedVRT resamples onto an axis-aligned bounding box around the reprojected
footprint, so corners with no source coverage exist whenever the source isn't already axis-aligned
with the target SRS. What fills them depends on the path:
- Single-band palette/gray (
CMapVRT) — handled. If the source declares a nodata value, GDAL uses it as both src/dst nodata (we never setpadfSrcNoDataReal/padfDstNoDataReal), so uncovered pixels come back as that index and the constructor zeroes its alpha in the colortable. With no source nodata there is no automatic transparency;Format_Indexed8has no alpha channel to retrofit one. - Multi-band RGB(A) without its own alpha band (
CMapVRT) — handled by a synthetic destination alpha band (GDALWarpInitDefaultBandMapping+psOptions->nDstAlphaBand = nBandCount + 1, mirroringgdalwarp -dstalpha). The warp tracks per-pixel source coverage into it;rasterBandCountis re-read from the warped dataset afterwards sodraw()'s band loop picks it up. Without it, uncovered corners come back solid black — GDAL's own warp fill, which unconditionally overwrites theimg.fill(white)pre-fill indraw(). CDemVRT— no handling. Uncovered elevation samples read back as whatever the destination buffer was zero-initialised to (notNOFLOAT);getElevationAt()/draw()never check warp coverage. Only matters for non-axis-aligned DEM sources. Open.
Symptom: hillshading renders at close zoom but is blank far out. CDemVRT::draw() reads via
ReadRaster() with automatic overview selection, so at high buf_scale GDAL can pick a corrupt or
all-NoData overview level and return an all-NoData buffer.
Check overview integrity before suspecting CDemVRT.cpp/IDem.cpp:
gdallocationinfo -valonly -overview <N> <vrt> <x> <y> at several sample points. Fix by rebuilding
the .ovr — delete the old one, then gdaladdo -ro -r average <vrt> <factors>.
Always resolve via IAppSetup::getPlatformInstance()->findExecutable("toolname"), never a bare
name. CAppSetupWin restricts PATH to the app directory to prevent DLL conflicts, so a bare name
silently yields QProcess::FailedToStart on Windows if the binary isn't co-located with the app.
helpers/CGdalZip.{h,cpp} reads ZIP archives through GDAL's /vsizip/. No ZIP library is needed.
Used by the BRouter installer only.
- Address an archive as
/vsizip/{<absolute path>}/<entry>. The braces bypass GDAL's list of accepted archive suffixes, which lacks.jar. VSIReadDirRecursive()marks directories with a trailing slash;fileList()drops them.extractAll()writes plain files, no permissions and no symlinks, and rejects entries pointing outside the destination.
Warns when a VRT-backed map/DEM has missing or inadequate GDAL overview pyramids, or too many source files, and can fix both.
Files:
helpers/COverviewAdvisory.{h,cpp}— the whole concept:advice_t,file_info_t,geometry_t,read_deadline_t,probe(),setGeometry(),onRenderTimeout(), suppressionhelpers/CGdalVrtUtil.{h,cpp}— small GDAL conveniences only (closeDataset(),isFileUtf8(),allReferencedFilesExist(),toMeters(),progressCallback()). Nothing advisory lives herehelpers/CVrtAdvisoryDialog.{h,cpp}+.ui— the fix/info/combine dialoghelpers/CVrtCombiner.{h,cpp}— "Combine files..." grid-split/footprint logicdem/CDemVRT.{h,cpp},map/CMapVRT.{h,cpp}— each hold oneCOverviewAdvisory advisoryand no advisory state of their owndem/CDemWCS.cpp— opts out viaadvisory.setEnabled(false)map/IMap.h,dem/IDem.h,map/IMapItem.h,map/CMapItemDelegate.{h,cpp}— badge + on-demand infomap/CMapItem.cpp,dem/CDemItem.cpp,map/CMapList.cpp,dem/CDemList.cpp— context-menu entrycanvas/CCanvas.cpp— owns and shows the dialog
Invariant: the dataset is always a VRT. new CMapVRT/new CDemVRT happen only for .vrt
files; every other format has its own class, and CDemWCS opts out. probe() relies on this —
there is no "concrete raster format" branch.
Invariant: the advice is immutable per instance. Any DEM/map list change — including a
successful Fix/Combine (sigContainerRebuilt → setupDemPath/setupMapPath) — destroys the
instance and creates a new one that probes again; nothing updates in place. probe() caches
needsAttention() so the delegate's per-paint badge poll (showsWarning()) is O(1) instead of
walking perFileInfo.
Trigger: draw() wraps ReadRaster() with a 5 s deadline (read_deadline_t + GDAL's own
progress-abort hook — do not add a second warp-options progress callback, it froze the UI). On
timeout onRenderTimeout() fires the advisory (render thread → GUI thread) once per loaded
instance per session, and only when showsWarning() — the same condition as the proactive tree
badge.
Never request a redraw from a timeout path. The view is unchanged, so the retry hits the same
deadline and retries again — emitSigCanvasUpdate() → slotTriggerCompleteUpdate() → update() →
draw thread, unthrottled. On a dataset that stays slow that is an unbounded loop, seen as a
never-finishing calculation with a blinking layer bar.
The dialog is application-modal (setModal(true)): while it is open the map/DEM it is about
must not be read. A pan's draw() or a mouse-move's getElevationAt() racing a Fix/Combine file
rewrite (an external gdaladdo/gdalbuildvrt process — the in-process dataset mutex cannot guard it)
crashes GDAL. Modal blocks user input; a background redraw from a sibling layer during a job is a
known unguarded residual.
A read can be sped up by two additive sources: the container's own overview, and each source file's own overview for the region read.
- The container's claim is trusted immediately only if a real
.ovrfile is inGetFileList(); a bare<OverviewList>is not trusted yet. - If the verified container factor already meets
targetFactor, every source file is skipped (perFileInfostill lists them,checked=false). - Otherwise every source is probed. An unverified
<OverviewList>becomes trusted if every source turns out to have its own overview; otherwise it is discarded. weakestMaxFactor = max(containerFactor, weakestSourceFactor).
containerHasOwnOvr is true only via step 1; a checked=false entry always implies
containerHasOwnOvr == true.
Factors are per-file pixel ratios (fullResSize / overviewSize), read from each file's own band
before any warp — no geotransform/CRS math, even when sources and container differ in CRS.
GetFileList() reports one level only. Sources with heterogeneous projections are commonly
wrapped one gdalwarp -of VRT each and then gdalbuildvrt-ed, so the container's sources are
themselves VRTs. collectLeafSources() follows those down to the real rasters; perFileInfo,
filesToFix(), the subfile count and the disk-usage sum all run on the leaves.
Measured on GDAL 3.12:
gdaladdoon a warped VRT builds nothing. It ignores-ro, writes<OverviewList>into the.vrtitself and exits 0 in ~40 ms. Those overviews are resampled from the source on the fly, so reads still hit the full-resolution leaf — andprobeSource()reads the declaration back as sufficient, clearing the badge for good. On a plain VRT the same command builds a real.vrt.ovr.- A warped VRT needs no
<OverviewList>of its own — it reports its source's overviews. Never add a step that writes one into an intermediate VRT. - Leaf
.ovris the only thing that matters. 10000×10000 warped source, full extent into a 1200 px buffer: 1.20 s bare, 0.41 s with the fake<OverviewList>alone, 0.03 s with a real leaf.ovr.
A nested VRT with its own .ovr is a leaf — real data, the rule step 1 applies to the container.
Nesting alone is not a defect: a VRT-of-VRTs over leaves with overviews is the fastest layout above,
so never warn on the nesting itself.
isVrtFile() asks GDALIdentifyDriver() (header sniff, ~5 µs), not the file extension.
fixPlan() is the single source for the after-fix table, the summary counts, the confirmation
dialog, the gdaladdo queue and the failure cleanup. It returns {path, action} steps —
BuildNew/CleanRebuild on a source raster, AddList/UpdateList on the container. Never derive
any of those five from perFileInfo again; that is how they drift apart.
slotFixOverviews() / finishFixOverviews():
gdaladdoruns on every source short ofsuggestedLevels(or onfilename_itself if there are no source files). Recipe:-r(nearest for palette, average otherwise),COMPRESS_OVERVIEW=DEFLATE, plusPREDICTOR_OVERVIEW=2for non-palette data — matches the source predictor for a ~2–3× smaller.ovr, harmful on palette indices..ovrblock size is inherited from each source automatically.fixContainerOverviewList()rewrites just the<OverviewList>element toadvice_.suggestedLevels— no fullgdalbuildvrtre-run, no re-probe. Container steps carry no command; they are this XML edit.
Rewrites the container VRT to reference a handful of large compressed/tiled GeoTIFFs instead of many small sources.
- Splits the container's own resolved raster, not the source files —
computeGrid()cuts a plain pixel-window grid (pixel_window_t, row/col-tagged). The merge step is a pure crop (gdal_translate -srcwin), no resampling. - Layout comes from the VRT XML, no pixel reads.
readVrtLayout()parses<VRTDataset rasterXSize/rasterYSize>and every source's<DstRect>footprint.tightenToFootprints()crops each cell to the bbox of the footprints overlapping it, or drops it (empty()) if none does. Resolves in ms even for a huge VRT, so it runs inline on the GUI thread — no background scan,CThreadorQProgressDialog. kMaxOutputTiles(40) andkMaxPixelsPerTile(150,000,000) are tuning placeholders, not settled values — they need real tuning against a large VRT.slotCombineFiles()reads the layout, computes and tightens the grid, confirms with the user, backsfilename_up tofilename_ + ".bak", runs onegdal_translate -srcwinper tile into the source files' directory (group_r<row>_c<col>.tif— which can differ from the VRT's own directory), then onegdalbuildvrt -overwrite. CompressionCOMPRESS=DEFLATE/PREDICTOR=2(not ZSTD — not a mandatory GDAL dependency),TILED=YES,BLOCKXSIZE/BLOCKYSIZE=512,BIGTIFF=IF_SAFER.- Offered only when every source sits in one directory (
sharedSourceDir()). Tiles are written beside the sources and the container is rewritten to reference them, so sources spread over several directories have no right answer — the button is withdrawn and the warning says why. Fix overviews has no such limit: each.ovrlands beside its own source wherever that is. JobKind(FixOverviews/Combine) dispatchesslotJobFinished()tofinishFixOverviews()/finishCombine(); both emitsigContainerRebuilt()on success.- Non-destructive: original sources are never deleted. On cancel/failure
filename_is restored from.bak(only the finalgdalbuildvrt -overwritetouches it) and partial tiles are removed. - Limitations: footprint tightening trims only the nodata border between sources, not nodata
inside one; re-running Combine with a different grid size can leave stale
group_rX_cY.tiffiles.
- Subfile-count check (independent of overviews):
hasTooManySubfiles()flags a VRT with more thankMaxSubfileCount(50) sources — GDAL opens and stats every source overlapping a read region, so reads stay slow regardless of overviews.needsAttention()isneedsOverviewFix() || hasTooManySubfiles(); the two problems have independent fixes and independent gating. - Disk usage (
diskUsageBytes/diskUsageIsEstimate): the real on-disk footprint, summed withQFileInfooverGetFileList()plus each source's.ovr/.aux.xmlsidecars (GetFileListomits source sidecars, includes only the container's own). Fully qualified → exact; shallow, missing or no overviews → sub-files × 5/3, flagged as an estimate. Formatted withQLocale::DataSizeSIFormatto matchdu --si, not the 1024-based IEC default. - Dialog table: the container is a synthesized row graded by the same
rowStatus()as source rows.htmlTd()and friends are static methods (not free functions) so they can calltr(); pass plain</>into them, they escape it themselves.hasExistingOverviews()'s container fallback keys offcontainerHasOwnOvr(notcontainerFactor > 0) so the "Update" / "Add<OverviewList>" wording agrees with the fix confirmation dialog. - Table sizing:
sizeTableBrowser()sizes every table browser, capped atkMaxTableWidth(900) andkMaxTableHeight(250); past either the browser scrolls and the dialog keeps its size. Pass it the narrow-tag HTML:idealWidth()on thewidth="100%"variant returns the widget width, not the content. Row counts include the header row, plus the container's synthesized row for the current-state table. - Confirmations: the fix plan goes through
confirmList()(QDialog+QTextBrowser+QDialogButtonBox) because aQMessageBoxcannot scroll its text.confirmYesNo()is aQMessageBoxand takes plain text only. raster_geometry_tcomes fromCGdalVrtUtil::sourceGeometry(pre-warp source)— the source file's own size and resolution, matchinggdalinfo(exact metres for a projected CRS,kMetersPerDegreeapproximation for geographic). Not the warped grid: reading the warped geotransform gives wrong pixel sizes (+26% for a UTM source drawn in EPSG:4326).- Badge and info:
showsOverviewWarning()(!suppress && needsAttention()) drives the tree badge;hasOverviewInfo()drives the "Overview Info..." context-menu entry regardless of attention state.CMapItemDelegate::overviewBadgeRect()is shared bypaint()/editorEvent()/helpEvent()so painted, clickable and tooltip areas cannot drift. - Lifecycle:
closeEvent()/reject()both confirm-cancel a running job and clean up partial output.CCanvas::showOverviewAdvisory()allows one advisory at a time, application-wide — the dialog isshow()n, notexec()d, so the event loop keeps running and modality blocks only user input, never a queuedsigOverviewAdvisoryfrom another layer's render timeout. It searches fromCMainWindow, not the canvas, because a second canvas parents its own dialogs. A dropped advisory keeps its tree badge, so the file stays reachable through Overview Info….
Analysed designs live in .notes/, each wanting its own branch. De-freeze one by pointing at the
file.
QMS-1135-overview-restore-plan.md— transactional rollback when a Fix overviews job is cancelled.waypoint-icon-resolution-plan.md— 32 → 96 px waypoint icons, gated on storingicon_t::focusrelative.
- Retire the platform blocks: the hardcoded
C:\...cache defaults and the macOSQT_DEV_PATH/ROUTINO_DEV_PATH/…FATAL_ERRORgauntlet can move intoCMakePresets.json, andMacOSX/build-QMS.shand the msvc batch files can call--preset. Needs someone who can test both release paths. - Shared
qms_commonlibrary: ten files undersrc/common/are compiled once per app. Blocker issrc/common/help/CHelp.cpp's"helpers/CSettings.h", which resolves per-app; the two copies are byte-identical, so moving that header tosrc/common/helpers/unblocks it. - Confirm on their own platforms that these removals were inert: macOS
LINK_FLAGS, the-frameworkentries inCMAKE_C_FLAGSand the three framework include dirs; Windows qmt_map2jnx'sWin32/include dir.msvc_64/cmake/{FindGDAL,FindPROJ,FindJPEG}.cmakecan go if the gisinternals GDAL shipsGDALConfig.cmake.
GDAL receives filenames as UTF-8 everywhere. A guard rejects .vrt files whose bytes aren't valid
UTF-8, which dead-ends users with legacy Windows-1252/Latin-1 VRTs. Offer a confirmed, .bak-backed
one-click conversion instead.
GDAL facts behind the design (tested on 3.12):
- The CPL XML parser ignores the VRT
<?xml encoding?>declaration —<SourceFilename>bytes go toopen()verbatim, so even a correctly-declaredISO-8859-1VRT fails. Only raw bytes matter, so a byte-level UTF-8 check has no false-rejection risk. - On-disk source names are UTF-8, so transcoding a Latin-1 VRT reproduces the real names.
- GDAL does not hard-fail a bad path: exit 0, checksum -1,
ERROR 4on stderr — a quietly broken dataset, which is what feeds the missing-file advisory.
Design: transcode from candidate encodings (Windows-1252 → ISO-8859-1 → system ANSI), then verify
by resolution — every <SourceFilename> must now exist on disk. First candidate that resolves all
sources wins, else fall back to plain rejection. Trigger at load time in CMapVRT/CDemVRT
construction (a non-UTF-8 VRT never constructs, so the advisory dialog cannot be the entry point),
reusing the advisory .bak/rewrite scaffolding. Not platform-gated — it reproduces on Linux. Also
rewrite the <?xml encoding?> declaration to UTF-8.
POIs are not waypoints — no canvas freeze applies. 303 SVGs already ship under
src/icons/poi/SJJB/svg/<category>/ and 249 of the 250 referenced PNGs have a counterpart, but none
are in resources.qrc. They are SJJB templates carrying a placeholder fill #111111 that their
build recolours per category, so the colour must be recovered per icon from the shipped PNGs
(16 categories, 6 with 2–3 variants).
Work: register ~250 SVGs, add a recolour step, change CPoiIconCategory's QPixmap members to
paths plus ~302 literals in CPoiFilePOI_TagMap.cpp — the path shape changes, so that needs a
lookup table, not a regex — and a render cache keyed by (icon, size, dpr). The cache is a win
regardless: CPoiFilePOI currently .scaled()s a pixmap per POI per repaint.
Gate it or do not do it: render each recoloured SVG at 32, diff against its shipped PNG, require
visible (>8) == 0. A diff that measures colour reports ~240 false failures.
Two defects found while scoping: CPoiFilePOI_TagMap.cpp asks for health_pharmacy_dispencing
(a typo; the SVG is pharmacy_dispensing), and one SVG embeds a raster that is not in the tree
(pastedpic_10102008_233747.png).
- Open: batch
getElevationAt()/getSlopeAt()point queries. Each is one smallRasterIOcall per point (e.g. per vertex of a track elevation profile) — a different path fromdraw()'s map rendering. Worth revisiting only if track-profile performance comes up. - Deliberately skipped: caching
1/xscale/1/yscaleforslopeOfWindowInterp(). Modest win, and hoisting needs either a signature change touching its three callers or new cached members with a staleness trap —xscale/yscaleare plain protected members assigned directly, with no setter to keep a reciprocal in sync.