From 208beb7f8cb624014e0d9824f15d8bea35ffe093 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Sat, 10 Aug 2019 06:43:59 +1000 Subject: [PATCH] [layouts] Improve logic of splitting layouts into separate layers when exporting to a multi-layer format Now, items are either - placed onto the same layer as other items (simple items like labels, lines, pictures) - placed onto the same layer as only other items of equal types (semi-complex items like scalebars or pages) - placed onto their own unique layers (complex items like legends, maps) Plus lots of tests covering this, where previously there was few --- .../layout/qgslayoutframe.sip.in | 2 + .../layout/qgslayoutitem.sip.in | 23 ++ .../layout/qgslayoutitemgroup.sip.in | 1 + .../layout/qgslayoutitemlegend.sip.in | 2 + .../layout/qgslayoutitemmap.sip.in | 2 + .../layout/qgslayoutitempage.sip.in | 2 + .../layout/qgslayoutitemscalebar.sip.in | 2 + src/core/layout/qgslayoutexporter.cpp | 135 +++++++--- src/core/layout/qgslayoutexporter.h | 5 +- src/core/layout/qgslayoutframe.cpp | 4 + src/core/layout/qgslayoutframe.h | 1 + src/core/layout/qgslayoutitem.cpp | 5 + src/core/layout/qgslayoutitem.h | 23 ++ src/core/layout/qgslayoutitemgroup.cpp | 5 + src/core/layout/qgslayoutitemgroup.h | 2 +- src/core/layout/qgslayoutitemlegend.cpp | 5 + src/core/layout/qgslayoutitemlegend.h | 1 + src/core/layout/qgslayoutitemmap.cpp | 5 + src/core/layout/qgslayoutitemmap.h | 1 + src/core/layout/qgslayoutitempage.cpp | 5 + src/core/layout/qgslayoutitempage.h | 1 + src/core/layout/qgslayoutitemscalebar.cpp | 5 + src/core/layout/qgslayoutitemscalebar.h | 1 + tests/src/core/CMakeLists.txt | 1 + tests/src/core/testqgslayoutexporter.cpp | 236 ++++++++++++++++++ 25 files changed, 440 insertions(+), 35 deletions(-) create mode 100644 tests/src/core/testqgslayoutexporter.cpp diff --git a/python/core/auto_generated/layout/qgslayoutframe.sip.in b/python/core/auto_generated/layout/qgslayoutframe.sip.in index de3fde23fc6..738062e4282 100644 --- a/python/core/auto_generated/layout/qgslayoutframe.sip.in +++ b/python/core/auto_generated/layout/qgslayoutframe.sip.in @@ -115,6 +115,8 @@ Returns whether the frame is empty. virtual QgsExpressionContext createExpressionContext() const; + virtual ExportLayerBehavior exportLayerBehavior() const; + protected: diff --git a/python/core/auto_generated/layout/qgslayoutitem.sip.in b/python/core/auto_generated/layout/qgslayoutitem.sip.in index 4b6a93fb0e5..6a648c145bd 100644 --- a/python/core/auto_generated/layout/qgslayoutitem.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitem.sip.in @@ -384,6 +384,25 @@ Sets the item's parent ``group``. .. seealso:: :py:func:`isGroupMember` .. seealso:: :py:func:`parentGroup` +%End + + enum ExportLayerBehavior + { + CanGroupWithAnyOtherItem, + CanGroupWithItemsOfSameType, + MustPlaceInOwnLayer, + ItemContainsSubLayers, + }; + + virtual ExportLayerBehavior exportLayerBehavior() const; +%Docstring +Returns the behavior of this item during exporting to layered exports (e.g. SVG). + +.. seealso:: :py:func:`numberExportLayers` + +.. seealso:: :py:func:`exportLayerDetails` + +.. versionadded:: 3.10 %End virtual int numberExportLayers() const; @@ -394,6 +413,10 @@ Returns 0 if this item is to be placed on the same layer as the previous item, Items which require multiply layers should check QgsLayoutContext.currentExportLayer() during their rendering to determine which layer should be drawn. + +.. seealso:: :py:func:`exportLayerBehavior` + +.. seealso:: :py:func:`exportLayerDetails` %End struct ExportLayerDetail diff --git a/python/core/auto_generated/layout/qgslayoutitemgroup.sip.in b/python/core/auto_generated/layout/qgslayoutitemgroup.sip.in index 5736590ac9c..4d0d5478bed 100644 --- a/python/core/auto_generated/layout/qgslayoutitemgroup.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemgroup.sip.in @@ -71,6 +71,7 @@ Returns a list of items contained by the group. virtual void finalizeRestoreFromXml(); + virtual ExportLayerBehavior exportLayerBehavior() const; protected: virtual void draw( QgsLayoutItemRenderContext &context ); diff --git a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in index 467d3229e44..c4309900beb 100644 --- a/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemlegend.sip.in @@ -527,6 +527,8 @@ Returns the legend's renderer settings object. virtual QgsExpressionContext createExpressionContext() const; + virtual ExportLayerBehavior exportLayerBehavior() const; + public slots: diff --git a/python/core/auto_generated/layout/qgslayoutitemmap.sip.in b/python/core/auto_generated/layout/qgslayoutitemmap.sip.in index 4d96593845f..2f4e1b50f20 100644 --- a/python/core/auto_generated/layout/qgslayoutitemmap.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemmap.sip.in @@ -89,6 +89,8 @@ The caller takes responsibility for deleting the returned object. virtual int numberExportLayers() const; + virtual ExportLayerBehavior exportLayerBehavior() const; + virtual QgsLayoutItem::ExportLayerDetail exportLayerDetails( int layer ) const; virtual void setFrameStrokeWidth( QgsLayoutMeasurement width ); diff --git a/python/core/auto_generated/layout/qgslayoutitempage.sip.in b/python/core/auto_generated/layout/qgslayoutitempage.sip.in index 7269fca7a6b..2f59afb4e8d 100644 --- a/python/core/auto_generated/layout/qgslayoutitempage.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitempage.sip.in @@ -97,6 +97,8 @@ page orientation. virtual QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = 0 ) /Factory/; + virtual ExportLayerBehavior exportLayerBehavior() const; + public slots: diff --git a/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in b/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in index 69fc5bb3bff..7d4ea703cc4 100644 --- a/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in +++ b/python/core/auto_generated/layout/qgslayoutitemscalebar.sip.in @@ -547,6 +547,8 @@ Adjusts the scale bar box size and updates the item. virtual bool accept( QgsStyleEntityVisitorInterface *visitor ) const; + virtual ExportLayerBehavior exportLayerBehavior() const; + protected: diff --git a/src/core/layout/qgslayoutexporter.cpp b/src/core/layout/qgslayoutexporter.cpp index b3642e4a476..2392e3749b0 100644 --- a/src/core/layout/qgslayoutexporter.cpp +++ b/src/core/layout/qgslayoutexporter.cpp @@ -880,9 +880,9 @@ QgsLayoutExporter::ExportResult QgsLayoutExporter::exportToSvg( const QString &f Qt::IntersectsItemBoundingRect, Qt::AscendingOrder ); - auto exportFunc = [this, &settings, width, height, i, bounds, fileName, &svg, &svgDocRoot]( unsigned int layerId, const QString & layerName )->QgsLayoutExporter::ExportResult + auto exportFunc = [this, &settings, width, height, i, bounds, fileName, &svg, &svgDocRoot]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult { - return renderToLayeredSvg( settings, width, height, i, bounds, fileName, layerId, layerName, svg, svgDocRoot, settings.exportMetadata ); + return renderToLayeredSvg( settings, width, height, i, bounds, fileName, layerId, layerDetail.name, svg, svgDocRoot, settings.exportMetadata ); }; ExportResult res = handleLayeredExport( items, exportFunc ); if ( res != Success ) @@ -1127,7 +1127,7 @@ void QgsLayoutExporter::updatePrinterPageSize( QgsLayout *layout, QPrinter &prin printer.setPageMargins( QMarginsF( 0, 0, 0, 0 ) ); } -QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, const QRectF &bounds, const QString &filename, int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const +QgsLayoutExporter::ExportResult QgsLayoutExporter::renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, const QRectF &bounds, const QString &filename, unsigned int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const { QBuffer svgBuffer; { @@ -1430,52 +1430,123 @@ bool QgsLayoutExporter::georeferenceOutputPrivate( const QString &file, QgsLayou } QgsLayoutExporter::ExportResult QgsLayoutExporter::handleLayeredExport( const QList &items, - const std::function &exportFunc ) + const std::function &exportFunc ) { LayoutItemHider itemHider( items ); ( void )itemHider; - int layoutItemLayerIdx = 0; - auto it = items.constBegin(); - for ( unsigned int layerId = 1; it != items.constEnd(); ++layerId ) + int prevType = -1; + QgsLayoutItem::ExportLayerBehavior prevItemBehavior = QgsLayoutItem::CanGroupWithAnyOtherItem; + unsigned int layerId = 1; + QgsLayoutItem::ExportLayerDetail layerDetails; + layerDetails.name = QObject::tr( "Layer %1" ).arg( layerId ); + itemHider.hideAll(); + bool haveUnexportedItems = false; + for ( auto it = items.constBegin(); it != items.constEnd(); ++it ) { - itemHider.hideAll(); QgsLayoutItem *layoutItem = dynamic_cast( *it ); - const QString layerName = QObject::tr( "Layer %1" ).arg( layerId ); - if ( layoutItem && layoutItem->numberExportLayers() > 0 ) + + bool canPlaceInExistingLayer = false; + if ( layoutItem ) { - layoutItem->show(); - mLayout->renderContext().setCurrentExportLayer( layoutItemLayerIdx ); - ++layoutItemLayerIdx; + switch ( layoutItem->exportLayerBehavior() ) + { + case QgsLayoutItem::CanGroupWithAnyOtherItem: + { + switch ( prevItemBehavior ) + { + case QgsLayoutItem::CanGroupWithAnyOtherItem: + canPlaceInExistingLayer = true; + break; + + case QgsLayoutItem::CanGroupWithItemsOfSameType: + canPlaceInExistingLayer = prevType == -1 || prevType == layoutItem->type(); + break; + + case QgsLayoutItem::MustPlaceInOwnLayer: + case QgsLayoutItem::ItemContainsSubLayers: + canPlaceInExistingLayer = false; + break; + } + break; + } + + case QgsLayoutItem::CanGroupWithItemsOfSameType: + { + switch ( prevItemBehavior ) + { + case QgsLayoutItem::CanGroupWithAnyOtherItem: + case QgsLayoutItem::CanGroupWithItemsOfSameType: + canPlaceInExistingLayer = prevType == -1 || prevType == layoutItem->type(); + break; + + case QgsLayoutItem::MustPlaceInOwnLayer: + case QgsLayoutItem::ItemContainsSubLayers: + canPlaceInExistingLayer = false; + break; + } + break; + } + + case QgsLayoutItem::MustPlaceInOwnLayer: + case QgsLayoutItem::ItemContainsSubLayers: + canPlaceInExistingLayer = false; + break; + } + prevItemBehavior = layoutItem->exportLayerBehavior(); + prevType = layoutItem->type(); } else { - // show all items until the next item that renders on a separate layer - for ( ; it != items.constEnd(); ++it ) + prevItemBehavior = QgsLayoutItem::MustPlaceInOwnLayer; + } + + if ( canPlaceInExistingLayer ) + { + ( *it )->show(); + haveUnexportedItems = true; + } + else + { + if ( haveUnexportedItems ) { - layoutItem = dynamic_cast( *it ); - if ( layoutItem && layoutItem->numberExportLayers() > 0 ) + ExportResult result = exportFunc( layerId, layerDetails ); + if ( result != Success ) + return result; + layerId++; + layerDetails.name = QObject::tr( "Layer %1" ).arg( layerId ); + haveUnexportedItems = false; + } + + itemHider.hideAll(); + ( *it )->show(); + + if ( layoutItem && layoutItem->exportLayerBehavior() == QgsLayoutItem::ItemContainsSubLayers ) + { + for ( int layoutItemLayerIdx = 0; layoutItemLayerIdx < layoutItem->numberExportLayers(); layoutItemLayerIdx++ ) { - break; - } - else - { - ( *it )->show(); + mLayout->renderContext().setCurrentExportLayer( layoutItemLayerIdx ); + layerDetails = layoutItem->exportLayerDetails( layoutItemLayerIdx ); + ExportResult result = exportFunc( layerId, layerDetails ); + if ( result != Success ) + return result; + layerId++; + layerDetails.name = QObject::tr( "Layer %1" ).arg( layerId ); } + mLayout->renderContext().setCurrentExportLayer( -1 ); + haveUnexportedItems = false; + } + else + { + haveUnexportedItems = true; } } - - - ExportResult result = exportFunc( layerId, layerName ); + } + if ( haveUnexportedItems ) + { + ExportResult result = exportFunc( layerId, layerDetails ); if ( result != Success ) return result; - - if ( layoutItem && layoutItem->numberExportLayers() > 0 && layoutItem->numberExportLayers() == layoutItemLayerIdx ) // restore and pass to next item - { - mLayout->renderContext().setCurrentExportLayer( -1 ); - layoutItemLayerIdx = 0; - ++it; - } } return Success; } diff --git a/src/core/layout/qgslayoutexporter.h b/src/core/layout/qgslayoutexporter.h index 951328d082f..0d7c4765f05 100644 --- a/src/core/layout/qgslayoutexporter.h +++ b/src/core/layout/qgslayoutexporter.h @@ -585,7 +585,7 @@ class CORE_EXPORT QgsLayoutExporter static void updatePrinterPageSize( QgsLayout *layout, QPrinter &printer, int page ); ExportResult renderToLayeredSvg( const SvgExportSettings &settings, double width, double height, int page, const QRectF &bounds, - const QString &filename, int svgLayerId, const QString &layerName, + const QString &filename, unsigned int svgLayerId, const QString &layerName, QDomDocument &svg, QDomNode &svgDocRoot, bool includeMetadata ) const; void appendMetadataToSvg( QDomDocument &svg ) const; @@ -593,10 +593,11 @@ class CORE_EXPORT QgsLayoutExporter bool georeferenceOutputPrivate( const QString &file, QgsLayoutItemMap *referenceMap = nullptr, const QRectF &exportRegion = QRectF(), double dpi = -1, bool includeGeoreference = true, bool includeMetadata = false ) const; - ExportResult handleLayeredExport( const QList &items, const std::function &exportFunc ); + ExportResult handleLayeredExport( const QList &items, const std::function &exportFunc ); static QgsVectorSimplifyMethod createExportSimplifyMethod(); friend class TestQgsLayout; + friend class TestQgsLayoutExporter; }; diff --git a/src/core/layout/qgslayoutframe.cpp b/src/core/layout/qgslayoutframe.cpp index 94ff15defdd..06d5d3f8fc9 100644 --- a/src/core/layout/qgslayoutframe.cpp +++ b/src/core/layout/qgslayoutframe.cpp @@ -134,6 +134,10 @@ QgsExpressionContext QgsLayoutFrame::createExpressionContext() const return context; } +QgsLayoutItem::ExportLayerBehavior QgsLayoutFrame::exportLayerBehavior() const +{ + return CanGroupWithItemsOfSameType; +} QString QgsLayoutFrame::displayName() const { diff --git a/src/core/layout/qgslayoutframe.h b/src/core/layout/qgslayoutframe.h index 19ab57e8a07..a3928968407 100644 --- a/src/core/layout/qgslayoutframe.h +++ b/src/core/layout/qgslayoutframe.h @@ -110,6 +110,7 @@ class CORE_EXPORT QgsLayoutFrame: public QgsLayoutItem bool isEmpty() const; QgsExpressionContext createExpressionContext() const override; + ExportLayerBehavior exportLayerBehavior() const override; protected: diff --git a/src/core/layout/qgslayoutitem.cpp b/src/core/layout/qgslayoutitem.cpp index a70f7acd37b..5a773ad27c4 100644 --- a/src/core/layout/qgslayoutitem.cpp +++ b/src/core/layout/qgslayoutitem.cpp @@ -244,6 +244,11 @@ void QgsLayoutItem::setParentGroup( QgsLayoutItemGroup *group ) setFlag( QGraphicsItem::ItemIsSelectable, !static_cast< bool>( group ) ); //item in groups cannot be selected } +QgsLayoutItem::ExportLayerBehavior QgsLayoutItem::exportLayerBehavior() const +{ + return CanGroupWithAnyOtherItem; +} + QgsLayoutItem::ExportLayerDetail QgsLayoutItem::exportLayerDetails( int ) const { return QgsLayoutItem::ExportLayerDetail(); diff --git a/src/core/layout/qgslayoutitem.h b/src/core/layout/qgslayoutitem.h index 4319b485ac0..d3438c3176a 100644 --- a/src/core/layout/qgslayoutitem.h +++ b/src/core/layout/qgslayoutitem.h @@ -414,6 +414,26 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt */ void setParentGroup( QgsLayoutItemGroup *group ); + /** + * Behavior of item when exporting to layered outputs. + * \since QGIS 3.10 + */ + enum ExportLayerBehavior + { + CanGroupWithAnyOtherItem, //!< Item can be placed on a layer with any other item (default behavior) + CanGroupWithItemsOfSameType, //!< Item can only be placed on layers with other items of the same type, but multiple items of this type can be grouped together + MustPlaceInOwnLayer, //!< Item must be placed in its own individual layer + ItemContainsSubLayers, //!< Item contains multiple sublayers which must be individually exported + }; + + /** + * Returns the behavior of this item during exporting to layered exports (e.g. SVG). + * \see numberExportLayers() + * \see exportLayerDetails() + * \since QGIS 3.10 + */ + virtual ExportLayerBehavior exportLayerBehavior() const; + /** * Returns the number of layers that this item requires for exporting during layered exports (e.g. SVG). * Returns 0 if this item is to be placed on the same layer as the previous item, @@ -421,6 +441,9 @@ class CORE_EXPORT QgsLayoutItem : public QgsLayoutObject, public QGraphicsRectIt * * Items which require multiply layers should check QgsLayoutContext::currentExportLayer() during * their rendering to determine which layer should be drawn. + * + * \see exportLayerBehavior() + * \see exportLayerDetails() */ virtual int numberExportLayers() const { return 0; } diff --git a/src/core/layout/qgslayoutitemgroup.cpp b/src/core/layout/qgslayoutitemgroup.cpp index f1b2d6b2b56..8b1e03e3ad4 100644 --- a/src/core/layout/qgslayoutitemgroup.cpp +++ b/src/core/layout/qgslayoutitemgroup.cpp @@ -272,6 +272,11 @@ void QgsLayoutItemGroup::finalizeRestoreFromXml() resetBoundingRect(); } +QgsLayoutItem::ExportLayerBehavior QgsLayoutItemGroup::exportLayerBehavior() const +{ + return MustPlaceInOwnLayer; +} + void QgsLayoutItemGroup::paint( QPainter *, const QStyleOptionGraphicsItem *, QWidget * ) { } diff --git a/src/core/layout/qgslayoutitemgroup.h b/src/core/layout/qgslayoutitemgroup.h index 7864acf9c96..1262be1a0e4 100644 --- a/src/core/layout/qgslayoutitemgroup.h +++ b/src/core/layout/qgslayoutitemgroup.h @@ -75,7 +75,7 @@ class CORE_EXPORT QgsLayoutItemGroup: public QgsLayoutItem void paint( QPainter *painter, const QStyleOptionGraphicsItem *itemStyle, QWidget *pWidget ) override; void finalizeRestoreFromXml() override; - + ExportLayerBehavior exportLayerBehavior() const override; protected: void draw( QgsLayoutItemRenderContext &context ) override; bool writePropertiesToElement( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context ) const override; diff --git a/src/core/layout/qgslayoutitemlegend.cpp b/src/core/layout/qgslayoutitemlegend.cpp index ee4ff825acf..60cb54e4708 100644 --- a/src/core/layout/qgslayoutitemlegend.cpp +++ b/src/core/layout/qgslayoutitemlegend.cpp @@ -865,6 +865,11 @@ QgsExpressionContext QgsLayoutItemLegend::createExpressionContext() const return context; } +QgsLayoutItem::ExportLayerBehavior QgsLayoutItemLegend::exportLayerBehavior() const +{ + return MustPlaceInOwnLayer; +} + // ------------------------------------------------------------------------- #include "qgslayertreemodellegendnode.h" diff --git a/src/core/layout/qgslayoutitemlegend.h b/src/core/layout/qgslayoutitemlegend.h index 23ca4f26165..eb3d3b7f87a 100644 --- a/src/core/layout/qgslayoutitemlegend.h +++ b/src/core/layout/qgslayoutitemlegend.h @@ -496,6 +496,7 @@ class CORE_EXPORT QgsLayoutItemLegend : public QgsLayoutItem void finalizeRestoreFromXml() override; QgsExpressionContext createExpressionContext() const override; + ExportLayerBehavior exportLayerBehavior() const override; public slots: diff --git a/src/core/layout/qgslayoutitemmap.cpp b/src/core/layout/qgslayoutitemmap.cpp index f7df2865699..42903f52bd1 100644 --- a/src/core/layout/qgslayoutitemmap.cpp +++ b/src/core/layout/qgslayoutitemmap.cpp @@ -993,6 +993,11 @@ int QgsLayoutItemMap::numberExportLayers() const + ( frameEnabled() ? 1 : 0 ); } +QgsLayoutItem::ExportLayerBehavior QgsLayoutItemMap::exportLayerBehavior() const +{ + return ItemContainsSubLayers; +} + QgsLayoutItem::ExportLayerDetail QgsLayoutItemMap::exportLayerDetails( int layer ) const { ExportLayerDetail detail; diff --git a/src/core/layout/qgslayoutitemmap.h b/src/core/layout/qgslayoutitemmap.h index 9ddd6115f00..4093c9dc982 100644 --- a/src/core/layout/qgslayoutitemmap.h +++ b/src/core/layout/qgslayoutitemmap.h @@ -117,6 +117,7 @@ class CORE_EXPORT QgsLayoutItemMap : public QgsLayoutItem // for now, map items behave a bit differently and don't implement draw. TODO - see if we can avoid this void paint( QPainter *painter, const QStyleOptionGraphicsItem *itemStyle, QWidget *pWidget ) override; int numberExportLayers() const override; + ExportLayerBehavior exportLayerBehavior() const override; QgsLayoutItem::ExportLayerDetail exportLayerDetails( int layer ) const override; void setFrameStrokeWidth( QgsLayoutMeasurement width ) override; diff --git a/src/core/layout/qgslayoutitempage.cpp b/src/core/layout/qgslayoutitempage.cpp index d75a97f9c30..f2cc56f9371 100644 --- a/src/core/layout/qgslayoutitempage.cpp +++ b/src/core/layout/qgslayoutitempage.cpp @@ -175,6 +175,11 @@ QgsAbstractLayoutUndoCommand *QgsLayoutItemPage::createCommand( const QString &t return new QgsLayoutItemPageUndoCommand( this, text, id, parent ); } +QgsLayoutItem::ExportLayerBehavior QgsLayoutItemPage::exportLayerBehavior() const +{ + return CanGroupWithItemsOfSameType; +} + void QgsLayoutItemPage::redraw() { QgsLayoutItem::redraw(); diff --git a/src/core/layout/qgslayoutitempage.h b/src/core/layout/qgslayoutitempage.h index 2bb6eb0a77b..a96427d756b 100644 --- a/src/core/layout/qgslayoutitempage.h +++ b/src/core/layout/qgslayoutitempage.h @@ -124,6 +124,7 @@ class CORE_EXPORT QgsLayoutItemPage : public QgsLayoutItem QRectF boundingRect() const override; void attemptResize( const QgsLayoutSize &size, bool includesFrame = false ) override; QgsAbstractLayoutUndoCommand *createCommand( const QString &text, int id, QUndoCommand *parent = nullptr ) override SIP_FACTORY; + ExportLayerBehavior exportLayerBehavior() const override; public slots: diff --git a/src/core/layout/qgslayoutitemscalebar.cpp b/src/core/layout/qgslayoutitemscalebar.cpp index 6e731805b51..9488e3152ef 100644 --- a/src/core/layout/qgslayoutitemscalebar.cpp +++ b/src/core/layout/qgslayoutitemscalebar.cpp @@ -884,3 +884,8 @@ bool QgsLayoutItemScaleBar::accept( QgsStyleEntityVisitorInterface *visitor ) co return true; } + +QgsLayoutItem::ExportLayerBehavior QgsLayoutItemScaleBar::exportLayerBehavior() const +{ + return CanGroupWithItemsOfSameType; +} diff --git a/src/core/layout/qgslayoutitemscalebar.h b/src/core/layout/qgslayoutitemscalebar.h index 91b82ac3112..4d4e56e4290 100644 --- a/src/core/layout/qgslayoutitemscalebar.h +++ b/src/core/layout/qgslayoutitemscalebar.h @@ -467,6 +467,7 @@ class CORE_EXPORT QgsLayoutItemScaleBar: public QgsLayoutItem void refreshDataDefinedProperty( QgsLayoutObject::DataDefinedProperty property = QgsLayoutObject::AllProperties ) override; void finalizeRestoreFromXml() override; bool accept( QgsStyleEntityVisitorInterface *visitor ) const override; + ExportLayerBehavior exportLayerBehavior() const override; protected: diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index e89d2d26aa1..f590cb15261 100644 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -136,6 +136,7 @@ SET(TESTS testqgslayout.cpp testqgslayoutatlas.cpp testqgslayoutcontext.cpp + testqgslayoutexporter.cpp testqgslayouthtml.cpp testqgslayoutitem.cpp testqgslayoutitemgroup.cpp diff --git a/tests/src/core/testqgslayoutexporter.cpp b/tests/src/core/testqgslayoutexporter.cpp new file mode 100644 index 00000000000..c044fba7d8f --- /dev/null +++ b/tests/src/core/testqgslayoutexporter.cpp @@ -0,0 +1,236 @@ +/*************************************************************************** + testqgslayoutexporter.cpp + ----------------- + begin : August 2019 + copyright : (C) 2019 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 "qgslayout.h" +#include "qgstest.h" +#include "qgsproject.h" +#include "qgslayoutexporter.h" +#include "qgslayoutitemlabel.h" +#include "qgslayoutitemshape.h" +#include "qgslayoutitemscalebar.h" +#include "qgslayoutitemmap.h" +#include "qgsvectorlayer.h" +#include "qgslayoutitemlegend.h" + +class TestQgsLayoutExporter: public QObject +{ + Q_OBJECT + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init();// will be called before each testfunction is executed. + void cleanup();// will be called after every testfunction. + void testHandleLayeredExport(); + +}; + +void TestQgsLayoutExporter::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); +} + +void TestQgsLayoutExporter::cleanupTestCase() +{ +} + +void TestQgsLayoutExporter::init() +{ + +} + +void TestQgsLayoutExporter::cleanup() +{ + +} + +void TestQgsLayoutExporter::testHandleLayeredExport() +{ + QgsProject p; + QgsLayout l( &p ); + QgsLayoutExporter exporter( &l ); + + QList< unsigned int > layerIds; + QStringList layerNames; + QStringList mapLayerIds; + auto exportFunc = [&layerIds, &layerNames, &mapLayerIds]( unsigned int layerId, const QgsLayoutItem::ExportLayerDetail & layerDetail )->QgsLayoutExporter::ExportResult + { + layerIds << layerId; + layerNames << layerDetail.name; + mapLayerIds << layerDetail.mapLayerId; + return QgsLayoutExporter::Success; + }; + + QList< QGraphicsItem * > items; + QgsLayoutExporter::ExportResult res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QVERIFY( layerIds.isEmpty() ); + QVERIFY( layerNames.isEmpty() ); + QVERIFY( mapLayerIds.isEmpty() ); + + // add two pages to a layout + QgsLayoutItemPage *page1 = new QgsLayoutItemPage( &l ); + items << page1; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemPage *page2 = new QgsLayoutItemPage( &l ); + items << page2; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label = new QgsLayoutItemLabel( &l ); + items << label; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemShape *shape = new QgsLayoutItemShape( &l ); + items << shape; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label2 = new QgsLayoutItemLabel( &l ); + items << label2; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + // add an item which can only be used with other similar items, should break the next label into a different layer + QgsLayoutItemScaleBar *scaleBar = new QgsLayoutItemScaleBar( &l ); + items << scaleBar; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) << QStringLiteral( "Layer 3" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + QgsLayoutItemLabel *label3 = new QgsLayoutItemLabel( &l ); + items << label3; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) << QStringLiteral( "Layer 3" ) << QStringLiteral( "Layer 4" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + // multiple scalebars can be placed in the same layer + QgsLayoutItemScaleBar *scaleBar2 = new QgsLayoutItemScaleBar( &l ); + items << scaleBar2; + QgsLayoutItemScaleBar *scaleBar3 = new QgsLayoutItemScaleBar( &l ); + items << scaleBar3; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) << QStringLiteral( "Layer 3" ) << QStringLiteral( "Layer 4" ) << QStringLiteral( "Layer 5" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + // with an item which has sublayers + QgsVectorLayer *linesLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/lines.shp" ), + QStringLiteral( "lines" ), QStringLiteral( "ogr" ) ); + QVERIFY( linesLayer->isValid() ); + QgsVectorLayer *pointsLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/points.shp" ), + QStringLiteral( "points" ), QStringLiteral( "ogr" ) ); + QVERIFY( pointsLayer->isValid() ); + + p.addMapLayer( linesLayer ); + p.addMapLayer( pointsLayer ); + + QgsLayoutItemMap *map = new QgsLayoutItemMap( &l ); + map->attemptSetSceneRect( QRectF( 20, 20, 200, 100 ) ); + map->setFrameEnabled( false ); + map->setBackgroundEnabled( false ); + map->setCrs( linesLayer->crs() ); + map->zoomToExtent( linesLayer->extent() ); + map->setLayers( QList() << linesLayer ); + + items << map; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) << QStringLiteral( "Layer 3" ) << QStringLiteral( "Layer 4" ) << QStringLiteral( "Layer 5" ) << QStringLiteral( "lines" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + map->setFrameEnabled( true ); + map->setBackgroundEnabled( true ); + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) << QStringLiteral( "Layer 3" ) << QStringLiteral( "Layer 4" ) << QStringLiteral( "Layer 5" ) << QStringLiteral( "Map Background" ) << QStringLiteral( "lines" ) << QStringLiteral( "Map Frame" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + // add two legends -- legends are complex and must be placed in an isolated layer + QgsLayoutItemLegend *legend = new QgsLayoutItemLegend( &l ); + QgsLayoutItemLegend *legend2 = new QgsLayoutItemLegend( &l ); + items << legend << legend2; + res = exporter.handleLayeredExport( items, exportFunc ); + QCOMPARE( res, QgsLayoutExporter::Success ); + QCOMPARE( layerIds, QList< unsigned int >() << 1 << 2 << 3 << 4 << 5 << 6 << 7 << 8 << 9 << 10 ); + QCOMPARE( layerNames, QStringList() << QStringLiteral( "Layer 1" ) << QStringLiteral( "Layer 2" ) << QStringLiteral( "Layer 3" ) << QStringLiteral( "Layer 4" ) << QStringLiteral( "Layer 5" ) << QStringLiteral( "Map Background" ) << QStringLiteral( "lines" ) << QStringLiteral( "Map Frame" ) << QStringLiteral( "Layer 9" ) << QStringLiteral( "Layer 10" ) ); + QCOMPARE( mapLayerIds, QStringList() << QString() << QString() << QString() << QString() << QString() << QString() << linesLayer->id() << QString() << QString() << QString() ); + layerIds.clear(); + layerNames.clear(); + mapLayerIds.clear(); + + qDeleteAll( items ); +} + +QGSTEST_MAIN( TestQgsLayoutExporter ) +#include "testqgslayoutexporter.moc"