9#include <QtCore/QByteArray>
10#include <QtCore/QDateTime>
11#include <QtCore/QDebug>
13#include <QtCore/QFile>
14#include <QtCore/QTimer>
15#include <QtCore/QtMath>
16#include <QtGui/QPainter>
19#include <android/log.h>
41 if (
scene == _scene) {
45 disconnect(_scene,
nullptr,
this,
nullptr);
54 connect(_scene, &QObject::destroyed,
this, [
this] {
57 _rebuildSurfaceModel();
60 _rebuildSurfaceModel();
65 return _scene ? _scene->
camera() :
nullptr;
68void SurfacePatchModel::_scheduleStatsChanged()
72 if (_statsNotifyPending) {
75 _statsNotifyPending =
true;
76 QTimer::singleShot(0,
this, [
this] {
77 _statsNotifyPending =
false;
82#ifdef QGC_UNITTEST_BUILD
83void SurfacePatchModel::drainUpdates()
86 _surfaceModel->drainUpdates();
96 qCDebug(GeoMapSurfacePatchModelLog) <<
"terrain" << _terrain <<
"->" <<
terrain;
99 _rebuildSurfaceModel();
107 qCDebug(GeoMapSurfacePatchModelLog) <<
"debugHills" << _debugHills <<
"->" <<
debugHills;
110 _rebuildSurfaceModel();
118 qCDebug(GeoMapSurfacePatchModelLog) <<
"mapType" << _mapType <<
"->" <<
mapType;
124 _tileSource =
nullptr;
125 if (!_mapType.isEmpty()) {
126 _tileSource =
new TileImageSource(_mapType,
this, _tileImageNetworkManager);
130 _requestTileImage(key);
135void SurfacePatchModel::_resetImagery()
139 for (
auto it = _imageRequestKey.cbegin(); it != _imageRequestKey.cend(); ++it) {
143 _imageRequestKey.clear();
144 _imageRequestByKey.clear();
145 _failedImageKeys.clear();
146 if (_imageRetryTimer) {
147 _imageRetryTimer->stop();
149 _retiredOrder.clear();
150 _fallbackCache.clear();
151 if (!_tileImages.isEmpty()) {
153 if (!_keys.isEmpty()) {
154 emit dataChanged(index(0), index(_keys.count() - 1), {TileImageRole, HasTileImageRole});
165 _imageRequestKey.insert(requestId, key);
166 _imageRequestByKey.insert(key, requestId);
169void SurfacePatchModel::_tileImageReady(
int requestId,
const QImage& image)
171 const auto it = _imageRequestKey.constFind(requestId);
172 if (it == _imageRequestKey.constEnd()) {
176 _imageRequestKey.erase(it);
177 _imageRequestByKey.remove(key);
179 const int row = _keys.indexOf(key);
183 _tileImages.insert(key, image);
184 _fallbackCache.remove(key);
185 qCDebug(GeoMapSurfacePatchModelVerboseLog) <<
"tile image ready for" << key;
186 _invalidateFallbacks(key);
187 _notifyTileImageChanged(key);
200 for (
auto it = _fallbackCache.begin(); it != _fallbackCache.end();) {
201 const int down = it.key().zoom - tile.
zoom;
202 if ((down > 0) && (down <= kMaxAncestorFallbackLevels) && ((it.key().x >> down) == tile.
x) &&
203 ((it.key().y >> down) == tile.
y)) {
204 it = _fallbackCache.erase(it);
216 for (
int row = 0; row < _keys.count(); row++) {
218 bool affected = (rowKey == key);
219 if (!affected && !_tileImages.contains(rowKey)) {
221 affected = (rowKey.
x == (key.
x >> 1)) && (rowKey.
y == (key.
y >> 1));
222 }
else if (rowKey.
zoom > key.
zoom) {
223 const int up = rowKey.
zoom - key.
zoom;
225 (up <= kMaxAncestorFallbackLevels) && ((rowKey.
x >> up) == key.
x) && ((rowKey.
y >> up) == key.
y);
229 const QModelIndex idx = index(row);
235void SurfacePatchModel::_tileImageFailed(
int requestId)
237 const auto it = _imageRequestKey.constFind(requestId);
238 if (it == _imageRequestKey.constEnd()) {
242 _imageRequestByKey.remove(key);
243 _imageRequestKey.erase(it);
248 if (!_keys.contains(key)) {
251 qCDebug(GeoMapSurfacePatchModelLog) <<
"tile image failed for" << key <<
"(retry scheduled)";
252 _failedImageKeys.insert(key);
253 if (!_imageRetryTimer) {
254 _imageRetryTimer =
new QTimer(
this);
255 _imageRetryTimer->setSingleShot(
true);
256 _imageRetryTimer->setInterval(kImageRetryMs);
257 connect(_imageRetryTimer, &QTimer::timeout,
this, &SurfacePatchModel::_retryFailedImages);
259 if (!_imageRetryTimer->isActive()) {
260 _imageRetryTimer->start();
264void SurfacePatchModel::_retryFailedImages()
266 const QSet<TileMath::TileKey> failed = std::exchange(_failedImageKeys, {});
270 if (!_keys.contains(key) || _tileImages.contains(key) || _imageRequestByKey.contains(key)) {
273 _requestTileImage(key);
277void SurfacePatchModel::_rebuildSurfaceModel()
282 delete _surfaceModel;
283 _surfaceModel =
nullptr;
284 delete _heightSource;
285 _heightSource =
nullptr;
287 _heightField =
nullptr;
293 }
else if (_terrain) {
298 qCDebug(GeoMapSurfacePatchModelLog) <<
"rebuilding surface model, height source:"
299 << (_debugHills ?
"debug hills" : (_terrain ?
"terrain" :
"flat"));
303 _surfaceModel =
new SurfaceModel(camera, _heightSource, _heightField,
this);
309 &SurfacePatchModel::_patchEdgeDeltasChanged);
326 if (!_heightField || !coordinate.isValid()) {
334 if (!_heightField || !coordinate.isValid()) {
341 double ignoreStartMeters,
double ignoreEndMeters)
const
343 if (!_heightField || !from.isValid() || !to.isValid() || std::isnan(from.altitude()) || std::isnan(to.altitude())) {
346 const double distance = from.distanceTo(to);
348 std::clamp(
static_cast<int>(std::ceil(distance / kSegmentSampleSpacingMeters)), 1, kMaxSegmentSamples);
352 for (
int i = 0; i <= steps; i++) {
353 const double fraction =
static_cast<double>(i) / steps;
354 const double along = distance * fraction;
355 if ((along < ignoreStartMeters) || (along > (distance - ignoreEndMeters))) {
358 const double altitude = from.altitude() + ((to.altitude() - from.altitude()) * fraction);
359 if (_heightField->
heightAt(fromWorld + ((toWorld - fromWorld) * fraction)) > altitude) {
366double SurfacePatchModel::_surfaceHeightAt(
const QPointF& world)
const
369 if (
const std::optional<double> rendered = _surfaceModel->
renderedHeightAt(world)) {
373 return _heightField->
heightAt(world);
380 if (!camera || !_heightField || (zScale < 0.0)) {
381 return QGeoCoordinate();
383 const auto rayOpt = camera->
pickRayAt(screenPos);
385 return QGeoCoordinate();
390 const auto surfaceOffset = [&](
double t) {
392 return (ray.
originZ + (t * ray.
dirZ)) - (_surfaceHeightAt(ground) * zScale);
398 constexpr double kMinTerrainMeters = -11000.0;
399 constexpr double kMaxTerrainMeters = 9000.0;
400 const double minZ = kMinTerrainMeters * zScale;
401 const double maxZ = kMaxTerrainMeters * zScale;
405 if (ray.
dirZ < 0.0) {
406 tStart = std::max(0.0, (maxZ - ray.
originZ) / ray.
dirZ);
408 }
else if (ray.
originZ < maxZ) {
411 tEnd = (ray.
dirZ > 0.0) ? ((maxZ - ray.
originZ) / ray.
dirZ) : std::numeric_limits<double>::infinity();
413 return QGeoCoordinate();
418 const double horizontal = std::hypot(ray.
dirX, ray.
dirY);
419 if (horizontal > 1e-12) {
422 if (!std::isfinite(tEnd) || (tEnd < tStart)) {
423 return QGeoCoordinate();
426 const auto coordinateAt = [&](
double t) {
430 if (surfaceOffset(tStart) <= 0.0) {
433 return coordinateAt(tStart);
443 constexpr double kStepsPerCameraDistance = 64.0;
444 constexpr int kBisectSteps = 24;
445 const double stepMeters = std::max(camera->
distance() / kStepsPerCameraDistance, 1.0);
446 const double groundRange = (tEnd - tStart) * horizontal;
447 const int coarseSteps = std::max(1,
static_cast<int>(std::ceil(groundRange / stepMeters)));
448 double prevT = tStart;
449 for (
int i = 1; i <= coarseSteps; i++) {
450 const double t = tStart + (((tEnd - tStart) * i) / coarseSteps);
451 if (surfaceOffset(t) <= 0.0) {
454 for (
int j = 0; j < kBisectSteps; j++) {
455 const double mid = (lo + hi) / 2.0;
456 if (surfaceOffset(mid) <= 0.0) {
462 return coordinateAt(hi);
466 return QGeoCoordinate();
481 return _surfaceModel ? _surfaceModel->
pendingCount() : 0;
488 maxZoom = std::max(maxZoom, key.
zoom);
494void SurfacePatchModel::_startStatsSampling()
497 _statsTimer =
new QTimer(
this);
498 _statsTimer->setInterval(1000);
499 connect(_statsTimer, &QTimer::timeout,
this, &SurfacePatchModel::_statsTick);
507 _statsTimer->start();
512 if (enabled == _statsEnabled) {
515 _statsEnabled = enabled;
522 _startStatsSampling();
524 }
else if (!_capturing && _statsTimer) {
531void SurfacePatchModel::_statsTick()
536 const double maxMs = stats.
maxUs / 1000.0;
538 const double addMaxMs = stats.
addMaxUs / 1000.0;
539 _statsText = QStringLiteral(
"upd/s %1 avg %2 ms max %3 ms | patch +%4 -%5 /s comp/s %6")
541 .arg(avgMs, 0,
'f', 2)
542 .arg(maxMs, 0,
'f', 2)
545 .arg(_statComposites);
547 _captureWorstMaxMs = std::max(_captureWorstMaxMs, maxMs);
548 _captureWorstAvgMs = std::max(_captureWorstAvgMs, avgMs);
550 _captureRows.append(QStringLiteral(
"%1,%2,%3,%4,%5,%6,%7,%8,%9,%10,%11,%12,%13,%14")
551 .arg(_captureClock.elapsed() / 1000.0, 0,
'f', 1)
553 .arg(avgMs, 0,
'f', 3)
554 .arg(maxMs, 0,
'f', 3)
555 .arg(addAvgMs, 0,
'f', 3)
556 .arg(addMaxMs, 0,
'f', 3)
559 .arg(_statComposites)
563 .arg(camera ? camera->
distance() : 0.0, 0,
'f', 0)
564 .arg(camera ? camera->tilt() : 0.0, 0,
'f', 1));
580 _captureRows.clear();
581 _captureWorstMaxMs = 0.0;
582 _captureWorstAvgMs = 0.0;
584 QStringLiteral(
"time_s,updates,avg_ms,max_ms,add_avg_ms,add_max_ms,patch_adds,patch_removes,composites,"
585 "patches,pending,max_zoom,cam_dist_m,cam_tilt_deg"));
586 _captureClock.start();
590 _startStatsSampling();
601 if (!_statsEnabled && _statsTimer) {
605 const QString path = QDir::temp().filePath(
606 QStringLiteral(
"qgc-geomap-capture-%1.csv").arg(QDateTime::currentDateTime().toString(
"yyyyMMdd-hhmmss")));
608 if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
609 qWarning() <<
"GeoMap capture: cannot write" << path;
610 _statsText = QStringLiteral(
"capture failed: %1").arg(path);
614 const QByteArray
data = _captureRows.join(QLatin1Char(
'\n')).toUtf8() +
'\n';
615 const bool written = (file.write(
data) ==
data.size()) && file.flush();
617 _captureRows.clear();
619 (void) file.remove();
620 qWarning() <<
"GeoMap capture: write failed" << path;
621 _statsText = QStringLiteral(
"capture failed: %1").arg(path);
629 for (
const QByteArray& line :
data.split(
'\n')) {
630 if (!line.isEmpty()) {
631 __android_log_print(ANDROID_LOG_WARN,
"GeoMapCapture",
"%s", line.constData());
639 _statsText = QStringLiteral(
"capture: worst max %1 ms worst avg %2 ms -> %3")
640 .arg(_captureWorstMaxMs, 0,
'f', 2)
641 .arg(_captureWorstAvgMs, 0,
'f', 2)
650 if (!_surfaceModel || !camera) {
651 qDebug() <<
"Surface analysis: no surface model active";
662 constexpr int kScreenGrid = 24;
663 if (!viewport.isEmpty()) {
664 for (
int row = 0; row < kScreenGrid; row++) {
665 for (
int col = 0; col < kScreenGrid; col++) {
666 const QPointF screenPos(viewport.width() * (col + 0.5) / kScreenGrid,
667 viewport.height() * (row + 0.5) / kScreenGrid);
668 const std::optional<QPointF> ground = camera->
groundPointCapped(screenPos, maxRange);
678 if (_scene && (camera->
tilt() > 0.0)) {
687 qDebug().noquote() << report.
text();
694 return parent.isValid() ? 0 : _keys.count();
699 if (!index.isValid() || (index.row() < 0) || (index.row() >= _keys.count()) || !_surfaceModel) {
705 const QPointF origin = _scene ? _scene->
sceneOrigin() : QPointF();
721 const auto patch = _surfaceModel->
patch(key);
722 return patch ? QVariant::fromValue(patch->heights) : QVariant::fromValue(QList<float>());
725 const auto patch = _surfaceModel->
patch(key);
726 return patch ? patch->ready :
false;
729 const auto patch = _surfaceModel->
patch(key);
730 return patch ? patch->covered :
false;
733 return QVariant::fromValue(_surfaceModel->
edgeLodDeltas(key));
735 const QImage own = _tileImages.value(key);
739 const auto cached = _fallbackCache.constFind(key);
740 if (cached != _fallbackCache.constEnd()) {
743 const QImage fallback = _fallbackImage(key);
744 _fallbackCache.insert(key, fallback);
748 return _tileImages.contains(key) || _fallbackAvailable(key);
756 for (
int childY = 0; childY < 2; childY++) {
757 for (
int childX = 0; childX < 2; childX++) {
758 if (_tileImages.contains(
TileMath::TileKey{(key.x * 2) + childX, (key.y * 2) + childY, key.zoom + 1})) {
763 for (
int up = 1; (up <= kMaxAncestorFallbackLevels) && ((key.
zoom - up) >= 0); up++) {
764 if (_tileImages.contains(
TileMath::TileKey{key.x >> up, key.y >> up, key.zoom - up})) {
774 QImage image = _compositeFromChildren(key);
775 if (image.isNull()) {
777 image = _cropFromAncestor(key);
782QImage SurfacePatchModel::_compositeFromChildren(
const TileMath::TileKey& key)
const
784 bool anyChild =
false;
785 for (
int childY = 0; (childY < 2) && !anyChild; childY++) {
786 for (
int childX = 0; (childX < 2) && !anyChild; childX++) {
794 constexpr int kCanvas = 256;
796 QImage canvas(kCanvas, kCanvas, QImage::Format_RGBA8888);
797 canvas.fill(QColor(0x3a, 0x40, 0x48));
798 QPainter painter(&canvas);
799 for (
int childY = 0; childY < 2; childY++) {
800 for (
int childX = 0; childX < 2; childX++) {
803 if (!child.isNull()) {
805 painter.drawImage(QRect((childX * kCanvas) / 2, (childY * kCanvas) / 2, kCanvas / 2, kCanvas / 2),
815 for (
int up = 1; (up <= kMaxAncestorFallbackLevels) && ((key.
zoom - up) >= 0); up++) {
817 const QImage image = _tileImages.value(ancestor);
818 if (image.isNull()) {
821 const int split = 1 << up;
822 const double cellWidth =
static_cast<double>(image.width()) / split;
823 const double cellHeight =
static_cast<double>(image.height()) / split;
824 const int cellX = key.
x - (ancestor.x << up);
825 const int cellY = key.
y - (ancestor.y << up);
827 return image.copy(QRect(qRound(cellX * cellWidth), qRound(cellY * cellHeight), qMax(1, qRound(cellWidth)),
828 qMax(1, qRound(cellHeight))));
853 beginInsertRows(QModelIndex(), _keys.count(), _keys.count());
858 if (_tileImages.contains(key)) {
859 _retiredOrder.removeOne(key);
861 _requestTileImage(key);
864 _scheduleStatsChanged();
869 const int row = _keys.indexOf(key);
873 const QModelIndex idx = index(row);
876 _notifyCoveredMayHaveChanged(key);
877 _scheduleStatsChanged();
882 const int row = _keys.indexOf(key);
886 const QModelIndex idx = index(row);
892 const int row = _keys.indexOf(key);
896 beginRemoveRows(QModelIndex(), row, row);
903 if (_tileImages.contains(key)) {
904 _retiredOrder.append(key);
905 while (_retiredOrder.count() > kMaxRetiredImages) {
907 _tileImages.remove(evicted);
908 _invalidateFallbacks(evicted);
911 _fallbackCache.remove(key);
912 _failedImageKeys.remove(key);
913 const auto requestIt = _imageRequestByKey.constFind(key);
914 if (requestIt != _imageRequestByKey.constEnd()) {
916 _imageRequestKey.remove(requestIt.value());
917 _imageRequestByKey.erase(requestIt);
920 _notifyCoveredMayHaveChanged(key);
921 _scheduleStatsChanged();
924void SurfacePatchModel::_notifyCoveredMayHaveChanged(
const TileMath::TileKey& changedKey)
932 for (
int row = 0; row < _keys.count(); row++) {
938 const auto patch = _surfaceModel->
patch(rowKey);
939 if (patch && !patch->ready) {
940 const QModelIndex idx = index(row);
946void SurfacePatchModel::_sceneOriginChanged()
949 if (!_keys.isEmpty()) {
950 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
std::optional< PickRay > pickRayAt(const QPointF &screenPos) 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)
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 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)
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 terrainDataChanged()
terrainDataHeightAt / segmentBelowTerrain answers changed somewhere (not on patch churn)
void statsEnabledChanged()
double maxRangeMultiplier() const
void setDebugHills(bool debugHills)
int rowCount(const QModelIndex &parent=QModelIndex()) const override
Q_INVOKABLE double terrainDataHeightAt(const QGeoCoordinate &coordinate) const
QVariant data(const QModelIndex &index, int role) const override
void terrainHeightsChanged()
terrainHeightAt answers changed somewhere: consumers re-query
Q_INVOKABLE QGeoCoordinate surfaceCoordinateAtScreenPoint(const GeoMapCamera *camera, const QPointF &screenPos, double zScale) const
Q_INVOKABLE bool segmentBelowTerrain(const QGeoCoordinate &from, const QGeoCoordinate &to, double ignoreStartMeters, double ignoreEndMeters) const
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.
QGeoCoordinate worldToGeo(const QPointF &world)
World meters -> geo (altitude 0)
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
qint64 addTotalUs
time inside the patch-add loop (delegate creation share)