[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
This commit is contained in:
Nyall Dawson 2019-12-02 16:00:24 +10:00
parent bee6ab846e
commit 19b8d43388
5 changed files with 71 additions and 5 deletions

View File

@ -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++ )

View File

@ -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<std::unique_ptr<pal::LabelPosition> > &lPos, RTree<pal::FeaturePart *, double, 2, double> *obstacles, double bbx[4], double bby[4] );

View File

@ -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;
}

View File

@ -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;
/**

View File

@ -380,6 +380,27 @@ std::unique_ptr<Problem> 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;