21constexpr QChar kEdgeChars[] = {u
'N', u
'S', u
'E', u
'W'};
27 return QRectF(minCorner.x(), minCorner.y(), span, span);
46 return patch.
heights.at((row * verticesPerEdge) + col);
53 t = std::clamp(t, 0.0,
static_cast<double>(gridSize));
54 const int i0 = std::min(
static_cast<int>(std::floor(t)), gridSize - 1);
55 const double frac = t - i0;
56 const int vpe = gridSize + 1;
62 h0 = heightAt(patch, vpe, 0, i0);
63 h1 = heightAt(patch, vpe, 0, i0 + 1);
66 h0 = heightAt(patch, vpe, gridSize, i0);
67 h1 = heightAt(patch, vpe, gridSize, i0 + 1);
70 h0 = heightAt(patch, vpe, i0, gridSize);
71 h1 = heightAt(patch, vpe, i0 + 1, gridSize);
74 h0 = heightAt(patch, vpe, i0, 0);
75 h1 = heightAt(patch, vpe, i0 + 1, 0);
78 return (h0 * (1.0 - frac)) + (h1 * frac);
83 if (isPending(patch) || isPending(neighbor)) {
86 if (isDegraded(patch) || isDegraded(neighbor)) {
98std::optional<double> surfaceHeightAt(
const QList<SurfaceModel::Patch>& patches,
int gridSize,
const QPointF& point)
102 if (patch.
covered || patch.
heights.isEmpty() || !patchRect(patch.
key).contains(point)) {
113 const QRectF rect = patchRect(best->
key);
114 const double step = rect.width() / gridSize;
116 const double colF = std::clamp((point.x() - rect.left()) / step, 0.0,
static_cast<double>(gridSize));
118 std::clamp(((rect.top() + rect.height()) - point.y()) / step, 0.0,
static_cast<double>(gridSize));
119 const int col0 = std::min(
static_cast<int>(std::floor(colF)), gridSize - 1);
120 const int row0 = std::min(
static_cast<int>(std::floor(rowF)), gridSize - 1);
121 const double fCol = colF - col0;
122 const double fRow = rowF - row0;
123 const int vpe = gridSize + 1;
125 (heightAt(*best, vpe, row0, col0) * (1.0 - fCol)) + (heightAt(*best, vpe, row0, col0 + 1) * fCol);
127 (heightAt(*best, vpe, row0 + 1, col0) * (1.0 - fCol)) + (heightAt(*best, vpe, row0 + 1, col0 + 1) * fCol);
128 return (north * (1.0 - fRow)) + (south * fRow);
135std::optional<QPointF> sightlineHole(
const QList<SurfaceModel::Patch>& patches,
int gridSize,
const ViewState& view,
138 constexpr int kRaySteps = 64;
142 std::optional<double> nearbySurface;
143 for (
int step = 1; step <= kRaySteps; step++) {
144 const double t =
static_cast<double>(step) / kRaySteps;
145 const std::optional<double> height =
152 if (!nearbySurface) {
156 for (
int step = 1; step <= kRaySteps; step++) {
157 const double t =
static_cast<double>(step) / kRaySteps;
160 const std::optional<double> height = surfaceHeightAt(patches, gridSize, point);
163 if (rayZ <= surface) {
166 nearbySurface = surface;
167 }
else if (rayZ <= *nearbySurface) {
174void findHoles(
const QList<SurfaceModel::Patch>& patches,
int gridSize,
const ViewState& view,
Report& report)
182 const std::optional<QPointF> blocked = sightlineHole(patches, gridSize, view, point);
187 report.
holes.append(hole);
194 bool renderedHere =
false;
197 if (!patchRect(patch.
key).contains(point)) {
217 report.
holes.append(hole);
221void findBadHeights(
const QList<SurfaceModel::Patch>& patches,
Report& report)
225 for (
const float height : patch.
heights) {
226 if (!std::isfinite(height)) {
242 return QStringLiteral(
"patch renders flat while its terrain heights are still loading");
244 return QStringLiteral(
"terrain height fetch failed; patch degraded to a flat fallback");
246 return QStringLiteral(
247 "LOD boundary T-junction: fine edge vertices vs the coarse neighbor's interpolated edge");
249 return QStringLiteral(
"same-zoom edge mismatch (unexpected sampling inconsistency)");
258 return QStringLiteral(
"no resident patch covers this visible area (culling/refinement bug)");
260 return QStringLiteral(
261 "pending patch suppressed as covered, but the covering geometry does not span this area");
263 return QStringLiteral(
264 "sightline passes below nearby terrain height over unrendered ground "
265 "(ground-plane visibility culling misses tall terrain near the camera)");
273 out += QStringLiteral(
"GeoMap surface analysis\n");
274 out += QStringLiteral(
"Rendered patches: %1 (heights loading: %2, terrain fetch failed: %3)\n")
280 if (
holes.isEmpty()) {
281 out += QStringLiteral(
"Coverage holes: none (%1 sample points)\n").arg(
totalSamples);
283 out += QStringLiteral(
"Coverage holes: %1 of %2 sample points uncovered\n")
286 QHash<HoleCause, int> counts;
287 QHash<HoleCause, const Hole*> examples;
289 counts[hole.cause]++;
290 if (!examples.contains(hole.cause)) {
291 examples.insert(hole.cause, &hole);
294 for (
auto it = counts.cbegin(); it != counts.cend(); ++it) {
295 const Hole* example = examples.value(it.key());
297 out += QStringLiteral(
" e.g. at %1,%2")
298 .arg(example->
at.latitude(), 0,
'f', 5)
299 .arg(example->
at.longitude(), 0,
'f', 5);
301 out += QStringLiteral(
" - zoom %1 tile (%2,%3)")
306 out += QLatin1Char(
'\n');
313 out += QStringLiteral(
"Camera: %1 m above ground plane, surface under camera %2 m, max near camera %3 m")
318 out += QStringLiteral(
"Camera: %1 m above ground plane, no rendered terrain under the camera")
322 out += QStringLiteral(
" - CAMERA CLIPS INTO TERRAIN (eye below rendered surface; hole at screen bottom)");
324 out += QLatin1Char(
'\n');
328 out += QStringLiteral(
"Non-finite heights: none\n");
331 out += QStringLiteral(
"Non-finite heights: %1 vertices in zoom %2 tile (%3,%4)\n")
339 out += QStringLiteral(
"Seams >= %1 m: %2\n").arg(
minStep, 0,
'f', 1).arg(
seams.count());
340 constexpr qsizetype kMaxListed = 20;
341 for (qsizetype i = 0; i < std::min(
seams.count(), kMaxListed); i++) {
343 out += QStringLiteral(
"%1. %2 m step at %3,%4 - zoom %5 tile (%6,%7) %8 edge vs zoom %9 tile (%10,%11)\n")
346 .arg(seam.
worstAt.latitude(), 0,
'f', 5)
347 .arg(seam.
worstAt.longitude(), 0,
'f', 5)
357 if (
seams.count() > kMaxListed) {
358 out += QStringLiteral(
"... %1 more not listed\n").arg(
seams.count() - kMaxListed);
360 if (!
seams.isEmpty()) {
361 QHash<SeamCause, int> counts;
363 counts[seam.cause]++;
365 out += QStringLiteral(
"Seam cause summary:\n");
366 for (
auto it = counts.cbegin(); it != counts.cend(); ++it) {
379 QHash<TileMath::TileKey, const SurfaceModel::Patch*> rendered;
384 rendered.insert(patch.key, &patch);
386 if (isPending(patch)) {
388 }
else if (isDegraded(patch)) {
393 findHoles(patches, gridSize, view, report);
394 findBadHeights(patches, report);
400 const std::optional<double> underCamera = surfaceHeightAt(patches, gridSize, view.
cameraGround);
412 if (std::hypot(offset.x(), offset.y()) > view.
cameraHeight) {
415 const std::optional<double> height = surfaceHeightAt(patches, gridSize, point);
425 for (
auto it = rendered.cbegin(); it != rendered.cend(); ++it) {
428 const int tilesAtZoom = 1 << key.
zoom;
431 const double step = span / gridSize;
432 const double maxY = minCorner.y() + span;
434 for (
const Edge edge : {North, South, East, West}) {
450 if ((neighborKey.
x < 0) || (neighborKey.
x >= tilesAtZoom) || (neighborKey.
y < 0) ||
451 (neighborKey.
y >= tilesAtZoom)) {
459 if (rendered.contains(neighborKey)) {
460 if ((edge == West) || (edge == North)) {
463 neighbor = rendered.value(neighborKey);
468 if (candidate == patchAncestor) {
471 if (rendered.contains(candidate)) {
472 neighbor = rendered.value(candidate);
483 const double nbStep = nbSpan / gridSize;
484 const double nbMaxY = nbMin.y() + nbSpan;
488 double maxStep = 0.0;
490 const int vpe = gridSize + 1;
491 for (
int i = 0; i <= gridSize; i++) {
494 double patchHeight = 0.0;
495 double neighborHeight = 0.0;
498 x = minCorner.x() + (i * step);
500 patchHeight = heightAt(patch, vpe, 0, i);
501 neighborHeight = edgeHeight(*neighbor, gridSize, South, (x - nbMin.x()) / nbStep);
504 x = minCorner.x() + (i * step);
506 patchHeight = heightAt(patch, vpe, gridSize, i);
507 neighborHeight = edgeHeight(*neighbor, gridSize, North, (x - nbMin.x()) / nbStep);
510 x = minCorner.x() + span;
511 y = maxY - (i * step);
512 patchHeight = heightAt(patch, vpe, i, gridSize);
513 neighborHeight = edgeHeight(*neighbor, gridSize, West, (nbMaxY - y) / nbStep);
517 y = maxY - (i * step);
518 patchHeight = heightAt(patch, vpe, i, 0);
519 neighborHeight = edgeHeight(*neighbor, gridSize, East, (nbMaxY - y) / nbStep);
522 const double delta = std::abs(patchHeight - neighborHeight);
523 if (delta > maxStep) {
525 worstWorld = QPointF(x, y);
529 if (maxStep >= minStep) {
530 report.
seams.append(
Seam{key, neighbor->
key, kEdgeChars[edge], maxStep,
536 std::sort(report.
seams.begin(), report.
seams.end(),
537 [](
const Seam& a,
const Seam& b) { return a.maxStep > b.maxStep; });