[geopdf] Allow users to reorder layers in the generated layer tree

Fixes #36535
This commit is contained in:
Nyall Dawson 2020-06-08 16:32:46 +10:00
parent 8ccd127f4d
commit 12dcfabb26
8 changed files with 134 additions and 31 deletions

View File

@ -4339,6 +4339,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport
bool geoPdf = false; bool geoPdf = false;
bool useOgcBestPracticeFormat = false; bool useOgcBestPracticeFormat = false;
QStringList exportThemes; QStringList exportThemes;
QStringList geoPdfLayerOrder;
if ( mLayout ) if ( mLayout )
{ {
settings.flags = mLayout->renderContext().flags(); settings.flags = mLayout->renderContext().flags();
@ -4352,6 +4353,10 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport
const QString themes = mLayout->customProperty( QStringLiteral( "pdfExportThemes" ) ).toString(); const QString themes = mLayout->customProperty( QStringLiteral( "pdfExportThemes" ) ).toString();
if ( !themes.isEmpty() ) if ( !themes.isEmpty() )
exportThemes = themes.split( QStringLiteral( "~~~" ) ); exportThemes = themes.split( QStringLiteral( "~~~" ) );
const QString layerOrder = mLayout->customProperty( QStringLiteral( "pdfLayerOrder" ) ).toString();
if ( !layerOrder.isEmpty() )
geoPdfLayerOrder = layerOrder.split( QStringLiteral( "~~~" ) );
const int prevLayoutSettingLabelsAsOutlines = mLayout->customProperty( QStringLiteral( "pdfTextFormat" ), -1 ).toInt(); const int prevLayoutSettingLabelsAsOutlines = mLayout->customProperty( QStringLiteral( "pdfTextFormat" ), -1 ).toInt();
if ( prevLayoutSettingLabelsAsOutlines >= 0 ) if ( prevLayoutSettingLabelsAsOutlines >= 0 )
{ {
@ -4383,7 +4388,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport
} }
} }
QgsLayoutPdfExportOptionsDialog dialog( this, allowGeoPdfExport, dialogGeoPdfReason ); QgsLayoutPdfExportOptionsDialog dialog( this, allowGeoPdfExport, dialogGeoPdfReason, geoPdfLayerOrder );
dialog.setTextRenderFormat( prevTextRenderFormat ); dialog.setTextRenderFormat( prevTextRenderFormat );
dialog.setForceVector( forceVector ); dialog.setForceVector( forceVector );
@ -4408,6 +4413,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport
geoPdf = dialog.exportGeoPdf(); geoPdf = dialog.exportGeoPdf();
useOgcBestPracticeFormat = dialog.useOgcBestPracticeFormat(); useOgcBestPracticeFormat = dialog.useOgcBestPracticeFormat();
exportThemes = dialog.exportThemes(); exportThemes = dialog.exportThemes();
geoPdfLayerOrder = dialog.geoPdfLayerOrder();
if ( mLayout ) if ( mLayout )
{ {
@ -4421,6 +4427,7 @@ bool QgsLayoutDesignerDialog::getPdfExportSettings( QgsLayoutExporter::PdfExport
mLayout->setCustomProperty( QStringLiteral( "pdfCreateGeoPdf" ), geoPdf ? 1 : 0 ); mLayout->setCustomProperty( QStringLiteral( "pdfCreateGeoPdf" ), geoPdf ? 1 : 0 );
mLayout->setCustomProperty( QStringLiteral( "pdfOgcBestPracticeFormat" ), useOgcBestPracticeFormat ? 1 : 0 ); mLayout->setCustomProperty( QStringLiteral( "pdfOgcBestPracticeFormat" ), useOgcBestPracticeFormat ? 1 : 0 );
mLayout->setCustomProperty( QStringLiteral( "pdfExportThemes" ), exportThemes.join( QStringLiteral( "~~~" ) ) ); mLayout->setCustomProperty( QStringLiteral( "pdfExportThemes" ), exportThemes.join( QStringLiteral( "~~~" ) ) );
mLayout->setCustomProperty( QStringLiteral( "pdfLayerOrder" ), geoPdfLayerOrder.join( QStringLiteral( "~~~" ) ) );
} }
settings.forceVectorOutput = forceVector; settings.forceVectorOutput = forceVector;

View File

@ -145,7 +145,25 @@ QgsLayoutGeoPdfExporter::QgsLayoutGeoPdfExporter( QgsLayout *layout )
map->addRenderedFeatureHandler( handler ); map->addRenderedFeatureHandler( handler );
} }
const QList< QgsMapLayer * > layerOrder = mLayout->project()->layerTreeRoot()->layerOrder(); // start with project layer order, and then apply custom layer order if set
QStringList geoPdfLayerOrder;
const QString presetLayerOrder = mLayout->customProperty( QStringLiteral( "pdfLayerOrder" ) ).toString();
if ( !presetLayerOrder.isEmpty() )
geoPdfLayerOrder = presetLayerOrder.split( QStringLiteral( "~~~" ) );
QList< QgsMapLayer * > layerOrder = mLayout->project()->layerTreeRoot()->layerOrder();
for ( auto it = geoPdfLayerOrder.rbegin(); it != geoPdfLayerOrder.rend(); ++it )
{
for ( int i = 0; i < layerOrder.size(); ++i )
{
if ( layerOrder.at( i )->id() == *it )
{
layerOrder.move( i, 0 );
break;
}
}
}
for ( const QgsMapLayer *layer : layerOrder ) for ( const QgsMapLayer *layer : layerOrder )
mLayerOrder << layer->id(); mLayerOrder << layer->id();
} }

View File

@ -22,10 +22,10 @@
#include "qgsvectorlayer.h" #include "qgsvectorlayer.h"
#include "qgsapplication.h" #include "qgsapplication.h"
QgsGeoPdfLayerTreeModel::QgsGeoPdfLayerTreeModel( QgsLayerTree *rootNode, QObject *parent ) QgsGeoPdfLayerTreeModel::QgsGeoPdfLayerTreeModel( const QList<QgsMapLayer *> &layers, QObject *parent )
: QgsLayerTreeModel( rootNode, parent ) : QgsMapLayerModel( layers, parent )
{ {
setFlags( nullptr ); // ideally we'd just show embedded legend nodes - but the api doesn't exist for this setItemsCanBeReordered( true );
} }
int QgsGeoPdfLayerTreeModel::columnCount( const QModelIndex &parent ) const int QgsGeoPdfLayerTreeModel::columnCount( const QModelIndex &parent ) const
@ -36,37 +36,36 @@ int QgsGeoPdfLayerTreeModel::columnCount( const QModelIndex &parent ) const
Qt::ItemFlags QgsGeoPdfLayerTreeModel::flags( const QModelIndex &idx ) const Qt::ItemFlags QgsGeoPdfLayerTreeModel::flags( const QModelIndex &idx ) const
{ {
if ( !idx.isValid() )
return Qt::ItemIsDropEnabled;
if ( idx.column() == IncludeVectorAttributes ) if ( idx.column() == IncludeVectorAttributes )
{ {
if ( vectorLayer( idx ) ) if ( vectorLayer( idx ) )
return QgsLayerTreeModel::flags( idx ) | Qt::ItemIsUserCheckable; return QgsMapLayerModel::flags( idx ) | Qt::ItemIsUserCheckable;
else else
return QgsLayerTreeModel::flags( idx ); return QgsMapLayerModel::flags( idx );
} }
if ( idx.column() == InitiallyVisible ) if ( idx.column() == InitiallyVisible )
{ {
return QgsLayerTreeModel::flags( idx ) | Qt::ItemIsUserCheckable; return QgsMapLayerModel::flags( idx ) | Qt::ItemIsUserCheckable;
} }
if ( !mapLayer( idx ) ) if ( !mapLayer( idx ) )
{ {
return Qt::NoItemFlags; return nullptr;
} }
else else
{ {
return Qt::ItemIsEnabled | Qt::ItemIsEditable; return Qt::ItemIsEnabled | Qt::ItemIsEditable | Qt::ItemIsDragEnabled;
} }
return Qt::NoItemFlags; return Qt::NoItemFlags;
} }
QgsMapLayer *QgsGeoPdfLayerTreeModel::mapLayer( const QModelIndex &idx ) const QgsMapLayer *QgsGeoPdfLayerTreeModel::mapLayer( const QModelIndex &idx ) const
{ {
QgsLayerTreeNode *node = index2node( index( idx.row(), LayerColumn, idx.parent() ) ); return layerFromIndex( index( idx.row(), LayerColumn, idx.parent() ) );
if ( !node || !QgsLayerTree::isLayer( node ) )
return nullptr;
return QgsLayerTree::toLayer( node )->layer();
} }
QgsVectorLayer *QgsGeoPdfLayerTreeModel::vectorLayer( const QModelIndex &idx ) const QgsVectorLayer *QgsGeoPdfLayerTreeModel::vectorLayer( const QModelIndex &idx ) const
@ -95,7 +94,7 @@ QVariant QgsGeoPdfLayerTreeModel::headerData( int section, Qt::Orientation orien
} }
} }
} }
return QgsLayerTreeModel::headerData( section, orientation, role ); return QgsMapLayerModel::headerData( section, orientation, role );
} }
QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const
@ -106,7 +105,7 @@ QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const
if ( role == Qt::CheckStateRole ) if ( role == Qt::CheckStateRole )
return QVariant(); return QVariant();
return QgsLayerTreeModel::data( idx, role ); return QgsMapLayerModel::data( idx, role );
case GroupColumn: case GroupColumn:
{ {
@ -145,14 +144,13 @@ QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const
} }
return QVariant(); return QVariant();
} }
return QgsLayerTreeModel::data( idx, role ); return QVariant();
} }
case IncludeVectorAttributes: case IncludeVectorAttributes:
{ {
if ( role == Qt::CheckStateRole ) if ( role == Qt::CheckStateRole )
{ {
QgsLayerTreeNode *node = index2node( index( idx.row(), LayerColumn, idx.parent() ) );
if ( QgsVectorLayer *vl = vectorLayer( idx ) ) if ( QgsVectorLayer *vl = vectorLayer( idx ) )
{ {
const QVariant v = vl->customProperty( QStringLiteral( "geopdf/includeFeatures" ) ); const QVariant v = vl->customProperty( QStringLiteral( "geopdf/includeFeatures" ) );
@ -162,8 +160,8 @@ QVariant QgsGeoPdfLayerTreeModel::data( const QModelIndex &idx, int role ) const
} }
else else
{ {
// otherwise, we default to the layer's visibility // otherwise, we default to true
return node->itemVisibilityChecked() ? Qt::Checked : Qt::Unchecked; return Qt::Checked;
} }
} }
return QVariant(); return QVariant();
@ -219,6 +217,9 @@ bool QgsGeoPdfLayerTreeModel::setData( const QModelIndex &index, const QVariant
} }
break; break;
} }
case LayerColumn:
return QgsMapLayerModel::setData( index, value, role );
} }
return false; return false;
} }
@ -244,10 +245,10 @@ QgsGeoPdfLayerFilteredTreeModel::QgsGeoPdfLayerFilteredTreeModel( QgsGeoPdfLayer
bool QgsGeoPdfLayerFilteredTreeModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const bool QgsGeoPdfLayerFilteredTreeModel::filterAcceptsRow( int source_row, const QModelIndex &source_parent ) const
{ {
if ( QgsLayerTreeNode *node = mLayerTreeModel->index2node( sourceModel()->index( source_row, 0, source_parent ) ) ) if ( QgsMapLayer *layer = mLayerTreeModel->layerFromIndex( sourceModel()->index( source_row, 0, source_parent ) ) )
{ {
// filter out non-spatial layers // filter out non-spatial layers
if ( QgsLayerTree::isLayer( node ) && QgsLayerTree::toLayer( node ) && QgsLayerTree::toLayer( node )->layer() && !QgsLayerTree::toLayer( node )->layer()->isSpatial() ) if ( !layer->isSpatial() )
return false; return false;
} }

View File

@ -22,10 +22,11 @@
#include <QItemDelegate> #include <QItemDelegate>
#include "qgis_gui.h" #include "qgis_gui.h"
#include "qgslayertreemodel.h" #include "qgsmaplayermodel.h"
class QgsMapCanvas; class QgsMapCanvas;
class QgsProject; class QgsProject;
class QgsVectorLayer;
/** /**
@ -35,7 +36,7 @@ class QgsProject;
* \note This class is not a part of public API * \note This class is not a part of public API
* \since QGIS 3.12 * \since QGIS 3.12
*/ */
class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsLayerTreeModel class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsMapLayerModel
{ {
Q_OBJECT Q_OBJECT
@ -51,7 +52,7 @@ class GUI_EXPORT QgsGeoPdfLayerTreeModel : public QgsLayerTreeModel
}; };
//! constructor //! constructor
QgsGeoPdfLayerTreeModel( QgsLayerTree *rootNode, QObject *parent = nullptr ); QgsGeoPdfLayerTreeModel( const QList< QgsMapLayer * > &layers, QObject *parent = nullptr );
int columnCount( const QModelIndex &parent ) const override; int columnCount( const QModelIndex &parent ) const override;
QVariant headerData( int section, Qt::Orientation orientation, int role ) const override; QVariant headerData( int section, Qt::Orientation orientation, int role ) const override;

View File

@ -23,12 +23,13 @@
#include "qgsproject.h" #include "qgsproject.h"
#include "qgsmapthemecollection.h" #include "qgsmapthemecollection.h"
#include "qgsgeopdflayertreemodel.h" #include "qgsgeopdflayertreemodel.h"
#include "qgslayertree.h"
#include <QCheckBox> #include <QCheckBox>
#include <QPushButton> #include <QPushButton>
#include <QMenu> #include <QMenu>
QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *parent, bool allowGeoPdfExport, const QString &geoPdfReason, Qt::WindowFlags flags ) QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *parent, bool allowGeoPdfExport, const QString &geoPdfReason, const QStringList &geoPdfLayerOrder, Qt::WindowFlags flags )
: QDialog( parent, flags ) : QDialog( parent, flags )
{ {
setupUi( this ); setupUi( this );
@ -67,12 +68,30 @@ QgsLayoutPdfExportOptionsDialog::QgsLayoutPdfExportOptionsDialog( QWidget *paren
mThemesList->addItem( item ); mThemesList->addItem( item );
} }
mGeoPdfStructureModel = new QgsGeoPdfLayerTreeModel( QgsProject::instance()->layerTreeRoot(), this ); QList< QgsMapLayer * > order = QgsProject::instance()->layerTreeRoot()->layerOrder();
for ( auto it = geoPdfLayerOrder.rbegin(); it != geoPdfLayerOrder.rend(); ++it )
{
for ( int i = 0; i < order.size(); ++i )
{
if ( order.at( i )->id() == *it )
{
order.move( i, 0 );
break;
}
}
}
mGeoPdfStructureModel = new QgsGeoPdfLayerTreeModel( order, this );
mGeoPdfStructureProxyModel = new QgsGeoPdfLayerFilteredTreeModel( mGeoPdfStructureModel, this ); mGeoPdfStructureProxyModel = new QgsGeoPdfLayerFilteredTreeModel( mGeoPdfStructureModel, this );
mGeoPdfStructureTree->setModel( mGeoPdfStructureProxyModel ); mGeoPdfStructureTree->setModel( mGeoPdfStructureProxyModel );
mGeoPdfStructureTree->resizeColumnToContents( 0 ); mGeoPdfStructureTree->resizeColumnToContents( 0 );
mGeoPdfStructureTree->header()->show(); mGeoPdfStructureTree->header()->show();
mGeoPdfStructureTree->setSelectionMode( QAbstractItemView::NoSelection ); mGeoPdfStructureTree->setSelectionMode( QAbstractItemView::SingleSelection );
mGeoPdfStructureTree->setSelectionBehavior( QAbstractItemView::SelectRows );
mGeoPdfStructureTree->setDragEnabled( true );
mGeoPdfStructureTree->setAcceptDrops( true );
mGeoPdfStructureTree->setDragDropMode( QAbstractItemView::InternalMove );
mGeoPdfStructureTree->setDefaultDropAction( Qt::MoveAction );
mGeoPdfStructureTree->setContextMenuPolicy( Qt::CustomContextMenu ); mGeoPdfStructureTree->setContextMenuPolicy( Qt::CustomContextMenu );
connect( mGeoPdfStructureTree, &QTreeView::customContextMenuRequested, this, [ = ]( const QPoint & point ) connect( mGeoPdfStructureTree, &QTreeView::customContextMenuRequested, this, [ = ]( const QPoint & point )
@ -219,6 +238,16 @@ QStringList QgsLayoutPdfExportOptionsDialog::exportThemes() const
return res; return res;
} }
QStringList QgsLayoutPdfExportOptionsDialog::geoPdfLayerOrder() const
{
QStringList order;
for ( int row = 0; row < mGeoPdfStructureProxyModel->rowCount(); ++row )
{
order << mGeoPdfStructureProxyModel->data( mGeoPdfStructureProxyModel->index( row, 0 ), QgsGeoPdfLayerTreeModel::LayerIdRole ).toString();
}
return order;
}
void QgsLayoutPdfExportOptionsDialog::showHelp() void QgsLayoutPdfExportOptionsDialog::showHelp()
{ {
QgsHelp::openHelp( QStringLiteral( "print_composer/create_output.html" ) ); QgsHelp::openHelp( QStringLiteral( "print_composer/create_output.html" ) );

View File

@ -48,11 +48,14 @@ class GUI_EXPORT QgsLayoutPdfExportOptionsDialog: public QDialog, private Ui::Qg
* \param parent parent widget * \param parent parent widget
* \param allowGeoPdfExport set to FALSE if geoPdf export is blocked * \param allowGeoPdfExport set to FALSE if geoPdf export is blocked
* \param geoPdfReason set to a descriptive translated string explaining why geopdf export is not available if applicable * \param geoPdfReason set to a descriptive translated string explaining why geopdf export is not available if applicable
* \param geoPdfLayerOrder optional layer ID order list for layers in the geopdf file. Any layers not present in this list
* will instead be appended to the end of the geopdf layer list
* \param flags window flags * \param flags window flags
*/ */
QgsLayoutPdfExportOptionsDialog( QWidget *parent = nullptr, QgsLayoutPdfExportOptionsDialog( QWidget *parent = nullptr,
bool allowGeoPdfExport = true, bool allowGeoPdfExport = true,
const QString &geoPdfReason = QString(), const QString &geoPdfReason = QString(),
const QStringList &geoPdfLayerOrder = QStringList(),
Qt::WindowFlags flags = nullptr ); Qt::WindowFlags flags = nullptr );
//! Sets the text render format //! Sets the text render format
@ -97,6 +100,9 @@ class GUI_EXPORT QgsLayoutPdfExportOptionsDialog: public QDialog, private Ui::Qg
//! Returns the list of export themes //! Returns the list of export themes
QStringList exportThemes() const; QStringList exportThemes() const;
//! Returns a list of map layer IDs in the desired order they should appear in a generated GeoPDF file
QStringList geoPdfLayerOrder() const;
private slots: private slots:
void showHelp(); void showHelp();

View File

@ -101,7 +101,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>451</width> <width>451</width>
<height>630</height> <height>612</height>
</rect> </rect>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_6"> <layout class="QVBoxLayout" name="verticalLayout_6">
@ -193,7 +193,7 @@
<item> <item>
<widget class="QLabel" name="label_2"> <widget class="QLabel" name="label_2">
<property name="text"> <property name="text">
<string>Uncheck layers to avoid exporting vector feature information for those layers, and optionally set the group name to allow multiple layers to be joined into a single logical PDF group</string> <string>Uncheck layers to avoid exporting vector feature information for those layers, and optionally set the group name to allow multiple layers to be joined into a single logical PDF group. Layers can be dragged and dropped to rearrange their order in the generated GeoPDF table of contents.</string>
</property> </property>
<property name="wordWrap"> <property name="wordWrap">
<bool>true</bool> <bool>true</bool>

View File

@ -43,6 +43,7 @@ class TestQgsLayoutGeoPdfExport : public QObject
void cleanup();// will be called after every testfunction. void cleanup();// will be called after every testfunction.
void testCollectingFeatures(); void testCollectingFeatures();
void skipLayers(); void skipLayers();
void layerOrder();
private: private:
@ -405,5 +406,45 @@ void TestQgsLayoutGeoPdfExport::skipLayers()
QCOMPARE( polyFeatures.count(), 10 ); // should be features, layer did not have any setting set QCOMPARE( polyFeatures.count(), 10 ); // should be features, layer did not have any setting set
} }
void TestQgsLayoutGeoPdfExport::layerOrder()
{
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() );
QgsVectorLayer *polygonLayer = new QgsVectorLayer( TEST_DATA_DIR + QStringLiteral( "/polys.shp" ),
QStringLiteral( "polys" ), QStringLiteral( "ogr" ) );
QVERIFY( polygonLayer->isValid() );
pointsLayer->setDisplayExpression( QStringLiteral( "Staff" ) );
QgsProject p;
p.addMapLayer( linesLayer );
p.addMapLayer( pointsLayer );
p.addMapLayer( polygonLayer );
QgsLayout l( &p );
l.initializeDefaults();
QgsLayoutItemMap *map = new QgsLayoutItemMap( &l );
map->attemptSetSceneRect( QRectF( 20, 20, 200, 100 ) );
map->setFrameEnabled( true );
map->setLayers( QList<QgsMapLayer *>() << linesLayer << pointsLayer );
map->setCrs( linesLayer->crs() );
map->zoomToExtent( linesLayer->extent() );
map->setBackgroundColor( QColor( 200, 220, 230 ) );
map->setBackgroundEnabled( true );
l.addLayoutItem( map );
QgsLayoutGeoPdfExporter geoPdfExporter( &l );
// by default we should follow project layer order
QCOMPARE( geoPdfExporter.layerOrder(), QStringList() << polygonLayer->id() << pointsLayer->id() << linesLayer->id() );
// but if a custom order is specified, respected that
l.setCustomProperty( QStringLiteral( "pdfLayerOrder" ), QStringLiteral( "%1~~~%2" ).arg( linesLayer->id(), polygonLayer->id() ) );
QgsLayoutGeoPdfExporter geoPdfExporter2( &l );
QCOMPARE( geoPdfExporter2.layerOrder(), QStringList() << linesLayer->id() << polygonLayer->id() << pointsLayer->id() );
}
QGSTEST_MAIN( TestQgsLayoutGeoPdfExport ) QGSTEST_MAIN( TestQgsLayoutGeoPdfExport )
#include "testqgslayoutgeopdfexport.moc" #include "testqgslayoutgeopdfexport.moc"