Skip to content
Permalink
Browse files

[feature][labelling] Add an option to control how line features are

clipped before their anchor points are calculated

The previous behavior was to always use only the visible part
of the line when calculating the anchor point for labels. Now,
users have the option to choose "Use Entire Line" when setting
the label anchor point for lines, so that the entire line
feature is considered when calculating the point to place
the label instead of just the visible part of the line.

Sponsored by Stadt Zürich
  • Loading branch information
nyalldawson committed Apr 9, 2021
1 parent c9e5875 commit 7126faf27c623b3a4463315e2adf0144fd0d1abc
@@ -10,3 +10,8 @@
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__
# --
# monkey patching scoped based enum
QgsLabelLineSettings.AnchorClipping.UseVisiblePartsOfLine.__doc__ = "Only visible parts of lines are considered when calculating the line anchor for labels"
QgsLabelLineSettings.AnchorClipping.UseEntireLine.__doc__ = "Entire original feature line geometry is used when calculating the line anchor for labels"
QgsLabelLineSettings.AnchorClipping.__doc__ = 'Clipping behavior for line anchor calculation.\n\n.. versionadded:: 3.20\n\n' + '* ``UseVisiblePartsOfLine``: ' + QgsLabelLineSettings.AnchorClipping.UseVisiblePartsOfLine.__doc__ + '\n' + '* ``UseEntireLine``: ' + QgsLabelLineSettings.AnchorClipping.UseEntireLine.__doc__
# --
@@ -38,6 +38,12 @@ a "perimeter" style mode).
Strict,
};

enum class AnchorClipping
{
UseVisiblePartsOfLine,
UseEntireLine,
};

QgsLabeling::LinePlacementFlags placementFlags() const;
%Docstring
Returns the line placement flags, which dictate how line labels can be placed
@@ -268,6 +274,8 @@ the end of the line.
.. seealso:: :py:func:`setLineAnchorPercent`

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

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

void setLineAnchorPercent( double percent );
@@ -282,6 +290,8 @@ the end of the line.
.. seealso:: :py:func:`lineAnchorPercent`

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

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

AnchorType anchorType() const;
@@ -292,6 +302,8 @@ handled.
.. seealso:: :py:func:`setAnchorType`

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

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

void setAnchorType( AnchorType type );
@@ -302,6 +314,36 @@ handled.
.. seealso:: :py:func:`anchorType`

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

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

AnchorClipping anchorClipping() const;
%Docstring
Returns the line anchor clipping mode, which dictates how line strings are clipped
before calculating the line anchor placement.

.. seealso:: :py:func:`setAnchorClipping`

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

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

.. versionadded:: 3.20
%End

void setAnchorClipping( AnchorClipping clipping );
%Docstring
Sets the line anchor ``clipping`` mode, which dictates how line strings are clipped
before calculating the line anchor placement.

.. seealso:: :py:func:`anchorClipping`

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

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

.. versionadded:: 3.20
%End

};
@@ -231,6 +231,7 @@ Contains settings for how a map layer will be labeled.
LabelAllParts,
PolygonLabelOutside,
LineAnchorPercent,
LineAnchorClipping,

// rendering
ScaleVisibility,
@@ -43,4 +43,17 @@ void QgsLabelLineSettings::updateDataDefinedProperties( const QgsPropertyCollect
context.setOriginalValueVariable( mLineAnchorPercent );
mLineAnchorPercent = properties.valueAsDouble( QgsPalLayerSettings::LineAnchorPercent, context, mLineAnchorPercent );
}

if ( properties.isActive( QgsPalLayerSettings::LineAnchorClipping ) )
{
bool ok = false;
const QString value = properties.valueAsString( QgsPalLayerSettings::LineAnchorClipping, context, QString(), &ok ).trimmed();
if ( ok )
{
if ( value.compare( QLatin1String( "visible" ), Qt::CaseInsensitive ) == 0 )
mAnchorClipping = AnchorClipping::UseVisiblePartsOfLine;
else if ( value.compare( QLatin1String( "entire" ), Qt::CaseInsensitive ) == 0 )
mAnchorClipping = AnchorClipping::UseEntireLine;
}
}
}
@@ -59,6 +59,17 @@ class CORE_EXPORT QgsLabelLineSettings
Strict, //!< Line anchor is a strict placement, and other placements are not permitted
};

/**
* Clipping behavior for line anchor calculation.
*
* \since QGIS 3.20
*/
enum class AnchorClipping : int
{
UseVisiblePartsOfLine, //!< Only visible parts of lines are considered when calculating the line anchor for labels
UseEntireLine, //!< Entire original feature line geometry is used when calculating the line anchor for labels
};

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

@@ -264,6 +276,7 @@ class CORE_EXPORT QgsLabelLineSettings
*
* \see lineAnchorPercent()
* \see setAnchorType()
* \see setAnchorClipping()
*/
void setLineAnchorPercent( double percent ) { mLineAnchorPercent = percent; }

@@ -273,6 +286,7 @@ class CORE_EXPORT QgsLabelLineSettings
*
* \see setAnchorType()
* \see lineAnchorPercent()
* \see anchorClipping()
*/
AnchorType anchorType() const { return mAnchorType; }

@@ -282,9 +296,34 @@ class CORE_EXPORT QgsLabelLineSettings
*
* \see anchorType()
* \see setLineAnchorPercent()
* \see setAnchorClipping()
*/
void setAnchorType( AnchorType type ) { mAnchorType = type; }

/**
* Returns the line anchor clipping mode, which dictates how line strings are clipped
* before calculating the line anchor placement.
*
* \see setAnchorClipping()
* \see anchorType()
* \see lineAnchorPercent()
*
* \since QGIS 3.20
*/
AnchorClipping anchorClipping() const { return mAnchorClipping; }

/**
* Sets the line anchor \a clipping mode, which dictates how line strings are clipped
* before calculating the line anchor placement.
*
* \see anchorClipping()
* \see setAnchorType()
* \see setLineAnchorPercent()
*
* \since QGIS 3.20
*/
void setAnchorClipping( AnchorClipping clipping ) { mAnchorClipping = clipping; }

private:
QgsLabeling::LinePlacementFlags mPlacementFlags = QgsLabeling::LinePlacementFlag::AboveLine | QgsLabeling::LinePlacementFlag::MapOrientation;
bool mMergeLines = false;
@@ -299,6 +338,7 @@ class CORE_EXPORT QgsLabelLineSettings

double mLineAnchorPercent = 0.5;
AnchorType mAnchorType = AnchorType::HintOnly;
AnchorClipping mAnchorClipping = AnchorClipping::UseVisiblePartsOfLine;
};

#endif // QGSLABELLINESETTINGS_H
@@ -222,6 +222,7 @@ void QgsPalLayerSettings::initPropertyDefinitions()
{ 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::LineAnchorClipping, QgsPropertyDefinition( "LineAnchorClipping", QgsPropertyDefinition::DataTypeString, QObject::tr( "Line anchor clipping mode" ), QObject::tr( "string " ) + QStringLiteral( "[<b>visible</b>|<b>entire</b>]" ), 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 ) },
@@ -1052,6 +1053,7 @@ void QgsPalLayerSettings::readXml( const QDomElement &elem, const QgsReadWriteCo
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() ) );
mLineSettings.setAnchorClipping( static_cast< QgsLabelLineSettings::AnchorClipping >( placementElem.attribute( QStringLiteral( "lineAnchorClipping" ), QStringLiteral( "0" ) ).toInt() ) );

geometryGenerator = placementElem.attribute( QStringLiteral( "geometryGenerator" ) );
geometryGeneratorEnabled = placementElem.attribute( QStringLiteral( "geometryGeneratorEnabled" ) ).toInt();
@@ -1204,6 +1206,7 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument &doc, const QgsReadWrite
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( "lineAnchorClipping" ), static_cast< int >( mLineSettings.anchorClipping() ) );

placementElem.setAttribute( QStringLiteral( "geometryGenerator" ), geometryGenerator );
placementElem.setAttribute( QStringLiteral( "geometryGeneratorEnabled" ), geometryGeneratorEnabled );
@@ -2062,6 +2065,19 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
QgsLabelLineSettings lineSettings = mLineSettings;
lineSettings.updateDataDefinedProperties( mDataDefinedProperties, context.expressionContext() );

if ( geom.type() == QgsWkbTypes::LineGeometry )
{
switch ( lineSettings.anchorClipping() )
{
case QgsLabelLineSettings::AnchorClipping::UseVisiblePartsOfLine:
break;

case QgsLabelLineSettings::AnchorClipping::UseEntireLine:
doClip = false;
break;
}
}

// if using fitInPolygonOnly option, generate the permissible zone (must happen before geometry is modified - e.g.,
// as a result of using perimeter based labeling and the geometry is converted to a boundary)
// note that we also force this if we are permitting labels to be placed outside of polygons too!
@@ -336,6 +336,7 @@ class CORE_EXPORT QgsPalLayerSettings
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)
LineAnchorClipping = 112, //!< Clipping mode for line anchor calculation (since QGIS 3.20)

// rendering
ScaleVisibility = 23,
@@ -199,6 +199,7 @@ void QgsLabelingGui::showLineAnchorSettings()
const QgsLabelLineSettings widgetSettings = widget->settings();
mLineSettings.setLineAnchorPercent( widgetSettings.lineAnchorPercent() );
mLineSettings.setAnchorType( widgetSettings.anchorType() );
mLineSettings.setAnchorClipping( widgetSettings.anchorClipping() );
const QgsPropertyCollection obstacleDataDefinedProperties = widget->dataDefinedProperties();
widget->updateDataDefinedProperties( mDataDefinedProperties );
emit widgetChanged();
@@ -553,6 +554,7 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()

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

lyr.labelPerPart = chkLabelPerFeaturePart->isChecked();
lyr.displayAll = mPalShowAllLabelsForLayerChkBx->isChecked();
@@ -30,6 +30,9 @@ QgsLabelLineAnchorWidget::QgsLabelLineAnchorWidget( QWidget *parent, QgsVectorLa
mPercentPlacementComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabelAnchorEnd.svg" ) ), tr( "End of Line" ), 1.0 );
mPercentPlacementComboBox->addItem( QgsApplication::getThemeIcon( QStringLiteral( "/mActionLabelAnchorCustom.svg" ) ), tr( "Custom…" ), -1.0 );

mClippingComboBox->addItem( tr( "Use Visible Part of Line" ), static_cast< int >( QgsLabelLineSettings::AnchorClipping::UseVisiblePartsOfLine ) );
mClippingComboBox->addItem( tr( "Use Entire Line" ), static_cast< int >( QgsLabelLineSettings::AnchorClipping::UseEntireLine ) );

mAnchorTypeComboBox->addItem( tr( "Preferred Placement Hint" ), static_cast< int >( QgsLabelLineSettings::AnchorType::HintOnly ) );
mAnchorTypeComboBox->addItem( tr( "Strict" ), static_cast< int >( QgsLabelLineSettings::AnchorType::Strict ) );

@@ -62,7 +65,14 @@ QgsLabelLineAnchorWidget::QgsLabelLineAnchorWidget( QWidget *parent, QgsVectorLa
updateAnchorTypeHint();
} );

connect( mClippingComboBox, qOverload<int>( &QComboBox::currentIndexChanged ), this, [ = ]( int )
{
if ( !mBlockSignals )
emit changed();
} );

registerDataDefinedButton( mLinePlacementDDBtn, QgsPalLayerSettings::LineAnchorPercent );
registerDataDefinedButton( mLineClippingDDBtn, QgsPalLayerSettings::LineAnchorClipping );
updateAnchorTypeHint();
}

@@ -83,6 +93,7 @@ void QgsLabelLineAnchorWidget::setSettings( const QgsLabelLineSettings &settings
mCustomPlacementSpinBox->setEnabled( mPercentPlacementComboBox->currentData().toDouble() < 0 );

mAnchorTypeComboBox->setCurrentIndex( mAnchorTypeComboBox->findData( static_cast< int >( settings.anchorType() ) ) );
mClippingComboBox->setCurrentIndex( mClippingComboBox->findData( static_cast< int >( settings.anchorClipping() ) ) );
mBlockSignals = false;
}

@@ -100,12 +111,14 @@ QgsLabelLineSettings QgsLabelLineAnchorWidget::settings() const
}

settings.setAnchorType( static_cast< QgsLabelLineSettings::AnchorType >( mAnchorTypeComboBox->currentData().toInt() ) );
settings.setAnchorClipping( static_cast< QgsLabelLineSettings::AnchorClipping >( mClippingComboBox->currentData().toInt() ) );
return settings;
}

void QgsLabelLineAnchorWidget::updateDataDefinedProperties( QgsPropertyCollection &properties )
{
properties.setProperty( QgsPalLayerSettings::LineAnchorPercent, mDataDefinedProperties.property( QgsPalLayerSettings::LineAnchorPercent ) );
properties.setProperty( QgsPalLayerSettings::LineAnchorClipping, mDataDefinedProperties.property( QgsPalLayerSettings::LineAnchorClipping ) );
}

void QgsLabelLineAnchorWidget::updateAnchorTypeHint()
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>271</width>
<height>326</height>
<height>315</height>
</rect>
</property>
<property name="windowTitle">
@@ -26,26 +26,23 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Label Anchoring</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_4" columnstretch="0,1,0">
<item row="2" column="1">
<widget class="QComboBox" name="mClippingComboBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Clipping</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;b&gt;Controls the position along the line which labels will be placed close to.&lt;/b&gt;</string>
@@ -55,7 +52,14 @@
</property>
</widget>
</item>
<item>
<item row="2" column="2">
<widget class="QgsPropertyOverrideButton" name="mLineClippingDDBtn">
<property name="text">
<string>…</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="3">
<layout class="QGridLayout" name="gridLayout_2">
<property name="topMargin">
<number>10</number>
@@ -107,6 +111,19 @@
</layout>
</widget>
</item>
<item row="2" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<customwidgets>

0 comments on commit 7126faf

Please sign in to comment.