3#include <QtCore/QByteArray>
4#include <QtCore/QDateTime>
5#include <QtCore/QDebug>
8#include <QtCore/QTimer>
9#include <QtCore/QtMath>
10#include <QtGui/QPainter>
35 if (
scene == _scene) {
39 disconnect(_scene,
nullptr,
this,
nullptr);
48 connect(_scene, &QObject::destroyed,
this, [
this] {
51 _rebuildSurfaceModel();
54 _rebuildSurfaceModel();
59 return _scene ? _scene->
camera() :
nullptr;
62void SurfacePatchModel::_scheduleStatsChanged()
66 if (_statsNotifyPending) {
69 _statsNotifyPending =
true;
70 QTimer::singleShot(0,
this, [
this] {
71 _statsNotifyPending =
false;
76#ifdef QGC_UNITTEST_BUILD
77void SurfacePatchModel::drainUpdates()
80 _surfaceModel->drainUpdates();
90 qCDebug(GeoMapSurfacePatchModelLog) <<
"terrain" << _terrain <<
"->" <<
terrain;
93 _rebuildSurfaceModel();
101 qCDebug(GeoMapSurfacePatchModelLog) <<
"debugHills" << _debugHills <<
"->" <<
debugHills;
104 _rebuildSurfaceModel();
112 qCDebug(GeoMapSurfacePatchModelLog) <<
"mapType" << _mapType <<
"->" <<
mapType;
118 _tileSource =
nullptr;
119 if (!_mapType.isEmpty()) {
120 _tileSource =
new TileImageSource(_mapType,
this, _tileImageNetworkManager);
124 _requestTileImage(key);
129void SurfacePatchModel::_resetImagery()
133 for (
auto it = _imageRequestKey.cbegin(); it != _imageRequestKey.cend(); ++it) {
137 _imageRequestKey.clear();
138 _imageRequestByKey.clear();
139 _failedImageKeys.clear();
140 if (_imageRetryTimer) {
141 _imageRetryTimer->stop();
143 _retiredOrder.clear();
144 _fallbackCache.clear();
145 if (!_tileImages.isEmpty()) {
147 if (!_keys.isEmpty()) {
148 emit dataChanged(index(0), index(_keys.count() - 1), {TileImageRole, HasTileImageRole});
159 _imageRequestKey.insert(requestId, key);
160 _imageRequestByKey.insert(key, requestId);
163void SurfacePatchModel::_tileImageReady(
int requestId,
const QImage& image)
165 const auto it = _imageRequestKey.constFind(requestId);
166 if (it == _imageRequestKey.constEnd()) {
170 _imageRequestKey.erase(it);
171 _imageRequestByKey.remove(key);
173 const int row = _keys.indexOf(key);
177 _tileImages.insert(key, image);
178 _fallbackCache.remove(key);
179 qCDebug(GeoMapSurfacePatchModelVerboseLog) <<
"tile image ready for" << key;
180 _invalidateFallbacks(key);
181 _notifyTileImageChanged(key);
194 for (
auto it = _fallbackCache.begin(); it != _fallbackCache.end();) {
195 const int down = it.key().zoom - tile.
zoom;
196 if ((down > 0) && (down <= kMaxAncestorFallbackLevels) && ((it.key().x >> down) == tile.
x) &&
197 ((it.key().y >> down) == tile.
y)) {
198 it = _fallbackCache.erase(it);
210 for (
int row = 0; row < _keys.count(); row++) {
212 bool affected = (rowKey == key);
213 if (!affected && !_tileImages.contains(rowKey)) {
215 affected = (rowKey.
x == (key.
x >> 1)) && (rowKey.
y == (key.
y >> 1));
216 }
else if (rowKey.
zoom > key.
zoom) {
217 const int up = rowKey.
zoom - key.
zoom;
219 (up <= kMaxAncestorFallbackLevels) && ((rowKey.
x >> up) == key.
x) && ((rowKey.
y >> up) == key.
y);
223 const QModelIndex idx = index(row);
229void SurfacePatchModel::_tileImageFailed(
int requestId)
231 const auto it = _imageRequestKey.constFind(requestId);
232 if (it == _imageRequestKey.constEnd()) {
236 _imageRequestByKey.remove(key);
237 _imageRequestKey.erase(it);
242 if (!_keys.contains(key)) {
245 qCDebug(GeoMapSurfacePatchModelLog) <<
"tile image failed for" << key <<
"(retry scheduled)";
246 _failedImageKeys.insert(key);
247 if (!_imageRetryTimer) {
248 _imageRetryTimer =
new QTimer(
this);
249 _imageRetryTimer->setSingleShot(
true);
250 _imageRetryTimer->setInterval(kImageRetryMs);
251 connect(_imageRetryTimer, &QTimer::timeout,
this, &SurfacePatchModel::_retryFailedImages);
253 if (!_imageRetryTimer->isActive()) {
254 _imageRetryTimer->start();
258void SurfacePatchModel::_retryFailedImages()
260 const QSet<TileMath::TileKey> failed = std::exchange(_failedImageKeys, {});
264 if (!_keys.contains(key) || _tileImages.contains(key) || _imageRequestByKey.contains(key)) {
267 _requestTileImage(key);
271void SurfacePatchModel::_rebuildSurfaceModel()
276 delete _surfaceModel;
277 _surfaceModel =
nullptr;
278 delete _heightSource;
279 _heightSource =
nullptr;
281 _heightField =
nullptr;
287 }
else if (_terrain) {
292 qCDebug(GeoMapSurfacePatchModelLog) <<
"rebuilding surface model, height source:"
293 << (_debugHills ?
"debug hills" : (_terrain ?
"terrain" :
"flat"));
297 _surfaceModel =
new SurfaceModel(camera, _heightSource, _heightField,
this);
301 &SurfacePatchModel::_patchEdgeDeltasChanged);
317 if (!_heightField || !coordinate.isValid()) {
335 return _surfaceModel ? _surfaceModel->
pendingCount() : 0;
342 maxZoom = std::max(maxZoom, key.
zoom);
347void SurfacePatchModel::_startStatsSampling()
350 _statsTimer =
new QTimer(
this);
351 _statsTimer->setInterval(1000);
352 connect(_statsTimer, &QTimer::timeout,
this, &SurfacePatchModel::_statsTick);
360 _statsTimer->start();
365 if (enabled == _statsEnabled) {
368 _statsEnabled = enabled;
375 _startStatsSampling();
377 }
else if (!_capturing && _statsTimer) {
384void SurfacePatchModel::_statsTick()
389 const double maxMs = stats.
maxUs / 1000.0;
390 _statsText = QStringLiteral(
"upd/s %1 avg %2 ms max %3 ms | patch +%4 -%5 /s comp/s %6")
392 .arg(avgMs, 0,
'f', 2)
393 .arg(maxMs, 0,
'f', 2)
396 .arg(_statComposites);
398 _captureWorstMaxMs = std::max(_captureWorstMaxMs, maxMs);
399 _captureWorstAvgMs = std::max(_captureWorstAvgMs, avgMs);
401 _captureRows.append(QStringLiteral(
"%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12")
402 .arg(_captureClock.elapsed() / 1000.0, 0,
'f', 1)
404 .arg(avgMs, 0,
'f', 3)
405 .arg(maxMs, 0,
'f', 3)
408 .arg(_statComposites)
412 .arg(camera ? camera->
distance() : 0.0, 0,
'f', 0)
413 .arg(camera ? camera->tilt() : 0.0, 0,
'f', 1));
429 _captureRows.clear();
430 _captureWorstMaxMs = 0.0;
431 _captureWorstAvgMs = 0.0;
433 QStringLiteral(
"time_s,updates,avg_ms,max_ms,patch_adds,patch_removes,composites,patches,pending,max_zoom,cam_"
434 "dist_m,cam_tilt_deg"));
435 _captureClock.start();
439 _startStatsSampling();
450 if (!_statsEnabled && _statsTimer) {
454 const QString path = QDir::temp().filePath(
455 QStringLiteral(
"qgc-geomap-capture-%1.csv").arg(QDateTime::currentDateTime().toString(
"yyyyMMdd-hhmmss")));
457 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
458 qWarning() <<
"GeoMap capture: cannot write" << path;
459 _statsText = QStringLiteral(
"capture failed: %1").arg(path);
463 const QByteArray
data = _captureRows.join(QLatin1Char(
'\n')).toUtf8() +
'\n';
464 const bool written = (file.write(
data) ==
data.size()) && file.flush();
466 _captureRows.clear();
468 (void) file.remove();
469 qWarning() <<
"GeoMap capture: write failed" << path;
470 _statsText = QStringLiteral(
"capture failed: %1").arg(path);
478 _statsText = QStringLiteral(
"capture: worst max %1 ms worst avg %2 ms -> %3")
479 .arg(_captureWorstMaxMs, 0,
'f', 2)
480 .arg(_captureWorstAvgMs, 0,
'f', 2)
489 if (!_surfaceModel || !camera) {
490 qDebug() <<
"Surface analysis: no surface model active";
501 constexpr int kScreenGrid = 24;
502 if (!viewport.isEmpty()) {
503 for (
int row = 0; row < kScreenGrid; row++) {
504 for (
int col = 0; col < kScreenGrid; col++) {
505 const QPointF screenPos(viewport.width() * (col + 0.5) / kScreenGrid,
506 viewport.height() * (row + 0.5) / kScreenGrid);
507 const std::optional<QPointF> ground = camera->
groundPointCapped(screenPos, maxRange);
517 if (_scene && (camera->
tilt() > 0.0)) {
526 qDebug().noquote() << report.
text();
531 return parent.isValid() ? 0 : _keys.count();
536 if (!index.isValid() || (index.row() < 0) || (index.row() >= _keys.count()) || !_surfaceModel) {
542 const QPointF origin = _scene ? _scene->
sceneOrigin() : QPointF();
558 const auto patch = _surfaceModel->
patch(key);
559 return patch ? QVariant::fromValue(patch->heights) : QVariant::fromValue(QList<float>());
562 const auto patch = _surfaceModel->
patch(key);
563 return patch ? patch->ready :
false;
566 const auto patch = _surfaceModel->
patch(key);
567 return patch ? patch->covered :
false;
570 return QVariant::fromValue(_surfaceModel->
edgeLodDeltas(key));
572 const QImage own = _tileImages.value(key);
576 const auto cached = _fallbackCache.constFind(key);
577 if (cached != _fallbackCache.constEnd()) {
580 const QImage fallback = _fallbackImage(key);
581 _fallbackCache.insert(key, fallback);
585 return _tileImages.contains(key) || _fallbackAvailable(key);
593 for (
int childY = 0; childY < 2; childY++) {
594 for (
int childX = 0; childX < 2; childX++) {
595 if (_tileImages.contains(
TileMath::TileKey{(key.x * 2) + childX, (key.y * 2) + childY, key.zoom + 1})) {
600 for (
int up = 1; (up <= kMaxAncestorFallbackLevels) && ((key.
zoom - up) >= 0); up++) {
601 if (_tileImages.contains(
TileMath::TileKey{key.x >> up, key.y >> up, key.zoom - up})) {
611 bool anyChild =
false;
612 for (
int childY = 0; (childY < 2) && !anyChild; childY++) {
613 for (
int childX = 0; (childX < 2) && !anyChild; childX++) {
619 constexpr int kCanvas = 256;
621 QImage canvas(kCanvas, kCanvas, QImage::Format_RGBA8888);
622 canvas.fill(QColor(0x3a, 0x40, 0x48));
623 QPainter painter(&canvas);
624 for (
int childY = 0; childY < 2; childY++) {
625 for (
int childX = 0; childX < 2; childX++) {
628 if (!child.isNull()) {
630 painter.drawImage(QRect((childX * kCanvas) / 2, (childY * kCanvas) / 2, kCanvas / 2, kCanvas / 2),
639 for (
int up = 1; (up <= kMaxAncestorFallbackLevels) && ((key.
zoom - up) >= 0); up++) {
641 const QImage image = _tileImages.value(ancestor);
642 if (image.isNull()) {
645 const int split = 1 << up;
646 const double cellWidth =
static_cast<double>(image.width()) / split;
647 const double cellHeight =
static_cast<double>(image.height()) / split;
648 const int cellX = key.
x - (ancestor.x << up);
649 const int cellY = key.
y - (ancestor.y << up);
651 return image.copy(QRect(qRound(cellX * cellWidth), qRound(cellY * cellHeight), qMax(1, qRound(cellWidth)),
652 qMax(1, qRound(cellHeight))));
677 beginInsertRows(QModelIndex(), _keys.count(), _keys.count());
682 if (_tileImages.contains(key)) {
683 _retiredOrder.removeOne(key);
685 _requestTileImage(key);
688 _scheduleStatsChanged();
693 const int row = _keys.indexOf(key);
697 const QModelIndex idx = index(row);
700 _notifyCoveredMayHaveChanged(key);
701 _scheduleStatsChanged();
706 const int row = _keys.indexOf(key);
710 const QModelIndex idx = index(row);
716 const int row = _keys.indexOf(key);
720 beginRemoveRows(QModelIndex(), row, row);
727 if (_tileImages.contains(key)) {
728 _retiredOrder.append(key);
729 while (_retiredOrder.count() > kMaxRetiredImages) {
731 _tileImages.remove(evicted);
732 _invalidateFallbacks(evicted);
735 _fallbackCache.remove(key);
736 _failedImageKeys.remove(key);
737 const auto requestIt = _imageRequestByKey.constFind(key);
738 if (requestIt != _imageRequestByKey.constEnd()) {
740 _imageRequestKey.remove(requestIt.value());
741 _imageRequestByKey.erase(requestIt);
744 _notifyCoveredMayHaveChanged(key);
745 _scheduleStatsChanged();
748void SurfacePatchModel::_notifyCoveredMayHaveChanged(
const TileMath::TileKey& changedKey)
756 for (
int row = 0; row < _keys.count(); row++) {
762 const auto patch = _surfaceModel->
patch(rowKey);
763 if (patch && !patch->ready) {
764 const QModelIndex idx = index(row);
770void SurfacePatchModel::_sceneOriginChanged()
773 if (!_keys.isEmpty()) {
774 emit dataChanged(index(0), index(_keys.count() - 1), {CenterXRole, CenterYRole});
#define QGC_LOGGING_CATEGORY(name, categoryStr)
z=0 everywhere (Milestone 1 default)
QSizeF viewportSize() const
QPointF cameraGroundPosition() const
qreal centerElevation() const
std::optional< QPointF > groundPointCapped(const QPointF &screenPos, double maxRange) const
qreal verticalScale() const
void sceneOriginChanged()
GeoMapCamera * camera() const
QPointF sceneOrigin() const
double heightAt(const QPointF &world) const
void regionChanged(const QRectF &worldRect)
void setHeightField(HeightField *field)
Field that receives whole tiles requested via requestTile (not owned)
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 kGridSize
QList< Patch > patches() const
void patchEdgeDeltasChanged(const TileMath::TileKey &key)
QList< int > edgeLodDeltas(const TileMath::TileKey &key) const
void patchRemoved(const TileMath::TileKey &key)
void patchAdded(const TileMath::TileKey &key)
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.
void statsEnabledChanged()
double maxRangeMultiplier() const
void setDebugHills(bool debugHills)
int rowCount(const QModelIndex &parent=QModelIndex()) const override
QVariant data(const QModelIndex &index, int role) const override
void terrainHeightsChanged()
terrainHeightAt answers changed somewhere: consumers re-query
Q_INVOKABLE void startCapture()
Starts recording the per-second perf samples (plus camera context)
void setScene(GeoScene *scene)
@ SpanRole
patch edge length (m)
@ TileImageRole
drapable image: own tile, or ancestor/descendant fallback while loading
@ HasTileImageRole
QImage is opaque to QML; bool validity for bindings.
@ EdgeLodDeltasRole
{N,S,W,E} coarser-neighbor LOD deltas for edge stitching
@ ZoomRole
quadtree zoom level
@ TileYRole
slippy tile y of the patch key
@ CenterYRole
patch center y in scene meters
@ CoveredRole
pending but overlapped by a ready patch: delegate suppresses rendering
@ HeightsRole
vertex height grid (empty = flat)
@ CenterXRole
patch center x in scene meters
@ TileXRole
slippy tile x of the patch key
Q_INVOKABLE QString stopCapture()
~SurfacePatchModel() override
QHash< int, QByteArray > roleNames() const override
void setStatsEnabled(bool enabled)
void setMapType(const QString &mapType)
Q_INVOKABLE double terrainHeightAt(const QGeoCoordinate &coordinate) const
void setTerrain(bool terrain)
Q_INVOKABLE void analyzeSurface() const
void tileImageFailed(int requestId)
void cancelRequest(int requestId)
Cancel a pending request, aborting any in-flight network fetch.
int requestTileImage(const TileMath::TileKey &key)
Request the image for a tile. Returns a request id (> 0).
void tileImageReady(int requestId, const QImage &image)
Report analyze(const QList< SurfaceModel::Patch > &patches, int gridSize, const ViewState &view, double minStep)
QPointF geoToWorld(const QGeoCoordinate &coord)
Geo -> world meters. Latitude is clamped to +/-kMaxLatitude.
QPointF tileMinCorner(const TileKey &key)
South-west (minimum x/y) corner of a tile in world meters.
double tileSpanAtZoom(int zoom)
Edge length of one tile at zoom, in world meters.
QString text() const
human-readable report
double heightScale
vertical scale applied to patch heights; 0 skips the camera check
QPointF cameraGround
camera ground position (world meters)
double cameraHeight
camera height above the ground plane (scene units); 0 skips the camera check
QList< QPointF > groundSamples
world points visible on screen; must have rendered coverage
update() call/duration counters for the perf overlay