Skip to content

Commit

Permalink
[feature] New callout style for curved lines
Browse files Browse the repository at this point in the history
Renders a nice cartographically pleasing curved line between the
labels and features.

Options include selecting a specific curve orientation (clockwise
or counterclockwise), or an automatic orientation option which
determines optimal orientation for each individual label. Users
also have control over the amount of curvature applied to the
callout lines.
  • Loading branch information
nyalldawson committed Mar 19, 2021
1 parent 425f271 commit f759c59
Show file tree
Hide file tree
Showing 23 changed files with 2,496 additions and 0 deletions.
84 changes: 84 additions & 0 deletions python/core/auto_generated/callouts/qgscallout.sip.in
Expand Up @@ -34,6 +34,10 @@ relevant symbology elements to render them.
{
sipType = sipType_QgsManhattanLineCallout;
}
else if ( sipCpp->type() == "curved" && dynamic_cast<QgsCurvedLineCallout *>( sipCpp ) != NULL )
{
sipType = sipType_QgsCurvedLineCallout;
}
else
{
sipType = 0;
Expand All @@ -53,6 +57,8 @@ relevant symbology elements to render them.
OriginY,
DestinationX,
DestinationY,
Curvature,
Orientation,
};

enum DrawOrder
Expand Down Expand Up @@ -734,6 +740,84 @@ serialized in the ``properties`` map (corresponding to the output from
};


class QgsCurvedLineCallout : QgsSimpleLineCallout
{
%Docstring
Draws curved lines as callouts.

.. versionadded:: 3.20
%End

%TypeHeaderCode
#include "qgscallout.h"
%End
public:

enum Orientation
{
Automatic,
Clockwise,
CounterClockwise,
};

QgsCurvedLineCallout();


static QgsCallout *create( const QVariantMap &properties = QVariantMap(), const QgsReadWriteContext &context = QgsReadWriteContext() ) /Factory/;
%Docstring
Creates a new QgsCurvedLineCallout, using the settings
serialized in the ``properties`` map (corresponding to the output from
:py:func:`QgsCurvedLineCallout.properties()` ).
%End

virtual QString type() const;

virtual QgsCurvedLineCallout *clone() const;

virtual QVariantMap properties( const QgsReadWriteContext &context ) const;


double curvature() const;
%Docstring
Returns the callout line's curvature.

The curvature is a percentage value (with typical ranges between 0.0 and 1.0), representing the overall curvature of the line.

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

void setCurvature( double curvature );
%Docstring
Sets the callout line's ``curvature``.

The ``curvature`` is a percentage value (with typical ranges between 0.0 and 1.0), representing the overall curvature of the line.

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

Orientation orientation() const;
%Docstring
Returns the callout line's curve orientation.

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

void setOrientation( Orientation orientation );
%Docstring
Sets the callout line's curve ``orientation``.

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

protected:
virtual QgsCurve *createCalloutLine( const QgsPoint &start, const QgsPoint &end, QgsRenderContext &context, const QRectF &bodyBoundingBox, const double angle, const QgsGeometry &anchor, QgsCalloutContext &calloutContext ) const /Factory/;


private:
QgsCurvedLineCallout( const QgsCurvedLineCallout &other );
QgsCurvedLineCallout &operator=( const QgsCurvedLineCallout & );
};


/************************************************************************
* This file has been generated automatically from *
Expand Down
248 changes: 248 additions & 0 deletions src/core/callouts/qgscallout.cpp
Expand Up @@ -24,6 +24,8 @@
#include "qgslinestring.h"
#include "qgslogger.h"
#include "qgsgeos.h"
#include "qgsgeometryutils.h"
#include "qgscircularstring.h"
#include <QPainter>
#include <mutex>

Expand Down Expand Up @@ -51,6 +53,10 @@ void QgsCallout::initPropertyDefinitions()
{ QgsCallout::OriginY, QgsPropertyDefinition( "OriginY", QObject::tr( "Callout origin (Y)" ), QgsPropertyDefinition::Double, origin ) },
{ QgsCallout::DestinationX, QgsPropertyDefinition( "DestinationX", QObject::tr( "Callout destination (X)" ), QgsPropertyDefinition::Double, origin ) },
{ QgsCallout::DestinationY, QgsPropertyDefinition( "DestinationY", QObject::tr( "Callout destination (Y)" ), QgsPropertyDefinition::Double, origin ) },
{ QgsCallout::Curvature, QgsPropertyDefinition( "Curvature", QObject::tr( "Callout line curvature" ), QgsPropertyDefinition::Double, origin ) },
{
QgsCallout::Orientation, QgsPropertyDefinition( "Orientation", QgsPropertyDefinition::DataTypeString, QObject::tr( "Callout curve orientation" ), QObject::tr( "string " ) + "[<b>auto</b>|<b>clockwise</b>|<b>counterclockwise</b>]", origin )
},
};
}

Expand Down Expand Up @@ -745,3 +751,245 @@ QgsCurve *QgsManhattanLineCallout::createCalloutLine( const QgsPoint &start, con
QgsPoint mid1 = QgsPoint( start.x(), end.y() );
return new QgsLineString( QVector< QgsPoint >() << start << mid1 << end );
}


//
// QgsCurvedLineCallout
//

QgsCurvedLineCallout::QgsCurvedLineCallout()
{
}

QgsCurvedLineCallout::QgsCurvedLineCallout( const QgsCurvedLineCallout &other )
: QgsSimpleLineCallout( other )
, mOrientation( other.mOrientation )
, mCurvature( other.mCurvature )
{

}

QgsCallout *QgsCurvedLineCallout::create( const QVariantMap &properties, const QgsReadWriteContext &context )
{
std::unique_ptr< QgsCurvedLineCallout > callout = std::make_unique< QgsCurvedLineCallout >();
callout->readProperties( properties, context );

callout->setCurvature( properties.value( QStringLiteral( "curvature" ), 0.1 ).toDouble() );
callout->setOrientation( decodeOrientation( properties.value( QStringLiteral( "orientation" ), QStringLiteral( "auto" ) ).toString() ) );

return callout.release();
}

QString QgsCurvedLineCallout::type() const
{
return QStringLiteral( "curved" );
}

QgsCurvedLineCallout *QgsCurvedLineCallout::clone() const
{
return new QgsCurvedLineCallout( *this );
}

QVariantMap QgsCurvedLineCallout::properties( const QgsReadWriteContext &context ) const
{
QVariantMap props = QgsSimpleLineCallout::properties( context );
props.insert( QStringLiteral( "curvature" ), mCurvature );
props.insert( QStringLiteral( "orientation" ), encodeOrientation( mOrientation ) );
return props;
}

QgsCurve *QgsCurvedLineCallout::createCalloutLine( const QgsPoint &start, const QgsPoint &end, QgsRenderContext &context, const QRectF &rect, const double, const QgsGeometry &, QgsCallout::QgsCalloutContext & ) const
{
double curvature = mCurvature * 100;
if ( dataDefinedProperties().isActive( QgsCallout::Curvature ) )
{
context.expressionContext().setOriginalValueVariable( curvature );
curvature = dataDefinedProperties().valueAsDouble( QgsCallout::Curvature, context.expressionContext(), curvature );
}

Orientation orientation = mOrientation;
if ( dataDefinedProperties().isActive( QgsCallout::Orientation ) )
{
bool ok = false;
const QString orientationString = dataDefinedProperties().property( QgsCallout::Orientation ).valueAsString( context.expressionContext(), QString(), &ok );
if ( ok )
{
orientation = decodeOrientation( orientationString );
}
}

if ( orientation == Automatic )
{
// to calculate automatically the best curve orientation, we first check which side of the label bounding box
// the callout origin is nearest to
switch ( QgsGeometryUtils::closestSideOfRectangle( rect.right(), rect.bottom(), rect.left(), rect.top(), start.x(), start.y() ) )
{
case 1:
// closest to bottom
if ( qgsDoubleNear( end.x(), start.x() ) )
{
// if vertical line, we bend depending on whether the line sits towards the left or right side of the label
if ( start.x() < ( rect.left() + 0.5 * rect.width() ) )
orientation = CounterClockwise;
else
orientation = Clockwise;
}
else if ( end.x() > start.x() )
orientation = CounterClockwise;
else
orientation = Clockwise;
break;

case 2:
// closest to bottom-right
if ( end.x() < start.x() )
orientation = Clockwise;
else if ( end.y() < start.y() )
orientation = CounterClockwise;
else if ( end.x() - start.x() < end.y() - start.y() )
orientation = Clockwise;
else
orientation = CounterClockwise;
break;

case 3:
// closest to right
if ( qgsDoubleNear( end.y(), start.y() ) )
{
// if horizontal line, we bend depending on whether the line sits towards the top or bottom side of the label
if ( start.y() < ( rect.top() + 0.5 * rect.height() ) )
orientation = Clockwise;
else
orientation = CounterClockwise;
}
else if ( end.y() < start.y() )
orientation = CounterClockwise;
else
orientation = Clockwise;
break;

case 4:
// closest to top-right
if ( end.x() < start.x() )
orientation = CounterClockwise;
else if ( end.y() > start.y() )
orientation = Clockwise;
else if ( end.x() - start.x() < start.y() - end.y() )
orientation = CounterClockwise;
else
orientation = Clockwise;
break;

case 5:
// closest to top
if ( qgsDoubleNear( end.x(), start.x() ) )
{
// if vertical line, we bend depending on whether the line sits towards the left or right side of the label
if ( start.x() < ( rect.left() + 0.5 * rect.width() ) )
orientation = Clockwise;
else
orientation = CounterClockwise;
}
else if ( end.x() < start.x() )
orientation = CounterClockwise;
else
orientation = Clockwise;
break;

case 6:
// closest to top-left
if ( end.x() > start.x() )
orientation = Clockwise;
else if ( end.y() > start.y() )
orientation = CounterClockwise;
else if ( start.x() - end.x() < start.y() - end.y() )
orientation = Clockwise;
else
orientation = CounterClockwise;
break;

case 7:
//closest to left
if ( qgsDoubleNear( end.y(), start.y() ) )
{
// if horizontal line, we bend depending on whether the line sits towards the top or bottom side of the label
if ( start.y() < ( rect.top() + 0.5 * rect.height() ) )
orientation = CounterClockwise;
else
orientation = Clockwise;
}
else if ( end.y() > start.y() )
orientation = CounterClockwise;
else
orientation = Clockwise;
break;

case 8:
//closest to bottom-left
if ( end.x() > start.x() )
orientation = CounterClockwise;
else if ( end.y() < start.y() )
orientation = Clockwise;
else if ( start.x() - end.x() < end.y() - start.y() )
orientation = CounterClockwise;
else
orientation = Clockwise;
break;
}
}

// turn the line into a curved line. We do this by creating a circular string from the callout line's
// start to end point, where the curve point is in the middle of the callout line and perpendicularly offset
// by a proportion of the overall callout line length
const double distance = ( orientation == Clockwise ? 1 : -1 ) * start.distance( end ) * curvature / 100.0;
double midX, midY;
QgsGeometryUtils::perpendicularOffsetPointAlongSegment( start.x(), start.y(), end.x(), end.y(), 0.5, distance, &midX, &midY );

return new QgsCircularString( start, QgsPoint( midX, midY ), end );
}

QgsCurvedLineCallout::Orientation QgsCurvedLineCallout::decodeOrientation( const QString &string )
{
const QString cleaned = string.toLower().trimmed();
if ( cleaned == QLatin1String( "auto" ) )
return Automatic;
if ( cleaned == QLatin1String( "clockwise" ) )
return Clockwise;
if ( cleaned == QLatin1String( "counterclockwise" ) )
return CounterClockwise;
return Automatic;
}

QString QgsCurvedLineCallout::encodeOrientation( QgsCurvedLineCallout::Orientation orientation )
{
switch ( orientation )
{
case QgsCurvedLineCallout::Automatic:
return QStringLiteral( "auto" );
case QgsCurvedLineCallout::Clockwise:
return QStringLiteral( "clockwise" );
case QgsCurvedLineCallout::CounterClockwise:
return QStringLiteral( "counterclockwise" );
}
return QString();
}

QgsCurvedLineCallout::Orientation QgsCurvedLineCallout::orientation() const
{
return mOrientation;
}

void QgsCurvedLineCallout::setOrientation( Orientation orientation )
{
mOrientation = orientation;
}

double QgsCurvedLineCallout::curvature() const
{
return mCurvature;
}

void QgsCurvedLineCallout::setCurvature( double curvature )
{
mCurvature = curvature;
}

0 comments on commit f759c59

Please sign in to comment.