Skip to content

Commit

Permalink
[FEATURE] When the "show pinned labels" option is enabled, also
Browse files Browse the repository at this point in the history
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 5d06c1d
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 35 deletions.
8 changes: 6 additions & 2 deletions python/core/auto_generated/callouts/qgscallout.sip.in
Expand Up @@ -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

Expand Down
44 changes: 44 additions & 0 deletions python/core/auto_generated/labeling/qgscalloutposition.sip.in
Expand Up @@ -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

};
Expand Down
52 changes: 44 additions & 8 deletions src/app/labeling/qgsmaptoolpinlabels.cpp
Expand Up @@ -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()
{
Expand All @@ -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 );
Expand All @@ -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() )
Expand Down Expand Up @@ -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;
}
Expand Down
5 changes: 5 additions & 0 deletions src/app/labeling/qgsmaptoolpinlabels.h
Expand Up @@ -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 );

Expand Down
29 changes: 21 additions & 8 deletions src/core/callouts/qgscallout.cpp
Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 );
Expand Down
9 changes: 7 additions & 2 deletions src/core/callouts/qgscallout.h
Expand Up @@ -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:

Expand Down
43 changes: 43 additions & 0 deletions src/core/labeling/qgscalloutposition.h
Expand Up @@ -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.