Skip to content
Permalink
Browse files

[feature][labeling] Expose option to allow users to control whether

anchor point for line labels is a hint or a strict requirement

Strict: Labels are placed exactly on the label anchor only, and no
other fallback placements are permitted.

Hint: The label anchor is treated as a hint for the preferred label
placement, but other placements close to the anchor point are permitted.
  • Loading branch information
nyalldawson committed Aug 20, 2020
1 parent ae4d769 commit f62c6d5d56c2b27858b0545c8b99c3bba0d16d3a
Showing with 470 additions and 20 deletions.
  1. +5 −0 python/core/auto_additions/qgslabellinesettings.py
  2. +30 −0 python/core/auto_generated/labeling/qgslabellinesettings.sip.in
  3. +22 −0 src/core/labeling/qgslabelfeature.h
  4. +30 −0 src/core/labeling/qgslabellinesettings.h
  5. +3 −0 src/core/labeling/qgspallabeling.cpp
  6. +57 −15 src/core/pal/feature.cpp
  7. +2 −1 src/core/pal/feature.h
  8. +2 −0 src/gui/labeling/qgslabelinggui.cpp
  9. +35 −2 src/gui/labeling/qgslabellineanchorwidget.cpp
  10. +24 −2 src/ui/labeling/qgslabellineanchorwidgetbase.ui
  11. +260 −0 tests/src/core/testqgslabelingengine.cpp
  12. BIN ...control_images/labelingengine/expected_curved_hint_anchor_end/expected_curved_hint_anchor_end.png
  13. BIN ...rol_images/labelingengine/expected_curved_hint_anchor_start/expected_curved_hint_anchor_start.png
  14. BIN ...rol_images/labelingengine/expected_curved_strict_anchor_end/expected_curved_strict_anchor_end.png
  15. BIN ...images/labelingengine/expected_curved_strict_anchor_start/expected_curved_strict_anchor_start.png
  16. BIN ...images/labelingengine/expected_horizontal_hint_anchor_end/expected_horizontal_hint_anchor_end.png
  17. BIN ...es/labelingengine/expected_horizontal_hint_anchor_start/expected_horizontal_hint_anchor_start.png
  18. BIN ...es/labelingengine/expected_horizontal_strict_anchor_end/expected_horizontal_strict_anchor_end.png
  19. BIN ...abelingengine/expected_horizontal_strict_anchor_start/expected_horizontal_strict_anchor_start.png
  20. BIN ...rol_images/labelingengine/expected_parallel_hint_anchor_end/expected_parallel_hint_anchor_end.png
  21. BIN ...images/labelingengine/expected_parallel_hint_anchor_start/expected_parallel_hint_anchor_start.png
  22. BIN ...images/labelingengine/expected_parallel_strict_anchor_end/expected_parallel_strict_anchor_end.png
  23. BIN ...es/labelingengine/expected_parallel_strict_anchor_start/expected_parallel_strict_anchor_start.png
@@ -5,3 +5,8 @@
QgsLabelLineSettings.DirectionSymbolPlacement.SymbolBelow.__doc__ = "Place direction symbols on below label"
QgsLabelLineSettings.DirectionSymbolPlacement.__doc__ = 'Placement options for direction symbols.\n\n' + '* ``SymbolLeftRight``: ' + QgsLabelLineSettings.DirectionSymbolPlacement.SymbolLeftRight.__doc__ + '\n' + '* ``SymbolAbove``: ' + QgsLabelLineSettings.DirectionSymbolPlacement.SymbolAbove.__doc__ + '\n' + '* ``SymbolBelow``: ' + QgsLabelLineSettings.DirectionSymbolPlacement.SymbolBelow.__doc__
# --
# monkey patching scoped based enum
QgsLabelLineSettings.AnchorType.HintOnly.__doc__ = "Line anchor is a hint for preferred placement only, but other placements close to the hint are permitted"
QgsLabelLineSettings.AnchorType.Strict.__doc__ = "Line anchor is a strict placement, and other placements are not permitted"
QgsLabelLineSettings.AnchorType.__doc__ = 'Line anchor types\n\n' + '* ``HintOnly``: ' + QgsLabelLineSettings.AnchorType.HintOnly.__doc__ + '\n' + '* ``Strict``: ' + QgsLabelLineSettings.AnchorType.Strict.__doc__
# --
@@ -32,6 +32,12 @@ a "perimeter" style mode).
SymbolBelow
};

enum class AnchorType
{
HintOnly,
Strict,
};

QgsLabeling::LinePlacementFlags placementFlags() const;
%Docstring
Returns the line placement flags, which dictate how line labels can be placed
@@ -260,6 +266,8 @@ as close to the start of the line as possible, while a value of 1.0 pushes label
the end of the line.

.. seealso:: :py:func:`setLineAnchorPercent`

.. seealso:: :py:func:`anchorType`
%End

void setLineAnchorPercent( double percent );
@@ -272,6 +280,28 @@ as close to the start of the line as possible, while a value of 1.0 pushes label
the end of the line.

.. seealso:: :py:func:`lineAnchorPercent`

.. seealso:: :py:func:`setAnchorType`
%End

AnchorType anchorType() const;
%Docstring
Returns the line anchor type, which dictates how the :py:func:`~QgsLabelLineSettings.lineAnchorPercent` setting is
handled.

.. seealso:: :py:func:`setAnchorType`

.. seealso:: :py:func:`lineAnchorPercent`
%End

void setAnchorType( AnchorType type );
%Docstring
Sets the line anchor ``type``, which dictates how the :py:func:`~QgsLabelLineSettings.lineAnchorPercent` setting is
handled.

.. seealso:: :py:func:`anchorType`

.. seealso:: :py:func:`setLineAnchorPercent`
%End

};
@@ -439,6 +439,7 @@ class CORE_EXPORT QgsLabelFeature
* the end of the line.
*
* \see setLineAnchorPercent()
* \see lineAnchorType()
* \since QGIS 3.16
*/
double lineAnchorPercent() const { return mLineAnchorPercent; }
@@ -452,10 +453,30 @@ class CORE_EXPORT QgsLabelFeature
* the end of the line.
*
* \see lineAnchorPercent()
* \see setLineAnchorType()
* \since QGIS 3.16
*/
void setLineAnchorPercent( double percent ) { mLineAnchorPercent = percent; }


/**
* Returns the line anchor type, which dictates how the lineAnchorPercent() setting is
* handled.
*
* \see setLineAnchorType()
* \see lineAnchorPercent()
*/
QgsLabelLineSettings::AnchorType lineAnchorType() const { return mLineAnchorType; }

/**
* Sets the line anchor \a type, which dictates how the lineAnchorPercent() setting is
* handled.
*
* \see lineAnchorType()
* \see setLineAnchorPercent()
*/
void setLineAnchorType( QgsLabelLineSettings::AnchorType type ) { mLineAnchorType = type; }

/**
* Returns TRUE if all parts of the feature should be labeled.
* \see setLabelAllParts()
@@ -571,6 +592,7 @@ class CORE_EXPORT QgsLabelFeature
QgsPointXY mAnchorPosition;

double mLineAnchorPercent = 0.5;
QgsLabelLineSettings::AnchorType mLineAnchorType = QgsLabelLineSettings::AnchorType::HintOnly;
};

#endif // QGSLABELFEATURE_H
@@ -50,6 +50,15 @@ class CORE_EXPORT QgsLabelLineSettings
SymbolBelow //!< Place direction symbols on below label
};

/**
* Line anchor types
*/
enum class AnchorType : int
{
HintOnly, //!< Line anchor is a hint for preferred placement only, but other placements close to the hint are permitted
Strict, //!< Line anchor is a strict placement, and other placements are not permitted
};

/**
* Returns the line placement flags, which dictate how line labels can be placed
* above or below the lines.
@@ -241,6 +250,7 @@ class CORE_EXPORT QgsLabelLineSettings
* the end of the line.
*
* \see setLineAnchorPercent()
* \see anchorType()
*/
double lineAnchorPercent() const { return mLineAnchorPercent; }

@@ -253,9 +263,28 @@ class CORE_EXPORT QgsLabelLineSettings
* the end of the line.
*
* \see lineAnchorPercent()
* \see setAnchorType()
*/
void setLineAnchorPercent( double percent ) { mLineAnchorPercent = percent; }

/**
* Returns the line anchor type, which dictates how the lineAnchorPercent() setting is
* handled.
*
* \see setAnchorType()
* \see lineAnchorPercent()
*/
AnchorType anchorType() const { return mAnchorType; }

/**
* Sets the line anchor \a type, which dictates how the lineAnchorPercent() setting is
* handled.
*
* \see anchorType()
* \see setLineAnchorPercent()
*/
void setAnchorType( AnchorType type ) { mAnchorType = type; }

private:
QgsLabeling::LinePlacementFlags mPlacementFlags = QgsLabeling::LinePlacementFlag::AboveLine | QgsLabeling::LinePlacementFlag::MapOrientation;
bool mMergeLines = false;
@@ -269,6 +298,7 @@ class CORE_EXPORT QgsLabelLineSettings
QgsMapUnitScale mOverrunDistanceMapUnitScale;

double mLineAnchorPercent = 0.5;
AnchorType mAnchorType = AnchorType::HintOnly;
};

#endif // QGSLABELLINESETTINGS_H
@@ -1044,6 +1044,7 @@ void QgsPalLayerSettings::readXml( const QDomElement &elem, const QgsReadWriteCo
mLineSettings.setOverrunDistanceUnit( QgsUnitTypes::decodeRenderUnit( placementElem.attribute( QStringLiteral( "overrunDistanceUnit" ) ) ) );
mLineSettings.setOverrunDistanceMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( placementElem.attribute( QStringLiteral( "overrunDistanceMapUnitScale" ) ) ) );
mLineSettings.setLineAnchorPercent( placementElem.attribute( QStringLiteral( "lineAnchorPercent" ), QStringLiteral( "0.5" ) ).toDouble() );
mLineSettings.setAnchorType( static_cast< QgsLabelLineSettings::AnchorType >( placementElem.attribute( QStringLiteral( "lineAnchorType" ), QStringLiteral( "0" ) ).toInt() ) );

geometryGenerator = placementElem.attribute( QStringLiteral( "geometryGenerator" ) );
geometryGeneratorEnabled = placementElem.attribute( QStringLiteral( "geometryGeneratorEnabled" ) ).toInt();
@@ -1195,6 +1196,7 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument &doc, const QgsReadWrite
placementElem.setAttribute( QStringLiteral( "overrunDistanceUnit" ), QgsUnitTypes::encodeUnit( mLineSettings.overrunDistanceUnit() ) );
placementElem.setAttribute( QStringLiteral( "overrunDistanceMapUnitScale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mLineSettings.overrunDistanceMapUnitScale() ) );
placementElem.setAttribute( QStringLiteral( "lineAnchorPercent" ), mLineSettings.lineAnchorPercent() );
placementElem.setAttribute( QStringLiteral( "lineAnchorType" ), static_cast< int >( mLineSettings.anchorType() ) );

placementElem.setAttribute( QStringLiteral( "geometryGenerator" ), geometryGenerator );
placementElem.setAttribute( QStringLiteral( "geometryGeneratorEnabled" ), geometryGeneratorEnabled );
@@ -2471,6 +2473,7 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
( *labelFeature )->setOverrunDistance( overrunDistanceEval );
( *labelFeature )->setOverrunSmoothDistance( overrunSmoothDist );
( *labelFeature )->setLineAnchorPercent( lineSettings.lineAnchorPercent() );
( *labelFeature )->setLineAnchorType( lineSettings.anchorType() );
( *labelFeature )->setLabelAllParts( labelAll );
if ( geom.type() == QgsWkbTypes::PointGeometry && !obstacleGeometry.isNull() )
{
@@ -728,7 +728,10 @@ std::size_t FeaturePart::createCandidatesAlongLine( std::vector< std::unique_ptr
}

//prefer to label along straightish segments:
std::size_t candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );
std::size_t candidates = 0;

if ( mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::HintOnly )
candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );

const std::size_t candidateTargetCount = maximumLineCandidates();
if ( candidates < candidateTargetCount )
@@ -766,14 +769,25 @@ std::size_t FeaturePart::createHorizontalCandidatesAlongLine( std::vector<std::u
distanceToSegment[line->nbPoints - 1] = totalLineLength;

const std::size_t candidateTargetCount = maximumLineCandidates();
const double lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
double currentDistanceAlongLine = lineStepDistance;
double lineStepDistance = 0;

const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
double currentDistanceAlongLine = lineStepDistance;
switch ( mLF->lineAnchorType() )
{
case QgsLabelLineSettings::AnchorType::HintOnly:
lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
break;

case QgsLabelLineSettings::AnchorType::Strict:
currentDistanceAlongLine = lineAnchorPoint;
lineStepDistance = -1;
break;
}

double candidateCenterX, candidateCenterY;
int i = 0;
while ( currentDistanceAlongLine < totalLineLength )
while ( currentDistanceAlongLine <= totalLineLength )
{
if ( pal->isCanceled() )
{
@@ -1089,13 +1103,24 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std
currentDistanceAlongLine = std::numeric_limits< double >::max();
}

const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
const double lineAnchorPoint = totalLineLength * std::min( 0.99, mLF->lineAnchorPercent() ); // don't actually go **all** the way to end of line, just very close to!

switch ( mLF->lineAnchorType() )
{
case QgsLabelLineSettings::AnchorType::HintOnly:
break;

case QgsLabelLineSettings::AnchorType::Strict:
currentDistanceAlongLine = std::min( lineAnchorPoint, totalLineLength * 0.99 - labelWidth );
lineStepDistance = -1;
break;
}

double candidateLength;
double beta;
double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
int i = 0;
while ( currentDistanceAlongLine < totalLineLength - labelWidth )
while ( currentDistanceAlongLine <= totalLineLength - labelWidth || mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::Strict )
{
if ( pal->isCanceled() )
{
@@ -1197,7 +1222,7 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std
}


std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip )
std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip, bool applyAngleConstraints )
{
double offsetAlongSegment = offsetAlongLine;
int index = 1;
@@ -1309,10 +1334,10 @@ std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet
// normalise between -180 and 180
while ( angle_delta > M_PI ) angle_delta -= 2 * M_PI;
while ( angle_delta < -M_PI ) angle_delta += 2 * M_PI;
if ( ( li->max_char_angle_inside > 0 && angle_delta > 0
&& angle_delta > li->max_char_angle_inside * ( M_PI / 180 ) )
|| ( li->max_char_angle_outside < 0 && angle_delta < 0
&& angle_delta < li->max_char_angle_outside * ( M_PI / 180 ) ) )
if ( applyAngleConstraints && ( ( li->max_char_angle_inside > 0 && angle_delta > 0
&& angle_delta > li->max_char_angle_inside * ( M_PI / 180 ) )
|| ( li->max_char_angle_outside < 0 && angle_delta < 0
&& angle_delta < li->max_char_angle_outside * ( M_PI / 180 ) ) ) )
{
return nullptr;
}
@@ -1450,7 +1475,20 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag

// generate curved labels
for ( double distanceAlongLineToStartCandidate = 0; distanceAlongLineToStartCandidate < total_distance; distanceAlongLineToStartCandidate += delta )
double distanceAlongLineToStartCandidate = 0;
bool singleCandidateOnly = false;
switch ( mLF->lineAnchorType() )
{
case QgsLabelLineSettings::AnchorType::HintOnly:
break;

case QgsLabelLineSettings::AnchorType::Strict:
distanceAlongLineToStartCandidate = std::min( lineAnchorPoint, total_distance * 0.99 - getLabelWidth() );
singleCandidateOnly = true;
break;
}

for ( ; distanceAlongLineToStartCandidate <= total_distance; distanceAlongLineToStartCandidate += delta )
{
bool flip = false;
// placements may need to be reversed if using map orientation and the line has right-to-left direction
@@ -1468,7 +1506,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
orientation = 1;
}

std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip );
std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly );
if ( !slp )
continue;

@@ -1479,7 +1517,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
if ( ( showUprightLabels() && !flip ) )
{
orientation = -orientation;
slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip );
slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly );
}
}
if ( !slp )
@@ -1505,7 +1543,9 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
tmp = tmp->nextPart();
}

const bool anchorIsFlexiblePlacement = mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
// if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the
// anchor weighting is sufficient to push labels towards start/end
const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
double angle_diff_avg = li->char_num > 1 ? ( angle_diff / ( li->char_num - 1 ) ) : 0; // <0, pi> but pi/8 is much already
double cost = angle_diff_avg / 100; // <0, 0.031 > but usually <0, 0.003 >
if ( cost < 0.0001 )
@@ -1555,6 +1595,8 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
if ( p )
positions.emplace_back( std::move( p ) );
}
if ( singleCandidateOnly )
break;
}

for ( std::unique_ptr< LabelPosition > &pos : positions )
@@ -243,10 +243,11 @@ namespace pal
* \param distance distance to offset label along curve by
* \param reversed if TRUE label is reversed from lefttoright to righttoleft
* \param flip if TRUE label is placed on the other side of the line
* \param applyAngleConstraints TRUE if label feature character angle constraints should be applied
* \returns calculated label position
*/
std::unique_ptr< LabelPosition > curvedPlacementAtOffset( PointSet *path_positions, double *path_distances,
int &orientation, double distance, bool &reversed, bool &flip );
int &orientation, double distance, bool &reversed, bool &flip, bool applyAngleConstraints );

/**
* Generate curved candidates for line features.
@@ -195,6 +195,7 @@ void QgsLabelingGui::showLineAnchorSettings()
{
const QgsLabelLineSettings widgetSettings = widget->settings();
mLineSettings.setLineAnchorPercent( widgetSettings.lineAnchorPercent() );
mLineSettings.setAnchorType( widgetSettings.anchorType() );
const QgsPropertyCollection obstacleDataDefinedProperties = widget->dataDefinedProperties();
widget->updateDataDefinedProperties( mDataDefinedProperties );
emit widgetChanged();
@@ -536,6 +537,7 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
lyr.setObstacleSettings( mObstacleSettings );

lyr.lineSettings().setLineAnchorPercent( mLineSettings.lineAnchorPercent() );
lyr.lineSettings().setAnchorType( mLineSettings.anchorType() );

lyr.labelPerPart = chkLabelPerFeaturePart->isChecked();
lyr.displayAll = mPalShowAllLabelsForLayerChkBx->isChecked();

0 comments on commit f62c6d5

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