diff --git a/python/CMakeLists.txt b/python/CMakeLists.txt index 6b73ad7db4a..7b6212075f0 100644 --- a/python/CMakeLists.txt +++ b/python/CMakeLists.txt @@ -95,6 +95,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core/3d ${CMAKE_SOURCE_DIR}/src/core/annotations ${CMAKE_SOURCE_DIR}/src/core/auth + ${CMAKE_SOURCE_DIR}/src/core/callouts ${CMAKE_SOURCE_DIR}/src/core/expression ${CMAKE_SOURCE_DIR}/src/core/pal ${CMAKE_SOURCE_DIR}/src/core/diagram diff --git a/python/core/auto_generated/callouts/qgscallout.sip.in b/python/core/auto_generated/callouts/qgscallout.sip.in new file mode 100644 index 00000000000..ec468dd2b9f --- /dev/null +++ b/python/core/auto_generated/callouts/qgscallout.sip.in @@ -0,0 +1,232 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/callouts/qgscallout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ + + + + + +class QgsCallout +{ +%Docstring +Abstract base class for callout renderers. + +Implementations of QgsCallout are responsible for performing the actual render of +callouts, including determining the desired shape of the callout and using any +relevant symbology elements to render them. + +.. versionadded:: 3.10 +%End + +%TypeHeaderCode +#include "qgscallout.h" +%End + public: + + QgsCallout(); +%Docstring +Constructor for QgsCallout. +%End + virtual ~QgsCallout(); + + virtual QString type() const = 0; +%Docstring +Returns a unique string representing the callout type. +%End + + virtual QgsCallout *clone() const = 0 /Factory/; +%Docstring +Duplicates a callout by creating a deep copy of the callout. + +Caller takes ownership of the returned object. +%End + + virtual QVariantMap properties() const; +%Docstring +Returns the properties describing the callout encoded in a +string format. + +Subclasses must ensure that they include the base class' properties() +in their returned value. + +.. seealso:: :py:func:`readProperties` + +.. seealso:: :py:func:`saveProperties` +%End + + virtual void readProperties( const QVariantMap &props, const QgsReadWriteContext &context ); +%Docstring +Reads a string map of an callout's properties and restores the callout +to the state described by the properties map. + +Subclasses must ensure that they call the base class' readProperties() +method. + +.. seealso:: :py:func:`properties` +%End + + virtual bool saveProperties( QDomDocument &doc, QDomElement &element ) const; +%Docstring +Saves the current state of the callout to a DOM ``element``. The default +behavior is to save the properties string map returned by +properties(). + +:return: ``True`` if save was successful + +.. seealso:: :py:func:`readProperties` +%End + + virtual void restoreProperties( const QDomElement &element, const QgsReadWriteContext &context ); +%Docstring +Restores the callout's properties from a DOM element. + +The default behavior is the read the DOM contents and call readProperties() on the subclass. + +.. seealso:: :py:func:`readProperties` +%End + + virtual void startRender( QgsRenderContext &context ); +%Docstring +Prepares the callout for rendering on the specified render ``context``. + +.. warning:: + + This MUST be called prior to calling render() on the callout, and must always + be accompanied by a corresponding call to stopRender(). + +.. seealso:: :py:func:`stopRender` +%End + + virtual void stopRender( QgsRenderContext &context ); +%Docstring +Finalises the callout after a set of rendering operations on the specified render ``context``. + +.. warning:: + + This MUST be called after to after render() operations on the callout, and must always + be accompanied by a corresponding prior call to startRender(). + +.. seealso:: :py:func:`startRender` +%End + + virtual QSet< QString > referencedFields( const QgsRenderContext &context ) const; +%Docstring +Returns the set of attributes referenced by the callout. This includes attributes +required by any data defined properties associated with the callout. + +.. warning:: + + This must only be called after a corresponding call to startRender() with + the same render ``context``. +%End + + void render( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ); +%Docstring +Renders the callout onto the specified render ``context``. + +The ``rect`` argument gives the desired size and position of the body of the callout (e.g. the +actual label geometry). The ``angle`` argument specifies the rotation of the callout body +(in degrees clockwise from horizontal). It is assumed that angle rotation specified via ``angle`` +is applied around the center of ``rect``. + +The ``anchor`` argument dictates the geometry which the callout should connect to. Depending on the +callout subclass and anchor geometry type, the actual shape of the rendered callout may vary. +E.g. a subclass may prefer to attach to the centroid of the ``anchor``, while another subclass may +prefer to attach to the closest point on ``anchor`` instead. + +Both ``rect`` and ``anchor`` must be specified in painter coordinates (i.e. pixels). + +.. warning:: + + A prior call to startRender() must have been made before calling this method, and + after all render() operations are complete a call to stopRender() must be made. +%End + + bool enabled() const; +%Docstring +Returns ``True`` if the the callout is enabled. + +.. seealso:: :py:func:`setEnabled` +%End + + void setEnabled( bool enabled ); +%Docstring +Sets whether the callout is ``enabled``. + +.. seealso:: :py:func:`enabled` +%End + + protected: + + virtual void draw( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ) = 0; +%Docstring +Performs the actual rendering of the callout implementation onto the specified render ``context``. + +The ``rect`` argument gives the desired size and position of the body of the callout (e.g. the +actual label geometry). The ``angle`` argument specifies the rotation of the callout body +(in degrees clockwise from horizontal). It is assumed that angle rotation specified via ``angle`` +is applied around the center of ``rect``. + +The ``anchor`` argument dictates the geometry which the callout should connect to. Depending on the +callout subclass and anchor geometry type, the actual shape of the rendered callout may vary. +E.g. a subclass may prefer to attach to the centroid of the ``anchor``, while another subclass may +prefer to attach to the closest point on ``anchor`` instead. + +Both ``rect`` and ``anchor`` are specified in painter coordinates (i.e. pixels). +%End + +}; + +class QgsSimpleLineCallout : QgsCallout +{ +%Docstring +A simple direct line callout style. + +.. versionadded:: 3.10 +%End + +%TypeHeaderCode +#include "qgscallout.h" +%End + public: + + QgsSimpleLineCallout(); + QgsSimpleLineCallout( const QgsSimpleLineCallout &other ); + + virtual QString type() const; + + virtual QgsSimpleLineCallout *clone() const; + + virtual QVariantMap properties() const; + + virtual void readProperties( const QVariantMap &props, const QgsReadWriteContext &context ); + + virtual void startRender( QgsRenderContext &context ); + + virtual void stopRender( QgsRenderContext &context ); + + virtual QSet< QString > referencedFields( const QgsRenderContext &context ) const; + + + QgsLineSymbol *lineSymbol(); + + void setLineSymbol( QgsLineSymbol *symbol /Transfer/ ); + + protected: + virtual void draw( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ); + + +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/callouts/qgscallout.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.pl again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgspallabeling.sip.in b/python/core/auto_generated/qgspallabeling.sip.in index 94e12fb8edb..86a30de799e 100644 --- a/python/core/auto_generated/qgspallabeling.sip.in +++ b/python/core/auto_generated/qgspallabeling.sip.in @@ -11,7 +11,6 @@ - class QgsLabelPosition { @@ -538,6 +537,28 @@ Sets the label text formatting settings, e.g., font settings, buffer settings, e .. seealso:: :py:func:`format` .. versionadded:: 3.0 +%End + + QgsCallout *callout() const; +%Docstring +Returns the label callout renderer, responsible for drawing label callouts. + +Ownership is not transferred. + +.. seealso:: :py:func:`setCallout` + +.. versionadded:: 3.10 +%End + + void setCallout( QgsCallout *callout /Transfer/ ); +%Docstring +Sets the label ``callout`` renderer, responsible for drawing label callouts. + +Ownership of ``callout`` is transferred to the settings. + +.. seealso:: :py:func:`callout` + +.. versionadded:: 3.10 %End static QPixmap labelSettingsPreviewPixmap( const QgsPalLayerSettings &settings, QSize size, const QString &previewText = QString(), int padding = 0 ); diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index 6571c4e09c4..8fe5cab6d30 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -152,6 +152,7 @@ %Include auto_generated/auth/qgsauthconfig.sip %Include auto_generated/auth/qgsauthmanager.sip %Include auto_generated/auth/qgsauthmethod.sip +%Include auto_generated/callouts/qgscallout.sip %Include auto_generated/diagram/qgsdiagram.sip %Include auto_generated/diagram/qgspiediagram.sip %Include auto_generated/diagram/qgstextdiagram.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index e7739105067..6dd0eb43690 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -17,6 +17,8 @@ SET(QGIS_CORE_SRCS ${CMAKE_SOURCE_DIR}/external/poly2tri/sweep/sweep_context.cc ${CMAKE_SOURCE_DIR}/external/poly2tri/sweep/sweep.cc + callouts/qgscallout.cpp + gps/qgsgpsconnection.cpp gps/qgsgpsconnectionregistry.cpp gps/qgsgpsdconnection.cpp @@ -1046,6 +1048,8 @@ SET(QGIS_CORE_HDRS auth/qgsauthmethodmetadata.h auth/qgsauthmethodregistry.h + callouts/qgscallout.h + diagram/qgsdiagram.h diagram/qgspiediagram.h diagram/qgstextdiagram.h diff --git a/src/core/callouts/qgscallout.cpp b/src/core/callouts/qgscallout.cpp new file mode 100644 index 00000000000..650ffab9f7f --- /dev/null +++ b/src/core/callouts/qgscallout.cpp @@ -0,0 +1,227 @@ +/*************************************************************************** + qgscallout.cpp + ---------------- + begin : July 2019 + copyright : (C) 2019 Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgscallout.h" +#include "qgsrendercontext.h" +#include "qgssymbol.h" +#include "qgslinesymbollayer.h" +#include "qgssymbollayerutils.h" +#include "qgsxmlutils.h" +#include + +QVariantMap QgsCallout::properties() const +{ + QVariantMap props; + props.insert( QStringLiteral( "enabled" ), mEnabled ? "1" : "0" ); + return props; +} + +void QgsCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext & ) +{ + mEnabled = props.value( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt(); +} + +bool QgsCallout::saveProperties( QDomDocument &doc, QDomElement &element ) const +{ + if ( element.isNull() ) + { + return false; + } + + QDomElement calloutPropsElement = QgsXmlUtils::writeVariant( properties(), doc ); + + QDomElement calloutElement = doc.createElement( QStringLiteral( "callout" ) ); + calloutElement.setAttribute( QStringLiteral( "type" ), type() ); + calloutElement.appendChild( calloutPropsElement ); + + element.appendChild( calloutElement ); + return true; +} + +void QgsCallout::restoreProperties( const QDomElement &element, const QgsReadWriteContext &context ) +{ + const QVariantMap props = QgsXmlUtils::readVariant( element.firstChildElement() ).toMap(); + readProperties( props, context ); +} + +void QgsCallout::startRender( QgsRenderContext & ) +{ + +} +void QgsCallout::stopRender( QgsRenderContext & ) +{ + +} + +QSet QgsCallout::referencedFields( const QgsRenderContext & ) const +{ + return QSet< QString >(); +} + +void QgsCallout::render( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ) +{ + if ( !mEnabled ) + return; + +#if 0 // for debugging + QPainter *painter = context.painter(); + painter->save(); + painter->setRenderHint( QPainter::Antialiasing, false ); + painter->translate( rect.center() ); + painter->rotate( -angle ); + + painter->setBrush( QColor( 255, 0, 0, 100 ) ); + painter->setPen( QColor( 255, 0, 0, 150 ) ); + + painter->drawRect( rect.width() * -0.5, rect.height() * -0.5, rect.width(), rect.height() ); + painter->restore(); + + painter->setBrush( QColor( 0, 255, 0, 100 ) ); + painter->setPen( QColor( 0, 255, 0, 150 ) ); + + painter->drawRect( anchor.boundingBox( ).buffered( 30 ).toRectF() ); +#endif + + draw( context, rect, angle, anchor ); +} + +void QgsCallout::setEnabled( bool enabled ) +{ + mEnabled = enabled; +} + + +// +// QgsSimpleLineCallout +// + +QgsSimpleLineCallout::QgsSimpleLineCallout() +{ + mLineSymbol = qgis::make_unique< QgsLineSymbol >( QgsSymbolLayerList() << new QgsSimpleLineSymbolLayer( QColor( 60, 60, 60 ), .3 ) ); + +} + +QgsSimpleLineCallout::QgsSimpleLineCallout( const QgsSimpleLineCallout &other ) + : QgsCallout( other ) + , mLineSymbol( other.mLineSymbol ? other.mLineSymbol->clone() : nullptr ) +{ + +} + +QgsSimpleLineCallout &QgsSimpleLineCallout::operator=( const QgsSimpleLineCallout &other ) +{ + mLineSymbol.reset( other.mLineSymbol ? other.mLineSymbol->clone() : nullptr ); +} + +QString QgsSimpleLineCallout::type() const +{ + return QStringLiteral( "simple" ); +} + +QgsSimpleLineCallout *QgsSimpleLineCallout::clone() const +{ + return new QgsSimpleLineCallout( *this ); +} + +QVariantMap QgsSimpleLineCallout::properties() const +{ + QVariantMap props = QgsCallout::properties(); + + if ( mLineSymbol ) + { + props[ QStringLiteral( "lineSymbol" ) ] = QgsSymbolLayerUtils::symbolProperties( mLineSymbol.get() ); + } + + return props; +} + +void QgsSimpleLineCallout::readProperties( const QVariantMap &props, const QgsReadWriteContext &context ) +{ + QgsCallout::readProperties( props, context ); + + const QString lineSymbolDef = props.value( QStringLiteral( "lineSymbol" ) ).toString(); + QDomDocument doc( QStringLiteral( "symbol" ) ); + doc.setContent( lineSymbolDef ); + QDomElement symbolElem = doc.firstChildElement( QStringLiteral( "symbol" ) ); + std::unique_ptr< QgsLineSymbol > lineSymbol( QgsSymbolLayerUtils::loadSymbol< QgsLineSymbol >( symbolElem, context ) ); + if ( lineSymbol ) + mLineSymbol = std::move( lineSymbol ); +} + +void QgsSimpleLineCallout::startRender( QgsRenderContext &context ) +{ + QgsCallout::startRender( context ); + if ( mLineSymbol ) + mLineSymbol->startRender( context ); +} + +void QgsSimpleLineCallout::stopRender( QgsRenderContext &context ) +{ + QgsCallout::stopRender( context ); + if ( mLineSymbol ) + mLineSymbol->stopRender( context ); +} + +QSet QgsSimpleLineCallout::referencedFields( const QgsRenderContext &context ) const +{ + QSet fields = QgsCallout::referencedFields( context ); + if ( mLineSymbol ) + fields.unite( mLineSymbol->usedAttributes( context ) ); + return fields; +} + +QgsLineSymbol *QgsSimpleLineCallout::lineSymbol() +{ + return mLineSymbol.get(); +} + +void QgsSimpleLineCallout::setLineSymbol( QgsLineSymbol *symbol ) +{ + mLineSymbol.reset( symbol ); +} + +void QgsSimpleLineCallout::draw( QgsRenderContext &context, QRectF rect, const double, const QgsGeometry &anchor ) +{ + QgsGeometry label( QgsGeometry::fromRect( rect ) ); + QgsGeometry line; + switch ( anchor.type() ) + { + case QgsWkbTypes::PointGeometry: + line = label.shortestLine( anchor ); + break; + + case QgsWkbTypes::LineGeometry: + line = label.shortestLine( anchor ); + break; + + case QgsWkbTypes::PolygonGeometry: + if ( label.intersects( anchor ) ) + return; + + line = label.shortestLine( anchor.poleOfInaccessibility( std::max( anchor.boundingBox().width(), anchor.boundingBox().height() ) / 20.0 ) ); // really rough (but quick) pole of inaccessibility + break; + + case QgsWkbTypes::NullGeometry: + case QgsWkbTypes::UnknownGeometry: + return; // shouldn't even get here.. + } + + if ( qgsDoubleNear( line.length(), 0 ) ) + return; + + mLineSymbol->renderPolyline( line.asQPolygonF(), nullptr, context ); +} diff --git a/src/core/callouts/qgscallout.h b/src/core/callouts/qgscallout.h new file mode 100644 index 00000000000..9a959779f65 --- /dev/null +++ b/src/core/callouts/qgscallout.h @@ -0,0 +1,227 @@ +/*************************************************************************** + qgscallout.h + ---------------- + begin : July 2019 + copyright : (C) 2019 Nyall Dawson + email : nyall dot dawson at gmail dot com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSCALLOUT_H +#define QGSCALLOUT_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgsexpressioncontext.h" +#include +#include +#include + +class QgsLineSymbol; +class QgsGeometry; +class QgsRenderContext; + + +/** + * \ingroup core + * \brief Abstract base class for callout renderers. + * + * Implementations of QgsCallout are responsible for performing the actual render of + * callouts, including determining the desired shape of the callout and using any + * relevant symbology elements to render them. + * + * \since QGIS 3.10 + */ +class CORE_EXPORT QgsCallout +{ + + public: + + /** + * Constructor for QgsCallout. + */ + QgsCallout() = default; + virtual ~QgsCallout() = default; + + /** + * Returns a unique string representing the callout type. + */ + virtual QString type() const = 0; + + /** + * Duplicates a callout by creating a deep copy of the callout. + * + * Caller takes ownership of the returned object. + */ + virtual QgsCallout *clone() const = 0 SIP_FACTORY; + + /** + * Returns the properties describing the callout encoded in a + * string format. + * + * Subclasses must ensure that they include the base class' properties() + * in their returned value. + * + * \see readProperties() + * \see saveProperties() + */ + virtual QVariantMap properties() const; + + /** + * Reads a string map of an callout's properties and restores the callout + * to the state described by the properties map. + * + * Subclasses must ensure that they call the base class' readProperties() + * method. + * + * \see properties() + */ + virtual void readProperties( const QVariantMap &props, const QgsReadWriteContext &context ); + + /** + * Saves the current state of the callout to a DOM \a element. The default + * behavior is to save the properties string map returned by + * properties(). + * \returns TRUE if save was successful + * \see readProperties() + */ + virtual bool saveProperties( QDomDocument &doc, QDomElement &element ) const; + + /** + * Restores the callout's properties from a DOM element. + * + * The default behavior is the read the DOM contents and call readProperties() on the subclass. + * + * \see readProperties() + */ + virtual void restoreProperties( const QDomElement &element, const QgsReadWriteContext &context ); + + /** + * Prepares the callout for rendering on the specified render \a context. + * + * \warning This MUST be called prior to calling render() on the callout, and must always + * be accompanied by a corresponding call to stopRender(). + * + * \see stopRender() + */ + virtual void startRender( QgsRenderContext &context ); + + /** + * Finalises the callout after a set of rendering operations on the specified render \a context. + * + * \warning This MUST be called after to after render() operations on the callout, and must always + * be accompanied by a corresponding prior call to startRender(). + * + * \see startRender() + */ + virtual void stopRender( QgsRenderContext &context ); + + /** + * Returns the set of attributes referenced by the callout. This includes attributes + * required by any data defined properties associated with the callout. + * + * \warning This must only be called after a corresponding call to startRender() with + * the same render \a context. + */ + virtual QSet< QString > referencedFields( const QgsRenderContext &context ) const; + + /** + * Renders the callout onto the specified render \a context. + * + * The \a rect argument gives the desired size and position of the body of the callout (e.g. the + * actual label geometry). The \a angle argument specifies the rotation of the callout body + * (in degrees clockwise from horizontal). It is assumed that angle rotation specified via \a angle + * is applied around the center of \a rect. + * + * The \a anchor argument dictates the geometry which the callout should connect to. Depending on the + * callout subclass and anchor geometry type, the actual shape of the rendered callout may vary. + * E.g. a subclass may prefer to attach to the centroid of the \a anchor, while another subclass may + * prefer to attach to the closest point on \a anchor instead. + * + * Both \a rect and \a anchor must be specified in painter coordinates (i.e. pixels). + * + * \warning A prior call to startRender() must have been made before calling this method, and + * after all render() operations are complete a call to stopRender() must be made. + */ + void render( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ); + + /** + * Returns TRUE if the the callout is enabled. + * \see setEnabled() + */ + bool enabled() const { return mEnabled; } + + /** + * Sets whether the callout is \a enabled. + * \see enabled() + */ + void setEnabled( bool enabled ); + + protected: + + /** + * Performs the actual rendering of the callout implementation onto the specified render \a context. + * + * The \a rect argument gives the desired size and position of the body of the callout (e.g. the + * actual label geometry). The \a angle argument specifies the rotation of the callout body + * (in degrees clockwise from horizontal). It is assumed that angle rotation specified via \a angle + * is applied around the center of \a rect. + * + * The \a anchor argument dictates the geometry which the callout should connect to. Depending on the + * callout subclass and anchor geometry type, the actual shape of the rendered callout may vary. + * E.g. a subclass may prefer to attach to the centroid of the \a anchor, while another subclass may + * prefer to attach to the closest point on \a anchor instead. + * + * Both \a rect and \a anchor are specified in painter coordinates (i.e. pixels). + */ + virtual void draw( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ) = 0; + + private: + + bool mEnabled = true; + +}; + +/** + * \ingroup core + * \brief A simple direct line callout style. + * + * \since QGIS 3.10 + */ +class CORE_EXPORT QgsSimpleLineCallout : public QgsCallout +{ + public: + + QgsSimpleLineCallout(); + QgsSimpleLineCallout( const QgsSimpleLineCallout &other ); + QgsSimpleLineCallout &operator=( const QgsSimpleLineCallout & ); + + QString type() const override; + QgsSimpleLineCallout *clone() const override; + QVariantMap properties() const override; + void readProperties( const QVariantMap &props, const QgsReadWriteContext &context ) override; + void startRender( QgsRenderContext &context ) override; + void stopRender( QgsRenderContext &context ) override; + QSet< QString > referencedFields( const QgsRenderContext &context ) const override; + + QgsLineSymbol *lineSymbol(); + + void setLineSymbol( QgsLineSymbol *symbol SIP_TRANSFER ); + + protected: + void draw( QgsRenderContext &context, QRectF rect, const double angle, const QgsGeometry &anchor ) override; + + private: + + std::unique_ptr< QgsLineSymbol > mLineSymbol; +}; + +#endif // QGSCALLOUT_H + diff --git a/src/core/qgspallabeling.cpp b/src/core/qgspallabeling.cpp index 70a712564c2..acf57618534 100644 --- a/src/core/qgspallabeling.cpp +++ b/src/core/qgspallabeling.cpp @@ -63,6 +63,7 @@ #include "qgscurvepolygon.h" #include "qgsmessagelog.h" #include "qgsgeometrycollection.h" +#include "callouts/qgscallout.h" #include using namespace pal; @@ -303,6 +304,8 @@ QgsPalLayerSettings::QgsPalLayerSettings() obstacleFactor = 1.0; obstacleType = PolygonInterior; zIndex = 0.0; + + mCallout = qgis::make_unique< QgsSimpleLineCallout >(); } Q_NOWARN_DEPRECATED_POP @@ -395,6 +398,8 @@ QgsPalLayerSettings &QgsPalLayerSettings::operator=( const QgsPalLayerSettings & mFormat = s.mFormat; mDataDefinedProperties = s.mDataDefinedProperties; + mCallout.reset( s.mCallout ? s.mCallout->clone() : nullptr ); + geometryGenerator = s.geometryGenerator; geometryGeneratorEnabled = s.geometryGeneratorEnabled; geometryGeneratorType = s.geometryGeneratorType; @@ -512,10 +517,19 @@ bool QgsPalLayerSettings::prepare( QgsRenderContext &context, QSet &att } } + if ( mCallout ) + { + const auto referencedColumns = mCallout->referencedFields( context ); + for ( const QString &name : referencedColumns ) + { + attributeNames.insert( name ); + } + } + return true; } -void QgsPalLayerSettings::startRender( QgsRenderContext & ) +void QgsPalLayerSettings::startRender( QgsRenderContext &context ) { if ( mRenderStarted ) { @@ -523,10 +537,15 @@ void QgsPalLayerSettings::startRender( QgsRenderContext & ) return; } + if ( mCallout ) + { + mCallout->startRender( context ); + } + mRenderStarted = true; } -void QgsPalLayerSettings::stopRender( QgsRenderContext & ) +void QgsPalLayerSettings::stopRender( QgsRenderContext &context ) { if ( !mRenderStarted ) { @@ -534,6 +553,11 @@ void QgsPalLayerSettings::stopRender( QgsRenderContext & ) return; } + if ( mCallout ) + { + mCallout->stopRender( context ); + } + mRenderStarted = false; } @@ -1108,7 +1132,11 @@ void QgsPalLayerSettings::readXml( const QDomElement &elem, const QgsReadWriteCo mDataDefinedProperties.setProperty( MaxScale, QgsProperty() ); } + // TODO - replace with registry when multiple callout styles exist + if ( !mCallout ) + mCallout = qgis::make_unique< QgsSimpleLineCallout >(); + mCallout->restoreProperties( elem.firstChildElement( QStringLiteral( "callout" ) ), context ); } @@ -1204,9 +1232,20 @@ QDomElement QgsPalLayerSettings::writeXml( QDomDocument &doc, const QgsReadWrite elem.appendChild( placementElem ); elem.appendChild( renderingElem ); elem.appendChild( ddElem ); + + if ( mCallout ) + { + mCallout->saveProperties( doc, elem ); + } + return elem; } +void QgsPalLayerSettings::setCallout( QgsCallout *callout ) +{ + mCallout.reset( callout ); +} + QPixmap QgsPalLayerSettings::labelSettingsPreviewPixmap( const QgsPalLayerSettings &settings, QSize size, const QString &previewText, int padding ) { // for now, just use format diff --git a/src/core/qgspallabeling.h b/src/core/qgspallabeling.h index de4be066432..6acb8231288 100644 --- a/src/core/qgspallabeling.h +++ b/src/core/qgspallabeling.h @@ -70,7 +70,7 @@ class QgsVectorLayerLabelProvider; class QgsDxfExport; class QgsVectorLayerDiagramProvider; class QgsExpressionContext; - +class QgsCallout; /** * \ingroup core @@ -951,6 +951,26 @@ class CORE_EXPORT QgsPalLayerSettings */ void setFormat( const QgsTextFormat &format ) { mFormat = format; } + /** + * Returns the label callout renderer, responsible for drawing label callouts. + * + * Ownership is not transferred. + * + * \see setCallout() + * \since QGIS 3.10 + */ + QgsCallout *callout() const { return mCallout.get(); } + + /** + * Sets the label \a callout renderer, responsible for drawing label callouts. + * + * Ownership of \a callout is transferred to the settings. + + * \see callout() + * \since QGIS 3.10 + */ + void setCallout( QgsCallout *callout SIP_TRANSFER ); + /** * Returns a pixmap preview for label \a settings. * \param settings label settings @@ -1050,6 +1070,8 @@ class CORE_EXPORT QgsPalLayerSettings QgsTextFormat mFormat; + std::unique_ptr< QgsCallout > mCallout; + QgsExpression mGeometryGeneratorExpression; bool mRenderStarted = false; diff --git a/src/core/qgsvectorlayerlabelprovider.cpp b/src/core/qgsvectorlayerlabelprovider.cpp index c378668c44e..a823a9f9f8e 100644 --- a/src/core/qgsvectorlayerlabelprovider.cpp +++ b/src/core/qgsvectorlayerlabelprovider.cpp @@ -30,6 +30,7 @@ #include "feature.h" #include "labelposition.h" +#include "callouts/qgscallout.h" #include @@ -347,6 +348,20 @@ void QgsVectorLayerLabelProvider::drawLabel( QgsRenderContext &context, pal::Lab // Render the components of a label in reverse order // (backgrounds -> text) + // render callout + if ( mSettings.callout() ) + { + QgsMapToPixel xform = context.mapToPixel(); + xform.setMapRotation( 0, 0, 0 ); + QPointF outPt = xform.transform( label->getX(), label->getY() ).toQPointF(); + QgsPointXY outPt2 = xform.transform( label->getX() + label->getWidth(), label->getY() + label->getHeight() ); + QRectF rect( outPt.x(), outPt.y(), outPt2.x() - outPt.x(), outPt2.y() - outPt.y() ); + + QgsGeometry g( QgsGeos::fromGeos( label->getFeaturePart()->feature()->geometry() ) ); + g.transform( xform.transform() ); + mSettings.callout()->render( context, rect, label->getAlpha() * 180 / M_PI, g ); + } + if ( tmpLyr.format().shadow().enabled() && tmpLyr.format().shadow().shadowPlacement() == QgsTextShadowSettings::ShadowLowest ) { QgsTextFormat format = tmpLyr.format(); diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index b2638311eb5..62b705df695 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -999,6 +999,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/src/core ${CMAKE_SOURCE_DIR}/src/core/annotations ${CMAKE_SOURCE_DIR}/src/core/auth + ${CMAKE_SOURCE_DIR}/src/core/callouts ${CMAKE_SOURCE_DIR}/src/core/fieldformatter ${CMAKE_SOURCE_DIR}/src/core/geometry ${CMAKE_SOURCE_DIR}/src/core/layertree diff --git a/src/gui/qgslabelinggui.cpp b/src/gui/qgslabelinggui.cpp index ecf35a15638..0dca45ecd1c 100644 --- a/src/gui/qgslabelinggui.cpp +++ b/src/gui/qgslabelinggui.cpp @@ -25,6 +25,7 @@ #include "qgsexpressioncontextutils.h" #include "qgsexpressionbuilderdialog.h" #include "qgsstylesavedialog.h" +#include "qgscallout.h" #include #include @@ -274,6 +275,12 @@ void QgsLabelingGui::setLayer( QgsMapLayer *mapLayer ) mDataDefinedProperties = mSettings.dataDefinedProperties(); + // callout settings, to move to custom widget when multiple styles exist + mCalloutLineStyleButton->setLayer( mLayer ); + if ( mSettings.callout() ) + mCalloutLineStyleButton->setSymbol( static_cast< QgsSimpleLineCallout * >( mSettings.callout() )->lineSymbol()->clone() ); + mCalloutsDrawCheckBox->setChecked( mSettings.callout()->enabled() ); + updatePlacementWidgets(); updateLinePlacementOptions(); @@ -454,6 +461,13 @@ QgsPalLayerSettings QgsLabelingGui::layerSettings() lyr.setDataDefinedProperties( mDataDefinedProperties ); + // callout settings, to move to custom widget when multiple styles exist + // callout settings, to move to custom widget when multiple styles exist + std::unique_ptr< QgsSimpleLineCallout > callout = qgis::make_unique< QgsSimpleLineCallout >(); + callout->setEnabled( mCalloutsDrawCheckBox->isChecked() ); + callout->setLineSymbol( mCalloutLineStyleButton->clonedSymbol< QgsLineSymbol >() ); + lyr.setCallout( callout.release() ); + return lyr; } diff --git a/src/gui/qgstextformatwidget.cpp b/src/gui/qgstextformatwidget.cpp index 8e7a1d2cc75..93af87ff90f 100644 --- a/src/gui/qgstextformatwidget.cpp +++ b/src/gui/qgstextformatwidget.cpp @@ -322,6 +322,12 @@ void QgsTextFormatWidget::initWidget() setDockMode( false ); + // Callout options - to move to custom widgets when multiple callout styles exist + mCalloutLineStyleButton->setSymbolType( QgsSymbol::Line ); + mCalloutLineStyleButton->setDialogTitle( tr( "Callout Symbol" ) ); + mCalloutLineStyleButton->registerExpressionContextGenerator( this ); + mCalloutLineStyleButton->setMapCanvas( mMapCanvas ); + QList widgets; widgets << btnBufferColor << btnTextColor @@ -539,7 +545,9 @@ void QgsTextFormatWidget::initWidget() << mGeometryGenerator << mGeometryGeneratorType << mLinePlacementFlagsDDBtn - << mBackgroundSymbolButton; + << mBackgroundSymbolButton + << mCalloutLineStyleButton + << mCalloutsDrawCheckBox; connectValueChanged( widgets, SLOT( updatePreview() ) ); connect( mQuadrantBtnGrp, static_cast( &QButtonGroup::buttonClicked ), this, &QgsTextFormatWidget::updatePreview ); diff --git a/src/ui/qgstextformatwidgetbase.ui b/src/ui/qgstextformatwidgetbase.ui index 5a911d3410a..c9df2bc69a6 100644 --- a/src/ui/qgstextformatwidgetbase.ui +++ b/src/ui/qgstextformatwidgetbase.ui @@ -7,7 +7,7 @@ 0 0 828 - 1081 + 608 @@ -112,7 +112,7 @@ 0 0 - 807 + 806 300 @@ -335,7 +335,7 @@ QTabWidget::Rounded - 0 + 5 @@ -397,6 +397,18 @@ + + + + :/images/themes/default/mIconSnappingSegment.svg:/images/themes/default/mIconSnappingSegment.svg + + + + + + Callouts + + @@ -534,6 +546,18 @@ :/images/themes/default/propertyicons/labelshadow.svg:/images/themes/default/propertyicons/labelshadow.svg + + + Callouts + + + Callouts + + + + :/images/themes/default/mIconSnappingSegment.svg:/images/themes/default/mIconSnappingSegment.svg + + Placement @@ -577,21 +601,21 @@ - 6 + 0 - 6 + 0 0 - 6 + 0 - 4 + 5 @@ -620,8 +644,8 @@ 0 0 - 770 - 866 + 311 + 292 @@ -1203,8 +1227,8 @@ font-style: italic; 0 0 - 770 - 847 + 374 + 644 @@ -2078,8 +2102,8 @@ font-style: italic; 0 0 - 770 - 866 + 308 + 308 @@ -2424,8 +2448,8 @@ font-style: italic; 0 0 - 770 - 866 + 474 + 786 @@ -3185,8 +3209,8 @@ font-style: italic; 0 0 - 770 - 866 + 768 + 457 @@ -3586,6 +3610,151 @@ font-style: italic; + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + QFrame::NoFrame + + + true + + + + + 0 + 0 + 782 + 387 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + Callouts + + + + + + + + 20 + + + 0 + + + 0 + + + 0 + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + Qt::Horizontal + + + + 0 + 20 + + + + + + + + 20 + + + + + + 0 + 0 + + + + Symbol… + + + + + + + Line style + + + + + + + + + + 0 + 0 + + + + Draw callouts + + + + + + + + + + + @@ -3626,8 +3795,8 @@ font-style: italic; 0 0 - 335 - 884 + 768 + 1095 @@ -5334,8 +5503,8 @@ font-style: italic; 0 0 - 302 - 674 + 415 + 852 @@ -6622,7 +6791,6 @@ font-style: italic; -