Skip to content
Permalink
Browse files

[labeling] Add api to set line anchor point for labels, which represents

the percentage along line features at which labels for that feature
should gravitate toward
  • Loading branch information
nyalldawson committed Aug 18, 2020
1 parent 6a77302 commit b14bb32104487faa3c5300575f47e531ee143ed5
@@ -248,6 +248,30 @@ Sets the map unit ``scale`` for label overrun distance.
.. seealso:: :py:func:`overrunDistance`

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

double lineAnchorPercent() const;
%Docstring
Returns the percent along the line at which labels should be placed.

By default, this is 0.5 which indicates that labels should be placed as close to the
center of the line as possible. A value of 0.0 indicates that the labels should be placed
as close to the start of the line as possible, while a value of 1.0 pushes labels towards
the end of the line.

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

void setLineAnchorPercent( double percent );
%Docstring
Sets the ``percent`` along the line at which labels should be placed.

By default, this is 0.5 which indicates that labels should be placed as close to the
center of the line as possible. A value of 0.0 indicates that the labels should be placed
as close to the start of the line as possible, while a value of 1.0 pushes labels towards
the end of the line.

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

};
@@ -292,6 +292,7 @@ class QgsPalLayerSettings
OverrunDistance,
LabelAllParts,
PolygonLabelOutside,
LineAnchorPercent,

// rendering
ScaleVisibility,
@@ -430,6 +430,32 @@ class CORE_EXPORT QgsLabelFeature
*/
void setOverrunSmoothDistance( double distance );

/**
* Returns the percent along the line at which labels should be placed, for line labels only.
*
* By default, this is 0.5 which indicates that labels should be placed as close to the
* center of the line as possible. A value of 0.0 indicates that the labels should be placed
* as close to the start of the line as possible, while a value of 1.0 pushes labels towards
* the end of the line.
*
* \see setLineAnchorPercent()
* \since QGIS 3.16
*/
double lineAnchorPercent() const { return mLineAnchorPercent; }

/**
* Sets the \a percent along the line at which labels should be placed, for line labels only.
*
* By default, this is 0.5 which indicates that labels should be placed as close to the
* center of the line as possible. A value of 0.0 indicates that the labels should be placed
* as close to the start of the line as possible, while a value of 1.0 pushes labels towards
* the end of the line.
*
* \see lineAnchorPercent()
* \since QGIS 3.16
*/
void setLineAnchorPercent( double percent ) { mLineAnchorPercent = percent; }

/**
* Returns TRUE if all parts of the feature should be labeled.
* \see setLabelAllParts()
@@ -543,6 +569,8 @@ class CORE_EXPORT QgsLabelFeature
QgsLabelObstacleSettings mObstacleSettings{};

QgsPointXY mAnchorPosition;

double mLineAnchorPercent = 0.5;
};

#endif // QGSLABELFEATURE_H
@@ -37,4 +37,10 @@ void QgsLabelLineSettings::updateDataDefinedProperties( const QgsPropertyCollect
context.setOriginalValueVariable( mOverrunDistance );
mOverrunDistance = properties.valueAsDouble( QgsPalLayerSettings::OverrunDistance, context, mOverrunDistance );
}

if ( properties.isActive( QgsPalLayerSettings::LineAnchorPercent ) )
{
context.setOriginalValueVariable( mLineAnchorPercent );
mLineAnchorPercent = properties.valueAsDouble( QgsPalLayerSettings::LineAnchorPercent, context, mLineAnchorPercent );
}
}
@@ -232,6 +232,30 @@ class CORE_EXPORT QgsLabelLineSettings
*/
void setOverrunDistanceMapUnitScale( const QgsMapUnitScale &scale ) { mOverrunDistanceMapUnitScale = scale; }

/**
* Returns the percent along the line at which labels should be placed.
*
* By default, this is 0.5 which indicates that labels should be placed as close to the
* center of the line as possible. A value of 0.0 indicates that the labels should be placed
* as close to the start of the line as possible, while a value of 1.0 pushes labels towards
* the end of the line.
*
* \see setLineAnchorPercent()
*/
double lineAnchorPercent() const { return mLineAnchorPercent; }

/**
* Sets the \a percent along the line at which labels should be placed.
*
* By default, this is 0.5 which indicates that labels should be placed as close to the
* center of the line as possible. A value of 0.0 indicates that the labels should be placed
* as close to the start of the line as possible, while a value of 1.0 pushes labels towards
* the end of the line.
*
* \see lineAnchorPercent()
*/
void setLineAnchorPercent( double percent ) { mLineAnchorPercent = percent; }

private:
QgsLabeling::LinePlacementFlags mPlacementFlags = QgsLabeling::LinePlacementFlag::AboveLine | QgsLabeling::LinePlacementFlag::MapOrientation;
bool mMergeLines = false;
@@ -243,6 +267,8 @@ class CORE_EXPORT QgsLabelLineSettings
double mOverrunDistance = 0;
QgsUnitTypes::RenderUnit mOverrunDistanceUnit = QgsUnitTypes::RenderMillimeters;
QgsMapUnitScale mOverrunDistanceMapUnitScale;

double mLineAnchorPercent = 0.5;
};

#endif // QGSLABELLINESETTINGS_H
@@ -214,6 +214,7 @@ void QgsPalLayerSettings::initPropertyDefinitions()
{ QgsPalLayerSettings::RepeatDistance, QgsPropertyDefinition( "RepeatDistance", QObject::tr( "Repeat distance" ), QgsPropertyDefinition::DoublePositive, origin ) },
{ QgsPalLayerSettings::RepeatDistanceUnit, QgsPropertyDefinition( "RepeatDistanceUnit", QObject::tr( "Repeat distance unit" ), QgsPropertyDefinition::RenderUnits, origin ) },
{ QgsPalLayerSettings::OverrunDistance, QgsPropertyDefinition( "OverrunDistance", QObject::tr( "Overrun distance" ), QgsPropertyDefinition::DoublePositive, origin ) },
{ QgsPalLayerSettings::LineAnchorPercent, QgsPropertyDefinition( "LineAnchorPercent", QObject::tr( "Line anchor percentage, as fraction from 0.0 to 1.0" ), QgsPropertyDefinition::Double0To1, origin ) },
{ QgsPalLayerSettings::Priority, QgsPropertyDefinition( "Priority", QgsPropertyDefinition::DataTypeNumeric, QObject::tr( "Label priority" ), QObject::tr( "double [0.0-10.0]" ), origin ) },
{ QgsPalLayerSettings::IsObstacle, QgsPropertyDefinition( "IsObstacle", QObject::tr( "Feature is a label obstacle" ), QgsPropertyDefinition::Boolean, origin ) },
{ QgsPalLayerSettings::ObstacleFactor, QgsPropertyDefinition( "ObstacleFactor", QgsPropertyDefinition::DataTypeNumeric, QObject::tr( "Obstacle factor" ), QObject::tr( "double [0.0-10.0]" ), origin ) },
@@ -1042,6 +1043,7 @@ void QgsPalLayerSettings::readXml( const QDomElement &elem, const QgsReadWriteCo
mLineSettings.setOverrunDistance( placementElem.attribute( QStringLiteral( "overrunDistance" ), QStringLiteral( "0" ) ).toDouble() );
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() );

geometryGenerator = placementElem.attribute( QStringLiteral( "geometryGenerator" ) );
geometryGeneratorEnabled = placementElem.attribute( QStringLiteral( "geometryGeneratorEnabled" ) ).toInt();
@@ -1192,6 +1194,7 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument &doc, const QgsReadWrite
placementElem.setAttribute( QStringLiteral( "overrunDistance" ), mLineSettings.overrunDistance() );
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( "geometryGenerator" ), geometryGenerator );
placementElem.setAttribute( QStringLiteral( "geometryGeneratorEnabled" ), geometryGeneratorEnabled );
@@ -2467,6 +2470,7 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
( *labelFeature )->setPermissibleZone( permissibleZone );
( *labelFeature )->setOverrunDistance( overrunDistanceEval );
( *labelFeature )->setOverrunSmoothDistance( overrunSmoothDist );
( *labelFeature )->setLineAnchorPercent( lineSettings.lineAnchorPercent() );
( *labelFeature )->setLabelAllParts( labelAll );
if ( geom.type() == QgsWkbTypes::PointGeometry && !obstacleGeometry.isNull() )
{
@@ -453,6 +453,7 @@ class CORE_EXPORT QgsPalLayerSettings
OverrunDistance = 102, //!< Distance which labels can extend past either end of linear features
LabelAllParts = 103, //!< Whether all parts of multi-part features should be labeled
PolygonLabelOutside = 109, //!< Whether labels outside a polygon feature are permitted, or should be forced (since QGIS 3.14)
LineAnchorPercent = 111, //!< Portion along line at which labels should be anchored (since QGIS 3.16)

// rendering
ScaleVisibility = 23,
@@ -769,6 +769,8 @@ std::size_t FeaturePart::createHorizontalCandidatesAlongLine( std::vector<std::u
const double lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
double currentDistanceAlongLine = lineStepDistance;

const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();

double candidateCenterX, candidateCenterY;
int i = 0;
while ( currentDistanceAlongLine < totalLineLength )
@@ -780,8 +782,8 @@ std::size_t FeaturePart::createHorizontalCandidatesAlongLine( std::vector<std::u

line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateCenterX, &candidateCenterY );

// penalize positions which are further from the line's midpoint
double cost = std::fabs( totalLineLength / 2 - currentDistanceAlongLine ) / totalLineLength; // <0, 0.5>
// penalize positions which are further from the line's anchor point
double cost = std::fabs( lineAnchorPoint - currentDistanceAlongLine ) / totalLineLength; // <0, 0.5>
cost /= 1000; // < 0, 0.0005 >

lPos.emplace_back( qgis::make_unique< LabelPosition >( i, candidateCenterX - labelWidth / 2, candidateCenterY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) );
@@ -881,7 +883,7 @@ std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vec
straightSegmentLengths << currentStraightSegmentLength;
straightSegmentAngles << QgsGeometryUtils::normalizedAngle( std::atan2( y[numberNodes - 1] - segmentStartY, x[numberNodes - 1] - segmentStartX ) );
longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
double middleOfLine = totalLineLength / 2.0;
const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();

if ( totalLineLength < labelWidth )
{
@@ -947,20 +949,28 @@ std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vec

// penalize positions which are further from the straight segments's midpoint
double labelCenter = currentDistanceAlongLine + labelWidth / 2.0;
double costCenter = 2 * std::fabs( labelCenter - distanceToCenterOfSegment ) / ( distanceToEndOfSegment - distanceToStartOfSegment ); // 0 -> 1
cost += costCenter * 0.0005; // < 0, 0.0005 >
const bool placementIsFlexible = mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
if ( placementIsFlexible )
{
// only apply this if labels are being placed toward the center of overall lines -- otherwise it messes with the distance from anchor cost
double costCenter = 2 * std::fabs( labelCenter - distanceToCenterOfSegment ) / ( distanceToEndOfSegment - distanceToStartOfSegment ); // 0 -> 1
cost += costCenter * 0.0005; // < 0, 0.0005 >
}

if ( !closedLine )
{
// penalize positions which are further from absolute center of whole linestring
// this only applies to non closed linestrings, since the middle of a closed linestring is effectively arbitrary
// and irrelevant to labeling
double costLineCenter = 2 * std::fabs( labelCenter - middleOfLine ) / totalLineLength; // 0 -> 1
double costLineCenter = 2 * std::fabs( labelCenter - lineAnchorPoint ) / totalLineLength; // 0 -> 1
cost += costLineCenter * 0.0005; // < 0, 0.0005 >
}

cost += segmentCost * 0.0005; // prefer labels on longer straight segments
cost += segmentAngleCost * 0.0001; // prefer more horizontal segments, but this is less important than length considerations
if ( placementIsFlexible )
{
cost += segmentCost * 0.0005; // prefer labels on longer straight segments
cost += segmentAngleCost * 0.0001; // prefer more horizontal segments, but this is less important than length considerations
}

if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
{
@@ -1079,6 +1089,8 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std
currentDistanceAlongLine = std::numeric_limits< double >::max();
}

const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();

double candidateLength;
double beta;
double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
@@ -1114,8 +1126,8 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std
cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
}

// penalize positions which are further from the line's midpoint
double costCenter = std::fabs( totalLineLength / 2 - ( currentDistanceAlongLine + labelWidth / 2 ) ) / totalLineLength; // <0, 0.5>
// penalize positions which are further from the line's anchor point
double costCenter = std::fabs( lineAnchorPoint - ( currentDistanceAlongLine + labelWidth / 2 ) ) / totalLineLength; // <0, 0.5>
cost += costCenter / 1000; // < 0, 0.0005 >
cost += initialCost;

@@ -1424,6 +1436,8 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
return 0;
}

const double lineAnchorPoint = total_distance * mLF->lineAnchorPercent();

if ( pal->isCanceled() )
return 0;

@@ -1491,14 +1505,16 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
tmp = tmp->nextPart();
}

const bool anchorIsFlexiblePlacement = 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 ) cost = 0.0001;
if ( cost < 0.0001 )
cost = 0.0001;

// penalize positions which are further from the line's midpoint
// penalize positions which are further from the line's anchor point
double labelCenter = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
double costCenter = std::fabs( total_distance / 2 - labelCenter ) / total_distance; // <0, 0.5>
cost += costCenter / 100; // < 0, 0.005 >
double costCenter = std::fabs( lineAnchorPoint - labelCenter ) / total_distance; // <0, 0.5>
cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
slp->setCost( cost );

// average angle is calculated with respect to periodicity of angles

0 comments on commit b14bb32

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