From 15a7079a3941a7adfbb8682692abfd6e0837f9b8 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 2 Sep 2024 10:31:59 +1000 Subject: [PATCH] [api] Implementation of labeling engine rules See https://github.com/qgis/QGIS-Enhancement-Proposals/issues/299 Implements the API framework for setting advanced labeling engine rules for a project, and implements 4 initial rule types: - QgsLabelingEngineRuleMinimumDistanceLabelToFeature: prevents labels being placed too *close* to features from a different layer - QgsLabelingEngineRuleMaximumDistanceLabelToFeature: prevents labels being placed too *far* from features from a different layer - QgsLabelingEngineRuleMinimumDistanceLabelToLabel: prevents labels being placed too close to labels from a different layer - QgsLabelingEngineRuleAvoidLabelOverlapWithFeature: prevents labels being placed overlapping features from a different layer (note that the first 3 rules require a build based on GEOS >= 3.10, they are not available for older GEOS builds) Also implements a registry for storing available rule classes, and serialization of rules and configuration in QGIS projects --- doc/CMakeLists.txt | 1 + .../core/auto_additions/qgsapplication.py | 1 + .../auto_additions/qgslabelingenginerule.py | 9 + .../qgslabelingenginerule_impl.py | 21 + .../qgslabelingengineruleregistry.py | 5 + .../labeling/qgslabelingenginesettings.sip.in | 99 +++- .../rules/qgslabelingenginerule.sip.in | 188 ++++++ .../rules/qgslabelingenginerule_impl.sip.in | 393 +++++++++++++ .../qgslabelingengineruleregistry.sip.in | 82 +++ .../core/auto_generated/qgsapplication.sip.in | 7 + python/PyQt6/core/core_auto.sip | 3 + python/core/auto_additions/qgsapplication.py | 1 + .../auto_additions/qgslabelingenginerule.py | 9 + .../qgslabelingenginerule_impl.py | 21 + .../qgslabelingengineruleregistry.py | 5 + .../labeling/qgslabelingenginesettings.sip.in | 99 +++- .../rules/qgslabelingenginerule.sip.in | 188 ++++++ .../rules/qgslabelingenginerule_impl.sip.in | 393 +++++++++++++ .../qgslabelingengineruleregistry.sip.in | 82 +++ .../core/auto_generated/qgsapplication.sip.in | 7 + python/core/core_auto.sip | 3 + src/core/CMakeLists.txt | 7 + src/core/labeling/qgslabelingengine.cpp | 14 + src/core/labeling/qgslabelingengine.h | 12 + .../labeling/qgslabelingenginesettings.cpp | 117 ++++ src/core/labeling/qgslabelingenginesettings.h | 104 +++- .../labeling/rules/qgslabelingenginerule.cpp | 78 +++ .../labeling/rules/qgslabelingenginerule.h | 238 ++++++++ .../rules/qgslabelingenginerule_impl.cpp | 548 ++++++++++++++++++ .../rules/qgslabelingenginerule_impl.h | 416 +++++++++++++ .../rules/qgslabelingengineruleregistry.cpp | 72 +++ .../rules/qgslabelingengineruleregistry.h | 96 +++ src/core/maprenderer/qgsmaprendererjob.cpp | 2 + src/core/project/qgsproject.cpp | 11 + src/core/qgsapplication.cpp | 12 + src/core/qgsapplication.h | 9 + tests/src/python/CMakeLists.txt | 1 + .../src/python/test_qgslabelingenginerule.py | 313 ++++++++++ 38 files changed, 3663 insertions(+), 4 deletions(-) create mode 100644 python/PyQt6/core/auto_additions/qgslabelingenginerule.py create mode 100644 python/PyQt6/core/auto_additions/qgslabelingenginerule_impl.py create mode 100644 python/PyQt6/core/auto_additions/qgslabelingengineruleregistry.py create mode 100644 python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in create mode 100644 python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in create mode 100644 python/PyQt6/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in create mode 100644 python/core/auto_additions/qgslabelingenginerule.py create mode 100644 python/core/auto_additions/qgslabelingenginerule_impl.py create mode 100644 python/core/auto_additions/qgslabelingengineruleregistry.py create mode 100644 python/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in create mode 100644 python/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in create mode 100644 python/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in create mode 100644 src/core/labeling/rules/qgslabelingenginerule.cpp create mode 100644 src/core/labeling/rules/qgslabelingenginerule.h create mode 100644 src/core/labeling/rules/qgslabelingenginerule_impl.cpp create mode 100644 src/core/labeling/rules/qgslabelingenginerule_impl.h create mode 100644 src/core/labeling/rules/qgslabelingengineruleregistry.cpp create mode 100644 src/core/labeling/rules/qgslabelingengineruleregistry.h create mode 100644 tests/src/python/test_qgslabelingenginerule.py diff --git a/doc/CMakeLists.txt b/doc/CMakeLists.txt index 990bc90c376d..d9572f277a82 100644 --- a/doc/CMakeLists.txt +++ b/doc/CMakeLists.txt @@ -69,6 +69,7 @@ if(WITH_APIDOC) ${CMAKE_SOURCE_DIR}/src/core/geometry ${CMAKE_SOURCE_DIR}/src/core/gps ${CMAKE_SOURCE_DIR}/src/core/labeling + ${CMAKE_SOURCE_DIR}/src/core/labeling/rules ${CMAKE_SOURCE_DIR}/src/core/layertree ${CMAKE_SOURCE_DIR}/src/core/layout ${CMAKE_SOURCE_DIR}/src/core/locator diff --git a/python/PyQt6/core/auto_additions/qgsapplication.py b/python/PyQt6/core/auto_additions/qgsapplication.py index 84157b930c15..aec18e23f4e6 100644 --- a/python/PyQt6/core/auto_additions/qgsapplication.py +++ b/python/PyQt6/core/auto_additions/qgsapplication.py @@ -133,6 +133,7 @@ QgsApplication.renderer3DRegistry = staticmethod(QgsApplication.renderer3DRegistry) QgsApplication.symbol3DRegistry = staticmethod(QgsApplication.symbol3DRegistry) QgsApplication.scaleBarRendererRegistry = staticmethod(QgsApplication.scaleBarRendererRegistry) + QgsApplication.labelingEngineRuleRegistry = staticmethod(QgsApplication.labelingEngineRuleRegistry) QgsApplication.projectStorageRegistry = staticmethod(QgsApplication.projectStorageRegistry) QgsApplication.layerMetadataProviderRegistry = staticmethod(QgsApplication.layerMetadataProviderRegistry) QgsApplication.externalStorageRegistry = staticmethod(QgsApplication.externalStorageRegistry) diff --git a/python/PyQt6/core/auto_additions/qgslabelingenginerule.py b/python/PyQt6/core/auto_additions/qgslabelingenginerule.py new file mode 100644 index 000000000000..c994a01988c9 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgslabelingenginerule.py @@ -0,0 +1,9 @@ +# The following has been generated automatically from src/core/labeling/rules/qgslabelingenginerule.h +try: + QgsLabelingEngineContext.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsAbstractLabelingEngineRule.__group__ = ['labeling', 'rules'] +except NameError: + pass diff --git a/python/PyQt6/core/auto_additions/qgslabelingenginerule_impl.py b/python/PyQt6/core/auto_additions/qgslabelingenginerule_impl.py new file mode 100644 index 000000000000..8393328f6a10 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgslabelingenginerule_impl.py @@ -0,0 +1,21 @@ +# The following has been generated automatically from src/core/labeling/rules/qgslabelingenginerule_impl.h +try: + QgsAbstractLabelingEngineRuleDistanceFromFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleMinimumDistanceLabelToFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass diff --git a/python/PyQt6/core/auto_additions/qgslabelingengineruleregistry.py b/python/PyQt6/core/auto_additions/qgslabelingengineruleregistry.py new file mode 100644 index 000000000000..a2a0bff3f7d7 --- /dev/null +++ b/python/PyQt6/core/auto_additions/qgslabelingengineruleregistry.py @@ -0,0 +1,5 @@ +# The following has been generated automatically from src/core/labeling/rules/qgslabelingengineruleregistry.h +try: + QgsLabelingEngineRuleRegistry.__group__ = ['labeling', 'rules'] +except NameError: + pass diff --git a/python/PyQt6/core/auto_generated/labeling/qgslabelingenginesettings.sip.in b/python/PyQt6/core/auto_generated/labeling/qgslabelingenginesettings.sip.in index bfe003e1a39c..2a9ad1d288b0 100644 --- a/python/PyQt6/core/auto_generated/labeling/qgslabelingenginesettings.sip.in +++ b/python/PyQt6/core/auto_generated/labeling/qgslabelingenginesettings.sip.in @@ -30,6 +30,9 @@ Stores global configuration for labeling engine }; QgsLabelingEngineSettings(); + ~QgsLabelingEngineSettings(); + + QgsLabelingEngineSettings( const QgsLabelingEngineSettings &other ); void clear(); %Docstring @@ -125,13 +128,66 @@ Which search method to use for removal collisions between labels Chain is always used. %End + void readSettingsFromProject( QgsProject *project ); %Docstring Read configuration of the labeling engine from a project + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.readXml` must be called to completely restore the object's state from a project. %End + void writeSettingsToProject( QgsProject *project ); %Docstring -Write configuration of the labeling engine to a project +Write configuration of the labeling engine to a project. + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.writeXml` must be called to completely store the object's state in a project. +%End + + void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; +%Docstring +Writes the label engine settings to an XML ``element``. + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.writeSettingsToProject` must be called to completely store the object's state in a project. + +.. seealso:: :py:func:`readXml` + +.. seealso:: :py:func:`writeSettingsToProject` + +.. versionadded:: 3.40 +%End + + void readXml( const QDomElement &element, const QgsReadWriteContext &context ); +%Docstring +Reads the label engine settings from an XML ``element``. + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.readSettingsFromProject` must be called to completely restore the object's state from a project. + +.. note:: + + :py:func:`~QgsLabelingEngineSettings.resolveReferences` must be called following this method. + +.. seealso:: :py:func:`writeXml` + +.. seealso:: :py:func:`readSettingsFromProject` + +.. versionadded:: 3.40 +%End + + void resolveReferences( const QgsProject *project ); +%Docstring +Resolves reference to layers from stored layer ID. + +Should be called following a call :py:func:`~QgsLabelingEngineSettings.readXml`. + +.. versionadded:: 3.40 %End @@ -187,6 +243,47 @@ Sets the placement engine ``version``, which dictates how the label placement pr .. seealso:: :py:func:`placementVersion` .. versionadded:: 3.10.2 +%End + + QList< QgsAbstractLabelingEngineRule * > rules(); +%Docstring +Returns a list of labeling engine rules which must be satifisfied +while placing labels. + +.. seealso:: :py:func:`addRule` + +.. seealso:: :py:func:`setRules` + +.. versionadded:: 3.40 +%End + + + void addRule( QgsAbstractLabelingEngineRule *rule /Transfer/ ); +%Docstring +Adds a labeling engine ``rule`` which must be satifisfied +while placing labels. + +Ownership of the rule is transferred to the settings. + +.. seealso:: :py:func:`rules` + +.. seealso:: :py:func:`setRules` + +.. versionadded:: 3.40 +%End + + void setRules( const QList< QgsAbstractLabelingEngineRule * > &rules /Transfer/ ); +%Docstring +Sets the labeling engine ``rules`` which must be satifisfied +while placing labels. + +Ownership of the rules are transferred to the settings. + +.. seealso:: :py:func:`addRule` + +.. seealso:: :py:func:`rules` + +.. versionadded:: 3.40 %End }; diff --git a/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in b/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in new file mode 100644 index 000000000000..02d71e9dcbdf --- /dev/null +++ b/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in @@ -0,0 +1,188 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + +class QgsLabelingEngineContext +{ +%Docstring(signature="appended") +Encapsulates the context for a labeling engine run. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule.h" +%End + public: + + QgsLabelingEngineContext( QgsRenderContext &renderContext ); +%Docstring +Constructor for QgsLabelingEngineContext. +%End + + + QgsRenderContext &renderContext(); +%Docstring +Returns a reference to the context's render context. +%End + + + QgsRectangle extent() const; +%Docstring +Returns the map extent defining the limits for labeling. + +.. seealso:: :py:func:`mapBoundaryGeometry` + +.. seealso:: :py:func:`setExtent` +%End + + void setExtent( const QgsRectangle &extent ); +%Docstring +Sets the map ``extent`` defining the limits for labeling. + +.. seealso:: :py:func:`setMapBoundaryGeometry` + +.. seealso:: :py:func:`extent` +%End + + QgsGeometry mapBoundaryGeometry() const; +%Docstring +Returns the map label boundary geometry, which defines the limits within which labels may be placed +in the map. + +The map boundary geometry specifies the actual geometry of the map +boundary, which will be used to detect whether a label is visible (or partially visible) in +the rendered map. This may differ from :py:func:`~QgsLabelingEngineContext.extent` in the case of rotated or non-rectangular +maps. + +.. seealso:: :py:func:`setMapBoundaryGeometry` + +.. seealso:: :py:func:`extent` +%End + + void setMapBoundaryGeometry( const QgsGeometry &geometry ); +%Docstring +Sets the map label boundary ``geometry``, which defines the limits within which labels may be placed +in the map. + +The map boundary geometry specifies the actual geometry of the map +boundary, which will be used to detect whether a label is visible (or partially visible) in +the rendered map. This may differ from :py:func:`~QgsLabelingEngineContext.extent` in the case of rotated or non-rectangular +maps. + +.. seealso:: :py:func:`setExtent` + +.. seealso:: :py:func:`mapBoundaryGeometry` +%End + + private: + QgsLabelingEngineContext( const QgsLabelingEngineContext &other ); +}; + +class QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +Abstract base class for labeling engine rules. + +Labeling engine rules implement custom logic to modify the labeling solution for a map render, +e.g. by preventing labels being placed which violate custom constraints. + +.. note:: + + :py:class:`QgsAbstractLabelingEngineRule` cannot be subclassed in Python. Use one of the existing + implementations of this class instead. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule.h" +%End +%ConvertToSubClassCode + if ( sipCpp->id() == "minimumDistanceLabelToFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleMinimumDistanceLabelToFeature; + } + else if ( sipCpp->id() == "minimumDistanceLabelToLabel" ) + { + sipType = sipType_QgsLabelingEngineRuleMinimumDistanceLabelToLabel; + } + else if ( sipCpp->id() == "maximumDistanceLabelToFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleMaximumDistanceLabelToFeature; + } + else if ( sipCpp->id() == "avoidLabelOverlapWithFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleAvoidLabelOverlapWithFeature; + } + else + { + sipType = 0; + } +%End + public: + + virtual ~QgsAbstractLabelingEngineRule(); + + virtual QgsAbstractLabelingEngineRule *clone() const = 0 /Factory/; +%Docstring +Creates a clone of this rule. + +The caller takes ownership of the returned object. +%End + + virtual QString id() const = 0; +%Docstring +Returns a string uniquely identifying the rule subclass. +%End + + virtual bool prepare( QgsRenderContext &context ) = 0; +%Docstring +Prepares the rule. + +This must be called on the main render thread, prior to commencing the render operation. Thread sensitive +logic (such as creation of feature sources) can be performed in this method. +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const = 0; +%Docstring +Writes the rule properties to an XML ``element``. + +.. seealso:: :py:func:`readXml` +%End + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ) = 0; +%Docstring +Reads the rule properties from an XML ``element``. + +.. seealso:: :py:func:`resolveReferences` + +.. seealso:: :py:func:`writeXml` +%End + + virtual void resolveReferences( const QgsProject *project ); +%Docstring +Resolves reference to layers from stored layer ID. + +Should be called following a call :py:func:`~QgsAbstractLabelingEngineRule.readXml`. +%End + + + + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in b/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in new file mode 100644 index 000000000000..cb86ee5368b8 --- /dev/null +++ b/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in @@ -0,0 +1,393 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule_impl.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + +class QgsAbstractLabelingEngineRuleDistanceFromFeature : QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +Base class for labeling engine rules which prevents labels being placed too close or to far from features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + + QgsAbstractLabelingEngineRuleDistanceFromFeature(); + ~QgsAbstractLabelingEngineRuleDistanceFromFeature(); + virtual bool prepare( QgsRenderContext &context ); + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void resolveReferences( const QgsProject *project ); + + + QgsVectorLayer *labeledLayer(); +%Docstring +Returns the layer providing the labels. + +.. seealso:: :py:func:`setLabeledLayer` +%End + + void setLabeledLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels. + +.. seealso:: :py:func:`labeledLayer` +%End + + QgsVectorLayer *targetLayer(); +%Docstring +Returns the layer providing the features which labels must be distant from (or close to). + +.. seealso:: :py:func:`setTargetLayer` +%End + + void setTargetLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the features which labels must be distant from (or close to). + +.. seealso:: :py:func:`targetLayer` +%End + + double distance() const; +%Docstring +Returns the acceptable distance threshold between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`setDistance` + +.. seealso:: :py:func:`distanceUnits` +%End + + void setDistance( double distance ); +%Docstring +Sets the acceptable ``distance`` threshold between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`distance` + +.. seealso:: :py:func:`setDistanceUnits` +%End + + Qgis::RenderUnit distanceUnit() const; +%Docstring +Returns the units for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnit` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`distanceUnit` + +.. seealso:: :py:func:`setDistance` +%End + + const QgsMapUnitScale &distanceUnitScale() const; +%Docstring +Returns the scaling for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnitScale` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the ``scale`` for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`distanceUnitScale` + +.. seealso:: :py:func:`setDistance` +%End + + double cost() const; +%Docstring +Returns the penalty cost incurred when the rule is violated. + +This is a value between 0 and 10, where 10 indicates that the rule must never be violated, +and 1-9 = nice to have if possible, where higher numbers will try harder to avoid violating the rule. + +.. seealso:: :py:func:`setCost` +%End + + void setCost( double cost ); +%Docstring +Sets the penalty ``cost`` incurred when the rule is violated. + +This is a value between 0 and 10, where 10 indicates that the rule must never be violated, +and 1-9 = nice to have if possible, where higher numbers will try harder to avoid violating the rule. + +.. seealso:: :py:func:`cost` +%End + + protected: + + void copyCommonProperties( QgsAbstractLabelingEngineRuleDistanceFromFeature *other ) const; +%Docstring +Copies common properties from this object to an ``other``. +%End + + + private: + QgsAbstractLabelingEngineRuleDistanceFromFeature( const QgsAbstractLabelingEngineRuleDistanceFromFeature &other ); +}; + + +class QgsLabelingEngineRuleMinimumDistanceLabelToFeature : QgsAbstractLabelingEngineRuleDistanceFromFeature +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed too close to features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + + QgsLabelingEngineRuleMinimumDistanceLabelToFeature(); + ~QgsLabelingEngineRuleMinimumDistanceLabelToFeature(); + virtual QgsLabelingEngineRuleMinimumDistanceLabelToFeature *clone() const /Factory/; + + virtual QString id() const; + + + private: + QgsLabelingEngineRuleMinimumDistanceLabelToFeature( const QgsLabelingEngineRuleMinimumDistanceLabelToFeature & ); +}; + + +class QgsLabelingEngineRuleMaximumDistanceLabelToFeature : QgsAbstractLabelingEngineRuleDistanceFromFeature +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed too far from features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature(); + ~QgsLabelingEngineRuleMaximumDistanceLabelToFeature(); + virtual QgsLabelingEngineRuleMaximumDistanceLabelToFeature *clone() const /Factory/; + + virtual QString id() const; + + + private: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature( const QgsLabelingEngineRuleMaximumDistanceLabelToFeature & ); +}; + +class QgsLabelingEngineRuleMinimumDistanceLabelToLabel : QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed too close to labels from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel(); + ~QgsLabelingEngineRuleMinimumDistanceLabelToLabel(); + + virtual QgsLabelingEngineRuleMinimumDistanceLabelToLabel *clone() const /Factory/; + + virtual QString id() const; + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void resolveReferences( const QgsProject *project ); + + virtual bool prepare( QgsRenderContext &context ); + + + QgsVectorLayer *labeledLayer(); +%Docstring +Returns the layer providing the labels. + +.. seealso:: :py:func:`setLabeledLayer` +%End + + void setLabeledLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels. + +.. seealso:: :py:func:`labeledLayer` +%End + + QgsVectorLayer *targetLayer(); +%Docstring +Returns the layer providing the labels which labels must be distant from. + +.. seealso:: :py:func:`setTargetLayer` +%End + + void setTargetLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels which labels must be distant from. + +.. seealso:: :py:func:`targetLayer` +%End + + double distance() const; +%Docstring +Returns the minimum permitted distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`setDistance` + +.. seealso:: :py:func:`distanceUnits` +%End + + void setDistance( double distance ); +%Docstring +Sets the minimum permitted ``distance`` between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`distance` + +.. seealso:: :py:func:`setDistanceUnits` +%End + + Qgis::RenderUnit distanceUnit() const; +%Docstring +Returns the units for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnit` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`distanceUnit` + +.. seealso:: :py:func:`setDistance` +%End + + const QgsMapUnitScale &distanceUnitScale() const; +%Docstring +Returns the scaling for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnitScale` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the ``scale`` for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`distanceUnitScale` + +.. seealso:: :py:func:`setDistance` +%End + + private: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel( const QgsLabelingEngineRuleMinimumDistanceLabelToLabel & ); +}; + + +class QgsLabelingEngineRuleAvoidLabelOverlapWithFeature : QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed overlapping features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature(); + ~QgsLabelingEngineRuleAvoidLabelOverlapWithFeature(); + virtual QgsLabelingEngineRuleAvoidLabelOverlapWithFeature *clone() const /Factory/; + + virtual QString id() const; + + virtual bool prepare( QgsRenderContext &context ); + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void resolveReferences( const QgsProject *project ); + + + QgsVectorLayer *labeledLayer(); +%Docstring +Returns the layer providing the labels. + +.. seealso:: :py:func:`setLabeledLayer` +%End + + void setLabeledLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels. + +.. seealso:: :py:func:`labeledLayer` +%End + + QgsVectorLayer *targetLayer(); +%Docstring +Returns the layer providing the features which labels must not overlap. + +.. seealso:: :py:func:`setTargetLayer` +%End + + void setTargetLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the features which labels must not overlap. + +.. seealso:: :py:func:`targetLayer` +%End + + private: + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature( const QgsLabelingEngineRuleAvoidLabelOverlapWithFeature & ); +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule_impl.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in b/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in new file mode 100644 index 000000000000..92228da909a8 --- /dev/null +++ b/python/PyQt6/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in @@ -0,0 +1,82 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingengineruleregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + +class QgsLabelingEngineRuleRegistry +{ +%Docstring(signature="appended") +A registry for labeling engine rules. + +Labeling engine rules implement custom logic to modify the labeling solution for a map render, +e.g. by preventing labels being placed which violate custom constraints. + +This registry stores available rules and is responsible for creating rules. + +:py:class:`QgsLabelingEngineRuleRegistry` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.labelEngineRuleRegistry()`. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingengineruleregistry.h" +%End + public: + + QgsLabelingEngineRuleRegistry(); +%Docstring +Constructor for QgsLabelingEngineRuleRegistry, containing a set of +default rules. +%End + ~QgsLabelingEngineRuleRegistry(); + + + QStringList ruleIds() const; +%Docstring +Returns a list of the rule IDs for rules present in the registry. +%End + + QgsAbstractLabelingEngineRule *create( const QString &id ) const /TransferBack/; +%Docstring +Creates a new rule from the type with matching ``id``. + +Returns ``None`` if no matching rule was found in the registry. + +The caller takes ownership of the returned object. +%End + + bool addRule( QgsAbstractLabelingEngineRule *rule /Transfer/ ); +%Docstring +Adds a new ``rule`` type to the registry. + +The registry takes ownership of ``rule``. + +:return: ``True`` if the rule was successfully added. + +.. seealso:: :py:func:`removeRule` +%End + + void removeRule( const QString &id ); +%Docstring +Removes the rule with matching ``id`` from the registry. + +.. seealso:: :py:func:`addRule` +%End + + private: + QgsLabelingEngineRuleRegistry( const QgsLabelingEngineRuleRegistry &other ); +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingengineruleregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/PyQt6/core/auto_generated/qgsapplication.sip.in b/python/PyQt6/core/auto_generated/qgsapplication.sip.in index 72e6078da6be..d1447db36d24 100644 --- a/python/PyQt6/core/auto_generated/qgsapplication.sip.in +++ b/python/PyQt6/core/auto_generated/qgsapplication.sip.in @@ -944,6 +944,13 @@ Returns registry of available 3D symbols. Gets the registry of available scalebar renderers. .. versionadded:: 3.14 +%End + + static QgsLabelingEngineRuleRegistry *labelingEngineRuleRegistry() /KeepReference/; +%Docstring +Gets the registry of available labeling engine rules. + +.. versionadded:: 3.40 %End static QgsProjectStorageRegistry *projectStorageRegistry() /KeepReference/; diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip index eb82cbe71469..853d4d954e9f 100644 --- a/python/PyQt6/core/core_auto.sip +++ b/python/PyQt6/core/core_auto.sip @@ -391,6 +391,9 @@ %Include auto_generated/labeling/qgspallabeling.sip %Include auto_generated/labeling/qgsrulebasedlabeling.sip %Include auto_generated/labeling/qgsvectorlayerlabeling.sip +%Include auto_generated/labeling/rules/qgslabelingenginerule.sip +%Include auto_generated/labeling/rules/qgslabelingenginerule_impl.sip +%Include auto_generated/labeling/rules/qgslabelingengineruleregistry.sip %Include auto_generated/layertree/qgscolorramplegendnode.sip %Include auto_generated/layertree/qgscolorramplegendnodesettings.sip %Include auto_generated/layertree/qgslayertree.sip diff --git a/python/core/auto_additions/qgsapplication.py b/python/core/auto_additions/qgsapplication.py index ccd6f478c990..e679ad701764 100644 --- a/python/core/auto_additions/qgsapplication.py +++ b/python/core/auto_additions/qgsapplication.py @@ -122,6 +122,7 @@ QgsApplication.renderer3DRegistry = staticmethod(QgsApplication.renderer3DRegistry) QgsApplication.symbol3DRegistry = staticmethod(QgsApplication.symbol3DRegistry) QgsApplication.scaleBarRendererRegistry = staticmethod(QgsApplication.scaleBarRendererRegistry) + QgsApplication.labelingEngineRuleRegistry = staticmethod(QgsApplication.labelingEngineRuleRegistry) QgsApplication.projectStorageRegistry = staticmethod(QgsApplication.projectStorageRegistry) QgsApplication.layerMetadataProviderRegistry = staticmethod(QgsApplication.layerMetadataProviderRegistry) QgsApplication.externalStorageRegistry = staticmethod(QgsApplication.externalStorageRegistry) diff --git a/python/core/auto_additions/qgslabelingenginerule.py b/python/core/auto_additions/qgslabelingenginerule.py new file mode 100644 index 000000000000..c994a01988c9 --- /dev/null +++ b/python/core/auto_additions/qgslabelingenginerule.py @@ -0,0 +1,9 @@ +# The following has been generated automatically from src/core/labeling/rules/qgslabelingenginerule.h +try: + QgsLabelingEngineContext.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsAbstractLabelingEngineRule.__group__ = ['labeling', 'rules'] +except NameError: + pass diff --git a/python/core/auto_additions/qgslabelingenginerule_impl.py b/python/core/auto_additions/qgslabelingenginerule_impl.py new file mode 100644 index 000000000000..8393328f6a10 --- /dev/null +++ b/python/core/auto_additions/qgslabelingenginerule_impl.py @@ -0,0 +1,21 @@ +# The following has been generated automatically from src/core/labeling/rules/qgslabelingenginerule_impl.h +try: + QgsAbstractLabelingEngineRuleDistanceFromFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleMinimumDistanceLabelToFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel.__group__ = ['labeling', 'rules'] +except NameError: + pass +try: + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature.__group__ = ['labeling', 'rules'] +except NameError: + pass diff --git a/python/core/auto_additions/qgslabelingengineruleregistry.py b/python/core/auto_additions/qgslabelingengineruleregistry.py new file mode 100644 index 000000000000..a2a0bff3f7d7 --- /dev/null +++ b/python/core/auto_additions/qgslabelingengineruleregistry.py @@ -0,0 +1,5 @@ +# The following has been generated automatically from src/core/labeling/rules/qgslabelingengineruleregistry.h +try: + QgsLabelingEngineRuleRegistry.__group__ = ['labeling', 'rules'] +except NameError: + pass diff --git a/python/core/auto_generated/labeling/qgslabelingenginesettings.sip.in b/python/core/auto_generated/labeling/qgslabelingenginesettings.sip.in index 46afb5727ea0..12417c961f9e 100644 --- a/python/core/auto_generated/labeling/qgslabelingenginesettings.sip.in +++ b/python/core/auto_generated/labeling/qgslabelingenginesettings.sip.in @@ -30,6 +30,9 @@ Stores global configuration for labeling engine }; QgsLabelingEngineSettings(); + ~QgsLabelingEngineSettings(); + + QgsLabelingEngineSettings( const QgsLabelingEngineSettings &other ); void clear(); %Docstring @@ -125,13 +128,66 @@ Which search method to use for removal collisions between labels Chain is always used. %End + void readSettingsFromProject( QgsProject *project ); %Docstring Read configuration of the labeling engine from a project + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.readXml` must be called to completely restore the object's state from a project. %End + void writeSettingsToProject( QgsProject *project ); %Docstring -Write configuration of the labeling engine to a project +Write configuration of the labeling engine to a project. + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.writeXml` must be called to completely store the object's state in a project. +%End + + void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; +%Docstring +Writes the label engine settings to an XML ``element``. + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.writeSettingsToProject` must be called to completely store the object's state in a project. + +.. seealso:: :py:func:`readXml` + +.. seealso:: :py:func:`writeSettingsToProject` + +.. versionadded:: 3.40 +%End + + void readXml( const QDomElement &element, const QgsReadWriteContext &context ); +%Docstring +Reads the label engine settings from an XML ``element``. + +.. note:: + + Both this method and :py:func:`~QgsLabelingEngineSettings.readSettingsFromProject` must be called to completely restore the object's state from a project. + +.. note:: + + :py:func:`~QgsLabelingEngineSettings.resolveReferences` must be called following this method. + +.. seealso:: :py:func:`writeXml` + +.. seealso:: :py:func:`readSettingsFromProject` + +.. versionadded:: 3.40 +%End + + void resolveReferences( const QgsProject *project ); +%Docstring +Resolves reference to layers from stored layer ID. + +Should be called following a call :py:func:`~QgsLabelingEngineSettings.readXml`. + +.. versionadded:: 3.40 %End @@ -187,6 +243,47 @@ Sets the placement engine ``version``, which dictates how the label placement pr .. seealso:: :py:func:`placementVersion` .. versionadded:: 3.10.2 +%End + + QList< QgsAbstractLabelingEngineRule * > rules(); +%Docstring +Returns a list of labeling engine rules which must be satifisfied +while placing labels. + +.. seealso:: :py:func:`addRule` + +.. seealso:: :py:func:`setRules` + +.. versionadded:: 3.40 +%End + + + void addRule( QgsAbstractLabelingEngineRule *rule /Transfer/ ); +%Docstring +Adds a labeling engine ``rule`` which must be satifisfied +while placing labels. + +Ownership of the rule is transferred to the settings. + +.. seealso:: :py:func:`rules` + +.. seealso:: :py:func:`setRules` + +.. versionadded:: 3.40 +%End + + void setRules( const QList< QgsAbstractLabelingEngineRule * > &rules /Transfer/ ); +%Docstring +Sets the labeling engine ``rules`` which must be satifisfied +while placing labels. + +Ownership of the rules are transferred to the settings. + +.. seealso:: :py:func:`addRule` + +.. seealso:: :py:func:`rules` + +.. versionadded:: 3.40 %End }; diff --git a/python/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in b/python/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in new file mode 100644 index 000000000000..02d71e9dcbdf --- /dev/null +++ b/python/core/auto_generated/labeling/rules/qgslabelingenginerule.sip.in @@ -0,0 +1,188 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + +class QgsLabelingEngineContext +{ +%Docstring(signature="appended") +Encapsulates the context for a labeling engine run. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule.h" +%End + public: + + QgsLabelingEngineContext( QgsRenderContext &renderContext ); +%Docstring +Constructor for QgsLabelingEngineContext. +%End + + + QgsRenderContext &renderContext(); +%Docstring +Returns a reference to the context's render context. +%End + + + QgsRectangle extent() const; +%Docstring +Returns the map extent defining the limits for labeling. + +.. seealso:: :py:func:`mapBoundaryGeometry` + +.. seealso:: :py:func:`setExtent` +%End + + void setExtent( const QgsRectangle &extent ); +%Docstring +Sets the map ``extent`` defining the limits for labeling. + +.. seealso:: :py:func:`setMapBoundaryGeometry` + +.. seealso:: :py:func:`extent` +%End + + QgsGeometry mapBoundaryGeometry() const; +%Docstring +Returns the map label boundary geometry, which defines the limits within which labels may be placed +in the map. + +The map boundary geometry specifies the actual geometry of the map +boundary, which will be used to detect whether a label is visible (or partially visible) in +the rendered map. This may differ from :py:func:`~QgsLabelingEngineContext.extent` in the case of rotated or non-rectangular +maps. + +.. seealso:: :py:func:`setMapBoundaryGeometry` + +.. seealso:: :py:func:`extent` +%End + + void setMapBoundaryGeometry( const QgsGeometry &geometry ); +%Docstring +Sets the map label boundary ``geometry``, which defines the limits within which labels may be placed +in the map. + +The map boundary geometry specifies the actual geometry of the map +boundary, which will be used to detect whether a label is visible (or partially visible) in +the rendered map. This may differ from :py:func:`~QgsLabelingEngineContext.extent` in the case of rotated or non-rectangular +maps. + +.. seealso:: :py:func:`setExtent` + +.. seealso:: :py:func:`mapBoundaryGeometry` +%End + + private: + QgsLabelingEngineContext( const QgsLabelingEngineContext &other ); +}; + +class QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +Abstract base class for labeling engine rules. + +Labeling engine rules implement custom logic to modify the labeling solution for a map render, +e.g. by preventing labels being placed which violate custom constraints. + +.. note:: + + :py:class:`QgsAbstractLabelingEngineRule` cannot be subclassed in Python. Use one of the existing + implementations of this class instead. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule.h" +%End +%ConvertToSubClassCode + if ( sipCpp->id() == "minimumDistanceLabelToFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleMinimumDistanceLabelToFeature; + } + else if ( sipCpp->id() == "minimumDistanceLabelToLabel" ) + { + sipType = sipType_QgsLabelingEngineRuleMinimumDistanceLabelToLabel; + } + else if ( sipCpp->id() == "maximumDistanceLabelToFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleMaximumDistanceLabelToFeature; + } + else if ( sipCpp->id() == "avoidLabelOverlapWithFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleAvoidLabelOverlapWithFeature; + } + else + { + sipType = 0; + } +%End + public: + + virtual ~QgsAbstractLabelingEngineRule(); + + virtual QgsAbstractLabelingEngineRule *clone() const = 0 /Factory/; +%Docstring +Creates a clone of this rule. + +The caller takes ownership of the returned object. +%End + + virtual QString id() const = 0; +%Docstring +Returns a string uniquely identifying the rule subclass. +%End + + virtual bool prepare( QgsRenderContext &context ) = 0; +%Docstring +Prepares the rule. + +This must be called on the main render thread, prior to commencing the render operation. Thread sensitive +logic (such as creation of feature sources) can be performed in this method. +%End + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const = 0; +%Docstring +Writes the rule properties to an XML ``element``. + +.. seealso:: :py:func:`readXml` +%End + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ) = 0; +%Docstring +Reads the rule properties from an XML ``element``. + +.. seealso:: :py:func:`resolveReferences` + +.. seealso:: :py:func:`writeXml` +%End + + virtual void resolveReferences( const QgsProject *project ); +%Docstring +Resolves reference to layers from stored layer ID. + +Should be called following a call :py:func:`~QgsAbstractLabelingEngineRule.readXml`. +%End + + + + + +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in b/python/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in new file mode 100644 index 000000000000..cb86ee5368b8 --- /dev/null +++ b/python/core/auto_generated/labeling/rules/qgslabelingenginerule_impl.sip.in @@ -0,0 +1,393 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule_impl.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + + +class QgsAbstractLabelingEngineRuleDistanceFromFeature : QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +Base class for labeling engine rules which prevents labels being placed too close or to far from features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + + QgsAbstractLabelingEngineRuleDistanceFromFeature(); + ~QgsAbstractLabelingEngineRuleDistanceFromFeature(); + virtual bool prepare( QgsRenderContext &context ); + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void resolveReferences( const QgsProject *project ); + + + QgsVectorLayer *labeledLayer(); +%Docstring +Returns the layer providing the labels. + +.. seealso:: :py:func:`setLabeledLayer` +%End + + void setLabeledLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels. + +.. seealso:: :py:func:`labeledLayer` +%End + + QgsVectorLayer *targetLayer(); +%Docstring +Returns the layer providing the features which labels must be distant from (or close to). + +.. seealso:: :py:func:`setTargetLayer` +%End + + void setTargetLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the features which labels must be distant from (or close to). + +.. seealso:: :py:func:`targetLayer` +%End + + double distance() const; +%Docstring +Returns the acceptable distance threshold between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`setDistance` + +.. seealso:: :py:func:`distanceUnits` +%End + + void setDistance( double distance ); +%Docstring +Sets the acceptable ``distance`` threshold between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`distance` + +.. seealso:: :py:func:`setDistanceUnits` +%End + + Qgis::RenderUnit distanceUnit() const; +%Docstring +Returns the units for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnit` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`distanceUnit` + +.. seealso:: :py:func:`setDistance` +%End + + const QgsMapUnitScale &distanceUnitScale() const; +%Docstring +Returns the scaling for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnitScale` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the ``scale`` for the distance between labels and the features +from the :py:func:`~QgsAbstractLabelingEngineRuleDistanceFromFeature.targetLayer`. + +.. seealso:: :py:func:`distanceUnitScale` + +.. seealso:: :py:func:`setDistance` +%End + + double cost() const; +%Docstring +Returns the penalty cost incurred when the rule is violated. + +This is a value between 0 and 10, where 10 indicates that the rule must never be violated, +and 1-9 = nice to have if possible, where higher numbers will try harder to avoid violating the rule. + +.. seealso:: :py:func:`setCost` +%End + + void setCost( double cost ); +%Docstring +Sets the penalty ``cost`` incurred when the rule is violated. + +This is a value between 0 and 10, where 10 indicates that the rule must never be violated, +and 1-9 = nice to have if possible, where higher numbers will try harder to avoid violating the rule. + +.. seealso:: :py:func:`cost` +%End + + protected: + + void copyCommonProperties( QgsAbstractLabelingEngineRuleDistanceFromFeature *other ) const; +%Docstring +Copies common properties from this object to an ``other``. +%End + + + private: + QgsAbstractLabelingEngineRuleDistanceFromFeature( const QgsAbstractLabelingEngineRuleDistanceFromFeature &other ); +}; + + +class QgsLabelingEngineRuleMinimumDistanceLabelToFeature : QgsAbstractLabelingEngineRuleDistanceFromFeature +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed too close to features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + + QgsLabelingEngineRuleMinimumDistanceLabelToFeature(); + ~QgsLabelingEngineRuleMinimumDistanceLabelToFeature(); + virtual QgsLabelingEngineRuleMinimumDistanceLabelToFeature *clone() const /Factory/; + + virtual QString id() const; + + + private: + QgsLabelingEngineRuleMinimumDistanceLabelToFeature( const QgsLabelingEngineRuleMinimumDistanceLabelToFeature & ); +}; + + +class QgsLabelingEngineRuleMaximumDistanceLabelToFeature : QgsAbstractLabelingEngineRuleDistanceFromFeature +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed too far from features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature(); + ~QgsLabelingEngineRuleMaximumDistanceLabelToFeature(); + virtual QgsLabelingEngineRuleMaximumDistanceLabelToFeature *clone() const /Factory/; + + virtual QString id() const; + + + private: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature( const QgsLabelingEngineRuleMaximumDistanceLabelToFeature & ); +}; + +class QgsLabelingEngineRuleMinimumDistanceLabelToLabel : QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed too close to labels from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel(); + ~QgsLabelingEngineRuleMinimumDistanceLabelToLabel(); + + virtual QgsLabelingEngineRuleMinimumDistanceLabelToLabel *clone() const /Factory/; + + virtual QString id() const; + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void resolveReferences( const QgsProject *project ); + + virtual bool prepare( QgsRenderContext &context ); + + + QgsVectorLayer *labeledLayer(); +%Docstring +Returns the layer providing the labels. + +.. seealso:: :py:func:`setLabeledLayer` +%End + + void setLabeledLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels. + +.. seealso:: :py:func:`labeledLayer` +%End + + QgsVectorLayer *targetLayer(); +%Docstring +Returns the layer providing the labels which labels must be distant from. + +.. seealso:: :py:func:`setTargetLayer` +%End + + void setTargetLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels which labels must be distant from. + +.. seealso:: :py:func:`targetLayer` +%End + + double distance() const; +%Docstring +Returns the minimum permitted distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`setDistance` + +.. seealso:: :py:func:`distanceUnits` +%End + + void setDistance( double distance ); +%Docstring +Sets the minimum permitted ``distance`` between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`distance` + +.. seealso:: :py:func:`setDistanceUnits` +%End + + Qgis::RenderUnit distanceUnit() const; +%Docstring +Returns the units for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnit` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnit( Qgis::RenderUnit unit ); +%Docstring +Sets the ``unit`` for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`distanceUnit` + +.. seealso:: :py:func:`setDistance` +%End + + const QgsMapUnitScale &distanceUnitScale() const; +%Docstring +Returns the scaling for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`setDistanceUnitScale` + +.. seealso:: :py:func:`distance` +%End + + void setDistanceUnitScale( const QgsMapUnitScale &scale ); +%Docstring +Sets the ``scale`` for the distance between labels from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.labeledLayer` and the labels +from the :py:func:`~QgsLabelingEngineRuleMinimumDistanceLabelToLabel.targetLayer`. + +.. seealso:: :py:func:`distanceUnitScale` + +.. seealso:: :py:func:`setDistance` +%End + + private: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel( const QgsLabelingEngineRuleMinimumDistanceLabelToLabel & ); +}; + + +class QgsLabelingEngineRuleAvoidLabelOverlapWithFeature : QgsAbstractLabelingEngineRule +{ +%Docstring(signature="appended") +A labeling engine rule which prevents labels being placed overlapping features from a different layer. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingenginerule_impl.h" +%End + public: + + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature(); + ~QgsLabelingEngineRuleAvoidLabelOverlapWithFeature(); + virtual QgsLabelingEngineRuleAvoidLabelOverlapWithFeature *clone() const /Factory/; + + virtual QString id() const; + + virtual bool prepare( QgsRenderContext &context ); + + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + virtual void resolveReferences( const QgsProject *project ); + + + QgsVectorLayer *labeledLayer(); +%Docstring +Returns the layer providing the labels. + +.. seealso:: :py:func:`setLabeledLayer` +%End + + void setLabeledLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the labels. + +.. seealso:: :py:func:`labeledLayer` +%End + + QgsVectorLayer *targetLayer(); +%Docstring +Returns the layer providing the features which labels must not overlap. + +.. seealso:: :py:func:`setTargetLayer` +%End + + void setTargetLayer( QgsVectorLayer *layer ); +%Docstring +Sets the ``layer`` providing the features which labels must not overlap. + +.. seealso:: :py:func:`targetLayer` +%End + + private: + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature( const QgsLabelingEngineRuleAvoidLabelOverlapWithFeature & ); +}; + + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingenginerule_impl.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in b/python/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in new file mode 100644 index 000000000000..92228da909a8 --- /dev/null +++ b/python/core/auto_generated/labeling/rules/qgslabelingengineruleregistry.sip.in @@ -0,0 +1,82 @@ +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingengineruleregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ + + + +class QgsLabelingEngineRuleRegistry +{ +%Docstring(signature="appended") +A registry for labeling engine rules. + +Labeling engine rules implement custom logic to modify the labeling solution for a map render, +e.g. by preventing labels being placed which violate custom constraints. + +This registry stores available rules and is responsible for creating rules. + +:py:class:`QgsLabelingEngineRuleRegistry` is not usually directly created, but rather accessed through +:py:func:`QgsApplication.labelEngineRuleRegistry()`. + +.. versionadded:: 3.40 +%End + +%TypeHeaderCode +#include "qgslabelingengineruleregistry.h" +%End + public: + + QgsLabelingEngineRuleRegistry(); +%Docstring +Constructor for QgsLabelingEngineRuleRegistry, containing a set of +default rules. +%End + ~QgsLabelingEngineRuleRegistry(); + + + QStringList ruleIds() const; +%Docstring +Returns a list of the rule IDs for rules present in the registry. +%End + + QgsAbstractLabelingEngineRule *create( const QString &id ) const /TransferBack/; +%Docstring +Creates a new rule from the type with matching ``id``. + +Returns ``None`` if no matching rule was found in the registry. + +The caller takes ownership of the returned object. +%End + + bool addRule( QgsAbstractLabelingEngineRule *rule /Transfer/ ); +%Docstring +Adds a new ``rule`` type to the registry. + +The registry takes ownership of ``rule``. + +:return: ``True`` if the rule was successfully added. + +.. seealso:: :py:func:`removeRule` +%End + + void removeRule( const QString &id ); +%Docstring +Removes the rule with matching ``id`` from the registry. + +.. seealso:: :py:func:`addRule` +%End + + private: + QgsLabelingEngineRuleRegistry( const QgsLabelingEngineRuleRegistry &other ); +}; + +/************************************************************************ + * This file has been generated automatically from * + * * + * src/core/labeling/rules/qgslabelingengineruleregistry.h * + * * + * Do not edit manually ! Edit header and run scripts/sipify.py again * + ************************************************************************/ diff --git a/python/core/auto_generated/qgsapplication.sip.in b/python/core/auto_generated/qgsapplication.sip.in index 9a93216aac2d..e7cb74a2b825 100644 --- a/python/core/auto_generated/qgsapplication.sip.in +++ b/python/core/auto_generated/qgsapplication.sip.in @@ -944,6 +944,13 @@ Returns registry of available 3D symbols. Gets the registry of available scalebar renderers. .. versionadded:: 3.14 +%End + + static QgsLabelingEngineRuleRegistry *labelingEngineRuleRegistry() /KeepReference/; +%Docstring +Gets the registry of available labeling engine rules. + +.. versionadded:: 3.40 %End static QgsProjectStorageRegistry *projectStorageRegistry() /KeepReference/; diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip index eb82cbe71469..853d4d954e9f 100644 --- a/python/core/core_auto.sip +++ b/python/core/core_auto.sip @@ -391,6 +391,9 @@ %Include auto_generated/labeling/qgspallabeling.sip %Include auto_generated/labeling/qgsrulebasedlabeling.sip %Include auto_generated/labeling/qgsvectorlayerlabeling.sip +%Include auto_generated/labeling/rules/qgslabelingenginerule.sip +%Include auto_generated/labeling/rules/qgslabelingenginerule_impl.sip +%Include auto_generated/labeling/rules/qgslabelingengineruleregistry.sip %Include auto_generated/layertree/qgscolorramplegendnode.sip %Include auto_generated/layertree/qgscolorramplegendnodesettings.sip %Include auto_generated/layertree/qgslayertree.sip diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index eb6aacb80d44..506745ab98b2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -875,6 +875,9 @@ set(QGIS_CORE_SRCS labeling/qgstextlabelfeature.cpp labeling/qgsvectorlayerlabeling.cpp labeling/qgsvectorlayerlabelprovider.cpp + labeling/rules/qgslabelingenginerule.cpp + labeling/rules/qgslabelingenginerule_impl.cpp + labeling/rules/qgslabelingengineruleregistry.cpp geometry/qgsabstractgeometry.cpp geometry/qgsbox3d.cpp @@ -1538,6 +1541,9 @@ set(QGIS_CORE_HDRS labeling/qgstextlabelfeature.h labeling/qgsvectorlayerlabeling.h labeling/qgsvectorlayerlabelprovider.h + labeling/rules/qgslabelingenginerule.h + labeling/rules/qgslabelingenginerule_impl.h + labeling/rules/qgslabelingengineruleregistry.h layertree/qgscolorramplegendnode.h layertree/qgscolorramplegendnodesettings.h @@ -2352,6 +2358,7 @@ target_include_directories(qgis_core PUBLIC geocoding gps labeling + labeling/rules layertree layout locator diff --git a/src/core/labeling/qgslabelingengine.cpp b/src/core/labeling/qgslabelingengine.cpp index 4960376744ff..3e87f0eadf82 100644 --- a/src/core/labeling/qgslabelingengine.cpp +++ b/src/core/labeling/qgslabelingengine.cpp @@ -31,6 +31,7 @@ #include "qgslabelingresults.h" #include "qgsfillsymbol.h" #include "qgsruntimeprofiler.h" +#include "qgslabelingenginerule.h" // helper function for checking for job cancellation within PAL static bool _palIsCanceled( void *ctx ) @@ -100,6 +101,19 @@ void QgsLabelingEngine::setMapSettings( const QgsMapSettings &mapSettings ) mResults->setMapSettings( mapSettings ); } +bool QgsLabelingEngine::prepare( QgsRenderContext &context ) +{ + const QList rules = mMapSettings.labelingEngineSettings().rules(); + bool res = true; + for ( const QgsAbstractLabelingEngineRule *rule : rules ) + { + std::unique_ptr< QgsAbstractLabelingEngineRule > ruleClone( rule->clone() ); + res = ruleClone->prepare( context ) && res; + mEngineRules.emplace_back( std::move( ruleClone ) ); + } + return res; +} + QList< QgsMapLayer * > QgsLabelingEngine::participatingLayers() const { QList< QgsMapLayer * > layers; diff --git a/src/core/labeling/qgslabelingengine.h b/src/core/labeling/qgslabelingengine.h index ff1a4ef5c025..ef579958a2fe 100644 --- a/src/core/labeling/qgslabelingengine.h +++ b/src/core/labeling/qgslabelingengine.h @@ -356,6 +356,15 @@ class CORE_EXPORT QgsLabelingEngine //! Gets associated labeling engine settings const QgsLabelingEngineSettings &engineSettings() const { return mMapSettings.labelingEngineSettings(); } + /** + * Prepares the engine for rendering in the specified \a context. + * + * \warning This method must be called in advanced on the main rendering thread, not a background thread. + * + * \since QGIS 3.40 + */ + bool prepare( QgsRenderContext &context ); + /** * Returns a list of layers with providers in the engine. */ @@ -436,6 +445,9 @@ class CORE_EXPORT QgsLabelingEngine QList mProviders; QList mSubProviders; + //!< List of labeling engine rules (owned by the labeling engine) + std::vector< std::unique_ptr< QgsAbstractLabelingEngineRule > > mEngineRules; + //! Resulting labeling layout std::unique_ptr< QgsLabelingResults > mResults; diff --git a/src/core/labeling/qgslabelingenginesettings.cpp b/src/core/labeling/qgslabelingenginesettings.cpp index 64ebb776d940..84bed7af3981 100644 --- a/src/core/labeling/qgslabelingenginesettings.cpp +++ b/src/core/labeling/qgslabelingenginesettings.cpp @@ -17,11 +17,50 @@ #include "qgsproject.h" #include "qgscolorutils.h" +#include "qgslabelingenginerule.h" +#include "qgsapplication.h" +#include "qgslabelingengineruleregistry.h" QgsLabelingEngineSettings::QgsLabelingEngineSettings() { } +QgsLabelingEngineSettings::~QgsLabelingEngineSettings() = default; + +QgsLabelingEngineSettings::QgsLabelingEngineSettings( const QgsLabelingEngineSettings &other ) + : mFlags( other.mFlags ) + , mSearchMethod( other.mSearchMethod ) + , mMaxLineCandidatesPerCm( other.mMaxLineCandidatesPerCm ) + , mMaxPolygonCandidatesPerCmSquared( other.mMaxPolygonCandidatesPerCmSquared ) + , mUnplacedLabelColor( other.mUnplacedLabelColor ) + , mPlacementVersion( other.mPlacementVersion ) + , mDefaultTextRenderFormat( other.mDefaultTextRenderFormat ) +{ + mEngineRules.reserve( other.mEngineRules.size() ); + for ( const auto &rule : other.mEngineRules ) + { + mEngineRules.emplace_back( rule->clone() ); + } +} + +QgsLabelingEngineSettings &QgsLabelingEngineSettings::operator=( const QgsLabelingEngineSettings &other ) +{ + mFlags = other.mFlags; + mSearchMethod = other.mSearchMethod; + mMaxLineCandidatesPerCm = other.mMaxLineCandidatesPerCm; + mMaxPolygonCandidatesPerCmSquared = other.mMaxPolygonCandidatesPerCmSquared; + mUnplacedLabelColor = other.mUnplacedLabelColor; + mPlacementVersion = other.mPlacementVersion; + mDefaultTextRenderFormat = other.mDefaultTextRenderFormat; + mEngineRules.clear(); + mEngineRules.reserve( other.mEngineRules.size() ); + for ( const auto &rule : other.mEngineRules ) + { + mEngineRules.emplace_back( rule->clone() ); + } + return *this; +} + void QgsLabelingEngineSettings::clear() { *this = QgsLabelingEngineSettings(); @@ -76,6 +115,50 @@ void QgsLabelingEngineSettings::writeSettingsToProject( QgsProject *project ) project->writeEntry( QStringLiteral( "PAL" ), QStringLiteral( "/PlacementEngineVersion" ), static_cast< int >( mPlacementVersion ) ); } +void QgsLabelingEngineSettings::writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const +{ + if ( !mEngineRules.empty() ) + { + QDomElement rulesElement = doc.createElement( QStringLiteral( "rules" ) ); + for ( const auto &rule : mEngineRules ) + { + QDomElement ruleElement = doc.createElement( QStringLiteral( "rule" ) ); + ruleElement.setAttribute( QStringLiteral( "id" ), rule->id() ); + rule->writeXml( doc, ruleElement, context ); + rulesElement.appendChild( ruleElement ); + } + element.appendChild( rulesElement ); + } +} + +void QgsLabelingEngineSettings::readXml( const QDomElement &element, const QgsReadWriteContext &context ) +{ + mEngineRules.clear(); + { + const QDomElement rulesElement = element.firstChildElement( QStringLiteral( "rules" ) ); + const QDomNodeList rules = rulesElement.childNodes(); + for ( int i = 0; i < rules.length(); i++ ) + { + const QDomElement ruleElement = rules.at( i ).toElement(); + const QString id = ruleElement.attribute( QStringLiteral( "id" ) ); + std::unique_ptr< QgsAbstractLabelingEngineRule > rule( QgsApplication::labelingEngineRuleRegistry()->create( id ) ); + if ( rule ) + { + rule->readXml( ruleElement, context ); + mEngineRules.emplace_back( std::move( rule ) ); + } + } + } +} + +void QgsLabelingEngineSettings::resolveReferences( const QgsProject *project ) +{ + for ( const auto &rule : mEngineRules ) + { + rule->resolveReferences( project ); + } +} + QColor QgsLabelingEngineSettings::unplacedLabelColor() const { return mUnplacedLabelColor; @@ -96,4 +179,38 @@ void QgsLabelingEngineSettings::setPlacementVersion( Qgis::LabelPlacementEngineV mPlacementVersion = placementVersion; } +QList QgsLabelingEngineSettings::rules() +{ + QList res; + for ( const auto &it : mEngineRules ) + { + res << it.get(); + } + return res; +} + +QList QgsLabelingEngineSettings::rules() const +{ + QList res; + for ( const auto &it : mEngineRules ) + { + res << it.get(); + } + return res; +} + +void QgsLabelingEngineSettings::addRule( QgsAbstractLabelingEngineRule *rule ) +{ + mEngineRules.emplace_back( rule ); +} + +void QgsLabelingEngineSettings::setRules( const QList &rules ) +{ + mEngineRules.clear(); + for ( QgsAbstractLabelingEngineRule *rule : rules ) + { + mEngineRules.emplace_back( rule ); + } +} + diff --git a/src/core/labeling/qgslabelingenginesettings.h b/src/core/labeling/qgslabelingenginesettings.h index 91c904f0b175..c228c0b4296c 100644 --- a/src/core/labeling/qgslabelingenginesettings.h +++ b/src/core/labeling/qgslabelingenginesettings.h @@ -21,6 +21,10 @@ #include class QgsProject; +class QgsAbstractLabelingEngineRule; +class QDomDocument; +class QDomElement; +class QgsReadWriteContext; /** * \ingroup core @@ -46,6 +50,10 @@ class CORE_EXPORT QgsLabelingEngineSettings }; QgsLabelingEngineSettings(); + ~QgsLabelingEngineSettings(); + + QgsLabelingEngineSettings( const QgsLabelingEngineSettings &other ); + QgsLabelingEngineSettings &operator=( const QgsLabelingEngineSettings &other ); //! Returns the configuration to the defaults void clear(); @@ -125,11 +133,57 @@ class CORE_EXPORT QgsLabelingEngineSettings */ Q_DECL_DEPRECATED Search searchMethod() const SIP_DEPRECATED { return Chain; } - //! Read configuration of the labeling engine from a project + // TODO QGIS 4.0 -- remove these, and just use read/writeXml directly: + + /** + * Read configuration of the labeling engine from a project + * + * \note Both this method and readXml() must be called to completely restore the object's state from a project. + */ void readSettingsFromProject( QgsProject *project ); - //! Write configuration of the labeling engine to a project + + /** + * Write configuration of the labeling engine to a project. + * + * \note Both this method and writeXml() must be called to completely store the object's state in a project. + */ void writeSettingsToProject( QgsProject *project ); + /** + * Writes the label engine settings to an XML \a element. + * + * \note Both this method and writeSettingsToProject() must be called to completely store the object's state in a project. + * + * \see readXml() + * \see writeSettingsToProject() + * + * \since QGIS 3.40 + */ + void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const; + + /** + * Reads the label engine settings from an XML \a element. + * + * \note Both this method and readSettingsFromProject() must be called to completely restore the object's state from a project. + * + * \note resolveReferences() must be called following this method. + * + * \see writeXml() + * \see readSettingsFromProject() + * + * \since QGIS 3.40 + */ + void readXml( const QDomElement &element, const QgsReadWriteContext &context ); + + /** + * Resolves reference to layers from stored layer ID. + * + * Should be called following a call readXml(). + * + * \since QGIS 3.40 + */ + void resolveReferences( const QgsProject *project ); + // TODO QGIS 4.0: In reality the text render format settings don't only apply to labels, but also // ANY text rendered using QgsTextRenderer (including some non-label text items in layouts). // These methods should possibly be moved out of here and into the general QgsProject settings. @@ -188,6 +242,50 @@ class CORE_EXPORT QgsLabelingEngineSettings */ void setPlacementVersion( Qgis::LabelPlacementEngineVersion version ); + /** + * Returns a list of labeling engine rules which must be satifisfied + * while placing labels. + * + * \see addRule() + * \see setRules() + * \since QGIS 3.40 + */ + QList< QgsAbstractLabelingEngineRule * > rules(); + + /** + * Returns a list of labeling engine rules which must be satifisfied + * while placing labels. + * + * \see addRule() + * \see setRules() + * \since QGIS 3.40 + */ + QList< const QgsAbstractLabelingEngineRule * > rules() const SIP_SKIP; + + /** + * Adds a labeling engine \a rule which must be satifisfied + * while placing labels. + * + * Ownership of the rule is transferred to the settings. + * + * \see rules() + * \see setRules() + * \since QGIS 3.40 + */ + void addRule( QgsAbstractLabelingEngineRule *rule SIP_TRANSFER ); + + /** + * Sets the labeling engine \a rules which must be satifisfied + * while placing labels. + * + * Ownership of the rules are transferred to the settings. + * + * \see addRule() + * \see rules() + * \since QGIS 3.40 + */ + void setRules( const QList< QgsAbstractLabelingEngineRule * > &rules SIP_TRANSFER ); + private: //! Flags Qgis::LabelingFlags mFlags = Qgis::LabelingFlag::UsePartialCandidates; @@ -204,6 +302,8 @@ class CORE_EXPORT QgsLabelingEngineSettings Qgis::TextRenderFormat mDefaultTextRenderFormat = Qgis::TextRenderFormat::AlwaysOutlines; + std::vector< std::unique_ptr< QgsAbstractLabelingEngineRule > > mEngineRules; + }; #endif // QGSLABELINGENGINESETTINGS_H diff --git a/src/core/labeling/rules/qgslabelingenginerule.cpp b/src/core/labeling/rules/qgslabelingenginerule.cpp new file mode 100644 index 000000000000..8291fbcc9d4a --- /dev/null +++ b/src/core/labeling/rules/qgslabelingenginerule.cpp @@ -0,0 +1,78 @@ +/*************************************************************************** + qgslabelingenginerule.cpp + --------------------- + Date : August 2024 + Copyright : (C) 2024 by 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 "qgslabelingenginerule.h" + + +// +// QgsLabelingEngineContext +// + +QgsLabelingEngineContext::QgsLabelingEngineContext( QgsRenderContext &renderContext ) + : mRenderContext( renderContext ) +{ + +} + +QgsGeometry QgsLabelingEngineContext::mapBoundaryGeometry() const +{ + return mMapBoundaryGeometry; +} + +void QgsLabelingEngineContext::setMapBoundaryGeometry( const QgsGeometry &geometry ) +{ + mMapBoundaryGeometry = geometry; +} + +QgsRectangle QgsLabelingEngineContext::extent() const +{ + return mExtent; +} + +void QgsLabelingEngineContext::setExtent( const QgsRectangle &extent ) +{ + mExtent = extent; +} + +// +// QgsAbstractLabelingEngineRule +// + +QgsAbstractLabelingEngineRule::~QgsAbstractLabelingEngineRule() = default; + +void QgsAbstractLabelingEngineRule::resolveReferences( const QgsProject * ) +{ + +} + +bool QgsAbstractLabelingEngineRule::candidatesAreConflicting( const pal::LabelPosition *, const pal::LabelPosition * ) const +{ + return false; +} + +QgsRectangle QgsAbstractLabelingEngineRule::modifyCandidateConflictSearchBoundingBox( const QgsRectangle &candidateBounds ) const +{ + return candidateBounds; +} + +bool QgsAbstractLabelingEngineRule::candidateIsIllegal( const pal::LabelPosition *, QgsLabelingEngineContext & ) const +{ + return false; +} + +void QgsAbstractLabelingEngineRule::alterCandidateCost( pal::LabelPosition *, QgsLabelingEngineContext & ) const +{ + +} diff --git a/src/core/labeling/rules/qgslabelingenginerule.h b/src/core/labeling/rules/qgslabelingenginerule.h new file mode 100644 index 000000000000..bd70601787cf --- /dev/null +++ b/src/core/labeling/rules/qgslabelingenginerule.h @@ -0,0 +1,238 @@ +/*************************************************************************** + qgslabelingenginerule.h + --------------------- + Date : August 2024 + Copyright : (C) 2024 by 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 QGSLABELINGENGINERULE_H +#define QGSLABELINGENGINERULE_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgis.h" +#include "qgsgeometry.h" + +class QgsRenderContext; +class QDomDocument; +class QDomElement; +class QgsReadWriteContext; +class QgsProject; +#ifndef SIP_RUN +namespace pal +{ + class LabelPosition; +} +#endif + +/** + * \ingroup core + * \brief Encapsulates the context for a labeling engine run. + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsLabelingEngineContext +{ + public: + + /** + * Constructor for QgsLabelingEngineContext. + */ + QgsLabelingEngineContext( QgsRenderContext &renderContext ); + +#ifndef SIP_RUN + QgsLabelingEngineContext( const QgsLabelingEngineContext &other ) = delete; + QgsLabelingEngineContext &operator=( const QgsLabelingEngineContext &other ) = delete; +#endif + + /** + * Returns a reference to the context's render context. + */ + QgsRenderContext &renderContext() { return mRenderContext; } + + /** + * Returns a reference to the context's render context. + * \note Not available in Python bindings. + */ + const QgsRenderContext &renderContext() const { return mRenderContext; } SIP_SKIP + + /** + * Returns the map extent defining the limits for labeling. + * + * \see mapBoundaryGeometry() + * \see setExtent() + */ + QgsRectangle extent() const; + + /** + * Sets the map \a extent defining the limits for labeling. + * + * \see setMapBoundaryGeometry() + * \see extent() + */ + void setExtent( const QgsRectangle &extent ); + + /** + * Returns the map label boundary geometry, which defines the limits within which labels may be placed + * in the map. + * + * The map boundary geometry specifies the actual geometry of the map + * boundary, which will be used to detect whether a label is visible (or partially visible) in + * the rendered map. This may differ from extent() in the case of rotated or non-rectangular + * maps. + * + * \see setMapBoundaryGeometry() + * \see extent() + */ + QgsGeometry mapBoundaryGeometry() const; + + /** + * Sets the map label boundary \a geometry, which defines the limits within which labels may be placed + * in the map. + * + * The map boundary geometry specifies the actual geometry of the map + * boundary, which will be used to detect whether a label is visible (or partially visible) in + * the rendered map. This may differ from extent() in the case of rotated or non-rectangular + * maps. + * + * \see setExtent() + * \see mapBoundaryGeometry() + */ + void setMapBoundaryGeometry( const QgsGeometry &geometry ); + + private: + +#ifdef SIP_RUN + QgsLabelingEngineContext( const QgsLabelingEngineContext &other ); +#endif + + QgsRenderContext &mRenderContext; + QgsRectangle mExtent; + QgsGeometry mMapBoundaryGeometry; +}; + +/** + * Abstract base class for labeling engine rules. + * + * Labeling engine rules implement custom logic to modify the labeling solution for a map render, + * e.g. by preventing labels being placed which violate custom constraints. + * + * \note QgsAbstractLabelingEngineRule cannot be subclassed in Python. Use one of the existing + * implementations of this class instead. + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsAbstractLabelingEngineRule +{ + +#ifdef SIP_RUN + SIP_CONVERT_TO_SUBCLASS_CODE + if ( sipCpp->id() == "minimumDistanceLabelToFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleMinimumDistanceLabelToFeature; + } + else if ( sipCpp->id() == "minimumDistanceLabelToLabel" ) + { + sipType = sipType_QgsLabelingEngineRuleMinimumDistanceLabelToLabel; + } + else if ( sipCpp->id() == "maximumDistanceLabelToFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleMaximumDistanceLabelToFeature; + } + else if ( sipCpp->id() == "avoidLabelOverlapWithFeature" ) + { + sipType = sipType_QgsLabelingEngineRuleAvoidLabelOverlapWithFeature; + } + else + { + sipType = 0; + } + SIP_END +#endif + + public: + + virtual ~QgsAbstractLabelingEngineRule(); + + /** + * Creates a clone of this rule. + * + * The caller takes ownership of the returned object. + */ + virtual QgsAbstractLabelingEngineRule *clone() const = 0 SIP_FACTORY; + + /** + * Returns a string uniquely identifying the rule subclass. + */ + virtual QString id() const = 0; + + /** + * Prepares the rule. + * + * This must be called on the main render thread, prior to commencing the render operation. Thread sensitive + * logic (such as creation of feature sources) can be performed in this method. + */ + virtual bool prepare( QgsRenderContext &context ) = 0; + + /** + * Writes the rule properties to an XML \a element. + * + * \see readXml() + */ + virtual void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const = 0; + + /** + * Reads the rule properties from an XML \a element. + * + * \see resolveReferences() + * \see writeXml() + */ + virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ) = 0; + + /** + * Resolves reference to layers from stored layer ID. + * + * Should be called following a call readXml(). + */ + virtual void resolveReferences( const QgsProject *project ); + + /** + * Returns TRUE if a labeling candidate \a lp1 conflicts with \a lp2 after applying the rule. + * + * The default implementation returns FALSE. + */ + virtual bool candidatesAreConflicting( const pal::LabelPosition *lp1, const pal::LabelPosition *lp2 ) const SIP_SKIP; + + /** + * Returns a (possibly expanded) bounding box to use when searching for conflicts for a candidate. + * + * The return value is permitted to grow the bounding box, but may NOT shrink it. + * + * The default implementation returns the same bounds. + */ + virtual QgsRectangle modifyCandidateConflictSearchBoundingBox( const QgsRectangle &candidateBounds ) const SIP_SKIP; + + /** + * Returns TRUE if a labeling \a candidate violates the rule and should be eliminated. + * + * The default implementation returns FALSE. + */ + virtual bool candidateIsIllegal( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const SIP_SKIP; + + /** + * Provides an opportunity for the rule to alter the cost for a \a candidate. + * + * The default implementation does nothing. + */ + virtual void alterCandidateCost( pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const SIP_SKIP; + +}; + +#endif // QGSLABELINGENGINESETTINGS_H diff --git a/src/core/labeling/rules/qgslabelingenginerule_impl.cpp b/src/core/labeling/rules/qgslabelingenginerule_impl.cpp new file mode 100644 index 000000000000..99373d67ac87 --- /dev/null +++ b/src/core/labeling/rules/qgslabelingenginerule_impl.cpp @@ -0,0 +1,548 @@ +/*************************************************************************** + qgslabelingenginerule_impl.cpp + --------------------- + Date : August 2024 + Copyright : (C) 2024 by 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 "qgslabelingenginerule_impl.h" +#include "qgsunittypes.h" +#include "qgssymbollayerutils.h" +#include "labelposition.h" +#include "feature.h" +#include "qgsvectorlayerfeatureiterator.h" +#include "qgsthreadingutils.h" +#include "qgsspatialindex.h" +#include "qgsgeos.h" + +// +// QgsAbstractLabelingEngineRuleDistanceFromFeature +// + +QgsAbstractLabelingEngineRuleDistanceFromFeature::QgsAbstractLabelingEngineRuleDistanceFromFeature() = default; +QgsAbstractLabelingEngineRuleDistanceFromFeature::~QgsAbstractLabelingEngineRuleDistanceFromFeature() = default; + +bool QgsAbstractLabelingEngineRuleDistanceFromFeature::prepare( QgsRenderContext &context ) +{ + if ( !mTargetLayer ) + return false; + + QGIS_CHECK_OTHER_QOBJECT_THREAD_ACCESS( mTargetLayer ); + mTargetLayerSource = std::make_unique< QgsVectorLayerFeatureSource >( mTargetLayer.get() ); + + mDistanceMapUnits = context.convertToMapUnits( mDistance, mDistanceUnit, mDistanceUnitScale ); + return true; +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::writeXml( QDomDocument &, QDomElement &element, const QgsReadWriteContext & ) const +{ + element.setAttribute( QStringLiteral( "distance" ), mDistance ); + element.setAttribute( QStringLiteral( "distanceUnit" ), QgsUnitTypes::encodeUnit( mDistanceUnit ) ); + element.setAttribute( QStringLiteral( "distanceUnitScale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mDistanceUnitScale ) ); + element.setAttribute( QStringLiteral( "cost" ), mCost ); + + if ( mLabeledLayer ) + { + element.setAttribute( QStringLiteral( "labeledLayer" ), mLabeledLayer.layerId ); + element.setAttribute( QStringLiteral( "labeledLayerName" ), mLabeledLayer.name ); + element.setAttribute( QStringLiteral( "labeledLayerSource" ), mLabeledLayer.source ); + element.setAttribute( QStringLiteral( "labeledLayerProvider" ), mLabeledLayer.provider ); + } + if ( mTargetLayer ) + { + element.setAttribute( QStringLiteral( "targetLayer" ), mTargetLayer.layerId ); + element.setAttribute( QStringLiteral( "targetLayerName" ), mTargetLayer.name ); + element.setAttribute( QStringLiteral( "targetLayerSource" ), mTargetLayer.source ); + element.setAttribute( QStringLiteral( "targetLayerProvider" ), mTargetLayer.provider ); + } +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::readXml( const QDomElement &element, const QgsReadWriteContext & ) +{ + mDistance = element.attribute( QStringLiteral( "distance" ), QStringLiteral( "0" ) ).toDouble(); + mDistanceUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "distanceUnit" ) ) ); + mDistanceUnitScale = QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "distanceUnitScale" ) ) ); + mCost = element.attribute( QStringLiteral( "cost" ), QStringLiteral( "0" ) ).toDouble(); + + { + const QString layerId = element.attribute( QStringLiteral( "labeledLayer" ) ); + const QString layerName = element.attribute( QStringLiteral( "labeledLayerName" ) ); + const QString layerSource = element.attribute( QStringLiteral( "labeledLayerSource" ) ); + const QString layerProvider = element.attribute( QStringLiteral( "labeledLayerProvider" ) ); + mLabeledLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + } + { + const QString layerId = element.attribute( QStringLiteral( "targetLayer" ) ); + const QString layerName = element.attribute( QStringLiteral( "targetLayerName" ) ); + const QString layerSource = element.attribute( QStringLiteral( "targetLayerSource" ) ); + const QString layerProvider = element.attribute( QStringLiteral( "targetLayerProvider" ) ); + mTargetLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + } +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::resolveReferences( const QgsProject *project ) +{ + mLabeledLayer.resolve( project ); + mTargetLayer.resolve( project ); +} + +bool QgsAbstractLabelingEngineRuleDistanceFromFeature::candidateIsIllegal( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const +{ + // hard blocks on candidates only apply when cost == 10 + if ( mCost < 10 ) + return false; + + if ( candidate->getFeaturePart()->feature()->provider()->layerId() != mLabeledLayer.layerId ) + { + return false; + } + + if ( !mTargetLayerSource ) + return false; + + return candidateExceedsTolerance( candidate, context ); +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::alterCandidateCost( pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const +{ + // cost of 10 = hard block, handled in candidateIsIllegal + if ( mCost >= 10 ) + return; + + if ( candidate->getFeaturePart()->feature()->provider()->layerId() != mLabeledLayer.layerId ) + { + return; + } + + if ( !mTargetLayerSource ) + return; + + if ( candidateExceedsTolerance( candidate, context ) ) + { + // magic number alert! / 1000 here is completely arbitrary, an attempt to balance against the cost scaling of other factors + // assigned by the inscrutible logic of the pal engine internals + candidate->setCost( candidate->cost() + mCost / 1000 ); + } +} + +QgsVectorLayer *QgsAbstractLabelingEngineRuleDistanceFromFeature::labeledLayer() +{ + return mLabeledLayer.get(); +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::setLabeledLayer( QgsVectorLayer *layer ) +{ + mLabeledLayer = layer; +} + +QgsVectorLayer *QgsAbstractLabelingEngineRuleDistanceFromFeature::targetLayer() +{ + return mTargetLayer.get(); +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::setTargetLayer( QgsVectorLayer *layer ) +{ + mTargetLayer = layer; +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::copyCommonProperties( QgsAbstractLabelingEngineRuleDistanceFromFeature *other ) const +{ + other->mLabeledLayer = mLabeledLayer; + other->mTargetLayer = mTargetLayer; + other->mDistance = mDistance; + other->mDistanceUnit = mDistanceUnit; + other->mDistanceUnitScale = mDistanceUnitScale; + other->mCost = mCost; +} + +void QgsAbstractLabelingEngineRuleDistanceFromFeature::initialize( QgsLabelingEngineContext &context ) +{ + QgsFeatureRequest req; + req.setDestinationCrs( context.renderContext().coordinateTransform().destinationCrs(), context.renderContext().transformContext() ); + req.setFilterRect( context.extent() ); + req.setNoAttributes(); + + QgsFeatureIterator it = mTargetLayerSource->getFeatures( req ); + + mIndex = std::make_unique< QgsSpatialIndex >( it, context.renderContext().feedback(), QgsSpatialIndex::Flag::FlagStoreFeatureGeometries ); + + mInitialized = true; +} + +bool QgsAbstractLabelingEngineRuleDistanceFromFeature::candidateExceedsTolerance( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const +{ + if ( !mInitialized ) + const_cast< QgsAbstractLabelingEngineRuleDistanceFromFeature * >( this )->initialize( context ); + + const QgsRectangle candidateBounds = candidate->boundingBox(); + const QgsRectangle expandedBounds = candidateBounds.buffered( mDistanceMapUnits ); + + const QList overlapCandidates = mIndex->intersects( expandedBounds ); + if ( overlapCandidates.empty() ) + return !mMustBeDistant; + + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); + + const GEOSPreparedGeometry *candidateGeos = candidate->preparedMultiPartGeom(); + for ( const QgsFeatureId overlapCandidateId : overlapCandidates ) + { + if ( context.renderContext().feedback() && context.renderContext().feedback()->isCanceled() ) + break; + + try + { + geos::unique_ptr featureCandidate = QgsGeos::asGeos( mIndex->geometry( overlapCandidateId ).constGet() ); +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) + if ( GEOSPreparedDistanceWithin_r( geosctxt, candidateGeos, featureCandidate.get(), mDistanceMapUnits ) ) + { + return mMustBeDistant; + } +#else + QgsDebugError( QStringLiteral( "This rule requires GEOS 3.10+" ) ); + return false; +#endif + } + catch ( GEOSException &e ) + { + QgsDebugError( QStringLiteral( "GEOS exception: %1" ).arg( e.what() ) ); + } + } + + return !mMustBeDistant; +} + +// +// QgsLabelingEngineRuleMinimumDistanceLabelToFeature +// + +QgsLabelingEngineRuleMinimumDistanceLabelToFeature::QgsLabelingEngineRuleMinimumDistanceLabelToFeature() = default; +QgsLabelingEngineRuleMinimumDistanceLabelToFeature::~QgsLabelingEngineRuleMinimumDistanceLabelToFeature() = default; + +QgsLabelingEngineRuleMinimumDistanceLabelToFeature *QgsLabelingEngineRuleMinimumDistanceLabelToFeature::clone() const +{ + std::unique_ptr< QgsLabelingEngineRuleMinimumDistanceLabelToFeature> res = std::make_unique< QgsLabelingEngineRuleMinimumDistanceLabelToFeature >(); + copyCommonProperties( res.get() ); + return res.release(); +} + +QString QgsLabelingEngineRuleMinimumDistanceLabelToFeature::id() const +{ + return QStringLiteral( "minimumDistanceLabelToFeature" ); +} + + +// +// QgsLabelingEngineRuleMaximumDistanceLabelToFeature +// + +QgsLabelingEngineRuleMaximumDistanceLabelToFeature::QgsLabelingEngineRuleMaximumDistanceLabelToFeature() +{ + mMustBeDistant = false; +} + +QgsLabelingEngineRuleMaximumDistanceLabelToFeature::~QgsLabelingEngineRuleMaximumDistanceLabelToFeature() = default; + +QgsLabelingEngineRuleMaximumDistanceLabelToFeature *QgsLabelingEngineRuleMaximumDistanceLabelToFeature::clone() const +{ + std::unique_ptr< QgsLabelingEngineRuleMaximumDistanceLabelToFeature > res = std::make_unique< QgsLabelingEngineRuleMaximumDistanceLabelToFeature >(); + copyCommonProperties( res.get() ); + return res.release(); +} + +QString QgsLabelingEngineRuleMaximumDistanceLabelToFeature::id() const +{ + return QStringLiteral( "maximumDistanceLabelToFeature" ); +} + + +// +// QgsLabelingEngineRuleMinimumDistanceLabelToLabel +// + +QgsLabelingEngineRuleMinimumDistanceLabelToLabel::QgsLabelingEngineRuleMinimumDistanceLabelToLabel() = default; +QgsLabelingEngineRuleMinimumDistanceLabelToLabel::~QgsLabelingEngineRuleMinimumDistanceLabelToLabel() = default; + +QgsLabelingEngineRuleMinimumDistanceLabelToLabel *QgsLabelingEngineRuleMinimumDistanceLabelToLabel::clone() const +{ + std::unique_ptr< QgsLabelingEngineRuleMinimumDistanceLabelToLabel> res = std::make_unique< QgsLabelingEngineRuleMinimumDistanceLabelToLabel >(); + res->mLabeledLayer = mLabeledLayer; + res->mTargetLayer = mTargetLayer; + res->mDistance = mDistance; + res->mDistanceUnit = mDistanceUnit; + res->mDistanceUnitScale = mDistanceUnitScale; + return res.release(); +} + +QString QgsLabelingEngineRuleMinimumDistanceLabelToLabel::id() const +{ + return QStringLiteral( "minimumDistanceLabelToLabel" ); +} + +void QgsLabelingEngineRuleMinimumDistanceLabelToLabel::writeXml( QDomDocument &, QDomElement &element, const QgsReadWriteContext & ) const +{ + element.setAttribute( QStringLiteral( "distance" ), mDistance ); + element.setAttribute( QStringLiteral( "distanceUnit" ), QgsUnitTypes::encodeUnit( mDistanceUnit ) ); + element.setAttribute( QStringLiteral( "distanceUnitScale" ), QgsSymbolLayerUtils::encodeMapUnitScale( mDistanceUnitScale ) ); + + if ( mLabeledLayer ) + { + element.setAttribute( QStringLiteral( "labeledLayer" ), mLabeledLayer.layerId ); + element.setAttribute( QStringLiteral( "labeledLayerName" ), mLabeledLayer.name ); + element.setAttribute( QStringLiteral( "labeledLayerSource" ), mLabeledLayer.source ); + element.setAttribute( QStringLiteral( "labeledLayerProvider" ), mLabeledLayer.provider ); + } + if ( mTargetLayer ) + { + element.setAttribute( QStringLiteral( "targetLayer" ), mTargetLayer.layerId ); + element.setAttribute( QStringLiteral( "targetLayerName" ), mTargetLayer.name ); + element.setAttribute( QStringLiteral( "targetLayerSource" ), mTargetLayer.source ); + element.setAttribute( QStringLiteral( "targetLayerProvider" ), mTargetLayer.provider ); + } +} + +void QgsLabelingEngineRuleMinimumDistanceLabelToLabel::readXml( const QDomElement &element, const QgsReadWriteContext & ) +{ + mDistance = element.attribute( QStringLiteral( "distance" ), QStringLiteral( "0" ) ).toDouble(); + mDistanceUnit = QgsUnitTypes::decodeRenderUnit( element.attribute( QStringLiteral( "distanceUnit" ) ) ); + mDistanceUnitScale = QgsSymbolLayerUtils::decodeMapUnitScale( element.attribute( QStringLiteral( "distanceUnitScale" ) ) ); + + { + const QString layerId = element.attribute( QStringLiteral( "labeledLayer" ) ); + const QString layerName = element.attribute( QStringLiteral( "labeledLayerName" ) ); + const QString layerSource = element.attribute( QStringLiteral( "labeledLayerSource" ) ); + const QString layerProvider = element.attribute( QStringLiteral( "labeledLayerProvider" ) ); + mLabeledLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + } + { + const QString layerId = element.attribute( QStringLiteral( "targetLayer" ) ); + const QString layerName = element.attribute( QStringLiteral( "targetLayerName" ) ); + const QString layerSource = element.attribute( QStringLiteral( "targetLayerSource" ) ); + const QString layerProvider = element.attribute( QStringLiteral( "targetLayerProvider" ) ); + mTargetLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + } +} + +void QgsLabelingEngineRuleMinimumDistanceLabelToLabel::resolveReferences( const QgsProject *project ) +{ + mLabeledLayer.resolve( project ); + mTargetLayer.resolve( project ); +} + +bool QgsLabelingEngineRuleMinimumDistanceLabelToLabel::prepare( QgsRenderContext &context ) +{ + mDistanceMapUnits = context.convertToMapUnits( mDistance, mDistanceUnit, mDistanceUnitScale ); + return true; +} + +QgsRectangle QgsLabelingEngineRuleMinimumDistanceLabelToLabel::modifyCandidateConflictSearchBoundingBox( const QgsRectangle &candidateBounds ) const +{ + return candidateBounds.buffered( mDistanceMapUnits ); +} + +bool QgsLabelingEngineRuleMinimumDistanceLabelToLabel::candidatesAreConflicting( const pal::LabelPosition *lp1, const pal::LabelPosition *lp2 ) const +{ + // conflicts are commutative -- we need to check both layers + if ( + ( lp1->getFeaturePart()->feature()->provider()->layerId() == mLabeledLayer.layerId + && lp2->getFeaturePart()->feature()->provider()->layerId() == mTargetLayer.layerId ) + || + ( lp2->getFeaturePart()->feature()->provider()->layerId() == mLabeledLayer.layerId + && lp1->getFeaturePart()->feature()->provider()->layerId() == mTargetLayer.layerId ) + ) + { + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); + try + { +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) + if ( GEOSPreparedDistanceWithin_r( geosctxt, lp1->preparedMultiPartGeom(), lp2->multiPartGeom(), mDistanceMapUnits ) ) + { + return true; + } +#else + QgsDebugError( QStringLiteral( "This rule requires GEOS 3.10+" ) ); + return false; +#endif + } + catch ( GEOSException &e ) + { + QgsDebugError( QStringLiteral( "GEOS exception: %1" ).arg( e.what() ) ); + } + } + + return false; +} + +QgsVectorLayer *QgsLabelingEngineRuleMinimumDistanceLabelToLabel::labeledLayer() +{ + return mLabeledLayer.get(); +} + +void QgsLabelingEngineRuleMinimumDistanceLabelToLabel::setLabeledLayer( QgsVectorLayer *layer ) +{ + mLabeledLayer = layer; +} + +QgsVectorLayer *QgsLabelingEngineRuleMinimumDistanceLabelToLabel::targetLayer() +{ + return mTargetLayer.get(); +} + +void QgsLabelingEngineRuleMinimumDistanceLabelToLabel::setTargetLayer( QgsVectorLayer *layer ) +{ + mTargetLayer = layer; +} + + +// +// QgsLabelingEngineRuleAvoidLabelOverlapWithFeature +// + +QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() = default; +QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::~QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() = default; + +QgsLabelingEngineRuleAvoidLabelOverlapWithFeature *QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::clone() const +{ + std::unique_ptr< QgsLabelingEngineRuleAvoidLabelOverlapWithFeature> res = std::make_unique< QgsLabelingEngineRuleAvoidLabelOverlapWithFeature >(); + res->mLabeledLayer = mLabeledLayer; + res->mTargetLayer = mTargetLayer; + return res.release(); +} + +QString QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::id() const +{ + return QStringLiteral( "avoidLabelOverlapWithFeature" ); +} + +bool QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::prepare( QgsRenderContext & ) +{ + if ( !mTargetLayer ) + return false; + + QGIS_CHECK_OTHER_QOBJECT_THREAD_ACCESS( mTargetLayer ); + mTargetLayerSource = std::make_unique< QgsVectorLayerFeatureSource >( mTargetLayer.get() ); + return true; +} + +void QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::writeXml( QDomDocument &, QDomElement &element, const QgsReadWriteContext & ) const +{ + if ( mLabeledLayer ) + { + element.setAttribute( QStringLiteral( "labeledLayer" ), mLabeledLayer.layerId ); + element.setAttribute( QStringLiteral( "labeledLayerName" ), mLabeledLayer.name ); + element.setAttribute( QStringLiteral( "labeledLayerSource" ), mLabeledLayer.source ); + element.setAttribute( QStringLiteral( "labeledLayerProvider" ), mLabeledLayer.provider ); + } + if ( mTargetLayer ) + { + element.setAttribute( QStringLiteral( "targetLayer" ), mTargetLayer.layerId ); + element.setAttribute( QStringLiteral( "targetLayerName" ), mTargetLayer.name ); + element.setAttribute( QStringLiteral( "targetLayerSource" ), mTargetLayer.source ); + element.setAttribute( QStringLiteral( "targetLayerProvider" ), mTargetLayer.provider ); + } +} + +void QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::readXml( const QDomElement &element, const QgsReadWriteContext & ) +{ + { + const QString layerId = element.attribute( QStringLiteral( "labeledLayer" ) ); + const QString layerName = element.attribute( QStringLiteral( "labeledLayerName" ) ); + const QString layerSource = element.attribute( QStringLiteral( "labeledLayerSource" ) ); + const QString layerProvider = element.attribute( QStringLiteral( "labeledLayerProvider" ) ); + mLabeledLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + } + { + const QString layerId = element.attribute( QStringLiteral( "targetLayer" ) ); + const QString layerName = element.attribute( QStringLiteral( "targetLayerName" ) ); + const QString layerSource = element.attribute( QStringLiteral( "targetLayerSource" ) ); + const QString layerProvider = element.attribute( QStringLiteral( "targetLayerProvider" ) ); + mTargetLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + } +} + +void QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::resolveReferences( const QgsProject *project ) +{ + mLabeledLayer.resolve( project ); + mTargetLayer.resolve( project ); +} + +bool QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::candidateIsIllegal( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const +{ + if ( candidate->getFeaturePart()->feature()->provider()->layerId() != mLabeledLayer.layerId ) + { + return false; + } + + if ( !mTargetLayerSource ) + return false; + + if ( !mInitialized ) + const_cast< QgsLabelingEngineRuleAvoidLabelOverlapWithFeature * >( this )->initialize( context ); + + const QList overlapCandidates = mIndex->intersects( candidate->boundingBox() ); + if ( overlapCandidates.empty() ) + return false; + + GEOSContextHandle_t geosctxt = QgsGeosContext::get(); + + const GEOSPreparedGeometry *candidateGeos = candidate->preparedMultiPartGeom(); + for ( const QgsFeatureId overlapCandidateId : overlapCandidates ) + { + if ( context.renderContext().feedback() && context.renderContext().feedback()->isCanceled() ) + break; + + try + { + geos::unique_ptr featureCandidate = QgsGeos::asGeos( mIndex->geometry( overlapCandidateId ).constGet() ); + if ( GEOSPreparedIntersects_r( geosctxt, candidateGeos, featureCandidate.get() ) == 1 ) + return true; + } + catch ( GEOSException &e ) + { + QgsDebugError( QStringLiteral( "GEOS exception: %1" ).arg( e.what() ) ); + } + } + + return false; +} + +QgsVectorLayer *QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::labeledLayer() +{ + return mLabeledLayer.get(); +} + +void QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::setLabeledLayer( QgsVectorLayer *layer ) +{ + mLabeledLayer = layer; +} + +QgsVectorLayer *QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::targetLayer() +{ + return mTargetLayer.get(); +} + +void QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::setTargetLayer( QgsVectorLayer *layer ) +{ + mTargetLayer = layer; +} + +void QgsLabelingEngineRuleAvoidLabelOverlapWithFeature::initialize( QgsLabelingEngineContext &context ) +{ + QgsFeatureRequest req; + req.setDestinationCrs( context.renderContext().coordinateTransform().destinationCrs(), context.renderContext().transformContext() ); + req.setFilterRect( context.extent() ); + req.setNoAttributes(); + + QgsFeatureIterator it = mTargetLayerSource->getFeatures( req ); + + mIndex = std::make_unique< QgsSpatialIndex >( it, context.renderContext().feedback(), QgsSpatialIndex::Flag::FlagStoreFeatureGeometries ); + + mInitialized = true; +} diff --git a/src/core/labeling/rules/qgslabelingenginerule_impl.h b/src/core/labeling/rules/qgslabelingenginerule_impl.h new file mode 100644 index 000000000000..5fdb03be1b85 --- /dev/null +++ b/src/core/labeling/rules/qgslabelingenginerule_impl.h @@ -0,0 +1,416 @@ +/*************************************************************************** + qgslabelingenginerule_impl.h + --------------------- + Date : August 2024 + Copyright : (C) 2024 by 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 QGSLABELINGENGINERULEIMPL_H +#define QGSLABELINGENGINERULEIMPL_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgis.h" +#include "qgslabelingenginerule.h" +#include "qgsvectorlayerref.h" +#include "qgsmapunitscale.h" + +class QgsSpatialIndex; + + +/** + * Base class for labeling engine rules which prevents labels being placed too close or to far from features from a different layer. + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsAbstractLabelingEngineRuleDistanceFromFeature : public QgsAbstractLabelingEngineRule +{ + public: + + QgsAbstractLabelingEngineRuleDistanceFromFeature(); + ~QgsAbstractLabelingEngineRuleDistanceFromFeature() override; + bool prepare( QgsRenderContext &context ) override; + void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const override; + void readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; + void resolveReferences( const QgsProject *project ) override; + bool candidateIsIllegal( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const override SIP_SKIP; + void alterCandidateCost( pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const override SIP_SKIP; + + /** + * Returns the layer providing the labels. + * + * \see setLabeledLayer() + */ + QgsVectorLayer *labeledLayer(); + + /** + * Sets the \a layer providing the labels. + * + * \see labeledLayer() + */ + void setLabeledLayer( QgsVectorLayer *layer ); + + /** + * Returns the layer providing the features which labels must be distant from (or close to). + * + * \see setTargetLayer() + */ + QgsVectorLayer *targetLayer(); + + /** + * Sets the \a layer providing the features which labels must be distant from (or close to). + * + * \see targetLayer() + */ + void setTargetLayer( QgsVectorLayer *layer ); + + /** + * Returns the acceptable distance threshold between labels and the features + * from the targetLayer(). + * + * \see setDistance() + * \see distanceUnits() + */ + double distance() const { return mDistance; } + + /** + * Sets the acceptable \a distance threshold between labels and the features + * from the targetLayer(). + * + * \see distance() + * \see setDistanceUnits() + */ + void setDistance( double distance ) { mDistance = distance; } + + /** + * Returns the units for the distance between labels and the features + * from the targetLayer(). + * + * \see setDistanceUnit() + * \see distance() + */ + Qgis::RenderUnit distanceUnit() const { return mDistanceUnit; } + + /** + * Sets the \a unit for the distance between labels and the features + * from the targetLayer(). + * + * \see distanceUnit() + * \see setDistance() + */ + void setDistanceUnit( Qgis::RenderUnit unit ) { mDistanceUnit = unit; } + + /** + * Returns the scaling for the distance between labels and the features + * from the targetLayer(). + * + * \see setDistanceUnitScale() + * \see distance() + */ + const QgsMapUnitScale &distanceUnitScale() const { return mDistanceUnitScale; } + + /** + * Sets the \a scale for the distance between labels and the features + * from the targetLayer(). + * + * \see distanceUnitScale() + * \see setDistance() + */ + void setDistanceUnitScale( const QgsMapUnitScale &scale ) { mDistanceUnitScale = scale; } + + /** + * Returns the penalty cost incurred when the rule is violated. + * + * This is a value between 0 and 10, where 10 indicates that the rule must never be violated, + * and 1-9 = nice to have if possible, where higher numbers will try harder to avoid violating the rule. + * + * \see setCost() + */ + double cost() const { return mCost; } + + /** + * Sets the penalty \a cost incurred when the rule is violated. + * + * This is a value between 0 and 10, where 10 indicates that the rule must never be violated, + * and 1-9 = nice to have if possible, where higher numbers will try harder to avoid violating the rule. + * + * \see cost() + */ + void setCost( double cost ) { mCost = cost; } + + protected: + + /** + * Copies common properties from this object to an \a other. + */ + void copyCommonProperties( QgsAbstractLabelingEngineRuleDistanceFromFeature *other ) const; + + //! TRUE if labels must be distant from features, FALSE if they must be close + bool mMustBeDistant = true; + + private: +#ifdef SIP_RUN + QgsAbstractLabelingEngineRuleDistanceFromFeature( const QgsAbstractLabelingEngineRuleDistanceFromFeature &other ); +#endif + + void initialize( QgsLabelingEngineContext &context ); + + //! Returns TRUE if \a candidate is too close / too far from features from target layer + bool candidateExceedsTolerance( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const; + + //! Labeled layer + QgsVectorLayerRef mLabeledLayer; + //! Target layer + QgsVectorLayerRef mTargetLayer; + //! Distance threshold + double mDistance = 0; + //! Distance threshold unit + Qgis::RenderUnit mDistanceUnit = Qgis::RenderUnit::Millimeters; + //! Distance threshold map unit scale + QgsMapUnitScale mDistanceUnitScale; + //! Associated cost + double mCost = 0; + + // cached variables + double mDistanceMapUnits = 0; + std::unique_ptr< QgsAbstractFeatureSource > mTargetLayerSource; + std::unique_ptr< QgsSpatialIndex > mIndex; + bool mInitialized = false; +}; + + +/** + * A labeling engine rule which prevents labels being placed too close to features from a different layer. + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsLabelingEngineRuleMinimumDistanceLabelToFeature : public QgsAbstractLabelingEngineRuleDistanceFromFeature +{ + public: + + QgsLabelingEngineRuleMinimumDistanceLabelToFeature(); + ~QgsLabelingEngineRuleMinimumDistanceLabelToFeature() override; + QgsLabelingEngineRuleMinimumDistanceLabelToFeature *clone() const override SIP_FACTORY; + QString id() const override; + + private: +#ifdef SIP_RUN + QgsLabelingEngineRuleMinimumDistanceLabelToFeature( const QgsLabelingEngineRuleMinimumDistanceLabelToFeature & ); +#endif +}; + + +/** + * A labeling engine rule which prevents labels being placed too far from features from a different layer. + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsLabelingEngineRuleMaximumDistanceLabelToFeature : public QgsAbstractLabelingEngineRuleDistanceFromFeature +{ + public: + QgsLabelingEngineRuleMaximumDistanceLabelToFeature(); + ~QgsLabelingEngineRuleMaximumDistanceLabelToFeature() override; + QgsLabelingEngineRuleMaximumDistanceLabelToFeature *clone() const override SIP_FACTORY; + QString id() const override; + + private: +#ifdef SIP_RUN + QgsLabelingEngineRuleMaximumDistanceLabelToFeature( const QgsLabelingEngineRuleMaximumDistanceLabelToFeature & ); +#endif + +}; + +/** + * A labeling engine rule which prevents labels being placed too close to labels from a different layer. + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsLabelingEngineRuleMinimumDistanceLabelToLabel : public QgsAbstractLabelingEngineRule +{ + public: + QgsLabelingEngineRuleMinimumDistanceLabelToLabel(); + ~QgsLabelingEngineRuleMinimumDistanceLabelToLabel() override; + + QgsLabelingEngineRuleMinimumDistanceLabelToLabel *clone() const override SIP_FACTORY; + QString id() const override; + void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const override; + void readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; + void resolveReferences( const QgsProject *project ) override; + bool prepare( QgsRenderContext &context ) override; + QgsRectangle modifyCandidateConflictSearchBoundingBox( const QgsRectangle &candidateBounds ) const override SIP_SKIP; + bool candidatesAreConflicting( const pal::LabelPosition *lp1, const pal::LabelPosition *lp2 ) const override SIP_SKIP; + + /** + * Returns the layer providing the labels. + * + * \see setLabeledLayer() + */ + QgsVectorLayer *labeledLayer(); + + /** + * Sets the \a layer providing the labels. + * + * \see labeledLayer() + */ + void setLabeledLayer( QgsVectorLayer *layer ); + + /** + * Returns the layer providing the labels which labels must be distant from. + * + * \see setTargetLayer() + */ + QgsVectorLayer *targetLayer(); + + /** + * Sets the \a layer providing the labels which labels must be distant from. + * + * \see targetLayer() + */ + void setTargetLayer( QgsVectorLayer *layer ); + + /** + * Returns the minimum permitted distance between labels from the labeledLayer() and the labels + * from the targetLayer(). + * + * \see setDistance() + * \see distanceUnits() + */ + double distance() const { return mDistance; } + + /** + * Sets the minimum permitted \a distance between labels from the labeledLayer() and the labels + * from the targetLayer(). + * + * \see distance() + * \see setDistanceUnits() + */ + void setDistance( double distance ) { mDistance = distance; } + + /** + * Returns the units for the distance between labels from the labeledLayer() and the labels + * from the targetLayer(). + * + * \see setDistanceUnit() + * \see distance() + */ + Qgis::RenderUnit distanceUnit() const { return mDistanceUnit; } + + /** + * Sets the \a unit for the distance between labels from the labeledLayer() and the labels + * from the targetLayer(). + * + * \see distanceUnit() + * \see setDistance() + */ + void setDistanceUnit( Qgis::RenderUnit unit ) { mDistanceUnit = unit; } + + /** + * Returns the scaling for the distance between labels from the labeledLayer() and the labels + * from the targetLayer(). + * + * \see setDistanceUnitScale() + * \see distance() + */ + const QgsMapUnitScale &distanceUnitScale() const { return mDistanceUnitScale; } + + /** + * Sets the \a scale for the distance between labels from the labeledLayer() and the labels + * from the targetLayer(). + * + * \see distanceUnitScale() + * \see setDistance() + */ + void setDistanceUnitScale( const QgsMapUnitScale &scale ) { mDistanceUnitScale = scale; } + + private: +#ifdef SIP_RUN + QgsLabelingEngineRuleMinimumDistanceLabelToLabel( const QgsLabelingEngineRuleMinimumDistanceLabelToLabel & ); +#endif + + QgsVectorLayerRef mLabeledLayer; + QgsVectorLayerRef mTargetLayer; + double mDistance = 0; + Qgis::RenderUnit mDistanceUnit = Qgis::RenderUnit::Millimeters; + QgsMapUnitScale mDistanceUnitScale; + + // cached variables + double mDistanceMapUnits = 0; +}; + + +/** + * A labeling engine rule which prevents labels being placed overlapping features from a different layer. + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsLabelingEngineRuleAvoidLabelOverlapWithFeature : public QgsAbstractLabelingEngineRule +{ + public: + + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature(); + ~QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() override; + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature *clone() const override SIP_FACTORY; + QString id() const override; + bool prepare( QgsRenderContext &context ) override; + void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const override; + void readXml( const QDomElement &element, const QgsReadWriteContext &context ) override; + void resolveReferences( const QgsProject *project ) override; + bool candidateIsIllegal( const pal::LabelPosition *candidate, QgsLabelingEngineContext &context ) const override SIP_SKIP; + + /** + * Returns the layer providing the labels. + * + * \see setLabeledLayer() + */ + QgsVectorLayer *labeledLayer(); + + /** + * Sets the \a layer providing the labels. + * + * \see labeledLayer() + */ + void setLabeledLayer( QgsVectorLayer *layer ); + + /** + * Returns the layer providing the features which labels must not overlap. + * + * \see setTargetLayer() + */ + QgsVectorLayer *targetLayer(); + + /** + * Sets the \a layer providing the features which labels must not overlap. + * + * \see targetLayer() + */ + void setTargetLayer( QgsVectorLayer *layer ); + + private: +#ifdef SIP_RUN + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature( const QgsLabelingEngineRuleAvoidLabelOverlapWithFeature & ); +#endif + void initialize( QgsLabelingEngineContext &context ); + + QgsVectorLayerRef mLabeledLayer; + QgsVectorLayerRef mTargetLayer; + + // cached variables + std::unique_ptr< QgsAbstractFeatureSource > mTargetLayerSource; + std::unique_ptr< QgsSpatialIndex > mIndex; + bool mInitialized = false; +}; + + +#endif // QGSLABELINGENGINERULEIMPL_H diff --git a/src/core/labeling/rules/qgslabelingengineruleregistry.cpp b/src/core/labeling/rules/qgslabelingengineruleregistry.cpp new file mode 100644 index 000000000000..b6b066b33ea1 --- /dev/null +++ b/src/core/labeling/rules/qgslabelingengineruleregistry.cpp @@ -0,0 +1,72 @@ +/*************************************************************************** + qgslabelingengineruleregistry.cpp + --------------------- + Date : August 2024 + Copyright : (C) 2024 by 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 "qgslabelingengineruleregistry.h" +#include "qgslabelingenginerule.h" +#include "qgslabelingenginerule_impl.h" +#include + +QgsLabelingEngineRuleRegistry::QgsLabelingEngineRuleRegistry() +{ +#if GEOS_VERSION_MAJOR>3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR>=10 ) + addRule( new QgsLabelingEngineRuleMinimumDistanceLabelToFeature() ); + addRule( new QgsLabelingEngineRuleMaximumDistanceLabelToFeature() ); + addRule( new QgsLabelingEngineRuleMinimumDistanceLabelToLabel() ); +#endif + + addRule( new QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() ); +} + +QgsLabelingEngineRuleRegistry::~QgsLabelingEngineRuleRegistry() = default; + +QStringList QgsLabelingEngineRuleRegistry::ruleIds() const +{ + QStringList res; + res.reserve( static_cast< int >( mRules.size() ) ); + for ( auto &it : mRules ) + { + res.append( it.first ); + } + return res; +} + +QgsAbstractLabelingEngineRule *QgsLabelingEngineRuleRegistry::create( const QString &id ) const +{ + auto it = mRules.find( id ); + if ( it == mRules.end() ) + return nullptr; + + return it->second->clone(); +} + +bool QgsLabelingEngineRuleRegistry::addRule( QgsAbstractLabelingEngineRule *rule ) +{ + if ( !rule ) + return false; + + if ( mRules.find( rule->id() ) != mRules.end() ) + { + delete rule; + return false; + } + + mRules[ rule->id() ] = std::unique_ptr< QgsAbstractLabelingEngineRule >( rule ); + return true; +} + +void QgsLabelingEngineRuleRegistry::removeRule( const QString &id ) +{ + mRules.erase( id ); +} + diff --git a/src/core/labeling/rules/qgslabelingengineruleregistry.h b/src/core/labeling/rules/qgslabelingengineruleregistry.h new file mode 100644 index 000000000000..9330859e5243 --- /dev/null +++ b/src/core/labeling/rules/qgslabelingengineruleregistry.h @@ -0,0 +1,96 @@ +/*************************************************************************** + qgslabelingengineruleregistry.h + --------------------- + Date : August 2024 + Copyright : (C) 2024 by 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 QGSLABELINGENGINERULEREGISTRY_H +#define QGSLABELINGENGINERULEREGISTRY_H + +#include "qgis_core.h" +#include "qgis_sip.h" +#include "qgis.h" + +class QgsAbstractLabelingEngineRule; + +/** + * A registry for labeling engine rules. + * + * Labeling engine rules implement custom logic to modify the labeling solution for a map render, + * e.g. by preventing labels being placed which violate custom constraints. + * + * This registry stores available rules and is responsible for creating rules. + * + * QgsLabelingEngineRuleRegistry is not usually directly created, but rather accessed through + * QgsApplication::labelEngineRuleRegistry(). + * + * \ingroup core + * \since QGIS 3.40 + */ +class CORE_EXPORT QgsLabelingEngineRuleRegistry +{ + public: + + /** + * Constructor for QgsLabelingEngineRuleRegistry, containing a set of + * default rules. + */ + QgsLabelingEngineRuleRegistry(); + ~QgsLabelingEngineRuleRegistry(); + + //! QgsLabelingEngineRuleRegistry cannot be copied + QgsLabelingEngineRuleRegistry( const QgsLabelingEngineRuleRegistry &other ) = delete; + //! QgsLabelingEngineRuleRegistry cannot be copied + QgsLabelingEngineRuleRegistry &operator=( const QgsLabelingEngineRuleRegistry &other ) = delete; + + /** + * Returns a list of the rule IDs for rules present in the registry. + */ + QStringList ruleIds() const; + + /** + * Creates a new rule from the type with matching \a id. + * + * Returns NULLPTR if no matching rule was found in the registry. + * + * The caller takes ownership of the returned object. + */ + QgsAbstractLabelingEngineRule *create( const QString &id ) const SIP_TRANSFERBACK; + + /** + * Adds a new \a rule type to the registry. + * + * The registry takes ownership of \a rule. + * + * \returns TRUE if the rule was successfully added. + * + * \see removeRule() + */ + bool addRule( QgsAbstractLabelingEngineRule *rule SIP_TRANSFER ); + + /** + * Removes the rule with matching \a id from the registry. + * + * \see addRule() + */ + void removeRule( const QString &id ); + + private: + +#ifdef SIP_RUN + QgsLabelingEngineRuleRegistry( const QgsLabelingEngineRuleRegistry &other ); +#endif + + std::map< QString, std::unique_ptr< QgsAbstractLabelingEngineRule > > mRules; + +}; + +#endif // QGSLABELINGENGINERULEREGISTRY_H diff --git a/src/core/maprenderer/qgsmaprendererjob.cpp b/src/core/maprenderer/qgsmaprendererjob.cpp index 0939965cdbf0..9190f87a2b78 100644 --- a/src/core/maprenderer/qgsmaprendererjob.cpp +++ b/src/core/maprenderer/qgsmaprendererjob.cpp @@ -1041,6 +1041,8 @@ LabelRenderJob QgsMapRendererJob::prepareLabelingJob( QPainter *painter, QgsLabe job.context.setPainter( painter ); job.context.setLabelingEngine( labelingEngine2 ); job.context.setFeedback( mLabelingEngineFeedback ); + if ( labelingEngine2 ) + job.context.labelingEngine()->prepare( job.context ); QgsRectangle r1 = mSettings.visibleExtent(); r1.grow( mSettings.extentBuffer() ); diff --git a/src/core/project/qgsproject.cpp b/src/core/project/qgsproject.cpp index ca7e5e15c5e6..70397d6ffd49 100644 --- a/src/core/project/qgsproject.cpp +++ b/src/core/project/qgsproject.cpp @@ -2496,6 +2496,12 @@ bool QgsProject::readProjectFile( const QString &filename, Qgis::ProjectReadFlag profile.switchTask( tr( "Loading label settings" ) ); mLabelingEngineSettings->readSettingsFromProject( this ); + { + const QDomElement labelEngineSettingsElement = doc->documentElement().firstChildElement( QStringLiteral( "labelEngineSettings" ) ); + mLabelingEngineSettings->readXml( labelEngineSettingsElement, context ); + } + mLabelingEngineSettings->resolveReferences( this ); + emit labelingEngineSettingsChanged(); profile.switchTask( tr( "Loading annotations" ) ); @@ -3365,6 +3371,11 @@ bool QgsProject::writeProjectFile( const QString &filename ) qgisNode.appendChild( layerOrderNode ); mLabelingEngineSettings->writeSettingsToProject( this ); + { + QDomElement labelEngineSettingsElement = doc->createElement( QStringLiteral( "labelEngineSettings" ) ); + mLabelingEngineSettings->writeXml( *doc, labelEngineSettingsElement, context ); + qgisNode.appendChild( labelEngineSettingsElement ); + } writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorRedPart" ), mBackgroundColor.red() ); writeEntry( QStringLiteral( "Gui" ), QStringLiteral( "/CanvasColorGreenPart" ), mBackgroundColor.green() ); diff --git a/src/core/qgsapplication.cpp b/src/core/qgsapplication.cpp index 8072b88a22c9..7a369e1592ea 100644 --- a/src/core/qgsapplication.cpp +++ b/src/core/qgsapplication.cpp @@ -36,6 +36,7 @@ #include "qgsnumericformatregistry.h" #include "qgsfieldformatterregistry.h" #include "qgsscalebarrendererregistry.h" +#include "qgslabelingengineruleregistry.h" #include "qgssvgcache.h" #include "qgsimagecache.h" #include "qgssourcecache.h" @@ -2631,6 +2632,11 @@ QgsScaleBarRendererRegistry *QgsApplication::scaleBarRendererRegistry() return members()->mScaleBarRendererRegistry; } +QgsLabelingEngineRuleRegistry *QgsApplication::labelingEngineRuleRegistry() +{ + return members()->mLabelingEngineRuleRegistry; +} + QgsProjectStorageRegistry *QgsApplication::projectStorageRegistry() { return members()->mProjectStorageRegistry; @@ -2808,6 +2814,11 @@ QgsApplication::ApplicationMembers::ApplicationMembers() mAnnotationItemRegistry->populate(); profiler->end(); } + { + profiler->start( tr( "Setup labeling engine rule registry" ) ); + mLabelingEngineRuleRegistry = new QgsLabelingEngineRuleRegistry(); + profiler->end(); + } { profiler->start( tr( "Setup sensor registry" ) ); mSensorRegistry = new QgsSensorRegistry(); @@ -2897,6 +2908,7 @@ QgsApplication::ApplicationMembers::~ApplicationMembers() delete mSourceCache; delete mCalloutRegistry; delete mRecentStyleHandler; + delete mLabelingEngineRuleRegistry; delete mSymbolLayerRegistry; delete mExternalStorageRegistry; delete mProfileSourceRegistry; diff --git a/src/core/qgsapplication.h b/src/core/qgsapplication.h index 49808d4e7ecc..2b2c9b343777 100644 --- a/src/core/qgsapplication.h +++ b/src/core/qgsapplication.h @@ -78,6 +78,7 @@ class QgsDatabaseQueryLog; class QgsFontManager; class QgsSensorRegistry; class QgsProfileSourceRegistry; +class QgsLabelingEngineRuleRegistry; /** * \ingroup core @@ -930,6 +931,13 @@ class CORE_EXPORT QgsApplication : public QApplication */ static QgsScaleBarRendererRegistry *scaleBarRendererRegistry() SIP_KEEPREFERENCE; + /** + * Gets the registry of available labeling engine rules. + * + * \since QGIS 3.40 + */ + static QgsLabelingEngineRuleRegistry *labelingEngineRuleRegistry() SIP_KEEPREFERENCE; + /** * Returns registry of available project storage implementations. * \since QGIS 3.2 @@ -1146,6 +1154,7 @@ class CORE_EXPORT QgsApplication : public QApplication QgsBabelFormatRegistry *mGpsBabelFormatRegistry = nullptr; QgsNetworkContentFetcherRegistry *mNetworkContentFetcherRegistry = nullptr; QgsScaleBarRendererRegistry *mScaleBarRendererRegistry = nullptr; + QgsLabelingEngineRuleRegistry *mLabelingEngineRuleRegistry = nullptr; QgsValidityCheckRegistry *mValidityCheckRegistry = nullptr; QgsMessageLog *mMessageLog = nullptr; QgsPaintEffectRegistry *mPaintEffectRegistry = nullptr; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index 01c136142ae8..40516eb717d0 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -117,6 +117,7 @@ ADD_PYTHON_TEST(PyQgsImageCache test_qgsimagecache.py) ADD_PYTHON_TEST(PyQgsInterpolatedLineSymbolLayer test_qgsinterpolatedlinesymbollayers.py) ADD_PYTHON_TEST(PyQgsInterval test_qgsinterval.py) ADD_PYTHON_TEST(PyQgsJsonUtils test_qgsjsonutils.py) +ADD_PYTHON_TEST(PyQgsLabelingEngineRule test_qgslabelingenginerule.py) ADD_PYTHON_TEST(PyQgsLabelLineSettings test_qgslabellinesettings.py) ADD_PYTHON_TEST(PyQgsLabelObstacleSettings test_qgslabelobstaclesettings.py) ADD_PYTHON_TEST(PyQgsLabelPlacementSettings test_qgslabelplacementsettings.py) diff --git a/tests/src/python/test_qgslabelingenginerule.py b/tests/src/python/test_qgslabelingenginerule.py new file mode 100644 index 000000000000..c850b1f6cc6b --- /dev/null +++ b/tests/src/python/test_qgslabelingenginerule.py @@ -0,0 +1,313 @@ +"""QGIS Unit tests for labeling engine rules + +.. note:: 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. +""" +import os +import tempfile + +from qgis.PyQt.QtXml import QDomDocument + +from qgis.core import ( + Qgis, + QgsLabelingEngineRuleRegistry, + QgsAbstractLabelingEngineRule, + QgsLabelingEngineRuleMinimumDistanceLabelToFeature, + QgsLabelingEngineRuleMinimumDistanceLabelToLabel, + QgsLabelingEngineRuleMaximumDistanceLabelToFeature, + QgsLabelingEngineRuleAvoidLabelOverlapWithFeature, + QgsProject, + QgsVectorLayer, + QgsMapUnitScale, + QgsReadWriteContext, + QgsLabelingEngineSettings +) +import unittest +from qgis.testing import start_app, QgisTestCase + +start_app() + + +class TestRule(QgsAbstractLabelingEngineRule): + + def id(self): + return 'test' + + def prepare(self, context): + pass + + def writeXml(self, doc, element, context): + pass + + def modifyProblem(self): + pass + + def readXml(self, element, context): + pass + + def clone(self): + return TestRule() + + +class TestQgsLabelingEngineRule(QgisTestCase): + + def testRegistry(self): + registry = QgsLabelingEngineRuleRegistry() + self.assertTrue(registry.ruleIds()) + for rule_id in registry.ruleIds(): + self.assertEqual(registry.create(rule_id).id(), rule_id) + + self.assertIsNone(registry.create('bad')) + + self.assertIn('minimumDistanceLabelToFeature', registry.ruleIds()) + + self.assertFalse(registry.addRule(None)) + + self.assertTrue(registry.addRule(TestRule())) + + self.assertIn('test', registry.ruleIds()) + self.assertIsInstance(registry.create('test'), TestRule) + + # no duplicates + self.assertFalse(registry.addRule(TestRule())) + + registry.removeRule('test') + + self.assertNotIn('test', registry.ruleIds()) + self.assertIsNone(registry.create('test')) + + registry.removeRule('test') + + def testMinimumDistanceLabelToFeature(self): + p = QgsProject() + vl = QgsVectorLayer('Point', 'layer 1', 'memory') + vl2 = QgsVectorLayer('Point', 'layer 2', 'memory') + p.addMapLayers([vl, vl2]) + + rule = QgsLabelingEngineRuleMinimumDistanceLabelToFeature() + rule.setLabeledLayer(vl) + rule.setTargetLayer(vl2) + rule.setDistance(14) + rule.setDistanceUnit(Qgis.RenderUnit.Inches) + rule.setDistanceUnitScale(QgsMapUnitScale(15, 25)) + rule.setCost(6.6) + + self.assertEqual(rule.labeledLayer(), vl) + self.assertEqual(rule.targetLayer(), vl2) + self.assertEqual(rule.distance(), 14) + self.assertEqual(rule.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule.distanceUnitScale().minScale, 15) + self.assertEqual(rule.distanceUnitScale().maxScale, 25) + self.assertEqual(rule.cost(), 6.6) + + rule2 = rule.clone() + self.assertEqual(rule2.labeledLayer(), vl) + self.assertEqual(rule2.targetLayer(), vl2) + self.assertEqual(rule2.distance(), 14) + self.assertEqual(rule2.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule2.distanceUnitScale().minScale, 15) + self.assertEqual(rule2.distanceUnitScale().maxScale, 25) + self.assertEqual(rule2.cost(), 6.6) + + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + rule.writeXml(doc, elem, QgsReadWriteContext()) + + rule3 = QgsLabelingEngineRuleMinimumDistanceLabelToFeature() + rule3.readXml(elem, QgsReadWriteContext()) + rule3.resolveReferences(p) + self.assertEqual(rule3.labeledLayer(), vl) + self.assertEqual(rule3.targetLayer(), vl2) + self.assertEqual(rule3.distance(), 14) + self.assertEqual(rule3.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule3.distanceUnitScale().minScale, 15) + self.assertEqual(rule3.distanceUnitScale().maxScale, 25) + self.assertEqual(rule3.cost(), 6.6) + + def testMinimumDistanceLabelToLabel(self): + p = QgsProject() + vl = QgsVectorLayer('Point', 'layer 1', 'memory') + vl2 = QgsVectorLayer('Point', 'layer 2', 'memory') + p.addMapLayers([vl, vl2]) + + rule = QgsLabelingEngineRuleMinimumDistanceLabelToLabel() + rule.setLabeledLayer(vl) + rule.setTargetLayer(vl2) + rule.setDistance(14) + rule.setDistanceUnit(Qgis.RenderUnit.Inches) + rule.setDistanceUnitScale(QgsMapUnitScale(15, 25)) + + self.assertEqual(rule.labeledLayer(), vl) + self.assertEqual(rule.targetLayer(), vl2) + self.assertEqual(rule.distance(), 14) + self.assertEqual(rule.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule.distanceUnitScale().minScale, 15) + self.assertEqual(rule.distanceUnitScale().maxScale, 25) + + rule2 = rule.clone() + self.assertEqual(rule2.labeledLayer(), vl) + self.assertEqual(rule2.targetLayer(), vl2) + self.assertEqual(rule2.distance(), 14) + self.assertEqual(rule2.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule2.distanceUnitScale().minScale, 15) + self.assertEqual(rule2.distanceUnitScale().maxScale, 25) + + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + rule.writeXml(doc, elem, QgsReadWriteContext()) + + rule3 = QgsLabelingEngineRuleMinimumDistanceLabelToLabel() + rule3.readXml(elem, QgsReadWriteContext()) + rule3.resolveReferences(p) + self.assertEqual(rule3.labeledLayer(), vl) + self.assertEqual(rule3.targetLayer(), vl2) + self.assertEqual(rule3.distance(), 14) + self.assertEqual(rule3.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule3.distanceUnitScale().minScale, 15) + self.assertEqual(rule3.distanceUnitScale().maxScale, 25) + + def testMaximumDistanceLabelToFeature(self): + p = QgsProject() + vl = QgsVectorLayer('Point', 'layer 1', 'memory') + vl2 = QgsVectorLayer('Point', 'layer 2', 'memory') + p.addMapLayers([vl, vl2]) + + rule = QgsLabelingEngineRuleMaximumDistanceLabelToFeature() + rule.setLabeledLayer(vl) + rule.setTargetLayer(vl2) + rule.setDistance(14) + rule.setDistanceUnit(Qgis.RenderUnit.Inches) + rule.setDistanceUnitScale(QgsMapUnitScale(15, 25)) + rule.setCost(6.6) + + self.assertEqual(rule.labeledLayer(), vl) + self.assertEqual(rule.targetLayer(), vl2) + self.assertEqual(rule.distance(), 14) + self.assertEqual(rule.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule.distanceUnitScale().minScale, 15) + self.assertEqual(rule.distanceUnitScale().maxScale, 25) + self.assertEqual(rule.cost(), 6.6) + + rule2 = rule.clone() + self.assertEqual(rule2.labeledLayer(), vl) + self.assertEqual(rule2.targetLayer(), vl2) + self.assertEqual(rule2.distance(), 14) + self.assertEqual(rule2.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule2.distanceUnitScale().minScale, 15) + self.assertEqual(rule2.distanceUnitScale().maxScale, 25) + self.assertEqual(rule2.cost(), 6.6) + + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + rule.writeXml(doc, elem, QgsReadWriteContext()) + + rule3 = QgsLabelingEngineRuleMaximumDistanceLabelToFeature() + rule3.readXml(elem, QgsReadWriteContext()) + rule3.resolveReferences(p) + self.assertEqual(rule3.labeledLayer(), vl) + self.assertEqual(rule3.targetLayer(), vl2) + self.assertEqual(rule3.distance(), 14) + self.assertEqual(rule3.distanceUnit(), Qgis.RenderUnit.Inches) + self.assertEqual(rule3.distanceUnitScale().minScale, 15) + self.assertEqual(rule3.distanceUnitScale().maxScale, 25) + self.assertEqual(rule3.cost(), 6.6) + + def testAvoidLabelOverlapWithFeature(self): + p = QgsProject() + vl = QgsVectorLayer('Point', 'layer 1', 'memory') + vl2 = QgsVectorLayer('Point', 'layer 2', 'memory') + p.addMapLayers([vl, vl2]) + + rule = QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() + rule.setLabeledLayer(vl) + rule.setTargetLayer(vl2) + + self.assertEqual(rule.labeledLayer(), vl) + self.assertEqual(rule.targetLayer(), vl2) + + rule2 = rule.clone() + self.assertEqual(rule2.labeledLayer(), vl) + self.assertEqual(rule2.targetLayer(), vl2) + + doc = QDomDocument("testdoc") + elem = doc.createElement("test") + rule.writeXml(doc, elem, QgsReadWriteContext()) + + rule3 = QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() + rule3.readXml(elem, QgsReadWriteContext()) + rule3.resolveReferences(p) + self.assertEqual(rule3.labeledLayer(), vl) + self.assertEqual(rule3.targetLayer(), vl2) + + def test_settings(self): + """ + Test attaching rules to QgsLabelingEngineSettings + """ + p = QgsProject() + vl = QgsVectorLayer('Point', 'layer 1', 'memory') + vl2 = QgsVectorLayer('Point', 'layer 2', 'memory') + p.addMapLayers([vl, vl2]) + + self.assertFalse(p.labelingEngineSettings().rules()) + + rule = QgsLabelingEngineRuleMaximumDistanceLabelToFeature() + rule.setLabeledLayer(vl) + rule.setTargetLayer(vl2) + rule.setCost(6.6) + + label_engine_settings = p.labelingEngineSettings() + label_engine_settings.addRule(rule) + self.assertEqual([r.id() for r in label_engine_settings.rules()], ['maximumDistanceLabelToFeature']) + + rule2 = QgsLabelingEngineRuleAvoidLabelOverlapWithFeature() + rule2.setLabeledLayer(vl2) + rule2.setTargetLayer(vl) + label_engine_settings.addRule(rule2) + self.assertEqual([r.id() for r in label_engine_settings.rules()], ['maximumDistanceLabelToFeature', 'avoidLabelOverlapWithFeature']) + + p.setLabelingEngineSettings(label_engine_settings) + + label_engine_settings = p.labelingEngineSettings() + self.assertEqual([r.id() for r in label_engine_settings.rules()], + ['maximumDistanceLabelToFeature', + 'avoidLabelOverlapWithFeature']) + + # save, restore project + with tempfile.TemporaryDirectory() as temp_dir: + self.assertTrue(p.write(os.path.join(temp_dir, 'p.qgs'))) + + p2 = QgsProject() + self.assertTrue(p2.read(os.path.join(temp_dir, 'p.qgs'))) + + label_engine_settings = p2.labelingEngineSettings() + self.assertEqual([r.id() for r in label_engine_settings.rules()], + ['maximumDistanceLabelToFeature', + 'avoidLabelOverlapWithFeature']) + + # check layers, settings + rule1 = label_engine_settings.rules()[0] + self.assertIsInstance(rule1, QgsLabelingEngineRuleMaximumDistanceLabelToFeature) + self.assertEqual(rule1.cost(), 6.6) + self.assertEqual(rule1.labeledLayer().name(), 'layer 1') + self.assertEqual(rule1.targetLayer().name(), 'layer 2') + + rule2 = label_engine_settings.rules()[1] + self.assertIsInstance(rule2, QgsLabelingEngineRuleAvoidLabelOverlapWithFeature) + self.assertEqual(rule2.labeledLayer().name(), 'layer 2') + self.assertEqual(rule2.targetLayer().name(), 'layer 1') + + # test setRules + rule = QgsLabelingEngineRuleMinimumDistanceLabelToFeature() + rule.setLabeledLayer(vl) + rule.setTargetLayer(vl2) + rule.setCost(6.6) + + label_engine_settings.setRules([rule]) + self.assertEqual([r.id() for r in label_engine_settings.rules()], + ['minimumDistanceLabelToFeature']) + + +if __name__ == '__main__': + unittest.main()