Skip to content
Permalink
Browse files

[FEATURE] When the "show pinned labels" option is enabled, also

highlight any pinned callout start or end points

This allows users to immediately see which callouts points have
been manually placed vs are automatically placed.
  • Loading branch information
nyalldawson committed Mar 18, 2021
1 parent e8280d2 commit 5d06c1d48daf500ce18f7efed2cd15a8190f80cc
@@ -409,20 +409,24 @@ Returns the anchor point geometry for a label with the given bounding box and ``
QGIS 3.20 use :py:func:`~QgsCallout.calloutLabelPoint` instead
%End

QgsGeometry calloutLabelPoint( QRectF bodyBoundingBox, double angle, LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCalloutContext &calloutContext ) const;
QgsGeometry calloutLabelPoint( QRectF bodyBoundingBox, double angle, LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCalloutContext &calloutContext, bool &pinned ) const;
%Docstring
Returns the anchor point geometry for a label with the given bounding box and ``anchor`` point mode.

The ``pinned`` argument will be set to ``True`` if the callout label point is pinned (manually placed).

.. versionadded:: 3.20
%End

QgsGeometry calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext ) const;
QgsGeometry calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext, bool &pinned ) const;
%Docstring
Calculates the direct line from a label geometry to an anchor geometry part, respecting the various
callout settings which influence how the callout end should be placed in the anchor geometry.

Returns a null geometry if the callout line cannot be calculated.

The ``pinned`` argument will be set to ``True`` if the callout anchor point is pinned (manually placed).

.. versionadded:: 3.20
%End

@@ -85,6 +85,50 @@ The destination of the callout line is the line point associated with the featur
.. seealso:: :py:func:`destination`

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

bool originIsPinned() const;
%Docstring
Returns ``True`` if the origin of the callout has pinned (manually placed).

The origin of the callout line is the line point associated with the label text.

.. seealso:: :py:func:`destinationIsPinned`

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

void setOriginIsPinned( bool pinned );
%Docstring
Sets whether the origin of the callout has pinned (manually placed).

The origin of the callout line is the line point associated with the label text.

.. seealso:: :py:func:`setDestinationIsPinned`

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

bool destinationIsPinned() const;
%Docstring
Returns ``True`` if the destination of the callout has pinned (manually placed).

The destination of the callout line is the line point associated with the feature's geometry.

.. seealso:: :py:func:`originIsPinned`

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

void setDestinationIsPinned( bool pinned );
%Docstring
Sets whether the destination of the callout has pinned (manually placed).

The destination of the callout line is the line point associated with the feature's geometry.

.. seealso:: :py:func:`setOriginIsPinned`

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

};
@@ -154,6 +154,20 @@ void QgsMapToolPinLabels::highlightLabel( const QgsLabelPosition &labelpos,
mHighlights.insert( id, rb );
}

void QgsMapToolPinLabels::highlightCallout( bool isOrigin, const QgsCalloutPosition &calloutPosition, const QString &id, const QColor &color )
{
double scaleFactor = mCanvas->fontMetrics().xHeight();

QgsRubberBand *rb = new QgsRubberBand( mCanvas, QgsWkbTypes::PointGeometry );
rb->setWidth( 2 );
rb->setSecondaryStrokeColor( QColor( 255, 255, 255, 100 ) );
rb->setColor( color );
rb->setIcon( QgsRubberBand::ICON_X );
rb->setIconSize( scaleFactor );
rb->addPoint( isOrigin ? calloutPosition.origin() : calloutPosition.destination() );
mHighlights.insert( id, rb );
}

// public slot to render highlight rectangles around pinned labels
void QgsMapToolPinLabels::highlightPinnedLabels()
{
@@ -164,7 +178,7 @@ void QgsMapToolPinLabels::highlightPinnedLabels()
return;
}

QgsDebugMsg( QStringLiteral( "Highlighting pinned labels" ) );
QgsDebugMsgLevel( QStringLiteral( "Highlighting pinned labels" ), 2 );

// get list of all drawn labels from all layers within given extent
const QgsLabelingResults *labelingResults = mCanvas->labelingResults( false );
@@ -174,16 +188,14 @@ void QgsMapToolPinLabels::highlightPinnedLabels()
}

QgsRectangle ext = mCanvas->extent();
QgsDebugMsg( QStringLiteral( "Getting labels from canvas extent" ) );
QgsDebugMsgLevel( QStringLiteral( "Getting labels from canvas extent" ), 2 );

QList<QgsLabelPosition> labelPosList = labelingResults->labelsWithinRect( ext );
const QList<QgsLabelPosition> labelPosList = labelingResults->labelsWithinRect( ext );

QApplication::setOverrideCursor( Qt::WaitCursor );
QList<QgsLabelPosition>::const_iterator it;
for ( it = labelPosList.constBegin() ; it != labelPosList.constEnd(); ++it )
for ( const QgsLabelPosition &pos : labelPosList )
{
const QgsLabelPosition &pos = *it;

mCurrentLabel = LabelDetails( pos );

if ( isPinned() )
@@ -216,14 +228,38 @@ void QgsMapToolPinLabels::highlightPinnedLabels()
highlightLabel( pos, labelStringID, lblcolor );
}
}

// highlight pinned callouts
const QList<QgsCalloutPosition> calloutPosList = labelingResults->calloutsWithinRectangle( ext );
const QColor calloutColor = QColor( 54, 129, 255, 160 );
for ( const QgsCalloutPosition &callout : calloutPosList )
{
if ( callout.originIsPinned() )
{
QString calloutStringID = QStringLiteral( "callout|%1|%2|origin" ).arg( callout.layerID, QString::number( callout.featureId ) );
// don't highlight again
if ( mHighlights.contains( calloutStringID ) )
continue;

highlightCallout( true, callout, calloutStringID, calloutColor );
}
if ( callout.destinationIsPinned() )
{
QString calloutStringID = QStringLiteral( "callout|%1|%2|destination" ).arg( callout.layerID, QString::number( callout.featureId ) );
// don't highlight again
if ( mHighlights.contains( calloutStringID ) )
continue;

highlightCallout( false, callout, calloutStringID, calloutColor );
}
}
QApplication::restoreOverrideCursor();
}

void QgsMapToolPinLabels::removePinnedHighlights()
{
QApplication::setOverrideCursor( Qt::BusyCursor );
const auto constMHighlights = mHighlights;
for ( QgsRubberBand *rb : constMHighlights )
for ( QgsRubberBand *rb : qgis::as_const( mHighlights ) )
{
delete rb;
}
@@ -82,6 +82,11 @@ class APP_EXPORT QgsMapToolPinLabels: public QgsMapToolLabel
const QString &id,
const QColor &color );

//! Highlights a given callout relative to whether its pinned and editable
void highlightCallout( bool isOrigin, const QgsCalloutPosition &labelpos,
const QString &id,
const QColor &color );

//! Select valid labels to pin or unpin
void pinUnpinLabels( const QgsRectangle &ext, QMouseEvent *e );

@@ -309,8 +309,9 @@ QgsGeometry QgsCallout::labelAnchorGeometry( QRectF rect, const double angle, La
return label;
}

QgsGeometry QgsCallout::calloutLabelPoint( QRectF rect, const double angle, QgsCallout::LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCallout::QgsCalloutContext &calloutContext ) const
QgsGeometry QgsCallout::calloutLabelPoint( QRectF rect, const double angle, QgsCallout::LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCallout::QgsCalloutContext &calloutContext, bool &pinned ) const
{
pinned = false;
if ( dataDefinedProperties().isActive( QgsCallout::OriginX ) && dataDefinedProperties().isActive( QgsCallout::OriginY ) )
{
bool ok = false;
@@ -320,6 +321,7 @@ QgsGeometry QgsCallout::calloutLabelPoint( QRectF rect, const double angle, QgsC
const double y = dataDefinedProperties().valueAsDouble( QgsCallout::OriginY, context.expressionContext(), 0, &ok );
if ( ok )
{
pinned = true;
// data defined label point, use it directly
QgsGeometry labelPoint = QgsGeometry::fromPointXY( QgsPointXY( x, y ) );
try
@@ -384,8 +386,9 @@ QgsGeometry QgsCallout::calloutLabelPoint( QRectF rect, const double angle, QgsC
return label;
}

QgsGeometry QgsCallout::calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext ) const
QgsGeometry QgsCallout::calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext, bool &pinned ) const
{
pinned = false;
AnchorPoint anchor = anchorPoint();
const QgsAbstractGeometry *evaluatedPartAnchor = partGeometry;
std::unique_ptr< QgsAbstractGeometry > tempPartAnchor;
@@ -399,6 +402,7 @@ QgsGeometry QgsCallout::calloutLineToPart( const QgsGeometry &labelGeometry, con
const double y = dataDefinedProperties().valueAsDouble( QgsCallout::DestinationY, context.expressionContext(), 0, &ok );
if ( ok )
{
pinned = true;
tempPartAnchor = std::make_unique< QgsPoint >( QgsWkbTypes::Point, x, y );
evaluatedPartAnchor = tempPartAnchor.get();
try
@@ -603,13 +607,16 @@ void QgsSimpleLineCallout::draw( QgsRenderContext &context, QRectF rect, const d
context.expressionContext().setOriginalValueVariable( encodedAnchor );
labelAnchor = decodeLabelAnchorPoint( dataDefinedProperties().valueAsString( QgsCallout::LabelAnchorPointPosition, context.expressionContext(), encodedAnchor ) );
}
const QgsGeometry label = calloutLabelPoint( rect, angle, labelAnchor, context, calloutContext );

bool originPinned = false;
const QgsGeometry label = calloutLabelPoint( rect, angle, labelAnchor, context, calloutContext, originPinned );
if ( label.isNull() )
return;

auto drawCalloutLine = [this, &context, &calloutContext, &label]( const QgsAbstractGeometry * partAnchor )
auto drawCalloutLine = [this, &context, &calloutContext, &label, originPinned]( const QgsAbstractGeometry * partAnchor )
{
QgsGeometry line = calloutLineToPart( label, partAnchor, context, calloutContext );
bool destinationPinned = false;
QgsGeometry line = calloutLineToPart( label, partAnchor, context, calloutContext, destinationPinned );
if ( line.isEmpty() )
return;

@@ -657,7 +664,9 @@ void QgsSimpleLineCallout::draw( QgsRenderContext &context, QRectF rect, const d

QgsCalloutPosition position;
position.setOrigin( context.mapToPixel().toMapCoordinates( points.at( 0 ).x(), points.at( 0 ).y() ).toQPointF() );
position.setOriginIsPinned( originPinned );
position.setDestination( context.mapToPixel().toMapCoordinates( points.constLast().x(), points.constLast().y() ).toQPointF() );
position.setDestinationIsPinned( destinationPinned );
calloutContext.addCalloutPosition( position );

mLineSymbol->renderPolyline( points, nullptr, context );
@@ -722,13 +731,15 @@ void QgsManhattanLineCallout::draw( QgsRenderContext &context, QRectF rect, cons
context.expressionContext().setOriginalValueVariable( encodedAnchor );
labelAnchor = decodeLabelAnchorPoint( dataDefinedProperties().valueAsString( QgsCallout::LabelAnchorPointPosition, context.expressionContext(), encodedAnchor ) );
}
const QgsGeometry label = calloutLabelPoint( rect, angle, labelAnchor, context, calloutContext );
bool originPinned = false;
const QgsGeometry label = calloutLabelPoint( rect, angle, labelAnchor, context, calloutContext, originPinned );
if ( label.isNull() )
return;

auto drawCalloutLine = [this, &context, &calloutContext, &label]( const QgsAbstractGeometry * partAnchor )
auto drawCalloutLine = [this, &context, &calloutContext, &label, originPinned]( const QgsAbstractGeometry * partAnchor )
{
QgsGeometry line = calloutLineToPart( label, partAnchor, context, calloutContext );
bool destinationPinned = false;
QgsGeometry line = calloutLineToPart( label, partAnchor, context, calloutContext, destinationPinned );
if ( line.isEmpty() )
return;

@@ -779,7 +790,9 @@ void QgsManhattanLineCallout::draw( QgsRenderContext &context, QRectF rect, cons

QgsCalloutPosition position;
position.setOrigin( context.mapToPixel().toMapCoordinates( points.at( 0 ).x(), points.at( 0 ).y() ).toQPointF() );
position.setOriginIsPinned( originPinned );
position.setDestination( context.mapToPixel().toMapCoordinates( points.constLast().x(), points.constLast().y() ).toQPointF() );
position.setDestinationIsPinned( destinationPinned );
calloutContext.addCalloutPosition( position );

lineSymbol()->renderPolyline( points, nullptr, context );
@@ -424,19 +424,24 @@ class CORE_EXPORT QgsCallout

/**
* Returns the anchor point geometry for a label with the given bounding box and \a anchor point mode.
*
* The \a pinned argument will be set to TRUE if the callout label point is pinned (manually placed).
*
* \since QGIS 3.20
*/
QgsGeometry calloutLabelPoint( QRectF bodyBoundingBox, double angle, LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCalloutContext &calloutContext ) const;
QgsGeometry calloutLabelPoint( QRectF bodyBoundingBox, double angle, LabelAnchorPoint anchor, QgsRenderContext &context, const QgsCalloutContext &calloutContext, bool &pinned ) const;

/**
* Calculates the direct line from a label geometry to an anchor geometry part, respecting the various
* callout settings which influence how the callout end should be placed in the anchor geometry.
*
* Returns a null geometry if the callout line cannot be calculated.
*
* The \a pinned argument will be set to TRUE if the callout anchor point is pinned (manually placed).
*
* \since QGIS 3.20
*/
QgsGeometry calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext ) const;
QgsGeometry calloutLineToPart( const QgsGeometry &labelGeometry, const QgsAbstractGeometry *partGeometry, QgsRenderContext &context, const QgsCalloutContext &calloutContext, bool &pinned ) const;

private:

@@ -103,11 +103,54 @@ class CORE_EXPORT QgsCalloutPosition
*/
void setDestination( const QPointF &destination ) { mDestination = destination; }

/**
* Returns TRUE if the origin of the callout has pinned (manually placed).
*
* The origin of the callout line is the line point associated with the label text.
*
* \see destinationIsPinned()
* \see setOriginIsPinned()
*/
bool originIsPinned() const { return mOriginIsPinned; }

/**
* Sets whether the origin of the callout has pinned (manually placed).
*
* The origin of the callout line is the line point associated with the label text.
*
* \see setDestinationIsPinned()
* \see originIsPinned()
*/
void setOriginIsPinned( bool pinned ) { mOriginIsPinned = pinned; }

/**
* Returns TRUE if the destination of the callout has pinned (manually placed).
*
* The destination of the callout line is the line point associated with the feature's geometry.
*
* \see originIsPinned()
* \see setDestinationIsPinned()
*/
bool destinationIsPinned() const { return mDestinationIsPinned; }

/**
* Sets whether the destination of the callout has pinned (manually placed).
*
* The destination of the callout line is the line point associated with the feature's geometry.
*
* \see setOriginIsPinned()
* \see destinationIsPinned()
*/
void setDestinationIsPinned( bool pinned ) { mDestinationIsPinned = pinned; }

private:

QPointF mOrigin;

QPointF mDestination;

bool mOriginIsPinned = false;
bool mDestinationIsPinned = false;
};

#endif // QGSCALLOUTPOSITION_H

0 comments on commit 5d06c1d

Please sign in to comment.