Skip to content

fix: Enhance QGCMapPolygon center logic and vertex translation#13883

Open
Eureak-zon wants to merge 2 commits intomavlink:masterfrom
Eureak-zon:fix-polygon-centroid-logic
Open

fix: Enhance QGCMapPolygon center logic and vertex translation#13883
Eureak-zon wants to merge 2 commits intomavlink:masterfrom
Eureak-zon:fix-polygon-centroid-logic

Conversation

@Eureak-zon
Copy link
Contributor

Summary

Improved the geometric accuracy and performance of polygon manipulations:

Corrected Center Calculation: Implemented an area-based centroid algorithm to prevent the center from being "weighted" toward locations where multiple vertices overlap.

Optimized setCenter: Enhanced efficiency during vertex translation by suppressing redundant signals and utilizing deferred updates.

Added Robustness: Provided a fallback to the arithmetic mean for degenerate polygons (zero area) to ensure stability.

Description

Problem

In the original QGCMapPolygon implementation, the center was calculated as the arithmetic mean of all vertex coordinates. When a polygon contains overlapping vertices (e.g., two points in a 5-vertex polygon are at the same location), that specific location gains 40% of the weight, pulling the center away from the true geometric heart of the shape. Additionally, updating each vertex individually during a center drag triggered multiple recursive path updates, leading to UI lag.

Solution

Geometric Centroid Algorithm: The _updateCenter function now uses the Shoelace formula (Surveyor's formula) to calculate the true centroid based on the polygon's area. This ensures the center remains mathematically accurate regardless of vertex density.

Signal Suppression: Introduced _ignoreCenterUpdates and utilized QTimer::singleShot in setCenter to batch pathChanged signals. This prevents the UI from stuttering during heavy manipulation.

Fallback Logic: If the polygon area is zero (degenerate case), the system gracefully falls back to the mean-point calculation to maintain a valid coordinate.

Type of Change

  • Bug fix (Fixed center offset caused by overlapping vertices)

  • New feature

  • Refactoring (Improved performance and math logic)

Testing

Test Cases
Overlap Test: Created a square polygon and added a 5th vertex directly on top of an existing one. Verified the center remained at the geometric center of the square.

Drag Performance: Dragged the center of complex polygons (15+ vertices) in the plan view to ensure smooth rendering and no "lag spikes."

Degenerate Case: Reduced a polygon to 2 points or moved all points to one location to verify no "divide by zero" errors occurred.

Platforms Tested

  • Linux

  • Windows

  • macOS

  • Android

Checklist

  • My code follows the project's coding standards.

  • I have used _ignoreCenterUpdates to prevent signal loops.

  • The centroid logic handles both clockwise and counter-clockwise windings.

Improved the geometric accuracy and performance of polygon manipulations:
1. Corrected center calculation by implementing an area-based centroid algorithm.
   This prevents the center from being "weighted" toward locations where
   multiple vertices overlap.
2. Optimized 'setCenter' to handle vertex translation more efficiently during
   drags by suppressing redundant signals and using deferred updates.
3. Added robustness for degenerate polygons (zero area) by providing a
   fallback to the arithmetic mean.
@github-actions
Copy link
Contributor

github-actions bot commented Jan 22, 2026

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Passed View
Android Passed View

All builds passed.

Test Results

linux_gcc_64: 479 passed, 0 skipped

Total: 479 passed, 0 skipped

Code Coverage

Coverage: 100.0%

No baseline available for comparison

Artifact Sizes

Artifact Size
QGroundControl-aarch64.AppImage 187.02 MB
QGroundControl-installer-AMD64-ARM64.exe 74.54 MB
QGroundControl-installer-AMD64.exe 162.39 MB
QGroundControl-installer-ARM64.exe 75.36 MB
QGroundControl-mac.apk 159.47 MB
QGroundControl-windows.apk 159.51 MB
QGroundControl-x86_64.AppImage 174.86 MB
QGroundControl.apk 312.22 MB
QGroundControl.dmg 311.48 MB

No baseline available for comparison

Updated: 2026-01-29 08:25:04 UTC • Triggered by: MacOS

@github-actions
Copy link
Contributor

Build Results

Platform Status

Platform Status Details
Linux Passed View
Windows Passed View
MacOS Running View
Android Running View

Some builds still in progress.

Test Results

linux_gcc_64: 479 passed, 0 skipped

Total: 479 passed, 0 skipped

Code Coverage

Coverage: 100.0%

No baseline available for comparison

Artifact Sizes

Artifact Size
QGroundControl-aarch64.AppImage 187.02 MB
QGroundControl-installer-AMD64-ARM64.exe 74.53 MB
QGroundControl-installer-AMD64.exe 162.42 MB
QGroundControl-installer-ARM64.exe 75.35 MB
QGroundControl-x86_64.AppImage 174.84 MB

No baseline available for comparison

Updated: 2026-01-22 07:08:13 UTC • Triggered by: Windows

@DonLakeFlyer DonLakeFlyer self-requested a review January 23, 2026 18:44
@DonLakeFlyer
Copy link
Collaborator

I'm not sure why CoPilot won't review this for me.

Here is a Claude Sonnet review on the _updateCenter method:

I'll review the _updateCenter method for bugs and performance issues.

Read QGCMapPolygon.cc, lines 340 to 375

Review of _updateCenter Method
I've identified several issues:

Bugs:

  1. Potential Division by Zero - Line 373: meanPoint / n
  • If _toPolygonF() returns an empty QPolygonF (which can happen if _polygonPath.count() <= 2), then n would be 0
  • The outer check _polygonPath.count() > 2 doesn't guarantee polygonF.count() > 0 because _toPolygonF() has its own conditional logic
  • Looking at _toPolygonF(), it only appends points if _polygonPath.count() > 2, so this creates a subtle dependency
  1. Inconsistent Guard Condition
  • The method checks _polygonPath.count() > 2 but _toPolygonF() also checks the same condition
  • If someone modifies _toPolygonF() to change this threshold, bugs could emerge

Performance Issues:

  1. Double Iteration - Lines 328 & 371:
  • When area is near-zero, the code iterates through polygonF twice (once for centroid calculation, once for mean point)
  • The mean point could be calculated in the first loop
  1. Unnecessary QPolygonF Copy - Line 342:
  • _toPolygonF() creates a full copy of the polygon with coordinate conversions
  • For large polygons, this is expensive
  • The copy is only needed for the iterations, not as a persistent object
  1. Redundant Coordinate Conversion - Lines 361 & 373:
  • Both branches call _coordFromPointF() which involves trigonometric calculations via QGCGeo::convertNedToGeo()

Recommendations:
High Priority:

  • Add explicit check: if (n == 0) return; after getting polygonF.count()
  • Calculate mean point during the first loop to avoid double iteration
  • Consider caching the centroid or only updating when polygon actually changes (not on every pathChanged signal)

Medium Priority:

  • Consider working directly with _polygonPath instead of creating polygonF copy
  • Add early return if _center would be invalid

@Eureak-zon Can you look through this and see what is appropriate. Also can you add a comment stating that this code is the shoelace algorithm.

@DonLakeFlyer
Copy link
Collaborator

DonLakeFlyer commented Feb 3, 2026

I'm not seeing the pathChanged problem you are fixing here. I added logging to the existing code and when I drag the center I get this pattern:

 18.630 QGCMapPolygon::setCenter called - default - (QGCMapPolygon::setCenter:368)
    18.630 QGCMapPolygon::adjustVertex called for index 0 to coordinate QGeoCoordinate(47.635526842, -122.0933392, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.630 QGCMapPolygon::adjustVertex called for index 1 to coordinate QGeoCoordinate(47.635526842, -122.08474578, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.630 QGCMapPolygon::adjustVertex called for index 2 to coordinate QGeoCoordinate(47.630972017, -122.08474579, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.630 QGCMapPolygon::adjustVertex called for index 3 to coordinate QGeoCoordinate(47.630972017, -122.09333921, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.632 QGCMapPolygon::pathChanged signalled - default - (QGCMapPolygon::_init()::(anonymous class)::operator():52)
    18.632 _updateCenter called - default - (QGCMapPolygon::_updateCenter:347)
    18.646 QGCMapPolygon::setCenter called - default - (QGCMapPolygon::setCenter:368)
    18.646 QGCMapPolygon::adjustVertex called for index 0 to coordinate QGeoCoordinate(47.635526842, -122.09330563, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.646 QGCMapPolygon::adjustVertex called for index 1 to coordinate QGeoCoordinate(47.635526842, -122.08471221, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.646 QGCMapPolygon::adjustVertex called for index 2 to coordinate QGeoCoordinate(47.630972017, -122.08471222, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.646 QGCMapPolygon::adjustVertex called for index 3 to coordinate QGeoCoordinate(47.630972017, -122.09330565, 0) - default - (QGCMapPolygon::adjustVertex:96)
    18.646 QGCMapPolygon::pathChanged signalled - default - (QGCMapPolygon::_init()::(anonymous class)::operator():52)
    18.646 _updateCenter called - default - (QGCMapPolygon::_updateCenter:347)

This shows a single pathChanged signaled per center update. Do I need to do something else to reproduce the problem?

Here's what I'm testing with: #13924

@DonLakeFlyer
Copy link
Collaborator

@Eureak-zon ?

@Eureak-zon
Copy link
Contributor Author

Eureak-zon commented Feb 25, 2026

@Eureak-zon ?

@DonLakeFlyer This submission addresses the issue that when dragging the vertices of a polygon, the center point is not actually in its original position, while dragging the center point itself works without any problem. When dragging the vertices of a polygon, for instance, if multiple vertices overlap, the original algorithm performs weighted calculations on these overlapping points, which leads to the problem.

@Eureak-zon
Copy link
Contributor Author

Eureak-zon commented Feb 25, 2026

Surveyor's formula

The following snippet is the actual implementation of the formula:

for (int i = 0; i < n; i++) {
    int j = (i + 1) % n; // Gets the index of the next vertex to close the loop
    
    // Core Shoelace calculation: x_i * y_{i+1} - x_{i+1} * y_i
    double crossProduct = (polygonF[i].x() * polygonF[j].y()) - (polygonF[j].x() * polygonF[i].y());
    
    area += crossProduct; // Summing the cross-product terms
    
    // Calculating the intermediate values for the Centroid coordinates
    centroid.rx() += (polygonF[i].x() + polygonF[j].x()) * crossProduct;
    centroid.ry() += (polygonF[i].y() + polygonF[j].y()) * crossProduct;
}

area *= 0.5; // Final step of the Shoelace formula: multiplying by 1/2

Mathematical Correlation

  1. Area Calculation:

    The Shoelace formula is mathematically expressed as:

image

In the code, the crossProduct variable represents each term inside the parentheses, and area *= 0.5 completes the calculation.

  1. Centroid Calculation:

    The code also calculates the centroid based on the area using:

image

This corresponds to the line: centroid.rx() += (polygonF[i].x() + polygonF[j].x()) * crossProduct;.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates QGCMapPolygon’s center handling to compute a geometric centroid (shoelace formula) instead of an arithmetic mean, and reduces UI churn when translating vertices during center moves.

Changes:

  • Replaced mean-based center computation with area-based centroid calculation, with a fallback for near-zero-area polygons.
  • Deferred centerChanged emission in setCenter to reduce redundant updates during interactive moves.
  • Added _deferredCenterChanged state to support deferred center signaling.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/QmlControls/QGCMapPolygon.h Adds _deferredCenterChanged to throttle/batch centerChanged emissions.
src/QmlControls/QGCMapPolygon.cc Implements area-based centroid logic in _updateCenter and defers centerChanged in setCenter.

Comment on lines +340 to +342
void QGCMapPolygon::_updateCenter(void) {
if (!_ignoreCenterUpdates && _polygonPath.count() > 2) {
QPolygonF polygonF = _toPolygonF();
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_updateCenter now returns early unless _polygonPath.count() > 2, which means the polygon center will no longer be updated (or invalidated) when the path is cleared or reduced to 1–2 vertices. This can leave a stale center value in QML (e.g. the center drag handle remains at the previous location during tracing/after clear). Consider updating the center for 1–2 vertices (mean of available points) and explicitly resetting it when the path becomes empty, while still keeping the area-based centroid for count() >= 3.

Copilot uses AI. Check for mistakes.
{
if (!_ignoreCenterUpdates) {
QGeoCoordinate center;
void QGCMapPolygon::_updateCenter(void) {
Copy link

Copilot AI Feb 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Brace/whitespace style in the updated function definitions is inconsistent with the rest of this file (e.g. void QGCMapPolygon::clear(void) uses the opening brace on the next line at src/QmlControls/QGCMapPolygon.cc:70-71). For consistency/readability, align _updateCenter (and setCenter below) with the prevailing brace placement and spacing used throughout this compilation unit.

Suggested change
void QGCMapPolygon::_updateCenter(void) {
void QGCMapPolygon::_updateCenter(void)
{

Copilot uses AI. Check for mistakes.
@DonLakeFlyer
Copy link
Collaborator

I'm not asking about the algorithm. In you modified code you made changes to the signalling which I asked about above. I can't reproduce why you had to change that.

@Eureak-zon
Copy link
Contributor Author

@DonLakeFlyer
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants