From f86c2988bb16ea5ba0ce197c90416b5e332e728f Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 19 Dec 2017 15:31:15 +1000 Subject: [PATCH] Serialize atlas settings --- python/core/layout/qgslayout.sip | 4 +- python/core/layout/qgslayoutatlas.sip | 15 +++- python/core/layout/qgsprintlayout.sip | 5 ++ src/core/layout/qgslayout.h | 4 +- src/core/layout/qgslayoutatlas.cpp | 78 +++++++++++++++++++++ src/core/layout/qgslayoutatlas.h | 12 +++- src/core/layout/qgsprintlayout.cpp | 17 +++++ src/core/layout/qgsprintlayout.h | 3 + tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgslayoutatlas.py | 91 +++++++++++++++++++++++++ 10 files changed, 220 insertions(+), 10 deletions(-) create mode 100644 tests/src/python/test_qgslayoutatlas.py diff --git a/python/core/layout/qgslayout.sip b/python/core/layout/qgslayout.sip index da9da711a1f..899567e42c2 100644 --- a/python/core/layout/qgslayout.sip +++ b/python/core/layout/qgslayout.sip @@ -500,14 +500,14 @@ If ``ok`` is specified, it will be set to true if the load was successful. Returns a list of loaded items. %End - QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; %Docstring Returns the layout's state encapsulated in a DOM element. .. seealso:: :py:func:`readXml()` %End - bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); %Docstring Sets the collection's state from a DOM element. ``layoutElement`` is the DOM node corresponding to the layout. diff --git a/python/core/layout/qgslayoutatlas.sip b/python/core/layout/qgslayoutatlas.sip index b9f239894db..c713b8043a3 100644 --- a/python/core/layout/qgslayoutatlas.sip +++ b/python/core/layout/qgslayoutatlas.sip @@ -8,7 +8,7 @@ -class QgsLayoutAtlas : QObject +class QgsLayoutAtlas : QObject, QgsLayoutSerializableObject { %Docstring Class used to render an Atlas, iterating over geometry features. @@ -34,6 +34,15 @@ QgsLayoutAtlas which is automatically created and attached to the composition. Constructor for new QgsLayoutAtlas. %End + virtual QString stringType() const; + + virtual QgsLayout *layout(); + + virtual bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ); + + bool enabled() const; %Docstring Returns whether the atlas generation is enabled @@ -72,7 +81,7 @@ atlas page. .. seealso:: :py:func:`filenameExpressionErrorString()` %End - bool setFilenameExpression( const QString &expression, QString &errorString ); + bool setFilenameExpression( const QString &expression, QString &errorString /Out/ ); %Docstring Sets the filename ``expression`` used for generating output filenames for each atlas page. @@ -222,7 +231,7 @@ This property has no effect is filterFeatures() is false. .. seealso:: :py:func:`filterFeatures()` %End - bool setFilterExpression( const QString &expression, QString &errorString ); + bool setFilterExpression( const QString &expression, QString &errorString /Out/ ); %Docstring Sets the ``expression`` used for filtering features in the coverage layer. diff --git a/python/core/layout/qgsprintlayout.sip b/python/core/layout/qgsprintlayout.sip index 628dc78b177..8a667fd564f 100644 --- a/python/core/layout/qgsprintlayout.sip +++ b/python/core/layout/qgsprintlayout.sip @@ -31,6 +31,11 @@ Constructor for QgsPrintLayout. Returns the print layout's atlas. %End + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + + virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + + }; /************************************************************************ diff --git a/src/core/layout/qgslayout.h b/src/core/layout/qgslayout.h index 737f595de3e..326d9ba7c21 100644 --- a/src/core/layout/qgslayout.h +++ b/src/core/layout/qgslayout.h @@ -514,13 +514,13 @@ class CORE_EXPORT QgsLayout : public QGraphicsScene, public QgsExpressionContext * Returns the layout's state encapsulated in a DOM element. * \see readXml() */ - QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; + virtual QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const; /** * Sets the collection's state from a DOM element. \a layoutElement is the DOM node corresponding to the layout. * \see writeXml() */ - bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); + virtual bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ); /** * Add items from an XML representation to the layout. Used for project file reading and pasting items from clipboard. diff --git a/src/core/layout/qgslayoutatlas.cpp b/src/core/layout/qgslayoutatlas.cpp index 7d2ae84fca2..3f0d9a40ed0 100644 --- a/src/core/layout/qgslayoutatlas.cpp +++ b/src/core/layout/qgslayoutatlas.cpp @@ -31,6 +31,84 @@ QgsLayoutAtlas::QgsLayoutAtlas( QgsLayout *layout ) connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QStringList & ) >( &QgsProject::layersWillBeRemoved ), this, &QgsLayoutAtlas::removeLayers ); } +QString QgsLayoutAtlas::stringType() const +{ + return QStringLiteral( "atlas" ); +} + +QgsLayout *QgsLayoutAtlas::layout() +{ + return mLayout; +} + +bool QgsLayoutAtlas::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const +{ + QDomElement atlasElem = document.createElement( QStringLiteral( "Atlas" ) ); + atlasElem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + + if ( mCoverageLayer ) + { + atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId ); + atlasElem.setAttribute( QStringLiteral( "coverageLayerName" ), mCoverageLayer.name ); + atlasElem.setAttribute( QStringLiteral( "coverageLayerSource" ), mCoverageLayer.source ); + atlasElem.setAttribute( QStringLiteral( "coverageLayerProvider" ), mCoverageLayer.provider ); + } + else + { + atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), QString() ); + } + + atlasElem.setAttribute( QStringLiteral( "hideCoverage" ), mHideCoverage ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + atlasElem.setAttribute( QStringLiteral( "filenamePattern" ), mFilenameExpressionString ); + atlasElem.setAttribute( QStringLiteral( "pageNameExpression" ), mPageNameExpression ); + + atlasElem.setAttribute( QStringLiteral( "sortFeatures" ), mSortFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mSortFeatures ) + { + atlasElem.setAttribute( QStringLiteral( "sortKey" ), mSortExpression ); + atlasElem.setAttribute( QStringLiteral( "sortAscending" ), mSortAscending ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + } + atlasElem.setAttribute( QStringLiteral( "filterFeatures" ), mFilterFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) ); + if ( mFilterFeatures ) + { + atlasElem.setAttribute( QStringLiteral( "featureFilter" ), mFilterExpression ); + } + + parentElement.appendChild( atlasElem ); + + return true; +} + +bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument &, const QgsReadWriteContext & ) +{ + mEnabled = atlasElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt(); + + // look for stored layer name + QString layerId = atlasElem.attribute( QStringLiteral( "coverageLayer" ) ); + QString layerName = atlasElem.attribute( QStringLiteral( "coverageLayerName" ) ); + QString layerSource = atlasElem.attribute( QStringLiteral( "coverageLayerSource" ) ); + QString layerProvider = atlasElem.attribute( QStringLiteral( "coverageLayerProvider" ) ); + + mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider ); + mCoverageLayer.resolveWeakly( mLayout->project() ); + + mPageNameExpression = atlasElem.attribute( QStringLiteral( "pageNameExpression" ), QString() ); + QString error; + setFilenameExpression( atlasElem.attribute( QStringLiteral( "filenamePattern" ), QString() ), error ); + + mSortFeatures = atlasElem.attribute( QStringLiteral( "sortFeatures" ), QStringLiteral( "0" ) ).toInt(); + mSortExpression = atlasElem.attribute( QStringLiteral( "sortKey" ) ); + mSortAscending = atlasElem.attribute( QStringLiteral( "sortAscending" ), QStringLiteral( "1" ) ).toInt(); + mFilterFeatures = atlasElem.attribute( QStringLiteral( "filterFeatures" ), QStringLiteral( "0" ) ).toInt(); + mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) ); + + mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt(); + + emit toggled( mEnabled ); + emit changed(); + return true; +} + void QgsLayoutAtlas::setEnabled( bool enabled ) { if ( enabled == mEnabled ) diff --git a/src/core/layout/qgslayoutatlas.h b/src/core/layout/qgslayoutatlas.h index 93a9380d9b6..6e740a3f704 100644 --- a/src/core/layout/qgslayoutatlas.h +++ b/src/core/layout/qgslayoutatlas.h @@ -18,6 +18,7 @@ #include "qgis_core.h" #include "qgsvectorlayerref.h" +#include "qgslayoutserializableobject.h" #include class QgsLayout; @@ -32,7 +33,7 @@ class QgsLayout; * QgsLayoutAtlas which is automatically created and attached to the composition. * \since QGIS 3.0 */ -class CORE_EXPORT QgsLayoutAtlas : public QObject +class CORE_EXPORT QgsLayoutAtlas : public QObject, public QgsLayoutSerializableObject { Q_OBJECT public: @@ -42,6 +43,11 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject */ QgsLayoutAtlas( QgsLayout *layout SIP_TRANSFERTHIS ); + QString stringType() const override; + QgsLayout *layout() override; + bool writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readXml( const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context ) override; + /** * Returns whether the atlas generation is enabled * \see setEnabled() @@ -81,7 +87,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject * will be set to the expression error. * \see filenameExpression() */ - bool setFilenameExpression( const QString &expression, QString &errorString ); + bool setFilenameExpression( const QString &expression, QString &errorString SIP_OUT ); /** * Returns the coverage layer used for the atlas features. @@ -209,7 +215,7 @@ class CORE_EXPORT QgsLayoutAtlas : public QObject * \see filterExpression() * \see setFilterFeatures() */ - bool setFilterExpression( const QString &expression, QString &errorString ); + bool setFilterExpression( const QString &expression, QString &errorString SIP_OUT ); public slots: diff --git a/src/core/layout/qgsprintlayout.cpp b/src/core/layout/qgsprintlayout.cpp index 1d1c031157b..a596054ed40 100644 --- a/src/core/layout/qgsprintlayout.cpp +++ b/src/core/layout/qgsprintlayout.cpp @@ -27,3 +27,20 @@ QgsLayoutAtlas *QgsPrintLayout::atlas() { return mAtlas; } + +QDomElement QgsPrintLayout::writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const +{ + QDomElement layoutElem = QgsLayout::writeXml( document, context ); + mAtlas->writeXml( layoutElem, document, context ); + return layoutElem; +} + +bool QgsPrintLayout::readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) +{ + if ( !QgsLayout::readXml( layoutElement, document, context ) ) + return false; + + QDomElement atlasElem = layoutElement.firstChildElement( QStringLiteral( "Atlas" ) ); + mAtlas->readXml( atlasElem, document, context ); + return true; +} diff --git a/src/core/layout/qgsprintlayout.h b/src/core/layout/qgsprintlayout.h index a3c0065ff1e..b7231857732 100644 --- a/src/core/layout/qgsprintlayout.h +++ b/src/core/layout/qgsprintlayout.h @@ -43,6 +43,9 @@ class CORE_EXPORT QgsPrintLayout : public QgsLayout */ QgsLayoutAtlas *atlas(); + QDomElement writeXml( QDomDocument &document, const QgsReadWriteContext &context ) const override; + bool readXml( const QDomElement &layoutElement, const QDomDocument &document, const QgsReadWriteContext &context ) override; + private: QgsLayoutAtlas *mAtlas = nullptr; diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index c673955aea6..39c76e0e85e 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -84,6 +84,7 @@ ADD_PYTHON_TEST(PyQgsLayerTreeMapCanvasBridge test_qgslayertreemapcanvasbridge.p ADD_PYTHON_TEST(PyQgsLayerTree test_qgslayertree.py) ADD_PYTHON_TEST(PyQgsLayout test_qgslayout.py) ADD_PYTHON_TEST(PyQgsLayoutAlign test_qgslayoutaligner.py) +ADD_PYTHON_TEST(PyQgsLayoutAtlas test_qgslayoutatlas.py) ADD_PYTHON_TEST(PyQgsLayoutExporter test_qgslayoutexporter.py) ADD_PYTHON_TEST(PyQgsLayoutFrame test_qgslayoutframe.py) ADD_PYTHON_TEST(PyQgsLayoutManager test_qgslayoutmanager.py) diff --git a/tests/src/python/test_qgslayoutatlas.py b/tests/src/python/test_qgslayoutatlas.py new file mode 100644 index 00000000000..ac2623761a2 --- /dev/null +++ b/tests/src/python/test_qgslayoutatlas.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for QgsLayoutAtlas + +.. 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. +""" +__author__ = 'Nyall Dawson' +__date__ = '19/12/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import qgis # NOQA +import sip +import tempfile +import shutil +import os + +from qgis.core import (QgsUnitTypes, + QgsLayout, + QgsPrintLayout, + QgsLayoutAtlas, + QgsLayoutItemPage, + QgsLayoutGuide, + QgsLayoutObject, + QgsProject, + QgsLayoutItemGroup, + QgsLayoutItem, + QgsProperty, + QgsLayoutPageCollection, + QgsLayoutMeasurement, + QgsFillSymbol, + QgsReadWriteContext, + QgsLayoutItemMap, + QgsLayoutItemLabel, + QgsLayoutSize, + QgsLayoutPoint, + QgsVectorLayer) +from qgis.PyQt.QtCore import QFileInfo +from qgis.PyQt.QtTest import QSignalSpy +from qgis.PyQt.QtXml import QDomDocument +from utilities import unitTestDataPath +from qgis.testing import start_app, unittest + +start_app() + + +class TestQgsLayoutAtlas(unittest.TestCase): + + def testReadWriteXml(self): + p = QgsProject() + vectorFileInfo = QFileInfo(unitTestDataPath() + "/france_parts.shp") + vector_layer = QgsVectorLayer(vectorFileInfo.filePath(), vectorFileInfo.completeBaseName(), "ogr") + self.assertTrue(vector_layer.isValid()) + p.addMapLayer(vector_layer) + + l = QgsPrintLayout(p) + atlas = l.atlas() + atlas.setEnabled(True) + atlas.setHideCoverage(True) + atlas.setFilenameExpression('filename exp') + atlas.setCoverageLayer(vector_layer) + atlas.setPageNameExpression('page name') + atlas.setSortFeatures(True) + atlas.setSortAscending(False) + atlas.setSortExpression('sort exp') + atlas.setFilterFeatures(True) + atlas.setFilterExpression('filter exp') + + doc = QDomDocument("testdoc") + elem = l.writeXml(doc, QgsReadWriteContext()) + + l2 = QgsPrintLayout(p) + self.assertTrue(l2.readXml(elem, doc, QgsReadWriteContext())) + atlas2 = l2.atlas() + self.assertTrue(atlas2.enabled()) + self.assertTrue(atlas2.hideCoverage()) + self.assertEqual(atlas2.filenameExpression(), 'filename exp') + self.assertEqual(atlas2.coverageLayer(), vector_layer) + self.assertEqual(atlas2.pageNameExpression(), 'page name') + self.assertTrue(atlas2.sortFeatures()) + self.assertFalse(atlas2.sortAscending()) + self.assertEqual(atlas2.sortExpression(), 'sort exp') + self.assertTrue(atlas2.filterFeatures()) + self.assertEqual(atlas2.filterExpression(), 'filter exp') + + +if __name__ == '__main__': + unittest.main()