Skip to content

Commit

Permalink
[FEATURE][labeling] Add option to allow polygon labels to be placed
Browse files Browse the repository at this point in the history
outside of polygon features when required

When a label can't be placed inside the polygon and this option is checked,
then it will be automatically placed at a nicely selected location
just outside of the polygon

Sponsored by QGIS Swiss user group!
  • Loading branch information
nyalldawson committed May 3, 2020
1 parent 10bc703 commit 51820ba
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 40 deletions.
4 changes: 4 additions & 0 deletions python/core/auto_additions/qgslabeling.py
Expand Up @@ -6,3 +6,7 @@
QgsLabeling.LinePlacementFlag.MapOrientation.__doc__ = "Signifies that the AboveLine and BelowLine flags should respect the map's orientation rather than the feature's orientation. For example, AboveLine will always result in label's being placed above a line, regardless of the line's direction."
QgsLabeling.LinePlacementFlag.__doc__ = 'Line placement flags, which control how candidates are generated for a linear feature.\n\n' + '* ``OnLine``: ' + QgsLabeling.LinePlacementFlag.OnLine.__doc__ + '\n' + '* ``AboveLine``: ' + QgsLabeling.LinePlacementFlag.AboveLine.__doc__ + '\n' + '* ``BelowLine``: ' + QgsLabeling.LinePlacementFlag.BelowLine.__doc__ + '\n' + '* ``MapOrientation``: ' + QgsLabeling.LinePlacementFlag.MapOrientation.__doc__
# --
# monkey patching scoped based enum
QgsLabeling.PolygonPlacementFlag.AllowPlacementOutsideOfPolygon.__doc__ = "Labels can be placed outside of a polygon feature if it was not possible to place them inside."
QgsLabeling.PolygonPlacementFlag.__doc__ = 'Polygon placement flags, which control how candidates are generated for a polygon feature.\n\n.. versionadded:: 3.14\n\n' + '* ``AllowPlacementOutsideOfPolygon``: ' + QgsLabeling.PolygonPlacementFlag.AllowPlacementOutsideOfPolygon.__doc__
# --
7 changes: 7 additions & 0 deletions python/core/auto_generated/labeling/qgslabeling.sip.in
Expand Up @@ -32,6 +32,13 @@ Contains constants and enums relating to labeling.
typedef QFlags<QgsLabeling::LinePlacementFlag> LinePlacementFlags;


enum class PolygonPlacementFlag
{
AllowPlacementOutsideOfPolygon,
};
typedef QFlags<QgsLabeling::PolygonPlacementFlag> PolygonPlacementFlags;


};

QFlags<QgsLabeling::LinePlacementFlag> operator|(QgsLabeling::LinePlacementFlag f1, QFlags<QgsLabeling::LinePlacementFlag> f2);
Expand Down
14 changes: 14 additions & 0 deletions python/core/auto_generated/labeling/qgspallabeling.sip.in
Expand Up @@ -401,6 +401,20 @@ Returns the QgsExpression for this label settings. May be ``None`` if isExpressi

unsigned int placementFlags;

QgsLabeling::PolygonPlacementFlags polygonPlacementFlags() const;
%Docstring
Returns the polygon placement flags, which dictate how polygon labels can be placed.

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

void setPolygonPlacementFlags( QgsLabeling::PolygonPlacementFlags flags );
%Docstring
Sets the polygon placement ``flags``, which dictate how polygon labels can be placed.

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

bool centroidWhole;

bool centroidInside;
Expand Down
14 changes: 14 additions & 0 deletions src/core/labeling/qgslabelfeature.h
Expand Up @@ -318,6 +318,19 @@ class CORE_EXPORT QgsLabelFeature
*/
void setArrangementFlags( QgsLabeling::LinePlacementFlags flags ) { mArrangementFlags = flags; }

/**
* Returns the polygon placement flags, which dictate how polygon labels can be placed.
*
* \see setPolygonPlacementFlags()
*/
QgsLabeling::PolygonPlacementFlags polygonPlacementFlags() const { return mPolygonPlacementFlags; }

/**
* Sets the polygon placement \a flags, which dictate how polygon labels can be placed.
*
* \see polygonPlacementFlags()
*/
void setPolygonPlacementFlags( QgsLabeling::PolygonPlacementFlags flags ) { mPolygonPlacementFlags = flags; }

/**
* Text of the label
Expand Down Expand Up @@ -509,6 +522,7 @@ class CORE_EXPORT QgsLabelFeature
double mOverrunSmoothDistance = 0;

QgsLabeling::LinePlacementFlags mArrangementFlags = nullptr;
QgsLabeling::PolygonPlacementFlags mPolygonPlacementFlags = nullptr;

private:

Expand Down
11 changes: 11 additions & 0 deletions src/core/labeling/qgslabeling.h
Expand Up @@ -44,6 +44,17 @@ class CORE_EXPORT QgsLabeling
};
Q_DECLARE_FLAGS( LinePlacementFlags, LinePlacementFlag )

/**
* Polygon placement flags, which control how candidates are generated for a polygon feature.
*
* \since QGIS 3.14
*/
enum class PolygonPlacementFlag : int
{
AllowPlacementOutsideOfPolygon = 1 << 0, //!< Labels can be placed outside of a polygon feature if it was not possible to place them inside.
};
Q_DECLARE_FLAGS( PolygonPlacementFlags, PolygonPlacementFlag )

};

Q_DECLARE_OPERATORS_FOR_FLAGS( QgsLabeling::LinePlacementFlags )
Expand Down
23 changes: 22 additions & 1 deletion src/core/labeling/qgspallabeling.cpp
Expand Up @@ -307,6 +307,7 @@ QgsPalLayerSettings &QgsPalLayerSettings::operator=( const QgsPalLayerSettings &
// placement
placement = s.placement;
placementFlags = s.placementFlags;
mPolygonPlacementFlags = s.mPolygonPlacementFlags;
centroidWhole = s.centroidWhole;
centroidInside = s.centroidInside;
predefinedPositionOrder = s.predefinedPositionOrder;
Expand Down Expand Up @@ -933,6 +934,8 @@ void QgsPalLayerSettings::readXml( const QDomElement &elem, const QgsReadWriteCo
QDomElement placementElem = elem.firstChildElement( QStringLiteral( "placement" ) );
placement = static_cast< Placement >( placementElem.attribute( QStringLiteral( "placement" ) ).toInt() );
placementFlags = placementElem.attribute( QStringLiteral( "placementFlags" ) ).toUInt();
mPolygonPlacementFlags = static_cast< QgsLabeling::PolygonPlacementFlags >( placementElem.attribute( QStringLiteral( "polygonPlacementFlags" ) ).toInt() );

centroidWhole = placementElem.attribute( QStringLiteral( "centroidWhole" ), QStringLiteral( "0" ) ).toInt();
centroidInside = placementElem.attribute( QStringLiteral( "centroidInside" ), QStringLiteral( "0" ) ).toInt();
predefinedPositionOrder = QgsLabelingUtils::decodePredefinedPositionOrder( placementElem.attribute( QStringLiteral( "predefinedPositionOrder" ) ) );
Expand Down Expand Up @@ -1166,6 +1169,7 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument &doc, const QgsReadWrite
// placement
QDomElement placementElem = doc.createElement( QStringLiteral( "placement" ) );
placementElem.setAttribute( QStringLiteral( "placement" ), placement );
placementElem.setAttribute( QStringLiteral( "polygonPlacementFlags" ), static_cast< int >( mPolygonPlacementFlags ) );
placementElem.setAttribute( QStringLiteral( "placementFlags" ), static_cast< unsigned int >( placementFlags ) );
placementElem.setAttribute( QStringLiteral( "centroidWhole" ), centroidWhole );
placementElem.setAttribute( QStringLiteral( "centroidInside" ), centroidInside );
Expand Down Expand Up @@ -1966,10 +1970,25 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
doClip = true;
}


QgsLabeling::PolygonPlacementFlags polygonPlacement = mPolygonPlacementFlags;
if ( mDataDefinedProperties.isActive( QgsPalLayerSettings::LinePlacementOptions ) )
{
#if 0
context.expressionContext().setOriginalValueVariable( QgsLabelingUtils::encodeLinePlacementFlags( featureArrangementFlags ) );
const QString dataDefinedLineArrangement = mDataDefinedProperties.valueAsString( QgsPalLayerSettings::LinePlacementOptions, context.expressionContext() );
if ( !dataDefinedLineArrangement.isEmpty() )
{
featureArrangementFlags = QgsLabelingUtils::decodeLinePlacementFlags( dataDefinedLineArrangement );
}
#endif
}

// 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!
QgsGeometry permissibleZone;
if ( geom.type() == QgsWkbTypes::PolygonGeometry && fitInPolygonOnly )
if ( geom.type() == QgsWkbTypes::PolygonGeometry && ( fitInPolygonOnly || polygonPlacement & QgsLabeling::PolygonPlacementFlag::AllowPlacementOutsideOfPolygon ) )
{
permissibleZone = geom;
if ( QgsPalLabeling::geometryRequiresPreparation( permissibleZone, context, ct, doClip ? extentGeom : QgsGeometry(), mergeLines ) )
Expand Down Expand Up @@ -2484,6 +2503,8 @@ void QgsPalLayerSettings::registerFeature( const QgsFeature &f, QgsRenderContext
}
( *labelFeature )->setArrangementFlags( featureArrangementFlags );

( *labelFeature )->setPolygonPlacementFlags( polygonPlacement );

// data defined z-index?
double z = zIndex;
if ( mDataDefinedProperties.isActive( QgsPalLayerSettings::ZIndex ) )
Expand Down
17 changes: 17 additions & 0 deletions src/core/labeling/qgspallabeling.h
Expand Up @@ -44,6 +44,7 @@
#include "qgspropertycollection.h"
#include "qgslabelobstaclesettings.h"
#include "qgslabelthinningsettings.h"
#include "qgslabeling.h"

namespace pal SIP_SKIP
{
Expand Down Expand Up @@ -645,6 +646,20 @@ class CORE_EXPORT QgsPalLayerSettings
unsigned int placementFlags;
#endif

/**
* Returns the polygon placement flags, which dictate how polygon labels can be placed.
*
* \see setPolygonPlacementFlags()
*/
QgsLabeling::PolygonPlacementFlags polygonPlacementFlags() const { return mPolygonPlacementFlags; }

/**
* Sets the polygon placement \a flags, which dictate how polygon labels can be placed.
*
* \see polygonPlacementFlags()
*/
void setPolygonPlacementFlags( QgsLabeling::PolygonPlacementFlags flags ) { mPolygonPlacementFlags = flags; }

/**
* TRUE if feature centroid should be calculated from the whole feature, or
* FALSE if only the visible part of the feature should be considered.
Expand Down Expand Up @@ -1166,6 +1181,8 @@ class CORE_EXPORT QgsPalLayerSettings
QgsLabelObstacleSettings mObstacleSettings;
QgsLabelThinningSettings mThinningSettings;

QgsLabeling::PolygonPlacementFlags mPolygonPlacementFlags = nullptr;

QgsExpression mGeometryGeneratorExpression;

bool mRenderStarted = false;
Expand Down
74 changes: 52 additions & 22 deletions src/core/pal/feature.cpp
Expand Up @@ -2026,34 +2026,64 @@ std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( P
break;

case GEOS_POLYGON:
switch ( mLF->layer()->arrangement() )
{
const double labelWidth = getLabelWidth();
const double labelHeight = getLabelHeight();

const bool allowOutside = mLF->polygonPlacementFlags() & QgsLabeling::PolygonPlacementFlag::AllowPlacementOutsideOfPolygon;
//check width/height of bbox is sufficient for label

if ( allowOutside && ( std::fabs( xmax - xmin ) < labelWidth ||
std::fabs( ymax - ymin ) < labelHeight ) )
{
case QgsPalLayerSettings::AroundPoint:
//no way label can fit in this polygon -- label outside
createCandidatesOutsidePolygon( lPos, pal );
}
else
{
std::size_t created = 0;
switch ( mLF->layer()->arrangement() )
{
double cx, cy;
getCentroid( cx, cy, mLF->layer()->centroidInside() );
if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
createCandidateCenteredOverPoint( cx, cy, lPos, angle );
createCandidatesAroundPoint( cx, cy, lPos, angle );
break;
case QgsPalLayerSettings::AroundPoint:
{
double cx, cy;
getCentroid( cx, cy, mLF->layer()->centroidInside() );
if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
created += createCandidateCenteredOverPoint( cx, cy, lPos, angle );
created += createCandidatesAroundPoint( cx, cy, lPos, angle );
break;
}
case QgsPalLayerSettings::OverPoint:
{
double cx, cy;
getCentroid( cx, cy, mLF->layer()->centroidInside() );
created += createCandidatesOverPoint( cx, cy, lPos, angle );
break;
}
case QgsPalLayerSettings::Line:
created += createCandidatesAlongLine( lPos, this, false, pal );
break;
case QgsPalLayerSettings::PerimeterCurved:
created += createCurvedCandidatesAlongLine( lPos, this, false, pal );
break;
default:
created += createCandidatesForPolygon( lPos, this, pal );
break;
}
case QgsPalLayerSettings::OverPoint:

if ( allowOutside )
{
double cx, cy;
getCentroid( cx, cy, mLF->layer()->centroidInside() );
createCandidatesOverPoint( cx, cy, lPos, angle );
break;
// add fallback for labels outside the polygon
createCandidatesOutsidePolygon( lPos, pal );

// increase cost for these
if ( created > 0 )
{

}
}
case QgsPalLayerSettings::Line:
createCandidatesAlongLine( lPos, this, false, pal );
break;
case QgsPalLayerSettings::PerimeterCurved:
createCurvedCandidatesAlongLine( lPos, this, false, pal );
break;
default:
createCandidatesForPolygon( lPos, this, pal );
break;
}
}
}
}

Expand Down
7 changes: 7 additions & 0 deletions src/gui/labeling/qgslabelinggui.cpp
Expand Up @@ -294,6 +294,8 @@ void QgsLabelingGui::setLayer( QgsMapLayer *mapLayer )
chkLineOn->setChecked( mSettings.placementFlags & QgsPalLayerSettings::OnLine );
chkLineOrientationDependent->setChecked( !( mSettings.placementFlags & QgsPalLayerSettings::MapOrientation ) );

mCheckAllowLabelsOutsidePolygons->setChecked( mSettings.polygonPlacementFlags() & QgsLabeling::PolygonPlacementFlag::AllowPlacementOutsideOfPolygon );

switch ( mSettings.placement )
{
case QgsPalLayerSettings::AroundPoint:
Expand Down Expand Up @@ -462,6 +464,11 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings()
lyr.dist = 0;
lyr.placementFlags = 0;

QgsLabeling::PolygonPlacementFlags polygonPlacementFlags = nullptr;
if ( mCheckAllowLabelsOutsidePolygons->isChecked() )
polygonPlacementFlags |= QgsLabeling::PolygonPlacementFlag::AllowPlacementOutsideOfPolygon;
lyr.setPolygonPlacementFlags( polygonPlacementFlags );

QWidget *curPlacementWdgt = stackedPlacement->currentWidget();
lyr.centroidWhole = mCentroidRadioWhole->isChecked();
lyr.centroidInside = mCentroidInsideCheckBox->isChecked();
Expand Down
5 changes: 4 additions & 1 deletion src/gui/qgstextformatwidget.cpp
Expand Up @@ -472,7 +472,8 @@ void QgsTextFormatWidget::initWidget()
<< mEnableMaskChkBx
<< mMaskJoinStyleComboBox
<< mMaskBufferSizeSpinBox
<< mMaskOpacityWidget;
<< mMaskOpacityWidget
<< mCheckAllowLabelsOutsidePolygons;

connectValueChanged( widgets, SLOT( updatePreview() ) );

Expand Down Expand Up @@ -1273,6 +1274,7 @@ void QgsTextFormatWidget::updatePlacementWidgets()
bool showDistanceFrame = false;
bool showRotationFrame = false;
bool showMaxCharAngleFrame = false;
bool showPolygonPlacementOptions = ( curWdgt == pagePolygon && !radPolygonPerimeter->isChecked() && !radPolygonPerimeterCurved->isChecked() );

bool enableMultiLinesFrame = true;

Expand Down Expand Up @@ -1323,6 +1325,7 @@ void QgsTextFormatWidget::updatePlacementWidgets()
}

mPlacementLineFrame->setVisible( showLineFrame );
mPlacementPolygonFrame->setVisible( showPolygonPlacementOptions );
mPlacementCentroidFrame->setVisible( showCentroidFrame );
mPlacementQuadrantFrame->setVisible( showQuadrantFrame );
mPlacementFixedQuadrantFrame->setVisible( showFixedQuadrantFrame );
Expand Down

0 comments on commit 51820ba

Please sign in to comment.