Skip to content

Commit f62c6d5

Browse files
committedAug 20, 2020
[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.
1 parent ae4d769 commit f62c6d5

File tree

23 files changed

+470
-20
lines changed

23 files changed

+470
-20
lines changed
 

‎python/core/auto_additions/qgslabellinesettings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,8 @@
55
QgsLabelLineSettings.DirectionSymbolPlacement.SymbolBelow.__doc__ = "Place direction symbols on below label"
66
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__
77
# --
8+
# monkey patching scoped based enum
9+
QgsLabelLineSettings.AnchorType.HintOnly.__doc__ = "Line anchor is a hint for preferred placement only, but other placements close to the hint are permitted"
10+
QgsLabelLineSettings.AnchorType.Strict.__doc__ = "Line anchor is a strict placement, and other placements are not permitted"
11+
QgsLabelLineSettings.AnchorType.__doc__ = 'Line anchor types\n\n' + '* ``HintOnly``: ' + QgsLabelLineSettings.AnchorType.HintOnly.__doc__ + '\n' + '* ``Strict``: ' + QgsLabelLineSettings.AnchorType.Strict.__doc__
12+
# --

‎python/core/auto_generated/labeling/qgslabellinesettings.sip.in

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ a "perimeter" style mode).
3232
SymbolBelow
3333
};
3434

35+
enum class AnchorType
36+
{
37+
HintOnly,
38+
Strict,
39+
};
40+
3541
QgsLabeling::LinePlacementFlags placementFlags() const;
3642
%Docstring
3743
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
260266
the end of the line.
261267

262268
.. seealso:: :py:func:`setLineAnchorPercent`
269+
270+
.. seealso:: :py:func:`anchorType`
263271
%End
264272

265273
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
272280
the end of the line.
273281

274282
.. seealso:: :py:func:`lineAnchorPercent`
283+
284+
.. seealso:: :py:func:`setAnchorType`
285+
%End
286+
287+
AnchorType anchorType() const;
288+
%Docstring
289+
Returns the line anchor type, which dictates how the :py:func:`~QgsLabelLineSettings.lineAnchorPercent` setting is
290+
handled.
291+
292+
.. seealso:: :py:func:`setAnchorType`
293+
294+
.. seealso:: :py:func:`lineAnchorPercent`
295+
%End
296+
297+
void setAnchorType( AnchorType type );
298+
%Docstring
299+
Sets the line anchor ``type``, which dictates how the :py:func:`~QgsLabelLineSettings.lineAnchorPercent` setting is
300+
handled.
301+
302+
.. seealso:: :py:func:`anchorType`
303+
304+
.. seealso:: :py:func:`setLineAnchorPercent`
275305
%End
276306

277307
};

‎src/core/labeling/qgslabelfeature.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,7 @@ class CORE_EXPORT QgsLabelFeature
439439
* the end of the line.
440440
*
441441
* \see setLineAnchorPercent()
442+
* \see lineAnchorType()
442443
* \since QGIS 3.16
443444
*/
444445
double lineAnchorPercent() const { return mLineAnchorPercent; }
@@ -452,10 +453,30 @@ class CORE_EXPORT QgsLabelFeature
452453
* the end of the line.
453454
*
454455
* \see lineAnchorPercent()
456+
* \see setLineAnchorType()
455457
* \since QGIS 3.16
456458
*/
457459
void setLineAnchorPercent( double percent ) { mLineAnchorPercent = percent; }
458460

461+
462+
/**
463+
* Returns the line anchor type, which dictates how the lineAnchorPercent() setting is
464+
* handled.
465+
*
466+
* \see setLineAnchorType()
467+
* \see lineAnchorPercent()
468+
*/
469+
QgsLabelLineSettings::AnchorType lineAnchorType() const { return mLineAnchorType; }
470+
471+
/**
472+
* Sets the line anchor \a type, which dictates how the lineAnchorPercent() setting is
473+
* handled.
474+
*
475+
* \see lineAnchorType()
476+
* \see setLineAnchorPercent()
477+
*/
478+
void setLineAnchorType( QgsLabelLineSettings::AnchorType type ) { mLineAnchorType = type; }
479+
459480
/**
460481
* Returns TRUE if all parts of the feature should be labeled.
461482
* \see setLabelAllParts()
@@ -571,6 +592,7 @@ class CORE_EXPORT QgsLabelFeature
571592
QgsPointXY mAnchorPosition;
572593

573594
double mLineAnchorPercent = 0.5;
595+
QgsLabelLineSettings::AnchorType mLineAnchorType = QgsLabelLineSettings::AnchorType::HintOnly;
574596
};
575597

576598
#endif // QGSLABELFEATURE_H

‎src/core/labeling/qgslabellinesettings.h

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,15 @@ class CORE_EXPORT QgsLabelLineSettings
5050
SymbolBelow //!< Place direction symbols on below label
5151
};
5252

53+
/**
54+
* Line anchor types
55+
*/
56+
enum class AnchorType : int
57+
{
58+
HintOnly, //!< Line anchor is a hint for preferred placement only, but other placements close to the hint are permitted
59+
Strict, //!< Line anchor is a strict placement, and other placements are not permitted
60+
};
61+
5362
/**
5463
* Returns the line placement flags, which dictate how line labels can be placed
5564
* above or below the lines.
@@ -241,6 +250,7 @@ class CORE_EXPORT QgsLabelLineSettings
241250
* the end of the line.
242251
*
243252
* \see setLineAnchorPercent()
253+
* \see anchorType()
244254
*/
245255
double lineAnchorPercent() const { return mLineAnchorPercent; }
246256

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

270+
/**
271+
* Returns the line anchor type, which dictates how the lineAnchorPercent() setting is
272+
* handled.
273+
*
274+
* \see setAnchorType()
275+
* \see lineAnchorPercent()
276+
*/
277+
AnchorType anchorType() const { return mAnchorType; }
278+
279+
/**
280+
* Sets the line anchor \a type, which dictates how the lineAnchorPercent() setting is
281+
* handled.
282+
*
283+
* \see anchorType()
284+
* \see setLineAnchorPercent()
285+
*/
286+
void setAnchorType( AnchorType type ) { mAnchorType = type; }
287+
259288
private:
260289
QgsLabeling::LinePlacementFlags mPlacementFlags = QgsLabeling::LinePlacementFlag::AboveLine | QgsLabeling::LinePlacementFlag::MapOrientation;
261290
bool mMergeLines = false;
@@ -269,6 +298,7 @@ class CORE_EXPORT QgsLabelLineSettings
269298
QgsMapUnitScale mOverrunDistanceMapUnitScale;
270299

271300
double mLineAnchorPercent = 0.5;
301+
AnchorType mAnchorType = AnchorType::HintOnly;
272302
};
273303

274304
#endif // QGSLABELLINESETTINGS_H

‎src/core/labeling/qgspallabeling.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,7 @@ void QgsPalLayerSettings::readXml( const QDomElement &elem, const QgsReadWriteCo
10441044
mLineSettings.setOverrunDistanceUnit( QgsUnitTypes::decodeRenderUnit( placementElem.attribute( QStringLiteral( "overrunDistanceUnit" ) ) ) );
10451045
mLineSettings.setOverrunDistanceMapUnitScale( QgsSymbolLayerUtils::decodeMapUnitScale( placementElem.attribute( QStringLiteral( "overrunDistanceMapUnitScale" ) ) ) );
10461046
mLineSettings.setLineAnchorPercent( placementElem.attribute( QStringLiteral( "lineAnchorPercent" ), QStringLiteral( "0.5" ) ).toDouble() );
1047+
mLineSettings.setAnchorType( static_cast< QgsLabelLineSettings::AnchorType >( placementElem.attribute( QStringLiteral( "lineAnchorType" ), QStringLiteral( "0" ) ).toInt() ) );
10471048

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

11991201
placementElem.setAttribute( QStringLiteral( "geometryGenerator" ), geometryGenerator );
12001202
placementElem.setAttribute( QStringLiteral( "geometryGeneratorEnabled" ), geometryGeneratorEnabled );
@@ -2471,6 +2473,7 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
24712473
( *labelFeature )->setOverrunDistance( overrunDistanceEval );
24722474
( *labelFeature )->setOverrunSmoothDistance( overrunSmoothDist );
24732475
( *labelFeature )->setLineAnchorPercent( lineSettings.lineAnchorPercent() );
2476+
( *labelFeature )->setLineAnchorType( lineSettings.anchorType() );
24742477
( *labelFeature )->setLabelAllParts( labelAll );
24752478
if ( geom.type() == QgsWkbTypes::PointGeometry && !obstacleGeometry.isNull() )
24762479
{

‎src/core/pal/feature.cpp

Lines changed: 57 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,10 @@ std::size_t FeaturePart::createCandidatesAlongLine( std::vector< std::unique_ptr
728728
}
729729

730730
//prefer to label along straightish segments:
731-
std::size_t candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );
731+
std::size_t candidates = 0;
732+
733+
if ( mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::HintOnly )
734+
candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );
732735

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

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

772774
const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
775+
double currentDistanceAlongLine = lineStepDistance;
776+
switch ( mLF->lineAnchorType() )
777+
{
778+
case QgsLabelLineSettings::AnchorType::HintOnly:
779+
lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
780+
break;
781+
782+
case QgsLabelLineSettings::AnchorType::Strict:
783+
currentDistanceAlongLine = lineAnchorPoint;
784+
lineStepDistance = -1;
785+
break;
786+
}
773787

774788
double candidateCenterX, candidateCenterY;
775789
int i = 0;
776-
while ( currentDistanceAlongLine < totalLineLength )
790+
while ( currentDistanceAlongLine <= totalLineLength )
777791
{
778792
if ( pal->isCanceled() )
779793
{
@@ -1089,13 +1103,24 @@ std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std
10891103
currentDistanceAlongLine = std::numeric_limits< double >::max();
10901104
}
10911105

1092-
const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
1106+
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!
1107+
1108+
switch ( mLF->lineAnchorType() )
1109+
{
1110+
case QgsLabelLineSettings::AnchorType::HintOnly:
1111+
break;
1112+
1113+
case QgsLabelLineSettings::AnchorType::Strict:
1114+
currentDistanceAlongLine = std::min( lineAnchorPoint, totalLineLength * 0.99 - labelWidth );
1115+
lineStepDistance = -1;
1116+
break;
1117+
}
10931118

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

11991224

1200-
std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip )
1225+
std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip, bool applyAngleConstraints )
12011226
{
12021227
double offsetAlongSegment = offsetAlongLine;
12031228
int index = 1;
@@ -1309,10 +1334,10 @@ std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet
13091334
// normalise between -180 and 180
13101335
while ( angle_delta > M_PI ) angle_delta -= 2 * M_PI;
13111336
while ( angle_delta < -M_PI ) angle_delta += 2 * M_PI;
1312-
if ( ( li->max_char_angle_inside > 0 && angle_delta > 0
1313-
&& angle_delta > li->max_char_angle_inside * ( M_PI / 180 ) )
1314-
|| ( li->max_char_angle_outside < 0 && angle_delta < 0
1315-
&& angle_delta < li->max_char_angle_outside * ( M_PI / 180 ) ) )
1337+
if ( applyAngleConstraints && ( ( li->max_char_angle_inside > 0 && angle_delta > 0
1338+
&& angle_delta > li->max_char_angle_inside * ( M_PI / 180 ) )
1339+
|| ( li->max_char_angle_outside < 0 && angle_delta < 0
1340+
&& angle_delta < li->max_char_angle_outside * ( M_PI / 180 ) ) ) )
13161341
{
13171342
return nullptr;
13181343
}
@@ -1450,7 +1475,20 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
14501475
flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
14511476

14521477
// generate curved labels
1453-
for ( double distanceAlongLineToStartCandidate = 0; distanceAlongLineToStartCandidate < total_distance; distanceAlongLineToStartCandidate += delta )
1478+
double distanceAlongLineToStartCandidate = 0;
1479+
bool singleCandidateOnly = false;
1480+
switch ( mLF->lineAnchorType() )
1481+
{
1482+
case QgsLabelLineSettings::AnchorType::HintOnly:
1483+
break;
1484+
1485+
case QgsLabelLineSettings::AnchorType::Strict:
1486+
distanceAlongLineToStartCandidate = std::min( lineAnchorPoint, total_distance * 0.99 - getLabelWidth() );
1487+
singleCandidateOnly = true;
1488+
break;
1489+
}
1490+
1491+
for ( ; distanceAlongLineToStartCandidate <= total_distance; distanceAlongLineToStartCandidate += delta )
14541492
{
14551493
bool flip = false;
14561494
// 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
14681506
orientation = 1;
14691507
}
14701508

1471-
std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip );
1509+
std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly );
14721510
if ( !slp )
14731511
continue;
14741512

@@ -1479,7 +1517,7 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
14791517
if ( ( showUprightLabels() && !flip ) )
14801518
{
14811519
orientation = -orientation;
1482-
slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip );
1520+
slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly );
14831521
}
14841522
}
14851523
if ( !slp )
@@ -1505,7 +1543,9 @@ std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::uniq
15051543
tmp = tmp->nextPart();
15061544
}
15071545

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

15601602
for ( std::unique_ptr< LabelPosition > &pos : positions )

‎src/core/pal/feature.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,11 @@ namespace pal
243243
* \param distance distance to offset label along curve by
244244
* \param reversed if TRUE label is reversed from lefttoright to righttoleft
245245
* \param flip if TRUE label is placed on the other side of the line
246+
* \param applyAngleConstraints TRUE if label feature character angle constraints should be applied
246247
* \returns calculated label position
247248
*/
248249
std::unique_ptr< LabelPosition > curvedPlacementAtOffset( PointSet *path_positions, double *path_distances,
249-
int &orientation, double distance, bool &reversed, bool &flip );
250+
int &orientation, double distance, bool &reversed, bool &flip, bool applyAngleConstraints );
250251

251252
/**
252253
* Generate curved candidates for line features.

‎src/gui/labeling/qgslabelinggui.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ void QgsLabelingGui::showLineAnchorSettings()
195195
{
196196
const QgsLabelLineSettings widgetSettings = widget->settings();
197197
mLineSettings.setLineAnchorPercent( widgetSettings.lineAnchorPercent() );
198+
mLineSettings.setAnchorType( widgetSettings.anchorType() );
198199
const QgsPropertyCollection obstacleDataDefinedProperties = widget->dataDefinedProperties();
199200
widget->updateDataDefinedProperties( mDataDefinedProperties );
200201
emit widgetChanged();
@@ -536,6 +537,7 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
536537
lyr.setObstacleSettings( mObstacleSettings );
537538

538539
lyr.lineSettings().setLineAnchorPercent( mLineSettings.lineAnchorPercent() );
540+
lyr.lineSettings().setAnchorType( mLineSettings.anchorType() );
539541

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

‎src/gui/labeling/qgslabellineanchorwidget.cpp

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,21 +30,50 @@ QgsLabelLineAnchorWidget::QgsLabelLineAnchorWidget( QWidget *parent, QgsVectorLa
3030
mPercentPlacementComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabelAnchorEnd.svg" ) ), tr( "End of Line" ), 1.0 );
3131
mPercentPlacementComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabelAnchorCustom.svg" ) ), tr( "Custom…" ), -1.0 );
3232

33+
mAnchorTypeComboBox->addItem( tr( "Preferred Placement Hint" ), static_cast< int >( QgsLabelLineSettings::AnchorType::HintOnly ) );
34+
mAnchorTypeComboBox->addItem( tr( "Strict" ), static_cast< int >( QgsLabelLineSettings::AnchorType::Strict ) );
35+
3336
connect( mPercentPlacementComboBox, qgis::overload<int>::of( &QComboBox::currentIndexChanged ), this, [ = ]( int )
3437
{
3538
if ( !mBlockSignals )
3639
emit changed();
3740

38-
mCustomPlacementSpinBox->setEnabled( mPercentPlacementComboBox->currentData().toDouble() < 0 );
41+
if ( mPercentPlacementComboBox->currentData().toDouble() < 0 )
42+
mCustomPlacementSpinBox->setEnabled( true );
43+
else
44+
{
45+
mCustomPlacementSpinBox->setEnabled( false );
46+
mBlockSignals = true;
47+
mCustomPlacementSpinBox->setValue( mPercentPlacementComboBox->currentData().toDouble() * 100 );
48+
mBlockSignals = false;
49+
}
3950
} );
4051
connect( mCustomPlacementSpinBox, qgis::overload<double>::of( &QDoubleSpinBox::valueChanged ), this, [ = ]( double )
4152
{
4253
if ( !mBlockSignals )
4354
emit changed();
4455
} );
4556

46-
registerDataDefinedButton( mLinePlacementDDBtn, QgsPalLayerSettings::LineAnchorPercent );
57+
connect( mAnchorTypeComboBox, qgis::overload<int>::of( &QComboBox::currentIndexChanged ), this, [ = ]( int )
58+
{
59+
if ( !mBlockSignals )
60+
emit changed();
61+
62+
QString hint;
63+
switch ( static_cast< QgsLabelLineSettings::AnchorType >( mAnchorTypeComboBox->currentData().toInt() ) )
64+
{
65+
case QgsLabelLineSettings::AnchorType::Strict:
66+
hint = tr( "Labels are placed exactly on the label anchor only, and no other fallback placements are permitted." );
67+
break;
4768

69+
case QgsLabelLineSettings::AnchorType::HintOnly:
70+
hint = tr( "The label anchor is treated as a hint for the preferred label placement, but other placements close to the anchor point are permitted." );
71+
break;
72+
}
73+
mAnchorTypeHintLabel->setText( hint );
74+
} );
75+
76+
registerDataDefinedButton( mLinePlacementDDBtn, QgsPalLayerSettings::LineAnchorPercent );
4877
}
4978

5079
void QgsLabelLineAnchorWidget::setSettings( const QgsLabelLineSettings &settings )
@@ -62,6 +91,8 @@ void QgsLabelLineAnchorWidget::setSettings( const QgsLabelLineSettings &settings
6291
mCustomPlacementSpinBox->setValue( settings.lineAnchorPercent() * 100.0 );
6392
}
6493
mCustomPlacementSpinBox->setEnabled( mPercentPlacementComboBox->currentData().toDouble() < 0 );
94+
95+
mAnchorTypeComboBox->setCurrentIndex( mAnchorTypeComboBox->findData( static_cast< int >( settings.anchorType() ) ) );
6596
mBlockSignals = false;
6697
}
6798

@@ -77,6 +108,8 @@ QgsLabelLineSettings QgsLabelLineAnchorWidget::settings() const
77108
{
78109
settings.setLineAnchorPercent( mCustomPlacementSpinBox->value() / 100.0 );
79110
}
111+
112+
settings.setAnchorType( static_cast< QgsLabelLineSettings::AnchorType >( mAnchorTypeComboBox->currentData().toInt() ) );
80113
return settings;
81114
}
82115

‎src/ui/labeling/qgslabellineanchorwidgetbase.ui

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>244</width>
10-
<height>225</height>
9+
<width>271</width>
10+
<height>326</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
@@ -85,6 +85,28 @@
8585
</layout>
8686
</widget>
8787
</item>
88+
<item row="1" column="0" colspan="2">
89+
<widget class="QGroupBox" name="groupBox_2">
90+
<property name="title">
91+
<string>Placement Behavior</string>
92+
</property>
93+
<layout class="QGridLayout" name="gridLayout_3">
94+
<item row="0" column="0">
95+
<widget class="QComboBox" name="mAnchorTypeComboBox"/>
96+
</item>
97+
<item row="1" column="0">
98+
<widget class="QLabel" name="mAnchorTypeHintLabel">
99+
<property name="text">
100+
<string>Hint</string>
101+
</property>
102+
<property name="wordWrap">
103+
<bool>true</bool>
104+
</property>
105+
</widget>
106+
</item>
107+
</layout>
108+
</widget>
109+
</item>
88110
</layout>
89111
</widget>
90112
<customwidgets>

‎tests/src/core/testqgslabelingengine.cpp

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,11 @@ class TestQgsLabelingEngine : public QObject
8484
void testReferencedFields();
8585
void testClipping();
8686
void testLineAnchorParallel();
87+
void testLineAnchorParallelConstraints();
8788
void testLineAnchorCurved();
89+
void testLineAnchorCurvedConstraints();
8890
void testLineAnchorHorizontal();
91+
void testLineAnchorHorizontalConstraints();
8992

9093
private:
9194
QgsVectorLayer *vl = nullptr;
@@ -2826,6 +2829,89 @@ void TestQgsLabelingEngine::testLineAnchorParallel()
28262829
QVERIFY( imageCheck( QStringLiteral( "parallel_anchor_end" ), img, 20 ) );
28272830
}
28282831

2832+
void TestQgsLabelingEngine::testLineAnchorParallelConstraints()
2833+
{
2834+
// test line label anchor with parallel labels
2835+
QgsPalLayerSettings settings;
2836+
setDefaultLabelParams( settings );
2837+
2838+
QgsTextFormat format = settings.format();
2839+
format.setSize( 20 );
2840+
format.setColor( QColor( 0, 0, 0 ) );
2841+
settings.setFormat( format );
2842+
2843+
settings.fieldName = QStringLiteral( "'XXXXXXXX'" );
2844+
settings.isExpression = true;
2845+
settings.placement = QgsPalLayerSettings::Line;
2846+
settings.lineSettings().setPlacementFlags( QgsLabeling::LinePlacementFlag::AboveLine );
2847+
settings.labelPerPart = false;
2848+
settings.lineSettings().setLineAnchorPercent( 0.0 );
2849+
settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::HintOnly );
2850+
2851+
std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
2852+
vl2->setRenderer( new QgsNullSymbolRenderer() );
2853+
2854+
QgsFeature f;
2855+
f.setAttributes( QgsAttributes() << 1 );
2856+
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000000, 190010 5000010, 190190 5000010, 190200 5000000)" ) ) );
2857+
QVERIFY( vl2->dataProvider()->addFeature( f ) );
2858+
2859+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary!
2860+
vl2->setLabelsEnabled( true );
2861+
2862+
// make a fake render context
2863+
QSize size( 640, 480 );
2864+
QgsMapSettings mapSettings;
2865+
mapSettings.setLabelingEngineSettings( createLabelEngineSettings() );
2866+
mapSettings.setDestinationCrs( vl2->crs() );
2867+
2868+
mapSettings.setOutputSize( size );
2869+
mapSettings.setExtent( f.geometry().boundingBox().buffered( 10 ) );
2870+
mapSettings.setLayers( QList<QgsMapLayer *>() << vl2.get() );
2871+
mapSettings.setOutputDpi( 96 );
2872+
2873+
QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings();
2874+
engineSettings.setFlag( QgsLabelingEngineSettings::UsePartialCandidates, false );
2875+
engineSettings.setFlag( QgsLabelingEngineSettings::DrawLabelRectOnly, true );
2876+
// engineSettings.setFlag( QgsLabelingEngineSettings::DrawCandidates, true );
2877+
mapSettings.setLabelingEngineSettings( engineSettings );
2878+
2879+
QgsMapRendererSequentialJob job( mapSettings );
2880+
job.start();
2881+
job.waitForFinished();
2882+
2883+
QImage img = job.renderedImage();
2884+
QVERIFY( imageCheck( QStringLiteral( "parallel_hint_anchor_start" ), img, 20 ) );
2885+
2886+
settings.lineSettings().setLineAnchorPercent( 1.0 );
2887+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
2888+
QgsMapRendererSequentialJob job2( mapSettings );
2889+
job2.start();
2890+
job2.waitForFinished();
2891+
2892+
img = job2.renderedImage();
2893+
QVERIFY( imageCheck( QStringLiteral( "parallel_hint_anchor_end" ), img, 20 ) );
2894+
2895+
settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::Strict );
2896+
settings.lineSettings().setLineAnchorPercent( 0.0 );
2897+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
2898+
QgsMapRendererSequentialJob job3( mapSettings );
2899+
job3.start();
2900+
job3.waitForFinished();
2901+
2902+
img = job3.renderedImage();
2903+
QVERIFY( imageCheck( QStringLiteral( "parallel_strict_anchor_start" ), img, 20 ) );
2904+
2905+
settings.lineSettings().setLineAnchorPercent( 1.0 );
2906+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
2907+
QgsMapRendererSequentialJob job4( mapSettings );
2908+
job4.start();
2909+
job4.waitForFinished();
2910+
2911+
img = job4.renderedImage();
2912+
QVERIFY( imageCheck( QStringLiteral( "parallel_strict_anchor_end" ), img, 20 ) );
2913+
}
2914+
28292915
void TestQgsLabelingEngine::testLineAnchorCurved()
28302916
{
28312917
// test line label anchor with curved labels
@@ -2889,6 +2975,89 @@ void TestQgsLabelingEngine::testLineAnchorCurved()
28892975
QVERIFY( imageCheck( QStringLiteral( "curved_anchor_end" ), img, 20 ) );
28902976
}
28912977

2978+
void TestQgsLabelingEngine::testLineAnchorCurvedConstraints()
2979+
{
2980+
// test line label anchor with parallel labels
2981+
QgsPalLayerSettings settings;
2982+
setDefaultLabelParams( settings );
2983+
2984+
QgsTextFormat format = settings.format();
2985+
format.setSize( 20 );
2986+
format.setColor( QColor( 0, 0, 0 ) );
2987+
settings.setFormat( format );
2988+
2989+
settings.fieldName = QStringLiteral( "'XXXXXXXX'" );
2990+
settings.isExpression = true;
2991+
settings.placement = QgsPalLayerSettings::Curved;
2992+
settings.lineSettings().setPlacementFlags( QgsLabeling::LinePlacementFlag::AboveLine );
2993+
settings.labelPerPart = false;
2994+
settings.lineSettings().setLineAnchorPercent( 0.0 );
2995+
settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::HintOnly );
2996+
2997+
std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=id:integer" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
2998+
vl2->setRenderer( new QgsNullSymbolRenderer() );
2999+
3000+
QgsFeature f;
3001+
f.setAttributes( QgsAttributes() << 1 );
3002+
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190000 5000000, 190010 5000010, 190190 5000010, 190200 5000000)" ) ) );
3003+
QVERIFY( vl2->dataProvider()->addFeature( f ) );
3004+
3005+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary!
3006+
vl2->setLabelsEnabled( true );
3007+
3008+
// make a fake render context
3009+
QSize size( 640, 480 );
3010+
QgsMapSettings mapSettings;
3011+
mapSettings.setLabelingEngineSettings( createLabelEngineSettings() );
3012+
mapSettings.setDestinationCrs( vl2->crs() );
3013+
3014+
mapSettings.setOutputSize( size );
3015+
mapSettings.setExtent( f.geometry().boundingBox().buffered( 10 ) );
3016+
mapSettings.setLayers( QList<QgsMapLayer *>() << vl2.get() );
3017+
mapSettings.setOutputDpi( 96 );
3018+
3019+
QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings();
3020+
engineSettings.setFlag( QgsLabelingEngineSettings::UsePartialCandidates, false );
3021+
engineSettings.setFlag( QgsLabelingEngineSettings::DrawLabelRectOnly, true );
3022+
// engineSettings.setFlag( QgsLabelingEngineSettings::DrawCandidates, true );
3023+
mapSettings.setLabelingEngineSettings( engineSettings );
3024+
3025+
QgsMapRendererSequentialJob job( mapSettings );
3026+
job.start();
3027+
job.waitForFinished();
3028+
3029+
QImage img = job.renderedImage();
3030+
QVERIFY( imageCheck( QStringLiteral( "curved_hint_anchor_start" ), img, 20 ) );
3031+
3032+
settings.lineSettings().setLineAnchorPercent( 1.0 );
3033+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
3034+
QgsMapRendererSequentialJob job2( mapSettings );
3035+
job2.start();
3036+
job2.waitForFinished();
3037+
3038+
img = job2.renderedImage();
3039+
QVERIFY( imageCheck( QStringLiteral( "curved_hint_anchor_end" ), img, 20 ) );
3040+
3041+
settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::Strict );
3042+
settings.lineSettings().setLineAnchorPercent( 0.0 );
3043+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
3044+
QgsMapRendererSequentialJob job3( mapSettings );
3045+
job3.start();
3046+
job3.waitForFinished();
3047+
3048+
img = job3.renderedImage();
3049+
QVERIFY( imageCheck( QStringLiteral( "curved_strict_anchor_start" ), img, 20 ) );
3050+
3051+
settings.lineSettings().setLineAnchorPercent( 1.0 );
3052+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
3053+
QgsMapRendererSequentialJob job4( mapSettings );
3054+
job4.start();
3055+
job4.waitForFinished();
3056+
3057+
img = job4.renderedImage();
3058+
QVERIFY( imageCheck( QStringLiteral( "curved_strict_anchor_end" ), img, 20 ) );
3059+
}
3060+
28923061
void TestQgsLabelingEngine::testLineAnchorHorizontal()
28933062
{
28943063
// test line label anchor with horizontal labels
@@ -2952,5 +3121,96 @@ void TestQgsLabelingEngine::testLineAnchorHorizontal()
29523121
QVERIFY( imageCheck( QStringLiteral( "horizontal_anchor_end" ), img, 20 ) );
29533122
}
29543123

3124+
void TestQgsLabelingEngine::testLineAnchorHorizontalConstraints()
3125+
{
3126+
// test line label anchor with parallel labels
3127+
QgsPalLayerSettings settings;
3128+
setDefaultLabelParams( settings );
3129+
3130+
QgsTextFormat format = settings.format();
3131+
format.setSize( 20 );
3132+
format.setColor( QColor( 0, 0, 0 ) );
3133+
settings.setFormat( format );
3134+
3135+
settings.fieldName = QStringLiteral( "l" );
3136+
settings.isExpression = false;
3137+
settings.placement = QgsPalLayerSettings::Horizontal;
3138+
settings.lineSettings().setPlacementFlags( QgsLabeling::LinePlacementFlag::AboveLine );
3139+
settings.labelPerPart = false;
3140+
settings.lineSettings().setLineAnchorPercent( 0.0 );
3141+
settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::HintOnly );
3142+
3143+
std::unique_ptr< QgsVectorLayer> vl2( new QgsVectorLayer( QStringLiteral( "LineString?crs=epsg:3946&field=l:string" ), QStringLiteral( "vl" ), QStringLiteral( "memory" ) ) );
3144+
vl2->setRenderer( new QgsNullSymbolRenderer() );
3145+
3146+
QgsFeature f;
3147+
f.setAttributes( QgsAttributes() << QVariant() );
3148+
3149+
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190030 5000000, 190030 5000010)" ) ) );
3150+
QVERIFY( vl2->dataProvider()->addFeature( f ) );
3151+
3152+
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190170 5000000, 190170 5000010)" ) ) );
3153+
QVERIFY( vl2->dataProvider()->addFeature( f ) );
3154+
3155+
f.setAttributes( QgsAttributes() << QStringLiteral( "XXXXXXXX" ) );
3156+
f.setGeometry( QgsGeometry::fromWkt( QStringLiteral( "LineString (190020 5000000, 190030 5000010, 190170 5000010, 190190 5000000)" ) ) );
3157+
QVERIFY( vl2->dataProvider()->addFeature( f ) );
3158+
3159+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) ); // TODO: this should not be necessary!
3160+
vl2->setLabelsEnabled( true );
3161+
3162+
// make a fake render context
3163+
QSize size( 640, 480 );
3164+
QgsMapSettings mapSettings;
3165+
mapSettings.setLabelingEngineSettings( createLabelEngineSettings() );
3166+
mapSettings.setDestinationCrs( vl2->crs() );
3167+
3168+
mapSettings.setOutputSize( size );
3169+
mapSettings.setExtent( f.geometry().boundingBox().buffered( 40 ) );
3170+
mapSettings.setLayers( QList<QgsMapLayer *>() << vl2.get() );
3171+
mapSettings.setOutputDpi( 96 );
3172+
3173+
QgsLabelingEngineSettings engineSettings = mapSettings.labelingEngineSettings();
3174+
engineSettings.setFlag( QgsLabelingEngineSettings::UsePartialCandidates, false );
3175+
engineSettings.setFlag( QgsLabelingEngineSettings::DrawLabelRectOnly, true );
3176+
// engineSettings.setFlag( QgsLabelingEngineSettings::DrawCandidates, true );
3177+
mapSettings.setLabelingEngineSettings( engineSettings );
3178+
3179+
QgsMapRendererSequentialJob job( mapSettings );
3180+
job.start();
3181+
job.waitForFinished();
3182+
3183+
QImage img = job.renderedImage();
3184+
QVERIFY( imageCheck( QStringLiteral( "horizontal_hint_anchor_start" ), img, 20 ) );
3185+
3186+
settings.lineSettings().setLineAnchorPercent( 1.0 );
3187+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
3188+
QgsMapRendererSequentialJob job2( mapSettings );
3189+
job2.start();
3190+
job2.waitForFinished();
3191+
3192+
img = job2.renderedImage();
3193+
QVERIFY( imageCheck( QStringLiteral( "horizontal_hint_anchor_end" ), img, 20 ) );
3194+
3195+
settings.lineSettings().setAnchorType( QgsLabelLineSettings::AnchorType::Strict );
3196+
settings.lineSettings().setLineAnchorPercent( 0.0 );
3197+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
3198+
QgsMapRendererSequentialJob job3( mapSettings );
3199+
job3.start();
3200+
job3.waitForFinished();
3201+
3202+
img = job3.renderedImage();
3203+
QVERIFY( imageCheck( QStringLiteral( "horizontal_strict_anchor_start" ), img, 20 ) );
3204+
3205+
settings.lineSettings().setLineAnchorPercent( 1.0 );
3206+
vl2->setLabeling( new QgsVectorLayerSimpleLabeling( settings ) );
3207+
QgsMapRendererSequentialJob job4( mapSettings );
3208+
job4.start();
3209+
job4.waitForFinished();
3210+
3211+
img = job4.renderedImage();
3212+
QVERIFY( imageCheck( QStringLiteral( "horizontal_strict_anchor_end" ), img, 20 ) );
3213+
}
3214+
29553215
QGSTEST_MAIN( TestQgsLabelingEngine )
29563216
#include "testqgslabelingengine.moc"

0 commit comments

Comments
 (0)
Please sign in to comment.