On custom layout checks in QGIS 3.6, and how they can do your work for you!
Recently, we had the opportunity to implement an exciting new feature within QGIS. An enterprise with a large number of QGIS installs was looking for a way to control the outputs which staff were creating from the software, and enforce a set of predefined policies. The policies were designed to ensure that maps created in QGIS’ print layout designer would meet a set of minimum standards, e.g.:
- Layouts must include a “Copyright 2019 by XXX” label somewhere on the page
- All maps must have a linked scale bar
- No layers from certain blacklisted sources (e.g. Google Maps tiles) are permitted
- Required attribution text for other layers must be included somewhere on the layout
Instead of just making a set of written policies and hoping that staff correctly follow them, it was instead decided that the checks should be performed automatically by QGIS itself. If any of the checks failed (indicating that the map wasn’t complying to the policies), the layout export would be blocked and the user would be advised what they needed to change in their map to make it compliant.
The result of this work is a brand new API for implementing custom “validity checks” within QGIS. Out of the box, QGIS 3.6 ships with two in-built validity checks. These are:
- A check to warn users when a layout includes a scale bar which isn’t linked to a map
- A check to warn users if a map overview in a layout isn’t linked to a map (e.g. if the linked map has been deleted)
All QGIS 3.6 users will see a friendly warning if either of these conditions are met, advising them of the potential issue.
The exciting stuff comes in custom, in-house checks. These are written in PyQGIS, so they can be deployed through in-house plugins or startup scripts. Let’s explore some examples to see how these work.
A basic check looks something like this:
from qgis.core import check @check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) def my_layout_check(context, feedback): results = ... return results
Checks are created using the @check.register decorator. This takes a single argument, the check type. For now, only layout checks are implemented, so this should be set to QgsAbstractValidityCheck.TypeLayoutCheck. The check function is given two arguments, a QgsValidityCheckContext argument, and a feedback argument. We can safely ignore the feedback option for now, but the context argument is important. This context contains information useful for the check to run — in the case of layout checks, the context contains a reference to the layout being checked. The check function should return a list of QgsValidityCheckResult objects, or an empty list if the check was passed successfully with no warnings or errors.
Here’s a more complete example. This one throws a warning whenever a layout map item is set to the web mercator (EPSG:3875) projection:
@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) def layout_map_crs_choice_check(context, feedback): layout = context.layout results = [] for i in layout.items(): if isinstance(i, QgsLayoutItemMap) and i.crs().authid() == 'EPSG:3857': res = QgsValidityCheckResult() res.type = QgsValidityCheckResult.Warning res.title='Map projection is misleading' res.detailedDescription='The projection for the map item {} is set to Web Mercator (EPSG:3857) which misrepresents areas and shapes. Consider using an appropriate local projection instead.'.format(i.displayName()) results.append(res) return results
Here, our check loops through all the items in the layout being tested, looking for QgsLayoutItemMap instances. It then checks the CRS for each map, and if that CRS is ‘EPSG:3857’, a warning result is returned. The warning includes a friendly message for users advising them why the check failed.
In this example our check is returning results with a QgsValidityCheckResult.Warning type. Warning results are shown to users, but they don’t prevent users from proceeding and continuing to export their layout.
Checks can also return “critical” results. If any critical results are obtained, then the actual export itself is blocked. The user is still shown the messages generated by the check so that they know how to resolve the issue, but they can’t proceed with the export until they’ve fixed their layout. Here’s an example of a check which returns critical results, preventing layout export if there’s no “Copyright 2019 North Road” labels included on their layout:
@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) def layout_map_crs_choice_check(context, feedback): layout = context.layout for i in layout.items(): if isinstance(i, QgsLayoutItemLabel) and 'Copyright 2019 North Road' in i.currentText(): return # did not find copyright text, block layout export res = QgsValidityCheckResult() res.type = QgsValidityCheckResult.Critical res.title = 'Missing copyright label' res.detailedDescription = 'Layout has no "Copyright" label. Please add a label containing the text "Copyright 2019 North Road".' return [res]
If we try to export a layout with the copyright notice, we now get this error:
Notice how the OK button is disabled, and users are forced to fix the error before they can export their layouts.
Here’s a final example. This one runs through all the layers included within maps in the layout, and if any of them come from a “blacklisted” source, the user is not permitted to proceed with the export:
@check.register(type=QgsAbstractValidityCheck.TypeLayoutCheck) def layout_map_crs_choice_check(context, feedback): layout = context.layout for i in layout.items(): if isinstance(i, QgsLayoutItemMap): for l in i.layersToRender(): # check if layer source is blacklisted if 'mt1.google.com' in l.source(): res = QgsValidityCheckResult() res.type = QgsValidityCheckResult.Critical res.title = 'Blacklisted layer source' res.detailedDescription = 'This layout includes a Google maps layer ("{}"), which is in violation of their Terms of Service.'.format(l.name()) return [res]
Of course, all checks are run each time — so if a layout fails multiple checks, the user will see a summary of ALL failed checks, and can click on each in turn to see the detailed description of the failure.
So there we go — when QGIS 3.6 is released in late February 2019, you’ll have access to this API and can start making QGIS automatically enforce your organisation policies for you! The really neat thing is that this doesn’t only apply to large organisations. Even if you’re a one-person shop using QGIS, you could write your own checks to make QGIS “remind” you when you’ve forgotten to include something in your products. It’d even be possible to hook into one of the available Python spell checking libraries to write a spelling check! With any luck, this should lead to better quality outputs and less back and forth with your clients.
North Road are leading experts in customising the QGIS application for enterprise installs. If you’d like to discuss how you can deploy in-house customisation like this within your organisation, contact us for further details!