Skip to content
Permalink
Browse files

[needs-docs] Rework label engine "maximum line candidates" and "maxim…

…um polygon candidates"

settings and logic

The previous approach of a single fixed value which applied to ALL line and ALL polygon
features was... not ideal. It meant that all line features would be assigned the same
number of candidates, regardless of length. So a road of length 1 cm on the rendered
map would have an identical number of candidates as a 30cm road covering the length of the
whole map!! This resulted in both a lot of wasted calculations (generating a ridiculous
number of candidates for small lines at barely discernable distances from each other)
AND an insufficient number of candidates for lengthy features (resulting in worse label
placement for these features).

(The situation was similar, but even worse for polygons)

Now, the setting is reworked to "Number of line candidates per cm" and "number of
polygon candidates per cm2". This means that small features get much less candidates,
and large features get much more features! Both a win for map rendering speed in many
circumstances AND good cartography... now that's a nice Christmas gift for QGIS :)
  • Loading branch information
nyalldawson committed Dec 20, 2019
1 parent df102a9 commit 1899f90a04a9fcb58cf28f1866ef9c9366bc2321
@@ -73,23 +73,60 @@ Test whether a particular flag is enabled
Sets whether a particual flag is enabled
%End

void numCandidatePositions( int &candPoint, int &candLine, int &candPolygon ) const;
double maximumLineCandidatesPerCm() const;
%Docstring
Returns the maximum number of line label candidate positions per centimeter.

.. seealso:: :py:func:`setMaximumLineCandidatesPerCm`

.. versionadded:: 3.12
%End

void setMaximumLineCandidatesPerCm( double candidates );
%Docstring
Sets the maximum number of line label ``candidates`` per centimeter.

.. seealso:: :py:func:`maximumLineCandidatesPerCm`

.. versionadded:: 3.12
%End

double maximumPolygonCandidatesPerCmSquared() const;
%Docstring
Returns the maximum number of polygon label candidate positions per centimeter squared.

.. seealso:: :py:func:`setMaximumPolygonCandidatesPerCmSquared`

.. versionadded:: 3.12
%End

void setMaximumPolygonCandidatesPerCmSquared( double candidates );
%Docstring
Sets the maximum number of polygon label ``candidates`` per centimeter squared.

.. seealso:: :py:func:`maximumPolygonCandidatesPerCmSquared`

.. versionadded:: 3.12
%End

void numCandidatePositions( int &candPoint, int &candLine, int &candPolygon ) const /Deprecated/;
%Docstring
Gets number of candidate positions that will be generated for each label feature.

.. deprecated:: QGIS 3.12
the ``candPoint`` argument is ignored.
use maximumPolygonCandidatesPerCmSquared() and
maximumLineCandidatesPerCm() instead.
%End

void setNumCandidatePositions( int candPoint, int candLine, int candPolygon );
void setNumCandidatePositions( int candPoint, int candLine, int candPolygon ) /Deprecated/;
%Docstring
Sets the number of candidate positions that will be generated for each label feature.

.. deprecated:: QGIS 3.12
the ``candPoint`` argument is ignored.
use setMaximumPolygonCandidatesPerCmSquared() and
setMaximumLineCandidatesPerCm() instead.
%End


void setSearchMethod( Search s ) /Deprecated/;
%Docstring
Used to set which search method to use for removal collisions between labels
@@ -55,11 +55,12 @@ QgsLabelEngineConfigWidget::QgsLabelEngineConfigWidget( QWidget *parent )
}
} );

spinCandLine->setClearValue( 5 );
spinCandPolygon->setClearValue( 10 );

// candidate numbers
int candPoint, candLine, candPolygon;
engineSettings.numCandidatePositions( candPoint, candLine, candPolygon );
spinCandLine->setValue( candLine );
spinCandPolygon->setValue( candPolygon );
spinCandLine->setValue( engineSettings.maximumLineCandidatesPerCm() );
spinCandPolygon->setValue( engineSettings.maximumPolygonCandidatesPerCmSquared() );

chkShowCandidates->setChecked( engineSettings.testFlag( QgsLabelingEngineSettings::DrawCandidates ) );
chkShowAllLabels->setChecked( engineSettings.testFlag( QgsLabelingEngineSettings::UseAllLabels ) );
@@ -108,7 +109,8 @@ void QgsLabelEngineConfigWidget::apply()
QgsLabelingEngineSettings engineSettings;

// save
engineSettings.setNumCandidatePositions( 0, spinCandLine->value(), spinCandPolygon->value() );
engineSettings.setMaximumLineCandidatesPerCm( spinCandLine->value() );
engineSettings.setMaximumPolygonCandidatesPerCmSquared( spinCandPolygon->value() );

engineSettings.setFlag( QgsLabelingEngineSettings::DrawCandidates, chkShowCandidates->isChecked() );
engineSettings.setFlag( QgsLabelingEngineSettings::UseAllLabels, chkShowAllLabels->isChecked() );
@@ -128,8 +130,8 @@ void QgsLabelEngineConfigWidget::apply()
void QgsLabelEngineConfigWidget::setDefaults()
{
pal::Pal p;
spinCandLine->setValue( p.maximumNumberOfLineCandidates() );
spinCandPolygon->setValue( p.maximumNumberOfPolygonCandidates() );
spinCandLine->setValue( 5 );
spinCandPolygon->setValue( 10 );
chkShowCandidates->setChecked( false );
chkShowAllLabels->setChecked( false );
chkShowPartialsLabels->setChecked( p.showPartialLabels() );
@@ -270,11 +270,8 @@ void QgsLabelingEngine::registerLabels( QgsRenderContext &context )

mPal = qgis::make_unique< pal::Pal >();

// set number of candidates generated per feature
int candPoint, candLine, candPolygon;
settings.numCandidatePositions( candPoint, candLine, candPolygon );
mPal->setMaximumNumberOfLineCandidates( candLine );
mPal->setMaximumNumberOfPolygonCandidates( candPolygon );
mPal->setMaximumLineCandidatesPerMapUnit( context.labelingEngine()->engineSettings().maximumLineCandidatesPerCm() / context.convertToMapUnits( 10, QgsUnitTypes::RenderMillimeters ) );
mPal->setMaximumPolygonCandidatesPerMapUnitSquared( context.labelingEngine()->engineSettings().maximumPolygonCandidatesPerCmSquared() / std::pow( context.convertToMapUnits( 10, QgsUnitTypes::RenderMillimeters ), 2 ) );

mPal->setShowPartialLabels( settings.testFlag( QgsLabelingEngineSettings::UsePartialCandidates ) );
mPal->setPlacementVersion( settings.placementVersion() );
@@ -32,8 +32,8 @@ void QgsLabelingEngineSettings::readSettingsFromProject( QgsProject *prj )
{
bool saved = false;
mSearchMethod = static_cast< Search >( prj->readNumEntry( QStringLiteral( "PAL" ), QStringLiteral( "/SearchMethod" ), static_cast< int >( Chain ), &saved ) );
mCandLine = prj->readNumEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesLine" ), 50, &saved );
mCandPolygon = prj->readNumEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesPolygon" ), 30, &saved );
mMaxLineCandidatesPerCm = prj->readDoubleEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesLinePerCM" ), 5, &saved );
mMaxPolygonCandidatesPerCmSquared = prj->readDoubleEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesPolygonPerCM" ), 10, &saved );

mFlags = nullptr;
if ( prj->readBoolEntry( QStringLiteral( "PAL" ), QStringLiteral( "/ShowingCandidates" ), false, &saved ) ) mFlags |= DrawCandidates;
@@ -59,8 +59,8 @@ void QgsLabelingEngineSettings::readSettingsFromProject( QgsProject *prj )
void QgsLabelingEngineSettings::writeSettingsToProject( QgsProject *project )
{
project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/SearchMethod" ), static_cast< int >( mSearchMethod ) );
project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesLine" ), mCandLine );
project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesPolygon" ), mCandPolygon );
project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesLinePerCM" ), mMaxLineCandidatesPerCm );
project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/CandidatesPolygonPerCM" ), mMaxPolygonCandidatesPerCmSquared );

project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/ShowingCandidates" ), mFlags.testFlag( DrawCandidates ) );
project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/DrawRectOnly" ), mFlags.testFlag( DrawLabelRectOnly ) );
@@ -83,29 +83,62 @@ class CORE_EXPORT QgsLabelingEngineSettings
//! Sets whether a particual flag is enabled
void setFlag( Flag f, bool enabled = true ) { if ( enabled ) mFlags |= f; else mFlags &= ~f; }

/**
* Returns the maximum number of line label candidate positions per centimeter.
*
* \see setMaximumLineCandidatesPerCm()
* \since QGIS 3.12
*/
double maximumLineCandidatesPerCm() const { return mMaxLineCandidatesPerCm; }

/**
* Sets the maximum number of line label \a candidates per centimeter.
*
* \see maximumLineCandidatesPerCm()
* \since QGIS 3.12
*/
void setMaximumLineCandidatesPerCm( double candidates ) { mMaxLineCandidatesPerCm = candidates; }

/**
* Returns the maximum number of polygon label candidate positions per centimeter squared.
*
* \see setMaximumPolygonCandidatesPerCmSquared()
* \since QGIS 3.12
*/
double maximumPolygonCandidatesPerCmSquared() const { return mMaxPolygonCandidatesPerCmSquared; }

/**
* Sets the maximum number of polygon label \a candidates per centimeter squared.
*
* \see maximumPolygonCandidatesPerCmSquared()
* \since QGIS 3.12
*/
void setMaximumPolygonCandidatesPerCmSquared( double candidates ) { mMaxPolygonCandidatesPerCmSquared = candidates; }

/**
* Gets number of candidate positions that will be generated for each label feature.
* \deprecated Since QGIS 3.12 the \a candPoint argument is ignored.
* \deprecated Since QGIS 3.12 use maximumPolygonCandidatesPerCmSquared() and
* maximumLineCandidatesPerCm() instead.
*/
void numCandidatePositions( int &candPoint, int &candLine, int &candPolygon ) const
Q_DECL_DEPRECATED void numCandidatePositions( int &candPoint, int &candLine, int &candPolygon ) const SIP_DEPRECATED
{
Q_UNUSED( candPoint )
candLine = mCandLine;
candPolygon = mCandPolygon;
Q_UNUSED( candLine )
Q_UNUSED( candPolygon )
}

/**
* Sets the number of candidate positions that will be generated for each label feature.
* \deprecated Since QGIS 3.12 the \a candPoint argument is ignored.
* \deprecated Since QGIS 3.12 use setMaximumPolygonCandidatesPerCmSquared() and
* setMaximumLineCandidatesPerCm() instead.
*/
void setNumCandidatePositions( int candPoint, int candLine, int candPolygon )
Q_DECL_DEPRECATED void setNumCandidatePositions( int candPoint, int candLine, int candPolygon ) SIP_DEPRECATED
{
Q_UNUSED( candPoint )
mCandLine = candLine;
mCandPolygon = candPolygon;
Q_UNUSED( candLine )
Q_UNUSED( candPolygon )
}


/**
* Used to set which search method to use for removal collisions between labels
* \deprecated since QGIS 3.10 - Chain is always used.
@@ -186,10 +219,10 @@ class CORE_EXPORT QgsLabelingEngineSettings
Flags mFlags;
//! search method to use for removal collisions between labels
Search mSearchMethod = Chain;
//! Number of candedate positions that will be generated for features
int mCandLine = 50, mCandPolygon = 30;


// maximum density of line/polygon candidates per mm
double mMaxLineCandidatesPerCm = 5;
double mMaxPolygonCandidatesPerCmSquared = 10;

QColor mUnplacedLabelColor = QColor( 255, 0, 0 );

@@ -157,6 +157,60 @@ QgsFeatureId FeaturePart::featureId() const
return mLF->id();
}

std::size_t FeaturePart::maximumLineCandidates() const
{
if ( mCachedMaxLineCandidates > 0 )
return mCachedMaxLineCandidates;

GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
try
{
double length = 0;
if ( GEOSLength_r( geosctxt, geos(), &length ) == 1 )
{
const std::size_t candidatesForLineLength = static_cast< std::size_t >( std::ceil( mLF->layer()->pal->maximumLineCandidatesPerMapUnit() * length ) );
const std::size_t maxForLayer = mLF->layer()->maximumLineLabelCandidates();
if ( maxForLayer == 0 )
mCachedMaxLineCandidates = candidatesForLineLength;
else
mCachedMaxLineCandidates = std::min( candidatesForLineLength, maxForLayer );
return mCachedMaxLineCandidates;
}
}
catch ( GEOSException & )
{
}
mCachedMaxLineCandidates = 1;
return mCachedMaxLineCandidates;
}

std::size_t FeaturePart::maximumPolygonCandidates() const
{
if ( mCachedMaxPolygonCandidates > 0 )
return mCachedMaxPolygonCandidates;

GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
try
{
double area = 0;
if ( GEOSArea_r( geosctxt, geos(), &area ) == 1 )
{
const std::size_t candidatesForArea = static_cast< std::size_t >( std::ceil( mLF->layer()->pal->maximumPolygonCandidatesPerMapUnitSquared() * area ) );
const std::size_t maxForLayer = mLF->layer()->maximumPolygonLabelCandidates();
if ( maxForLayer == 0 )
mCachedMaxPolygonCandidates = candidatesForArea;
else
mCachedMaxPolygonCandidates = std::min( candidatesForArea, maxForLayer );
return mCachedMaxPolygonCandidates;
}
}
catch ( GEOSException & )
{
}
mCachedMaxPolygonCandidates = 1;
return mCachedMaxPolygonCandidates;
}

bool FeaturePart::hasSameLabelFeatureAs( FeaturePart *part ) const
{
if ( !part )
@@ -635,7 +689,8 @@ std::size_t FeaturePart::createCandidatesAlongLine( std::vector< std::unique_ptr
//prefer to label along straightish segments:
std::size_t candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );

if ( static_cast< int >( candidates ) < mLF->layer()->maximumLineLabelCandidates() )
const std::size_t candidateTargetCount = maximumLineCandidates();
if ( candidates < candidateTargetCount )
{
// but not enough candidates yet, so fallback to labeling near whole line's midpoint
candidates = createCandidatesAlongLineNearMidpoint( lPos, mapShape, candidates > 0 ? 0.01 : 0.0, pal );
@@ -734,8 +789,9 @@ std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vec
return 0; //createCandidatesAlongLineNearMidpoint will be more appropriate
}

const std::size_t candidateTargetCount = maximumLineCandidates();
double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / mLF->layer()->maximumLineLabelCandidates() );
lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );

double distanceToEndOfSegment = 0.0;
int lastNodeInSegment = 0;
@@ -906,9 +962,11 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std
double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
double currentDistanceAlongLine = 0;

const std::size_t candidateTargetCount = maximumLineCandidates();

if ( totalLineLength > labelWidth )
{
lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / mLF->layer()->maximumLineLabelCandidates() );
lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
}
else if ( !line->isClosed() ) // line length < label width => centering label position
{
@@ -1272,7 +1330,8 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
return 0;

QLinkedList<LabelPosition *> positions;
double delta = std::max( li->label_height / 6, total_distance / mLF->layer()->maximumLineLabelCandidates() );
const std::size_t candidateTargetCount = maximumLineCandidates();
double delta = std::max( li->label_height / 6, total_distance / candidateTargetCount );

pal::LineArrangementFlags flags = mLF->arrangementFlags();
if ( flags == 0 )
@@ -126,6 +126,16 @@ namespace pal
*/
QgsFeatureId featureId() const;

/**
* Returns the maximum number of line candidates to generate for this feature.
*/
std::size_t maximumLineCandidates() const;

/**
* Returns the maximum number of polygon candidates to generate for this feature.
*/
std::size_t maximumPolygonCandidates() const;

/**
* Generates a list of candidate positions for labels for this feature.
*/
@@ -349,6 +359,9 @@ namespace pal
LabelPosition::Quadrant quadrantFromOffset() const;

int mTotalRepeats = 0;

mutable std::size_t mCachedMaxLineCandidates = 0;
mutable std::size_t mCachedMaxPolygonCandidates = 0;
};

} // end namespace pal

0 comments on commit 1899f90

Please sign in to comment.
You can’t perform that action at this time.