QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
SurfaceModel.cc
Go to the documentation of this file.
1#include "SurfaceModel.h"
2
3#include <algorithm>
4#include <cmath>
5#include <utility>
6#include <vector>
7
8#include <QtCore/QElapsedTimer>
9#include <QtCore/QSet>
10#include <QtCore/QString>
11#include <QtCore/QTimer>
12#include <QtCore/QVarLengthArray>
13#include <QtCore/QtMath>
14#include <QtPositioning/QGeoCoordinate>
15#include <queue>
16
17#include "Fact.h"
18#include "FlyViewSettings.h"
19#include "GeoMapCamera.h"
20#include "HeightField.h"
21#include "HeightSource.h"
22#include "PatchMesh.h"
23#include "QGCLoggingCategory.h"
24#include "SettingsManager.h"
25
26QGC_LOGGING_CATEGORY(GeoMapSurfaceModelLog, "GeoMap.SurfaceModel")
27QGC_LOGGING_CATEGORY(GeoMapSurfaceModelVerboseLog, "GeoMap.SurfaceModel.Verbose")
28QGC_LOGGING_CATEGORY(GeoMapSurfaceModelCliffsLog, "GeoMap.SurfaceModel.Cliffs")
29
30namespace {
31
32QRectF patchRect(const TileMath::TileKey& key)
33{
34 const double span = TileMath::tileSpanAtZoom(key.zoom);
35 const QPointF minCorner = TileMath::tileMinCorner(key);
36 return QRectF(minCorner.x(), minCorner.y(), span, span);
37}
38
42bool patchTouchesRegion(const TileMath::TileKey& key, const QRectF& region)
43{
44 const QRectF rect = patchRect(key);
45 const double margin = rect.width() * 1e-6;
46 return rect.marginsAdded(QMarginsF(margin, margin, margin, margin)).intersects(region);
47}
48
49float maxHeightOf(const QList<float>& heights)
50{
51 float maxHeight = 0.0f;
52 for (const float height : heights) {
53 if (std::isfinite(height)) {
54 maxHeight = std::max(maxHeight, height);
55 }
56 }
57 return maxHeight;
58}
59
61QString boundaryLabel(int row, int col, int gridSize)
62{
63 const QChar edge = (row == 0) ? QLatin1Char('N')
64 : (row == gridSize) ? QLatin1Char('S')
65 : (col == 0) ? QLatin1Char('W')
66 : QLatin1Char('E');
67 return edge + QStringLiteral(" edge");
68}
69
70} // namespace
71
72SurfaceModel::SurfaceModel(GeoMapCamera* camera, HeightSource* heightSource, HeightField* field, QObject* parent)
73 : QObject(parent), _camera(camera), _heightSource(heightSource), _field(field)
74{
75 qRegisterMetaType<TileMath::TileKey>();
76 _cliffClock.start();
77
78 connect(_field, &HeightField::regionChanged, this, &SurfaceModel::_fieldRegionChanged);
79 // Resident patches pick up (or drop) monitor state now, not on their next data change
80 connect(SettingsManager::instance()->flyViewSettings()->geoMapDebugUI(), &Fact::rawValueChanged, this, [this]() {
81 for (auto it = _patches.begin(); it != _patches.end(); ++it) {
82 _samplePatch(it.key(), it.value());
83 }
84 });
85
86 // Coalesce: camera signals fire per input event (up to 120/s during a
87 // gesture); one queued pass per event-loop iteration bounds the cost
88 connect(_camera, &GeoMapCamera::centerChanged, this, &SurfaceModel::_scheduleUpdate);
89 connect(_camera, &GeoMapCamera::headingChanged, this, &SurfaceModel::_scheduleUpdate);
90 connect(_camera, &GeoMapCamera::tiltChanged, this, &SurfaceModel::_scheduleUpdate);
91 connect(_camera, &GeoMapCamera::distanceChanged, this, &SurfaceModel::_scheduleUpdate);
92 connect(_camera, &GeoMapCamera::centerElevationChanged, this, &SurfaceModel::_scheduleUpdate);
93 connect(_camera, &GeoMapCamera::viewportSizeChanged, this, &SurfaceModel::_scheduleUpdate);
94 connect(_camera, &GeoMapCamera::fieldOfViewChanged, this, &SurfaceModel::_scheduleUpdate);
95}
96
97void SurfaceModel::_scheduleUpdate()
98{
99 if (_updatePending) {
100 return;
101 }
102 _updatePending = true;
103 QTimer::singleShot(0, this, [this] {
104 if (!_updatePending) {
105 return; // absorbed by a direct update() call meanwhile
106 }
107 update();
108 });
109}
110
112{
113 // Any direct pass supersedes a queued coalesced one (its singleShot
114 // becomes a no-op), so updateSettled() is truthful right after a call
115 _updatePending = false;
116 if (!_camera->isPositioned()) {
117 return; // the default pose is null island; building patches there fires doomed terrain fetches
118 }
119 if (_camera->viewportSize().isEmpty()) {
120 return; // keep the last valid set; transient 0-size viewports occur during layout
121 }
122
123 QElapsedTimer updateTimer;
124 updateTimer.start();
125
126 // Max terrain height scans every resident vertex: compute once per update
127 const double terrainZ = _maxTerrainZ();
128 const QRectF visible = _visibleGroundRect(terrainZ);
129 _culledTerrainZ = terrainZ;
130 const QPointF cameraGround = _camera->cameraGroundPosition();
131 const double cameraHeight = _camera->distance() * std::cos(qDegreesToRadians(_camera->tilt()));
132
133 const QList<TileMath::TileKey> desired = _desiredPatches(visible, cameraGround, cameraHeight);
134
135 QSet<TileMath::TileKey> desiredSet(desired.cbegin(), desired.cend());
136
137 QVarLengthArray<QRectF, 8> churnRects; // added/removed extents: neighbors there re-stitch
138 const AddResult added = _addDesiredPatches(desired, churnRects);
139 const int removals = _removeStalePatches(desired, desiredSet, churnRects);
140 _notifyEdgeChurn(churnRects);
141
142 if ((added.adds > 0) || (removals > 0)) {
143 _repinAncestors();
145 }
146
147 // The caps guarantee every deferred pass makes progress, so follow-ups
148 // always converge
149 if (_addsDeferred || _removalsDeferred) {
150 _scheduleUpdate();
151 }
152
153 const qint64 elapsedUs = updateTimer.nsecsElapsed() / 1000;
154 qCDebug(GeoMapSurfaceModelVerboseLog)
155 << "update pass: desired" << desired.count() << "resident" << _patches.count() << "adds" << added.adds
156 << "removals" << removals << "deferred adds" << _addsDeferred << "deferred removals" << _removalsDeferred
157 << "elapsedUs" << elapsedUs << "addUs" << added.addUs;
158 _updateStats.updates++;
159 _updateStats.totalUs += elapsedUs;
160 _updateStats.maxUs = std::max(_updateStats.maxUs, elapsedUs);
161}
162
168SurfaceModel::AddResult SurfaceModel::_addDesiredPatches(const QList<TileMath::TileKey>& desired,
169 QVarLengthArray<QRectF, 8>& churnRects)
170{
171 AddResult result;
172 _addsDeferred = false;
173 QElapsedTimer addTimer;
174 addTimer.start();
175 for (const TileMath::TileKey& key : desired) {
176 if (_patches.contains(key)) {
177 continue;
178 }
179 if (result.adds >= kMaxPatchAddsPerUpdate) {
180 _addsDeferred = true;
181 continue;
182 }
183 PatchData data;
184 _samplePatch(key, data);
185 _patches.insert(key, std::move(data));
186 churnRects.append(patchRect(key));
187 _heightSource->requestTile(key);
188 emit patchAdded(key);
189 result.adds++;
190 }
191 result.addUs = addTimer.nsecsElapsed() / 1000;
192 _updateStats.addTotalUs += result.addUs;
193 _updateStats.addMaxUs = std::max(_updateStats.addMaxUs, result.addUs);
194 return result;
195}
196
201int SurfaceModel::_removeStalePatches(const QList<TileMath::TileKey>& desired,
202 const QSet<TileMath::TileKey>& desiredSet, QVarLengthArray<QRectF, 8>& churnRects)
203{
204 // Rects of desired patches still not resident after the capped adds: a
205 // no-longer-desired patch overlapping one may not be removed yet, or the
206 // surface would show a hole until the adds catch up. Computed after the
207 // adds so a replaced patch drops in the same pass its last replacement
208 // arrives, instead of both rendering (and z-fighting) one extra pass.
209 QVarLengthArray<QRectF, 16> missingRects;
210 for (const TileMath::TileKey& key : desired) {
211 if (!_patches.contains(key)) {
212 missingRects.append(patchRect(key));
213 }
214 }
215 const auto overlapsMissing = [&missingRects](const TileMath::TileKey& key) {
216 const QRectF rect = patchRect(key);
217 for (const QRectF& missing : missingRects) {
218 if (rect.intersects(missing)) {
219 return true;
220 }
221 }
222 return false;
223 };
224
225 int removals = 0;
226 _removalsDeferred = false;
227 for (auto it = _patches.begin(); it != _patches.end();) {
228 if (desiredSet.contains(it.key())) {
229 ++it;
230 continue;
231 }
232 if ((removals >= kMaxPatchRemovalsPerUpdate) || overlapsMissing(it.key())) {
233 _removalsDeferred = true; // stays resident one more pass; follow-up finishes the cull
234 ++it;
235 continue;
236 }
237 const TileMath::TileKey removedKey = it.key();
238 if (it.value().cliffSinceMs >= 0) {
239 qCWarning(GeoMapSurfaceModelCliffsLog) << "cliff removed with patch" << removedKey << "after"
240 << (_cliffClock.elapsed() - it.value().cliffSinceMs) << "ms";
241 }
242 it = _patches.erase(it);
243 churnRects.append(patchRect(removedKey));
244 emit patchRemoved(removedKey);
245 removals++;
246 }
247 return removals;
248}
249
252void SurfaceModel::_notifyEdgeChurn(const QVarLengthArray<QRectF, 8>& churnRects)
253{
254 for (auto it = _patches.cbegin(); it != _patches.cend(); ++it) {
255 for (const QRectF& rect : churnRects) {
256 if (patchTouchesRegion(it.key(), rect)) {
257 emit patchEdgeDeltasChanged(it.key());
258 break;
259 }
260 }
261 }
262}
263
268void SurfaceModel::_repinAncestors()
269{
270 QSet<TileMath::TileKey> pinned;
271 const auto pinChain = [&pinned](TileMath::TileKey key) {
272 // A pinned key already has its whole chain pinned
273 while (!pinned.contains(key)) {
274 pinned.insert(key);
275 if (key.zoom == TileMath::kMinZoom) {
276 return;
277 }
278 key = TileMath::TileKey{key.x >> 1, key.y >> 1, key.zoom - 1};
279 }
280 };
281 for (auto it = _patches.cbegin(); it != _patches.cend(); ++it) {
282 const TileMath::TileKey key = it.key();
283 for (int dy = -1; dy <= 1; dy++) {
284 for (int dx = -1; dx <= 1; dx++) {
285 const TileMath::TileKey neighbor{key.x + dx, key.y + dy, key.zoom};
286 if (TileMath::isValidKey(neighbor)) {
287 pinChain(neighbor);
288 }
289 }
290 }
291 }
292 _field->setPinnedKeys(std::move(pinned));
293}
294
295// GCOVR_EXCL_START — perf-stats instrumentation for the debug overlay/capture
297{
298 const UpdateStats stats = _updateStats;
299 _updateStats = UpdateStats{};
300 return stats;
301}
302
303// GCOVR_EXCL_STOP
304
305QList<SurfaceModel::Patch> SurfaceModel::patches() const
306{
307 QList<Patch> result;
308 result.reserve(_patches.count());
309 for (auto it = _patches.cbegin(); it != _patches.cend(); ++it) {
310 result.append(Patch{it.key(), it.value().heights, true, false, it.value().edgeStep, it.value().edgeStepAt,
311 it.value().edgeCliff});
312 }
313 return result;
314}
315
316std::optional<SurfaceModel::Patch> SurfaceModel::patch(const TileMath::TileKey& key) const
317{
318 const auto it = _patches.constFind(key);
319 if (it == _patches.cend()) {
320 return std::nullopt;
321 }
322 return Patch{
323 key, it.value().heights, true, false, it.value().edgeStep, it.value().edgeStepAt, it.value().edgeCliff};
324}
325
326std::optional<double> SurfaceModel::renderedHeightAt(const QPointF& world) const
327{
328 // Finest first: a patch retiring behind resident replacements still overlaps them
329 for (int zoom = TileMath::kMaxZoom; zoom >= TileMath::kMinZoom; zoom--) {
330 const TileMath::TileKey owner = TileMath::tileForWorld(world, zoom);
331 const QRectF ownerRect = patchRect(owner);
332 // A point on a tile boundary maps to the east/south tile, but the west/north one draws it too
333 const double onEdge = ownerRect.width() * 1e-9;
334 const int westSteps = ((world.x() - ownerRect.left()) <= onEdge) ? 1 : 0;
335 const int northSteps = ((ownerRect.bottom() - world.y()) <= onEdge) ? 1 : 0;
336 TileMath::TileKey key = owner;
337 auto it = _patches.cend();
338 for (int dy = 0; (dy <= northSteps) && (it == _patches.cend()); dy++) {
339 for (int dx = 0; (dx <= westSteps) && (it == _patches.cend()); dx++) {
340 key = TileMath::TileKey{owner.x - dx, owner.y - dy, zoom};
341 it = TileMath::isValidKey(key) ? _patches.constFind(key) : _patches.cend();
342 }
343 }
344 if (it == _patches.cend()) {
345 continue;
346 }
347 const QRectF rect = patchRect(key);
348 const double cell = rect.width() / kGridSize;
349 // Row 0 is the north (max y) edge
350 const double col = (world.x() - rect.left()) / cell;
351 const double row = ((rect.top() + rect.height()) - world.y()) / cell;
352 // Stitching moves only edge vertices: interior cells skip the neighbor lookups
353 PatchMesh::LodDeltas deltas{};
354 if (row < 1.0) {
355 deltas[0] = _edgeDelta(key, 0, -1);
356 }
357 if (row >= (kGridSize - 1)) {
358 deltas[1] = _edgeDelta(key, 0, 1);
359 }
360 if (col < 1.0) {
361 deltas[2] = _edgeDelta(key, -1, 0);
362 }
363 if (col >= (kGridSize - 1)) {
364 deltas[3] = _edgeDelta(key, 1, 0);
365 }
366 return PatchMesh::surfaceHeight(it->heights, kGridSize, deltas, row, col);
367 }
368 return std::nullopt;
369}
370
372{
373 // {N,S,W,E} neighbor offsets; slippy y grows south, so north is y-1
374 static constexpr int kOffsets[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
375 QList<int> deltas;
376 deltas.reserve(4);
377 for (const auto& offset : kOffsets) {
378 deltas.append(_edgeDelta(key, offset[0], offset[1]));
379 }
380 return deltas;
381}
382
383int SurfaceModel::_edgeDelta(const TileMath::TileKey& key, int dx, int dy) const
384{
385 const TileMath::TileKey neighbor{key.x + dx, key.y + dy, key.zoom};
386 if (!TileMath::isValidKey(neighbor) || _patches.contains(neighbor)) {
387 return 0; // world edge or same-zoom neighbor: unconstrained
388 }
389 // First resident ancestor of the missing same-zoom neighbor is the coarse
390 // patch rendering across this edge; finer neighbors constrain themselves
391 TileMath::TileKey ancestor = neighbor;
392 while (ancestor.zoom > TileMath::kMinZoom) {
393 ancestor = TileMath::TileKey{ancestor.x >> 1, ancestor.y >> 1, ancestor.zoom - 1};
394 if (_patches.contains(ancestor)) {
395 const int delta = key.zoom - ancestor.zoom;
396 // No coincident vertices beyond this: leave unconstrained (skirts cover)
397 return ((kGridSize % (1 << delta)) == 0) ? delta : 0;
398 }
399 }
400 return 0;
401}
402
403QRectF SurfaceModel::_visibleGroundRect(double terrainZ) const
404{
405 const QSizeF viewport = _camera->viewportSize();
406 const double maxRange = _camera->distance() * kMaxRangeMultiplier;
407
408 // Terrain-aware near boundary: rays that hit the ground plane far ahead
409 // cross the terrain-top plane much closer to (or behind) the camera, so
410 // tall terrain there is visible even though its flat-ground point is not
411 const double eyeZ =
412 _camera->centerElevation() + (_camera->distance() * std::cos(qDegreesToRadians(_camera->tilt())));
413 const QPointF cameraGround = _camera->cameraGroundPosition();
414 const double terrainT = ((terrainZ > 0.0) && (eyeZ > terrainZ)) ? (1.0 - (terrainZ / eyeZ)) : 0.0;
415
416 // Sample a screen grid; horizon misses are capped at maxRange. Accumulate
417 // extremes manually (zero-size QRectFs are "null" and united() ignores them).
418 double minX = 0;
419 double minY = 0;
420 double maxX = 0;
421 double maxY = 0;
422 bool first = true;
423 const auto include = [&](const QPointF& point) {
424 if (first) {
425 minX = maxX = point.x();
426 minY = maxY = point.y();
427 first = false;
428 } else {
429 minX = std::min(minX, point.x());
430 maxX = std::max(maxX, point.x());
431 minY = std::min(minY, point.y());
432 maxY = std::max(maxY, point.y());
433 }
434 };
435 for (int row = 0; row < kVisibleSampleGrid; row++) {
436 for (int col = 0; col < kVisibleSampleGrid; col++) {
437 const QPointF screenPos((viewport.width() * col) / (kVisibleSampleGrid - 1),
438 (viewport.height() * row) / (kVisibleSampleGrid - 1));
439 const auto ground = _camera->groundPointCapped(screenPos, maxRange);
440 if (!ground) {
441 continue;
442 }
443 include(*ground);
444 if (terrainT > 0.0) {
445 include(cameraGround + ((*ground - cameraGround) * terrainT));
446 }
447 }
448 }
449 if (first) {
450 return QRectF();
451 }
452 // Always keep the camera's tile in range: the terrain ceiling above only
453 // knows resident patches, so terrain living solely in never-requested
454 // tiles near the camera could otherwise never be discovered
455 include(cameraGround);
456
457 const double half = TileMath::worldSize() / 2.0;
458 return QRectF(QPointF(minX, minY), QPointF(maxX, maxY))
459 .intersected(QRectF(-half, -half, TileMath::worldSize(), TileMath::worldSize()));
460}
461
464double SurfaceModel::_maxTerrainZ() const
465{
466 float maxHeight = 0.0f;
467 for (const PatchData& data : _patches) {
468 maxHeight = std::max(maxHeight, data.maxHeight);
469 }
470 if (maxHeight <= 0.0f) {
471 return 0.0;
472 }
473 return maxHeight * TileMath::mercatorScale(_camera->center().latitude());
474}
475
476double SurfaceModel::_projectedPixels(const TileMath::TileKey& key, const QPointF& cameraGround,
477 double cameraHeight) const
478{
479 const QRectF rect = patchRect(key);
480 const double span = rect.width();
481
482 // Distance from the camera to the closest point of the patch (heights ignored for LOD)
483 const double dx = std::max({rect.left() - cameraGround.x(), cameraGround.x() - rect.right(), 0.0});
484 const double dy = std::max({rect.top() - cameraGround.y(), cameraGround.y() - rect.bottom(), 0.0});
485 const double distance = std::hypot(std::hypot(dx, dy), cameraHeight);
486
487 const double metersPerPixel =
488 (2.0 * distance * std::tan(qDegreesToRadians(_camera->verticalFieldOfView()) / 2.0)) / _camera->viewportSize().height();
489 return span / metersPerPixel;
490}
491
492QList<TileMath::TileKey> SurfaceModel::_desiredPatches(const QRectF& visible, const QPointF& cameraGround,
493 double cameraHeight) const
494{
495 // Coverage-preserving refinement: repeatedly split the visible patch with
496 // the largest projected size while the budget allows. A split only ever
497 // replaces a patch with its visible children, so hitting kMaxPatches leaves
498 // the coarsest acceptable coverage instead of punching holes in it.
499 struct Entry
500 {
501 double pixels;
503 };
504
505 const auto lessPixels = [](const Entry& a, const Entry& b) { return a.pixels < b.pixels; };
506 std::priority_queue<Entry, std::vector<Entry>, decltype(lessPixels)> queue(lessPixels);
507
508 QList<TileMath::TileKey> desired;
509
510 const TileMath::TileKey root{0, 0, 0};
511 if (visible.intersects(patchRect(root))) {
512 queue.push(Entry{_projectedPixels(root, cameraGround, cameraHeight), root});
513 }
514
515 while (!queue.empty()) {
516 const Entry entry = queue.top();
517 if (entry.pixels <= kRefinePixelThreshold) {
518 break; // largest is small enough, so every remaining patch is too
519 }
520 if (entry.key.zoom >= TileMath::kMaxZoom) {
521 queue.pop();
522 desired.append(entry.key); // cannot refine further; keep as-is
523 continue;
524 }
525
526 QVarLengthArray<Entry, 4> children;
527 for (int childY = 0; childY < 2; childY++) {
528 for (int childX = 0; childX < 2; childX++) {
529 const TileMath::TileKey childKey{(entry.key.x * 2) + childX, (entry.key.y * 2) + childY,
530 entry.key.zoom + 1};
531 if (visible.intersects(patchRect(childKey))) {
532 children.append(Entry{_projectedPixels(childKey, cameraGround, cameraHeight), childKey});
533 }
534 }
535 }
536 const int totalAfterSplit = static_cast<int>(queue.size()) + desired.count() - 1 + children.count();
537 if (totalAfterSplit > kMaxPatches) {
538 break; // budget exhausted: keep remaining patches coarse
539 }
540 queue.pop();
541 for (const Entry& child : children) {
542 queue.push(child);
543 }
544 }
545
546 while (!queue.empty()) {
547 desired.append(queue.top().key);
548 queue.pop();
549 }
550 return desired;
551}
552
553void SurfaceModel::_fieldRegionChanged(const QRectF& worldRect)
554{
555 // Re-mesh exactly the patches touching the changed region: their field
556 // samples (and skirt/normal data downstream) may have changed. Patch edge
557 // vertices exactly on the region boundary sample the new data too, hence
558 // the inflated-rect contact test.
559 int remeshed = 0;
560 for (auto it = _patches.begin(); it != _patches.end(); ++it) {
561 if (!patchTouchesRegion(it.key(), worldRect)) {
562 continue;
563 }
564 _samplePatch(it.key(), it.value());
565 emit patchMeshChanged(it.key());
566 remeshed++;
567 }
568 qCDebug(GeoMapSurfaceModelVerboseLog)
569 << "regionChanged" << worldRect << "re-meshed" << remeshed << "of" << _patches.count() << "patches";
570 // Emitted even with nothing re-meshed: heights beyond the patches come from the field
572
573 // Terrain taller than the last cull assumed may be visible below/behind
574 // the camera: re-cull terrain-aware
575 if (_maxTerrainZ() > (_culledTerrainZ + kRecullHeightMargin)) {
576 _scheduleUpdate();
577 }
578}
579
580void SurfaceModel::_samplePatch(const TileMath::TileKey& key, PatchData& data)
581{
582 const bool monitorCliffs = SettingsManager::instance()->flyViewSettings()->geoMapDebugUI()->rawValue().toBool();
583 PatchSampler::EdgeStep edgeStep;
584 data.heights = _field->samplePatch(key, kGridSize, monitorCliffs ? &edgeStep : nullptr);
585 data.maxHeight = maxHeightOf(data.heights);
586 data.edgeStep = edgeStep.step;
587 data.edgeCliff = false;
588 if (!monitorCliffs) {
589 data.cliffSinceMs = -1;
590 return;
591 }
592 const QRectF rect = patchRect(key);
593 const double cell = rect.width() / kGridSize;
594 data.edgeStepAt =
595 QPointF(rect.left() + (edgeStep.col * cell), (rect.top() + rect.height()) - (edgeStep.row * cell));
596 const QGeoCoordinate stepAt = TileMath::worldToGeo(data.edgeStepAt);
597 // Mercator meters overstate ground distance by 1/cos(latitude)
598 const double groundCell = cell * std::cos(qDegreesToRadians(stepAt.latitude()));
599
600 const bool cliff = edgeStep.step >= (kCliffLogSlope * groundCell);
601 data.edgeCliff = cliff;
602 if (cliff && (data.cliffSinceMs < 0)) {
603 data.cliffSinceMs = _cliffClock.elapsed();
604 const QString interiorSource = (edgeStep.ownZoom < 0) ? QStringLiteral("no elevation data (flat 0 m)")
605 : QStringLiteral("z%1 tile").arg(edgeStep.ownZoom);
606 qCWarning(GeoMapSurfaceModelCliffsLog).noquote()
607 << "cliff appeared: patch" << key << boundaryLabel(edgeStep.row, edgeStep.col, kGridSize) << "step"
608 << edgeStep.step << "m over a" << qRound(groundCell) << "m cell"
609 << "at" << stepAt << "| patch interior:" << interiorSource
610 << QStringLiteral("| edge vertex: z%1 tile").arg(edgeStep.boundaryZoom);
611 } else if (!cliff && (data.cliffSinceMs >= 0)) {
612 qCWarning(GeoMapSurfaceModelCliffsLog)
613 << "cliff cleared: patch" << key << "after" << (_cliffClock.elapsed() - data.cliffSinceMs) << "ms";
614 data.cliffSinceMs = -1;
615 }
616}
#define QGC_LOGGING_CATEGORY(name, categoryStr)
int32_t height
Definition UBXPlan.cc:44
void rawValueChanged(const QVariant &value)
QSizeF viewportSize() const
void distanceChanged()
void centerElevationChanged()
qreal distance() const
void tiltChanged()
QPointF cameraGroundPosition() const
void headingChanged()
qreal centerElevation() const
void fieldOfViewChanged()
qreal verticalFieldOfView() const
bool isPositioned() const
void viewportSizeChanged()
std::optional< QPointF > groundPointCapped(const QPointF &screenPos, double maxRange) const
void centerChanged()
qreal tilt() const
QGeoCoordinate center() const
QList< float > samplePatch(const TileMath::TileKey &key, int gridSize, PatchSampler::EdgeStep *edgeStep=nullptr) const
void setPinnedKeys(QSet< TileMath::TileKey > keys)
Definition HeightField.h:55
void regionChanged(const QRectF &worldRect)
virtual bool requestTile(const TileMath::TileKey &key)
static SettingsManager * instance()
FlyViewSettings * flyViewSettings() const
static constexpr double kRecullHeightMargin
SurfaceModel(GeoMapCamera *camera, HeightSource *heightSource, HeightField *field, QObject *parent=nullptr)
static constexpr int kMaxPatchAddsPerUpdate
void surfaceHeightsChanged()
static constexpr double kMaxRangeMultiplier
visible-range cap in camera distances
std::optional< Patch > patch(const TileMath::TileKey &key) const
Single-patch lookup; std::nullopt when the key is not resident.
static constexpr int kMaxPatchRemovalsPerUpdate
static constexpr int kGridSize
QList< Patch > patches() const
std::optional< double > renderedHeightAt(const QPointF &world) const
void patchEdgeDeltasChanged(const TileMath::TileKey &key)
QList< int > edgeLodDeltas(const TileMath::TileKey &key) const
void patchRemoved(const TileMath::TileKey &key)
static constexpr int kVisibleSampleGrid
void patchAdded(const TileMath::TileKey &key)
static constexpr double kRefinePixelThreshold
subdivide above this projected size
void patchMeshChanged(const TileMath::TileKey &key)
The patch's mesh content changed (heights re-sampled): consumers must re-pull heights.
UpdateStats takeUpdateStats()
Returns the counters accumulated since the previous call and resets them.
static constexpr int kMaxPatches
double distance(const Ecef &left, const Ecef &right)
Straight-line distance between left and right, in metres.
std::array< int, 4 > LodDeltas
Definition PatchMesh.h:24
double surfaceHeight(const QList< float > &heights, int gridSize, const LodDeltas &lodDeltas, double row, double col)
Definition PatchMesh.h:69
double worldSize()
Full mercator world extent (2*pi*R) in world meters.
Definition TileMath.cc:18
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
constexpr int kMinZoom
Definition TileMath.h:30
double mercatorScale(double latitude)
Definition TileMath.cc:38
QGeoCoordinate worldToGeo(const QPointF &world)
World meters -> geo (altitude 0)
Definition TileMath.cc:31
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
int row
boundary vertex of the largest step
int boundaryZoom
zoom of the tile the boundary vertex resolved to
int ownZoom
zoom of the patch's backing tile; -1 = no data
TileMath::TileKey key
update() call/duration counters for the perf overlay
qint64 addTotalUs
time inside the patch-add loop (delegate creation share)