QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
HeightField.cc
Go to the documentation of this file.
1/****************************************************************************
2 *
3 * (c) 2009-2024 QGROUNDCONTROL PROJECT <http://www.qgroundcontrol.org>
4 *
5 * QGroundControl is licensed according to the terms in the file
6 * COPYING.md in the root of the source code directory.
7 *
8 ****************************************************************************/
9
10#include "HeightField.h"
11
12#include <QtCore/QVarLengthArray>
13#include <QtCore/QtMinMax>
14
15#include <cmath>
16#include <utility>
17
18#include "QGCLoggingCategory.h"
19
20QGC_LOGGING_CATEGORY(GeoMapHeightFieldLog, "GeoMap.HeightField")
21QGC_LOGGING_CATEGORY(GeoMapHeightFieldVerboseLog, "GeoMap.HeightField.Verbose")
22
23namespace {
24
29double heightAtUV(const ElevationTilePyramid::Grid& grid, double u, double v)
30{
31 const int w = grid.width;
32 const int h = grid.height;
33 const double px = (u * w) - 0.5;
34 const double py = (v * h) - 0.5;
35 const int x0 = qBound(0, static_cast<int>(std::floor(px)), w - 1);
36 const int y0 = qBound(0, static_cast<int>(std::floor(py)), h - 1);
37 const int x1 = qMin(x0 + 1, w - 1);
38 const int y1 = qMin(y0 + 1, h - 1);
39 const double fx = qBound(0.0, px - x0, 1.0);
40 const double fy = qBound(0.0, py - y0, 1.0);
41
42 const auto at = [&grid](int x, int y) { return double(grid.heights[(qsizetype(y) * grid.width) + x]); };
43 const double north = (at(x0, y0) * (1.0 - fx)) + (at(x1, y0) * fx);
44 const double south = (at(x0, y1) * (1.0 - fx)) + (at(x1, y1) * fx);
45 return (north * (1.0 - fy)) + (south * fy);
46}
47
48} // namespace
49
50HeightField::HeightField(QObject* parent) : QObject(parent) {}
51
53{
54 // Capture before the move: on rejection the grid has been consumed
55 const int gridWidth = grid.width;
56 const int gridHeight = grid.height;
57 TileMath::TileKey evicted{0, 0, -1};
58 if (!_pyramid.insertTile(key, std::move(grid), &evicted)) {
59 qCWarning(GeoMapHeightFieldLog) << "insertTile rejected: key" << key << "grid" << gridWidth << "x"
60 << gridHeight;
61 return false;
62 }
63 _memoView = ElevationTilePyramid::View{}; // grid pointers die on insert
64 qCDebug(GeoMapHeightFieldVerboseLog) << "inserted tile" << key;
65
66 const auto tileExtent = [](const TileMath::TileKey& k) {
67 const QPointF corner = TileMath::tileMinCorner(k);
68 const double span = TileMath::tileSpanAtZoom(k.zoom);
69 return QRectF(corner.x(), corner.y(), span, span);
70 };
71 if (TileMath::isValidKey(evicted)) {
72 qCDebug(GeoMapHeightFieldVerboseLog) << "evicted tile" << evicted;
73 emit regionChanged(tileExtent(evicted));
74 }
75 emit regionChanged(tileExtent(key));
76 return true;
77}
78
79double HeightField::heightAt(const QPointF& world) const
80{
82
83 // Memoized fast path: the last resolved tile answers when the position
84 // resolves to it (the memo is only populated when no stored descendant
85 // could override it, and every insert invalidates it). The hit test uses
86 // the same key arithmetic as the full lookup — a world-coordinate bounds
87 // check can disagree with tileForWorld by an ulp exactly on a tile
88 // boundary, silently answering with the wrong neighbor's data there.
89 if (_memoView.isValid()) {
90 const int shift = TileMath::kMaxZoom - _memoView.key.zoom;
91 if (((query.x >> shift) == _memoView.key.x) && ((query.y >> shift) == _memoView.key.y)) {
92 const double u = (world.x() - _memoMinX) / _memoSpan;
93 const double v = (_memoMaxY - world.y()) / _memoSpan; // grid origin is the NW corner
94 return heightAtUV(*_memoView.grid, u, v);
95 }
96 }
97
98 // The pyramid resolves the finest stored cover of the deepest-zoom query
99 const ElevationTilePyramid::View view = _pyramid.bestTileFor(query);
100 if (!view.isValid()) {
101 return 0.0;
102 }
103
104 const QPointF corner = TileMath::tileMinCorner(view.key);
105 const double span = TileMath::tileSpanAtZoom(view.key.zoom);
106 if (!_pyramid.hasDescendant(view.key)) {
107 // Nothing finer exists anywhere inside this tile, so it answers for
108 // every position within its bounds until the next insert
109 _memoView = view;
110 _memoMinX = corner.x();
111 _memoMaxY = corner.y() + span;
112 _memoSpan = span;
113 }
114
115 const double u = (world.x() - corner.x()) / span;
116 const double v = ((corner.y() + span) - world.y()) / span; // grid origin is the NW corner
117 return heightAtUV(*view.grid, u, v);
118}
119
120QList<float> HeightField::samplePatch(const TileMath::TileKey& key, int gridSize) const
121{
122 if ((gridSize < 1) || (gridSize > kMaxGridSize) || !TileMath::isValidKey(key)) {
123 qCWarning(GeoMapHeightFieldLog) << "samplePatch rejected: key" << key << "gridSize" << gridSize;
124 return QList<float>();
125 }
126
127 // Interior vertices resolve the patch's backing view once, by the
128 // patch's own key, and interpolate within that one grid. Boundary
129 // vertices are shared with neighbor patches whose backing views can
130 // differ (adjacent exact tiles, fine tile next to an ancestor-backed
131 // neighbor), so they resolve canonically by position instead: the
132 // deepest-zoom cells touching the vertex, tried east/south first, pick
133 // the same stored tile no matter which patch asks. Positions are exact
134 // dyadic values, so coincident vertices compute bit-identical UVs and
135 // sample bit-identical heights: meshes never crack where data exists.
136 const ElevationTilePyramid::View patchView = _pyramid.bestTileFor(key);
137
138 // Height at the exact vertex position (n/gridSize, m/gridSize in tile
139 // units at key.zoom) within a resolved view; ldexp rescales exactly, so
140 // equal positions give equal UV bits regardless of the asking patch
141 const auto viewHeight = [&key, gridSize](const ElevationTilePyramid::View& view, qint64 n, qint64 m) {
142 const double u = (std::ldexp(double(n), view.key.zoom - key.zoom) / gridSize) - view.key.x;
143 const double v = (std::ldexp(double(m), view.key.zoom - key.zoom) / gridSize) - view.key.y;
144 return static_cast<float>(heightAtUV(*view.grid, u, v));
145 };
146
147 // Memo of resolved views per key.zoom tile: when a tile has no stored
148 // descendant, every cell inside it resolves identically (chain below the
149 // tile is empty, ancestors are shared), so one lookup answers the whole
150 // edge run along it. An invalid view memoizes the same way — repeated
151 // misses over uncovered neighbors stay O(1). A patch's boundary touches
152 // at most 9 such tiles (own + 8 neighbors).
153 struct TileMemo
154 {
157 };
158
159 QVarLengthArray<TileMemo, 9> memos;
160
161 const int shiftToMax = TileMath::kMaxZoom - key.zoom;
162 const auto resolveCell = [&, this](qint64 cx, qint64 cy) {
163 const TileMath::TileKey tile{int(cx >> shiftToMax), int(cy >> shiftToMax), key.zoom};
164 for (const TileMemo& memo : memos) {
165 if (memo.tile == tile) {
166 return memo.view;
167 }
168 }
169 const ElevationTilePyramid::View view =
170 _pyramid.bestTileFor(TileMath::TileKey{int(cx), int(cy), TileMath::kMaxZoom});
171 if (!_pyramid.hasDescendant(tile) && (memos.size() < memos.capacity())) {
172 memos.append({tile, view});
173 }
174 return view;
175 };
176
177 const auto boundaryHeight = [&](qint64 n, qint64 m) -> float {
178 // Cells touching the vertex on each axis, east/south side first; a
179 // vertex not exactly on a cell boundary lies in a single cell
180 const qint64 cellCount = qint64(1) << TileMath::kMaxZoom;
181 const auto touchingCells = [&](qint64 s, qint64(&cells)[2]) {
182 int count = 0;
183 const qint64 cell = s / gridSize;
184 if (cell < cellCount) {
185 cells[count++] = cell;
186 }
187 if (((s % gridSize) == 0) && (cell > 0)) {
188 cells[count++] = cell - 1;
189 }
190 return count;
191 };
192 qint64 xCells[2];
193 qint64 yCells[2];
194 const int xCount = touchingCells(n << shiftToMax, xCells);
195 const int yCount = touchingCells(m << shiftToMax, yCells);
196
197 // Fixed candidate order derived purely from the position: every
198 // patch sharing this vertex walks the same cells and returns the
199 // first resolvable view, so the height is canonical
200 for (int yi = 0; yi < yCount; yi++) {
201 for (int xi = 0; xi < xCount; xi++) {
202 const ElevationTilePyramid::View view = resolveCell(xCells[xi], yCells[yi]);
203 if (view.isValid()) {
204 return viewHeight(view, n, m);
205 }
206 }
207 }
208 return 0.0f; // no stored data touches the vertex: every sharer agrees on zero
209 };
210
211 QList<float> heights;
212 heights.reserve(qsizetype(gridSize + 1) * (gridSize + 1));
213 for (int row = 0; row <= gridSize; row++) {
214 const qint64 m = (qint64(key.y) * gridSize) + row;
215 for (int col = 0; col <= gridSize; col++) {
216 const qint64 n = (qint64(key.x) * gridSize) + col;
217 if ((row == 0) || (row == gridSize) || (col == 0) || (col == gridSize)) {
218 heights.append(boundaryHeight(n, m));
219 } else {
220 heights.append(patchView.isValid() ? viewHeight(patchView, n, m) : 0.0f);
221 }
222 }
223 }
224 return heights;
225}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
View bestTileFor(const TileMath::TileKey &key) const
bool insertTile(const TileMath::TileKey &key, Grid grid, TileMath::TileKey *evictedKey=nullptr)
bool hasDescendant(const TileMath::TileKey &key) const
bool insertTile(const TileMath::TileKey &key, ElevationTilePyramid::Grid grid)
QList< float > samplePatch(const TileMath::TileKey &key, int gridSize) const
HeightField(QObject *parent=nullptr)
double heightAt(const QPointF &world) const
static constexpr int kMaxGridSize
Sanity cap on patch density: rejects absurd sizes before allocation.
Definition HeightField.h:44
void regionChanged(const QRectF &worldRect)
QPointF tileMinCorner(const TileKey &key)
South-west (minimum x/y) corner of a tile in world meters.
Definition TileMath.cc:58
constexpr int kMaxZoom
Definition TileMath.h:31
bool isValidKey(const TileKey &key)
True if zoom is within [kMinZoom, kMaxZoom] and x/y address a tile at that zoom.
Definition TileMath.cc:44
double tileSpanAtZoom(int zoom)
Edge length of one tile at zoom, in world meters.
Definition TileMath.cc:53
TileKey tileForWorld(const QPointF &world, int zoom)
Tile containing a world point. World coordinates are clamped to the world extent.
Definition TileMath.cc:67
Decoded elevation samples for one tile, row-major from the NW corner.
const Grid * grid
null when nothing stored covers the query
TileMath::TileKey key
stored tile the view samples