Skip to content

Commit

Permalink
[FEATURE][callouts] Add control over anchor point for callout on label
Browse files Browse the repository at this point in the history
This gives users control over where a callout should join to the label
text. (Previously, you only had control over where the callout would
join to the corresponding feature geometry).

Choices include:
- Closest point (previous behavior)
- Label Centroid
- Fixed corners: Top left/top right/bottom left/bottom right/etc

Data defined control over the label anchor is also possible
  • Loading branch information
nyalldawson committed Mar 9, 2020
1 parent 78bd73e commit bcf9c82
Show file tree
Hide file tree
Showing 17 changed files with 902 additions and 88 deletions.
67 changes: 67 additions & 0 deletions python/core/auto_generated/callouts/qgscallout.sip.in
Expand Up @@ -48,6 +48,7 @@ relevant symbology elements to render them.
OffsetFromLabel,
DrawCalloutToAllParts,
AnchorPointPosition,
LabelAnchorPointPosition,
};

enum DrawOrder
Expand All @@ -64,6 +65,20 @@ relevant symbology elements to render them.
Centroid,
};

enum LabelAnchorPoint
{
LabelPointOnExterior,
LabelCentroid,
LabelTopLeft,
LabelTopMiddle,
LabelTopRight,
LabelMiddleLeft,
LabelMiddleRight,
LabelBottomLeft,
LabelBottomMiddle,
LabelBottomRight,
};

QgsCallout();
%Docstring
Constructor for QgsCallout.
Expand Down Expand Up @@ -272,6 +287,51 @@ anchor point.
.. seealso:: :py:func:`encodeAnchorPoint`
%End


LabelAnchorPoint labelAnchorPoint() const;
%Docstring
Returns the label's anchor point position.

.. seealso:: :py:func:`setLabelAnchorPoint`

.. versionadded:: 3.14
%End

void setLabelAnchorPoint( LabelAnchorPoint anchor );
%Docstring
Sets the label's ``anchor`` point position.

.. seealso:: :py:func:`labelAnchorPoint`

.. versionadded:: 3.14
%End

static QString encodeLabelAnchorPoint( LabelAnchorPoint anchor );
%Docstring
Encodes a label ``anchor`` point to its string representation.

:return: encoded string

.. seealso:: :py:func:`decodeLabelAnchorPoint`

.. versionadded:: 3.14
%End

static QgsCallout::LabelAnchorPoint decodeLabelAnchorPoint( const QString &name, bool *ok = 0 );
%Docstring
Attempts to decode a string representation of a label anchor point name to the corresponding
anchor point.

:param name: encoded label anchor point name
:param ok: if specified, will be set to ``True`` if the anchor point was successfully decoded

:return: decoded name

.. seealso:: :py:func:`encodeLabelAnchorPoint`

.. versionadded:: 3.14
%End

protected:

virtual void draw( QgsRenderContext &context, QRectF bodyBoundingBox, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext ) = 0;
Expand All @@ -292,6 +352,13 @@ Both ``rect`` and ``anchor`` are specified in painter coordinates (i.e. pixels).

The ``calloutContext`` argument is used to specify additional contextual information about
how a callout is being rendered.
%End

QgsGeometry labelAnchorGeometry( QRectF bodyBoundingBox, const double angle, LabelAnchorPoint anchor ) const;
%Docstring
Returns the anchor point geometry for a label with the given bounding box and ``anchor`` point mode.

.. versionadded:: 3.14
%End

};
Expand Down
125 changes: 120 additions & 5 deletions src/core/callouts/qgscallout.cpp
Expand Up @@ -39,6 +39,13 @@ void QgsCallout::initPropertyDefinitions()
{ QgsCallout::OffsetFromLabel, QgsPropertyDefinition( "OffsetFromLabel", QObject::tr( "Offset from label" ), QgsPropertyDefinition::DoublePositive, origin ) },
{ QgsCallout::DrawCalloutToAllParts, QgsPropertyDefinition( "DrawCalloutToAllParts", QObject::tr( "Draw lines to all feature parts" ), QgsPropertyDefinition::Boolean, origin ) },
{ QgsCallout::AnchorPointPosition, QgsPropertyDefinition( "AnchorPointPosition", QgsPropertyDefinition::DataTypeString, QObject::tr( "Feature's anchor point position" ), QObject::tr( "string " ) + "[<b>pole_of_inaccessibility</b>|<b>point_on_exterior</b>|<b>point_on_surface</b>|<b>centroid</b>]", origin ) },
{
QgsCallout::LabelAnchorPointPosition, QgsPropertyDefinition( "LabelAnchorPointPosition", QgsPropertyDefinition::DataTypeString, QObject::tr( "Label's anchor point position" ), QObject::tr( "string " ) + "[<b>point_on_exterior</b>|<b>centroid</b>|<b>TL</b>=Top left|<b>T</b>=Top middle|"
"<b>TR</b>=Top right|<br>"
"<b>L</b>=Left|<b>R</b>=Right|<br>"
"<b>BL</b>=Bottom left|<b>B</b>=Bottom middle|"
"<b>BR</b>=Bottom right]", origin )
},
};
}

Expand All @@ -52,14 +59,16 @@ QVariantMap QgsCallout::properties( const QgsReadWriteContext & ) const
QVariantMap props;
props.insert( QStringLiteral( "enabled" ), mEnabled ? "1" : "0" );
props.insert( QStringLiteral( "anchorPoint" ), encodeAnchorPoint( mAnchorPoint ) );
props.insert( QStringLiteral( "labelAnchorPoint" ), encodeLabelAnchorPoint( mLabelAnchorPoint ) );
props.insert( QStringLiteral( "ddProperties" ), mDataDefinedProperties.toVariant( propertyDefinitions() ) );
return props;
}

void QgsCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext & )
{
mEnabled = props.value( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt();
mAnchorPoint = decodeAnchorPoint( props.value( QStringLiteral( "anchorPoint" ), QString( "" ) ).toString() );
mAnchorPoint = decodeAnchorPoint( props.value( QStringLiteral( "anchorPoint" ), QString() ).toString() );
mLabelAnchorPoint = decodeLabelAnchorPoint( props.value( QStringLiteral( "labelAnchorPoint" ), QString() ).toString() );
mDataDefinedProperties.loadVariant( props.value( QStringLiteral( "ddProperties" ) ), propertyDefinitions() );
}

Expand Down Expand Up @@ -184,6 +193,96 @@ QString QgsCallout::encodeAnchorPoint( AnchorPoint anchor )
return QString();
}

QString QgsCallout::encodeLabelAnchorPoint( QgsCallout::LabelAnchorPoint anchor )
{
switch ( anchor )
{
case LabelPointOnExterior:
return QStringLiteral( "point_on_exterior" );
case LabelCentroid:
return QStringLiteral( "centroid" );
case LabelTopLeft:
return QStringLiteral( "tl" );
case LabelTopMiddle:
return QStringLiteral( "t" );
case LabelTopRight:
return QStringLiteral( "tr" );
case LabelMiddleLeft:
return QStringLiteral( "l" );
case LabelMiddleRight:
return QStringLiteral( "r" );
case LabelBottomLeft:
return QStringLiteral( "bl" );
case LabelBottomMiddle:
return QStringLiteral( "b" );
case LabelBottomRight:
return QStringLiteral( "br" );
}

return QString();
}

QgsCallout::LabelAnchorPoint QgsCallout::decodeLabelAnchorPoint( const QString &name, bool *ok )
{
if ( ok )
*ok = true;
QString cleaned = name.toLower().trimmed();

if ( cleaned == QLatin1String( "point_on_exterior" ) )
return LabelPointOnExterior;
else if ( cleaned == QLatin1String( "centroid" ) )
return LabelCentroid;
else if ( cleaned == QLatin1String( "tl" ) )
return LabelTopLeft;
else if ( cleaned == QLatin1String( "t" ) )
return LabelTopMiddle;
else if ( cleaned == QLatin1String( "tr" ) )
return LabelTopRight;
else if ( cleaned == QLatin1String( "l" ) )
return LabelMiddleLeft;
else if ( cleaned == QLatin1String( "r" ) )
return LabelMiddleRight;
else if ( cleaned == QLatin1String( "bl" ) )
return LabelBottomLeft;
else if ( cleaned == QLatin1String( "b" ) )
return LabelBottomMiddle;
else if ( cleaned == QLatin1String( "br" ) )
return LabelBottomRight;

if ( ok )
*ok = false;
return LabelPointOnExterior;
}

QgsGeometry QgsCallout::labelAnchorGeometry( QRectF rect, const double, LabelAnchorPoint anchor ) const
{
QgsGeometry label( QgsGeometry::fromRect( rect ) );
switch ( anchor )
{
case LabelPointOnExterior:
return label;
case LabelCentroid:
return label.centroid();
case LabelTopLeft:
return QgsGeometry::fromPointXY( QgsPointXY( rect.bottomLeft() ) );
case LabelTopMiddle:
return QgsGeometry::fromPointXY( QgsPointXY( ( rect.left() + rect.right() ) / 2.0, rect.bottom() ) );
case LabelTopRight:
return QgsGeometry::fromPointXY( QgsPointXY( rect.bottomRight() ) );
case LabelMiddleLeft:
return QgsGeometry::fromPointXY( QgsPointXY( rect.left(), ( rect.top() + rect.bottom() ) / 2.0 ) );
case LabelMiddleRight:
return QgsGeometry::fromPointXY( QgsPointXY( rect.right(), ( rect.top() + rect.bottom() ) / 2.0 ) );
case LabelBottomLeft:
return QgsGeometry::fromPointXY( QgsPointXY( rect.topLeft() ) );
case LabelBottomMiddle:
return QgsGeometry::fromPointXY( QgsPointXY( ( rect.left() + rect.right() ) / 2.0, rect.top() ) );
case LabelBottomRight:
return QgsGeometry::fromPointXY( QgsPointXY( rect.topRight() ) );
}
return label;
}

//
// QgsSimpleLineCallout
//
Expand Down Expand Up @@ -312,9 +411,17 @@ void QgsSimpleLineCallout::setLineSymbol( QgsLineSymbol *symbol )
mLineSymbol.reset( symbol );
}

void QgsSimpleLineCallout::draw( QgsRenderContext &context, QRectF rect, const double, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
void QgsSimpleLineCallout::draw( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
{
QgsGeometry label( QgsGeometry::fromRect( rect ) );
LabelAnchorPoint labelAnchor = labelAnchorPoint();
if ( dataDefinedProperties().isActive( QgsCallout::LabelAnchorPointPosition ) )
{
QString encodedAnchor = encodeLabelAnchorPoint( labelAnchor );
context.expressionContext().setOriginalValueVariable( encodedAnchor );
labelAnchor = decodeLabelAnchorPoint( dataDefinedProperties().valueAsString( QgsCallout::LabelAnchorPointPosition, context.expressionContext(), encodedAnchor ) );
}
QgsGeometry label = labelAnchorGeometry( rect, angle, labelAnchor );

auto drawCalloutLine = [this, &context, &label]( const QgsGeometry & partAnchor )
{
QgsGeometry line;
Expand Down Expand Up @@ -451,9 +558,17 @@ QgsManhattanLineCallout *QgsManhattanLineCallout::clone() const
return new QgsManhattanLineCallout( *this );
}

void QgsManhattanLineCallout::draw( QgsRenderContext &context, QRectF rect, const double, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
void QgsManhattanLineCallout::draw( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext )
{
QgsGeometry label( QgsGeometry::fromRect( rect ) );
LabelAnchorPoint labelAnchor = labelAnchorPoint();
if ( dataDefinedProperties().isActive( QgsCallout::LabelAnchorPointPosition ) )
{
QString encodedAnchor = encodeLabelAnchorPoint( labelAnchor );
context.expressionContext().setOriginalValueVariable( encodedAnchor );
labelAnchor = decodeLabelAnchorPoint( dataDefinedProperties().valueAsString( QgsCallout::LabelAnchorPointPosition, context.expressionContext(), encodedAnchor ) );
}
QgsGeometry label = labelAnchorGeometry( rect, angle, labelAnchor );

auto drawCalloutLine = [this, &context, &label]( const QgsGeometry & partAnchor )
{
QgsGeometry line;
Expand Down
62 changes: 62 additions & 0 deletions src/core/callouts/qgscallout.h
Expand Up @@ -73,6 +73,7 @@ class CORE_EXPORT QgsCallout
OffsetFromLabel, //!< Distance to offset lines from label area
DrawCalloutToAllParts, //!< Whether callout lines should be drawn to all feature parts
AnchorPointPosition, //!< Feature's anchor point position
LabelAnchorPointPosition, //!< Label's anchor point position
};

//! Options for draw order (stacking) of callouts
Expand All @@ -91,6 +92,24 @@ class CORE_EXPORT QgsCallout
Centroid, //!< The surface's centroid is used as anchor for polygon geometries
};

/**
* Label's anchor point position.
* \since QGIS 3.14
*/
enum LabelAnchorPoint
{
LabelPointOnExterior, //!< The point on the label's boundary closest to the feature
LabelCentroid, //!< The labe's centroid
LabelTopLeft, //!< Top left corner of the label's boundary
LabelTopMiddle, //!< Top middle of the label's boundary
LabelTopRight, //!< Top right corner of the label's boundary
LabelMiddleLeft, //!< Middle left of the label's boundary
LabelMiddleRight, //!< Middle right of the label's boundary
LabelBottomLeft, //!< Bottom left corner of the label's boundary
LabelBottomMiddle, //!< Bottom middle of the label's boundary
LabelBottomRight, //!< Bottom right corner of the label's boundary
};

/**
* Constructor for QgsCallout.
*/
Expand Down Expand Up @@ -293,6 +312,42 @@ class CORE_EXPORT QgsCallout
*/
static QgsCallout::AnchorPoint decodeAnchorPoint( const QString &name, bool *ok = nullptr );


/**
* Returns the label's anchor point position.
*
* \see setLabelAnchorPoint()
* \since QGIS 3.14
*/
LabelAnchorPoint labelAnchorPoint() const { return mLabelAnchorPoint; }

/**
* Sets the label's \a anchor point position.
*
* \see labelAnchorPoint()
* \since QGIS 3.14
*/
void setLabelAnchorPoint( LabelAnchorPoint anchor ) { mLabelAnchorPoint = anchor; }

/**
* Encodes a label \a anchor point to its string representation.
* \returns encoded string
* \see decodeLabelAnchorPoint()
* \since QGIS 3.14
*/
static QString encodeLabelAnchorPoint( LabelAnchorPoint anchor );

/**
* Attempts to decode a string representation of a label anchor point name to the corresponding
* anchor point.
* \param name encoded label anchor point name
* \param ok if specified, will be set to TRUE if the anchor point was successfully decoded
* \returns decoded name
* \see encodeLabelAnchorPoint()
* \since QGIS 3.14
*/
static QgsCallout::LabelAnchorPoint decodeLabelAnchorPoint( const QString &name, bool *ok = nullptr );

protected:

/**
Expand All @@ -315,11 +370,18 @@ class CORE_EXPORT QgsCallout
*/
virtual void draw( QgsRenderContext &context, QRectF bodyBoundingBox, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext ) = 0;

/**
* Returns the anchor point geometry for a label with the given bounding box and \a anchor point mode.
* \since QGIS 3.14
*/
QgsGeometry labelAnchorGeometry( QRectF bodyBoundingBox, const double angle, LabelAnchorPoint anchor ) const;

private:

bool mEnabled = false;

AnchorPoint mAnchorPoint = PoleOfInaccessibility;
LabelAnchorPoint mLabelAnchorPoint = LabelPointOnExterior;

//! Property collection for data defined callout settings
QgsPropertyCollection mDataDefinedProperties;
Expand Down

0 comments on commit bcf9c82

Please sign in to comment.