Skip to content

Commit

Permalink
[needs-docs] Rework label engine "maximum line candidates" and "maxim…
Browse files Browse the repository at this point in the history
…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 26, 2019
1 parent df102a9 commit 1899f90
Show file tree
Hide file tree
Showing 12 changed files with 253 additions and 129 deletions.
Expand Up @@ -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
Expand Down
16 changes: 9 additions & 7 deletions src/app/labeling/qgslabelengineconfigdialog.cpp
Expand Up @@ -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 ) );
Expand Down Expand Up @@ -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() );
Expand All @@ -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() );
Expand Down
7 changes: 2 additions & 5 deletions src/core/labeling/qgslabelingengine.cpp
Expand Up @@ -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() );
Expand Down
8 changes: 4 additions & 4 deletions src/core/labeling/qgslabelingenginesettings.cpp
Expand Up @@ -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;
Expand All @@ -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 ) );
Expand Down
57 changes: 45 additions & 12 deletions src/core/labeling/qgslabelingenginesettings.h
Expand Up @@ -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.
Expand Down Expand Up @@ -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 );

Expand Down
67 changes: 63 additions & 4 deletions src/core/pal/feature.cpp
Expand Up @@ -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 )
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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 )
Expand Down
13 changes: 13 additions & 0 deletions src/core/pal/feature.h
Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 1899f90

Please sign in to comment.