From 19b8d433880378b27cd2c235bff3223d4837fd5e Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 2 Dec 2019 16:00:24 +1000 Subject: [PATCH] [needs-docs] When running in label engine v2 mode, discard any candidates where they collide with an obstacle feature of greater weight when compared to the label's priority Previously, obstacle weight was used ONLY to rank a features' label candidates relative to each other, but was never used to actually prune candidates completely. This meant that the labeling obstacle functionality was confusing and frustrating for users to work with -- because despite setting layers as the maximum possible blocking weight, you'd still see labels being placed over these features (e.g. where the labeling engine had no other choice). Now, (when a project is set to v2 labeling engine mode), labels will NEVER be placed over obstacles of greater weight. This means that labels will potentially be omitted if the only choice is to place them over a high weighting obstacle. But ultimately, that's much more understandable for users -- they've manually set a particular layer to a high obstacle factor, so we should respect that and never place labels on these features. In the end, this change makes the labeling placement much simpler to understand for users, and should give power users a much nicer experience all round. Funded by the QGIS grants program --- src/core/pal/costcalculator.cpp | 26 +++++++++++++++++++++++--- src/core/pal/costcalculator.h | 3 ++- src/core/pal/labelposition.cpp | 9 ++++++++- src/core/pal/labelposition.h | 17 +++++++++++++++++ src/core/pal/pal.cpp | 21 +++++++++++++++++++++ 5 files changed, 71 insertions(+), 5 deletions(-) diff --git a/src/core/pal/costcalculator.cpp b/src/core/pal/costcalculator.cpp index 29a13d78342..5117850891a 100644 --- a/src/core/pal/costcalculator.cpp +++ b/src/core/pal/costcalculator.cpp @@ -35,7 +35,7 @@ bool CostCalculator::candidateSortShrink( const std::unique_ptr< LabelPosition > return c1->cost() > c2->cost(); } -void CostCalculator::addObstacleCostPenalty( LabelPosition *lp, FeaturePart *obstacle ) +void CostCalculator::addObstacleCostPenalty( LabelPosition *lp, FeaturePart *obstacle, Pal *pal ) { int n = 0; double dist; @@ -83,11 +83,30 @@ void CostCalculator::addObstacleCostPenalty( LabelPosition *lp, FeaturePart *obs break; } + //scale cost by obstacle's factor + double obstacleCost = obstacle->obstacleFactor() * double( n ); if ( n > 0 ) lp->setConflictsWithObstacle( true ); - //scale cost by obstacle's factor - double obstacleCost = obstacle->obstacleFactor() * double( n ); + switch ( pal->placementVersion() ) + { + case QgsLabelingEngineSettings::PlacementEngineVersion1: + break; + + case QgsLabelingEngineSettings::PlacementEngineVersion2: + { + // obstacle factor is from 0 -> 2, label priority is from 1 -> 0. argh! + const double priority = 2 * ( 1 - lp->feature->calculatePriority() ); + const double obstaclePriority = obstacle->obstacleFactor(); + + // if feature priority is < obstaclePriorty, there's a hard conflict... + if ( n > 0 && ( priority < obstaclePriority && !qgsDoubleNear( priority, obstaclePriority, 0.001 ) ) ) + { + lp->setHasHardObstacleConflict( true ); + } + break; + } + } // label cost is penalized lp->setCost( lp->cost() + obstacleCost ); @@ -185,6 +204,7 @@ std::size_t CostCalculator::finalizeCandidatesCosts( Feats *feat, std::size_t ma } while ( stop == 0 && discrim < feat->candidates.back()->cost() + 2.0 ); + // THIS LOOKS SUSPICIOUS -- it clamps all costs to a fixed value?? if ( discrim > 1.5 ) { for ( std::size_t k = 0; k < stop; k++ ) diff --git a/src/core/pal/costcalculator.h b/src/core/pal/costcalculator.h index 307b1290576..0918e5eec8e 100644 --- a/src/core/pal/costcalculator.h +++ b/src/core/pal/costcalculator.h @@ -29,6 +29,7 @@ namespace pal { class Feats; class LabelPosition; + class Pal; /** * \ingroup core @@ -37,7 +38,7 @@ namespace pal { public: //! Increase candidate's cost according to its collision with passed feature - static void addObstacleCostPenalty( LabelPosition *lp, pal::FeaturePart *obstacle ); + static void addObstacleCostPenalty( LabelPosition *lp, pal::FeaturePart *obstacle, Pal *pal ); //! Calculates the costs for polygon label candidates static void setPolygonCandidatesCost( std::size_t nblp, std::vector > &lPos, RTree *obstacles, double bbx[4], double bby[4] ); diff --git a/src/core/pal/labelposition.cpp b/src/core/pal/labelposition.cpp index 6bc7bbfc7e9..9c3aa809cd1 100644 --- a/src/core/pal/labelposition.cpp +++ b/src/core/pal/labelposition.cpp @@ -407,6 +407,13 @@ void LabelPosition::setConflictsWithObstacle( bool conflicts ) nextPart->setConflictsWithObstacle( conflicts ); } +void LabelPosition::setHasHardObstacleConflict( bool conflicts ) +{ + mHasHardConflict = conflicts; + if ( nextPart ) + nextPart->setHasHardObstacleConflict( conflicts ); +} + bool LabelPosition::polygonObstacleCallback( FeaturePart *obstacle, void *ctx ) { PolygonCostCalculator *pCost = reinterpret_cast< PolygonCostCalculator * >( ctx ); @@ -453,7 +460,7 @@ bool LabelPosition::pruneCallback( LabelPosition *candidatePosition, void *ctx ) return true; } - CostCalculator::addObstacleCostPenalty( candidatePosition, obstaclePart ); + CostCalculator::addObstacleCostPenalty( candidatePosition, obstaclePart, ( reinterpret_cast< PruneCtx * >( ctx ) )->pal ); return true; } diff --git a/src/core/pal/labelposition.h b/src/core/pal/labelposition.h index 291a054e536..e6fb99f6b95 100644 --- a/src/core/pal/labelposition.h +++ b/src/core/pal/labelposition.h @@ -217,6 +217,22 @@ namespace pal */ bool conflictsWithObstacle() const { return mHasObstacleConflict; } + /** + * Sets whether the position is marked as having a hard conflict with an obstacle feature. + * A hard conflict means that the placement should (usually) not be considered, because the candidate + * conflicts with a obstacle of sufficient weight. + * \see hasHardObstacleConflict() + */ + void setHasHardObstacleConflict( bool conflicts ); + + /** + * Returns whether the position is marked as having a hard conflict with an obstacle feature. + * A hard conflict means that the placement should (usually) not be considered, because the candidate + * conflicts with a obstacle of sufficient weight. + * \see setHasHardObstacleConflict() + */ + bool hasHardObstacleConflict() const { return mHasHardConflict; } + //! Make sure the cost is less than 1 void validateCost(); @@ -329,6 +345,7 @@ namespace pal private: double mCost; bool mHasObstacleConflict; + bool mHasHardConflict = false; int mUpsideDownCharCount; /** diff --git a/src/core/pal/pal.cpp b/src/core/pal/pal.cpp index 520e3c71b23..4ee512a1344 100644 --- a/src/core/pal/pal.cpp +++ b/src/core/pal/pal.cpp @@ -380,6 +380,27 @@ std::unique_ptr Pal::extract( const QgsRectangle &extent, const QgsGeom feat->candidates.pop_back(); } + switch ( mPlacementVersion ) + { + case QgsLabelingEngineSettings::PlacementEngineVersion1: + break; + + case QgsLabelingEngineSettings::PlacementEngineVersion2: + { + // v2 placement rips out candidates where the candidate cost is too high when compared to + // their inactive cost + feat->candidates.erase( std::remove_if( feat->candidates.begin(), feat->candidates.end(), [ & ]( std::unique_ptr< LabelPosition > &candidate ) + { + if ( candidate->hasHardObstacleConflict() ) + { + feat->candidates.back()->removeFromIndex( prob->mAllCandidatesIndex ); + return true; + } + return false; + } ), feat->candidates.end() ); + } + } + if ( isCanceled() ) return nullptr;