qgis / QGIS-Enhancement-Proposals

QEP's (QGIS Enhancement Proposals) are used in the process of creating and discussing new enhancements for QGIS

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Advanced labeling rules

nyalldawson opened this issue · comments

QGIS Enhancement: Advanced labeling rules

Date 2024/07/09

Author Nyall Dawson (@nyalldawson)

Contact nyall dot dawson at gmail dot com

Version QGIS 3.40

Summary

This proposal covers introduction of advanced rules for map labeling. Advanced rules are project-level settings, which define specific constraints which must be satisfied during rendering of maps for that project.

Examples of advanced rules include:

  • "Labels from the XX layer must be at least YY millimetres distant from the features from the ZZ layer"
  • "Labels from the XX layer must be at most YY millimetres distant from the features from the ZZ layer"
  • "Labels from the XX layer must be at least YY millimetres distant from the labels from the ZZ layer"

Users can configure any combination of rules as desired, including multiple copies of the same rule with different properties (eg different target layers or different distances)

This will allow a very flexible means for users to fine-tune the exact label logic for their maps, and provide a framework for more custom advanced labeling rules to be implemented in future.

Proposed Solution

The initial set of rules to be implemented are:

  • Labels from X layer must be further than Y from the features in Z
  • Labels from X layer must be further than Y from the labels in Z
  • Labels from X layer must be within Y from the features from Z
  • Labels from X layer must not overlap features from Y layer

Configuration of Advanced Rules

Given the initial targeted set of rules, the GUI for configuring advanced labeling rules will be similar to that of the existing "Topology Checker" plugin:

image

Each rule will have an associated penalty "cost", exposed as a value from 1-10, reflecting how "important" the rule is (where 10 = Very important, must not be violated, 1-9 = Nice to have if possible, where higher numbers will try harder to avoid violating the rule).

Distance based settings will also expose control over the distance unit (eg Millimeters, Map Units, etc)

The advanced rule configuration will be exposed as an inline panel accessed from the project label settings panel:

image

API Changes

QgsAbstractLabelingEngineRule

A new abstract base class will be introduced for labeling engine rules. This class contains standard virtual methods for cloning, serializing and identifying the rule subclasses:

/**
 * 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.
 *
 * \ingroup core
 * \since QGIS 3.40
 */
class CORE_EXPORT QgsAbstractLabelingEngineRule SIP_ABSTRACT
{
public:

    virtual ~QgsAbstractLabelingEngineRule() = default;

    /**
     * Creates a clone of this rule.
     *
     * The caller takes ownership of the returned object.
     */
    virtual QgsAbstractLabelingEngineRule* clone() const SIP_FACTORY = 0;

    /**
     * Returns a string uniquely identifying the rule subclass.
     */
    virtual QString id() const = 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 writeXml()
     */
    virtual void readXml( const QDomElement &element, const QgsReadWriteContext &context ) = 0;

};

The base class will also have virtual methods which allow the rule to interact with the labeling problem, eg:

    virtual bool modifyProblem( ... ) {};

where arguments will be various internal members of the pal problem solving code, e.g. Pal::Problem. Since these PAL classes are all private, internal API, these methods will NOT be exposed to PyQGIS and accordingly it will NOT be possible for Python plugins to implement custom rule subclasses.

Additionally, there will be a virtual method for rule preparation, allowing for pre-preparation of required members in a thread safe manner which must be performed in advance on the main rendering thread. (eg creation of feature sources and iterators)

    /**
     * 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;

QgsLabelingEngineRuleRegistry

A registry for QgsAbstractLabelingEngineRule subclasses will be created:

/**
 * 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;

    /**
     * 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_FACTORY;

    /**
     * Adds a new \a rule type to the registry.
     *
     * The registry takes ownership of \a rule.
     */
    void addRule( QgsAbstractLabelingEngineRule* rule SIP_TRANSFER );

Like our other single-instance registries, this will be accessed via the QgsApplication class (via QgsApplication::labelEngineRuleRegistry() ).

Rule subclasses

The four rules initially included will be created as:

  • QgsLabelingEngineRuleMinimumDistanceLabelToFeature
  • QgsLabelingEngineRuleMinimumDistanceLabelToLabel
  • QgsLabelingEngineRuleMaximumDistanceLabelToFeature
  • QgsLabelingEngineRuleAvoidLabelOverlapWithFeature

The API for each subclass will be similar, with labeled layer getter/setters and target layer getter/setters, along with associated distance properties. Eg for QgsLabelingEngineRuleMinimumDistanceLabelToFeature the API will be:


/**
 * 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 QgsAbstractLabelingEngineRule
{
public:

    QgsLabelingEngineRuleMinimumDistanceLabelToFeature* clone() SIP_FACTORY const override;
    QString id() const override;
    bool prepare( QgsRenderContext& context ) override;
    bool modifyProblem() override;
    void writeXml( QDomDocument &doc, QDomElement &element, const QgsReadWriteContext &context ) const override;
    void readXml( const QDomElement &element, const QgsReadWriteContext &context ) override;

    /**
     * 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.
     *
     * \see setTargetLayer()
     */
    QgsVectorLayer* targetLayer();

    /**
     * Sets the \a layer providing the features which labels must be distant from.
     *
     * \see targetLayer()
     */
    void setTargetLayer( QgsVectorLayer* layer );

    /**
     * Returns the minimum permitted distance between labels and the features
     * from the targetLayer().
     *
     * \see setDistance()
     * \see distanceUnits()
     */
    double distance() const;

    /**
     * Sets the minimum permitted \a distance between labels and the features
     * from the targetLayer().
     *
     * \see distance()
     * \see setDistanceUnits()
     */
    void setDistance( double distance );

    /**
     * Returns the units for the distance between labels and the features
     * from the targetLayer().
     *
     * \see setDistanceUnit()
     * \see distance()
     */
    Qgis::RenderUnit distanceUnit() const;

    /**
     * Sets the \a unit for the distance between labels and the features
     * from the targetLayer().
     *
     * \see distanceUnit()
     * \see setDistance()
     */
    void setDistanceUnit( Qgis::RenderUnit unit );

    /**
     * Returns the scaling for the distance between labels and the features
     * from the targetLayer().
     *
     * \see setDistanceUnitScale()
     * \see distance()
     */
    const QgsMapUnitScale& distanceUnitScale() const;

    /**
     * Sets the \a scale for the distance between labels and the features
     * from the targetLayer().
     *
     * \see distanceUnitScale()
     * \see setDistance()
     */
    void setDistanceUnitScale( const QgsMapUnitScale& 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;

    /**
     * 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 );

QgsLabelingEngineSettings changes

The QgsLabelingEngineSettings class will be modified to add methods for adding rules and retrieving rules, via:

    /**
     * Returns a list of labeling engine rules which must be satifisfied
     * while placing labels.
     *
     * \see addRule()
     * \since QGIS 3.40
     */
    QList< QgsAbstractLabelingEngineRule* > rules();

    /**
     * Adds a labeling engine \a rule which must be satifisfied
     * while placing labels.
     *
     * Ownership of the rule is transferred to the settings.
     *
     * \see rules()
     * \since QGIS 3.40
     */
    void addRule(QgsAbstractLabelingEngineRule* rule SIP_TRANSFER );

Since QgsLabelingEngineSettings is attached to QgsProject, this will form the standard means of adding rules to a project. E.g.:

   QgsProject.instance().labelingEngineSettings().addRule(QgsLabelingEngineRuleMinimumDistanceLabelToFeature())

QgsLabelingEngineSettings will be modified to ensure that rules are seralized to xml when saving the project, and rules are restored from XML when reading projects.

QgsLabelingEngineSettings are passed to QgsMapSettings from the project settings object via QgsMapSettings::setLabelingEngineSettings -- accordingly rules configured for the project will be made available to the rendering code and internal PAL labeling logic via QgsMapSettings.

Performance Implications

None -- while adding rules to a project will impact the project's render speed, there will be no extra expense for rendering projects which do NOT have any rules configured.

Further Considerations/Improvements

Advanced label rules will also be respected when rendering maps via QGIS server.

Backwards Compatibility

Not applicable