qgis / QGIS-Enhancement-Proposals

QEP's (QGIS Enhancement Proposals) are used in the process of creating and discussing new enhancements for QGIS
119 stars 37 forks source link

An interface and registry for custom validity checks #133

Closed nyalldawson closed 4 years ago

nyalldawson commented 6 years ago

QGIS Enhancement: An interface and registry for custom validity checks

Date 2018/11/25

Author Nyall Dawson (@nyalldawson )

Contact nyall dot dawson at gmail dot com

Version QGIS 3.6

Summary

Upfront: This is NOT related to geometry validity!!!

This proposal concerns an interface for custom "validity checks", along with a corresponding registry of custom checks and GUI widgets for summarising check results.

Initially the proposal is focused toward print layout validity checks, but the interface has been designed to be generic enough to allow alternative types of validity checks (e.g. project save validity checks, processing model validity checks, etc. More details later).

The API is designed to be used both by internal validity checks and also to be extended by custom, organisation-specific validity checks. E.g., for print layout validity checks we could have:

In built checks:

Possible organisation specific checks, implemented via Python

The checks would be performed by iterating through the validity checks of the corresponding type which have been registered in the registry, and collating the results. A reusable gui widget would then present a summary of the checks which failed/triggered warnings, along with detailed explanatory text.

In the initial use case, BEFORE exporting a print layout, the layout designer will run through all registered layout related checks and if any errors or warnings are found present a dialog summarising these to the user. If errors were found, the export will be blocked until these errors are resolved.

API

QgsAbstractValidityCheck

QgsAbstractValidityCheck is the abstract base class for validity checks. Individual checks must implement this interface.

class QgsAbstractValidityCheck
{
  public:

    //! Check types
    enum Type
    {
        TypeLayoutCheck = 0, //!< Print layout validity check
        ...
        TypeUserCheck = 10000, //!< Starting point for custom user types
    };

    /**
     * Returns the unique ID of the check. 
     */
    virtual QString id() const = 0;

    /**
     * Returns the type of the check. 
     */
    virtual Type type() const = 0;

    /**
     * Returns the name of the check. 
     */
    virtual QString name() const = 0;

    /**
     * Runs the check and returns a list of results. If the check is "passed" and no warnings or errors are generated, then an empty list should be returned. The \a context argument gives the wider in which the check is being run.
     */
    virtual QList< QgsValidityCheckResult > runCheck( const QgsValidityCheckContext* context, QgsFeedback *feedback) const = 0;

};

QgsValidityCheckContext

QgsValidityCheckContext gives the context under which the check is being run. Much like QEvent, it is designed to be subclassed for each individual check type. E.g. QgsLayoutValidityCheckContext would subclass QgsValidityCheckContext.

class QgsValidityCheckContext
{
  public:
      // initially nothing in the base class!

};

class QgsLayoutValidityCheckContext : public QgsValidityCheckContext
{
  public:

     /**
      * Pointer to the layout which the check is being run against.
      */
     QgsLayout* layout;

};

QgsValidityCheckResult

QgsValidityCheckResult encapsulates the result of a validity check.

class QgsValidityCheckResult
{
  public:

     enum Type
     {
         Warning, //!< Warning only, allow operation to proceed but notify user of result
         Critical, //!< Critical error - notify user of result and prevent operation from proceeding
      }; 

       //! Result type
      Type type; 

       /**
        * A short, translated string summarising the result. Ideally a single sentence.
        */
       QString title;

       /**
        * Detailed description of the result (translated), giving users enough detail for them to resolve
        * the error. 
        */
       QString detailedDescription;

       /**
        * ID of the check which generated the result.
        */
       QString checkId;

};

QgsValidityCheckRegistry

QgsValidityCheckRegistry contains instances of all the inbuilt and custom (Python plugin based) checks. It will follow the same format as the existing QGIS registry classes, and an instance will be attached to QgsApplication.

One specialised method will be available in QgsValidityCheckResult which runs all checks of the specified type and returns the collated results:

    /**
     * Runs all checks of the specified \a type and returns a list of results. If all checks are "passed" and no warnings or errors are generated, then an empty list will be returned. The \a context argument gives the wider in which the check is being run.
     */
    QList< QgsValidityCheckResult > runChecks( QgsValidityCheck::Type type, const QgsValidityCheckContext* context, QgsFeedback *feedback) const;

Widgets

A custom, reusable widget QgsValidityCheckResultsWidget will be created which shows a summary of the results of checks (with icons showing the result type, e.g. warnings only or critical errors).

Example

An example check, QgsLayoutMapCrsValidityCheck, could be implemented using the following psuedo-code:

QList< QgsValidityCheckResult > QgsLayoutItemValidityCheck::runCheck( const QgsValidityCheckContext* context, QgsFeedback *feedback) const
{
   const QgsLayoutValidityCheckContext* layoutContext = static_cast< const QgsLayoutValidityCheckContext* >( context ); 

    const QgsLayout* layout = layoutContext->layout();

    // collect map items
    QList<QgsLayoutItemMap *> mapItems;
    layout->layoutItems( mapItems );

    QList< QgsValidityCheckResult > results;
    for ( QgsLayoutItemMap* map : mapItems )
    {
        if ( map->crs().authid() == "EPSG:3857")
        {
           QgsValidityCheckResult result;
           result.title = QObject::tr("Map %1 has a misleading projection").arg( map->id() );
           result.description = QObject::tr("The CRS for map %1 is set to web mercator, EPSG:3857. This CRS is known to present misleading representations of areas, etc etc more rant. It is suggested to utilise a suitable accurate local projection instead.")
           result.type = QgsValidityCheckResult::Warning;
           results.append( result );
         }
     } 
     return results;
} 

Further Considerations/Improvements

In future, when the need arises, the check API could be extended to allow for custom actions for check results. These would allow users easy access to fix problems which have automated solutions available. This API could be modelled of the current QgsGeometryCheck API, which covers a similar use case.

Backwards Compatibility

N/A

Votes

(required)

NathanW2 commented 6 years ago

Sounds like a good case to use a decorator here as well to make the API nice for Python.

nyalldawson commented 6 years ago

@nathanw2 I'm open to that - got an idea of a possible API?

NathanW2 commented 6 years ago

@nyalldawson

@prwelcome
def valid_check():
 pass

:)

no but really something like this I guess:

@validity_check(id="checkid", name="Check name")
def runMyCheck( context, feedback):
     pass
nyalldawson commented 6 years ago

Yeah ok

nyalldawson commented 5 years ago

Implemented at https://github.com/qgis/QGIS/pull/8589