Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add relations export option to package alg
Funded by: ARPA Piemonte
  • Loading branch information
elpaso committed Aug 1, 2022
1 parent 5d839f8 commit aa5192f
Show file tree
Hide file tree
Showing 5 changed files with 497 additions and 5 deletions.
209 changes: 204 additions & 5 deletions src/analysis/processing/qgsalgorithmpackage.cpp
Expand Up @@ -59,6 +59,7 @@ void QgsPackageAlgorithm::initAlgorithm( const QVariantMap & )
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "SAVE_STYLES" ), QObject::tr( "Save layer styles into GeoPackage" ), true ) );
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "SAVE_METADATA" ), QObject::tr( "Save layer metadata into GeoPackage" ), true ) );
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "SELECTED_FEATURES_ONLY" ), QObject::tr( "Save only selected features" ), false ) );
addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "EXPORT_RELATED_LAYERS" ), QObject::tr( "Export related layers following relations defined in the project" ), false ) );
addOutput( new QgsProcessingOutputMultipleLayers( QStringLiteral( "OUTPUT_LAYERS" ), QObject::tr( "Layers within new package" ) ) );
}

Expand All @@ -74,14 +75,197 @@ QgsPackageAlgorithm *QgsPackageAlgorithm::createInstance() const

bool QgsPackageAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
{

const bool exportRelatedLayers = parameterAsBoolean( parameters, QStringLiteral( "EXPORT_RELATED_LAYERS" ), context );
const bool selectedFeaturesOnly = parameterAsBoolean( parameters, QStringLiteral( "SELECTED_FEATURES_ONLY" ), context );

const QList< QgsMapLayer * > layers = parameterAsLayerList( parameters, QStringLiteral( "LAYERS" ), context );
for ( QgsMapLayer *layer : layers )

for ( const QgsMapLayer *layer : std::as_const( layers ) )
{
mLayers.emplace_back( layer->clone() );
QgsMapLayer *clonedLayer { layer->clone() };
mClonedLayerIds.insert( clonedLayer->id(), layer->id( ) );
mLayers.emplace_back( clonedLayer );
}

if ( mLayers.empty() )
{
feedback->reportError( QObject::tr( "No layers selected, geopackage will be empty" ), false );
}

// Collect related layers from project relations
else if ( exportRelatedLayers )
{
const QgsProject *project { context.project() };
if ( project && ! project->relationManager()->relations().isEmpty() )
{

// Infinite recursion should not happen in the real world but better safe than sorry
const int maxRecursion { 10 };
int recursionGuard { 0 };

// This function recursively finds referenced layers
const auto findReferenced = [ &project, &feedback, &recursionGuard, &layers, this ]( const QgsVectorLayer * vLayer, bool onlySaveSelected, auto &&findReferenced ) -> void
{
const QgsVectorLayer *originalLayer { qobject_cast<QgsVectorLayer *>( project->mapLayer( mClonedLayerIds.value( vLayer->id(), vLayer->id() ) ) ) };
Q_ASSERT( originalLayer );
const QList<QgsRelation> relations { project->relationManager()->referencingRelations( originalLayer ) };
for ( const QgsRelation &relation : std::as_const( relations ) )
{

QgsVectorLayer *referencedLayer { relation.referencedLayer() };
QgsVectorLayer *relatedLayer { nullptr };

// Check if the layer was already in the export list
bool alreadyAdded { false };
for ( const auto &layerToExport : std::as_const( mLayers ) )
{
const QString originalId { mClonedLayerIds.value( layerToExport->id() ) };
if ( originalId == referencedLayer->id() )
{
relatedLayer = qobject_cast<QgsVectorLayer *>( layerToExport.get() );
alreadyAdded = true;
break;
}
}

if ( !alreadyAdded )
{
feedback->pushInfo( QObject::tr( "Adding referenced layer '%1'" ).arg( referencedLayer->name() ) );
relatedLayer = referencedLayer->clone();
mLayers.emplace_back( relatedLayer );
mClonedLayerIds.insert( relatedLayer->id(), referencedLayer->id() );
}

// Export only relevant features unless the layer was in the original export list and no selection was made on that layer
// in that case the user explicitly marked the layer for export and it is supposed to be exported fully.
if ( onlySaveSelected )
{
if ( ! layers.contains( qobject_cast<QgsMapLayer *>( referencedLayer ) ) || referencedLayer->selectedFeatureCount() > 0 )
{
Q_ASSERT( relatedLayer );
QgsFeatureIds selected;
QgsFeatureIterator it { vLayer->getSelectedFeatures() };
QgsFeature selectedFeature;
while ( it.nextFeature( selectedFeature ) )
{
QgsFeature referencedFeature { relation.getReferencedFeature( selectedFeature ) };
if ( referencedFeature.isValid() )
{
selected.insert( referencedFeature.id() );
}
}
relatedLayer->selectByIds( selected, Qgis::SelectBehavior::AddToSelection );
}
}

// Recursive finding
if ( recursionGuard > maxRecursion )
{
feedback->pushWarning( QObject::tr( "Max recursion (%1) adding referenced layer '%2', layer was not added" ).arg( QLocale().toString( recursionGuard ), referencedLayer->name() ) );
}
else
{
recursionGuard++;
findReferenced( relatedLayer, onlySaveSelected, findReferenced );
}
}

};

// This function recursively finds referencing layers
const auto findReferencing = [ &project, &feedback, &recursionGuard, &layers, this ]( const QgsVectorLayer * vLayer, bool onlySaveSelected, auto &&findReferencing ) -> void
{
const QgsVectorLayer *originalLayer { qobject_cast<QgsVectorLayer *>( project->mapLayer( mClonedLayerIds.value( vLayer->id(), vLayer->id() ) ) ) };
Q_ASSERT( originalLayer );
const QList<QgsRelation> relations { project->relationManager()->referencedRelations( originalLayer ) };
for ( const QgsRelation &relation : std::as_const( relations ) )
{

QgsVectorLayer *referencingLayer { relation.referencingLayer() };
QgsVectorLayer *relatedLayer { nullptr };
const bool layerWasExplicitlyAdded { layers.contains( qobject_cast<QgsMapLayer *>( referencingLayer ) ) };

// Check if the layer was already in the export list
bool alreadyAdded { false };
for ( const auto &layerToExport : std::as_const( mLayers ) )
{
const QString originalId { mClonedLayerIds.value( layerToExport->id() ) };
if ( originalId == referencingLayer->id() )
{
relatedLayer = qobject_cast<QgsVectorLayer *>( layerToExport.get() );
alreadyAdded = true;
break;
}
}

// Export only relevant features unless the layer was in the original export list and no selection was made on that layer
// in that case the user explicitly marked the layer for export and it is supposed to be exported fully.
QgsFeatureIds selected;

if ( onlySaveSelected && ( ! layerWasExplicitlyAdded || referencingLayer->selectedFeatureCount() > 0 ) )
{
if ( ! layers.contains( qobject_cast<QgsMapLayer *>( referencingLayer ) ) || referencingLayer->selectedFeatureCount() > 0 )
{
QgsFeatureIterator it { vLayer->getSelectedFeatures() };
QgsFeature selectedFeature;
while ( it.nextFeature( selectedFeature ) )
{
QgsFeatureIterator referencingFeaturesIterator { relation.getRelatedFeatures( selectedFeature ) };
QgsFeature referencingFeature;
while ( referencingFeaturesIterator.nextFeature( referencingFeature ) )
{
if ( referencingFeature.isValid() )
{
selected.insert( referencingFeature.id() );
}
}
}
}
}

if ( ! alreadyAdded && ( ! onlySaveSelected || ! selected.isEmpty() ) )
{
feedback->pushInfo( QObject::tr( "Adding referencing layer '%1'" ).arg( referencingLayer->name() ) );
relatedLayer = referencingLayer->clone();
mLayers.emplace_back( relatedLayer );
mClonedLayerIds.insert( relatedLayer->id(), referencingLayer->id() );
}

if ( relatedLayer && ! selected.isEmpty() )
{
relatedLayer->selectByIds( selected, Qgis::SelectBehavior::AddToSelection );
}

// Recursive finding
if ( recursionGuard > maxRecursion )
{
feedback->pushWarning( QObject::tr( "Max recursion (%1) adding referencing layer '%2', layer was not added" ).arg( QLocale().toString( recursionGuard ), referencingLayer->name() ) );
}
else if ( relatedLayer )
{
recursionGuard++;
findReferencing( relatedLayer, onlySaveSelected, findReferencing ) ;
}
}

};

for ( const QgsMapLayer *layer : std::as_const( layers ) )
{
const QgsVectorLayer *vLayer { qobject_cast<const QgsVectorLayer *>( layer ) };
if ( vLayer )
{
const bool onlySaveSelected = vLayer->selectedFeatureCount() > 0 && selectedFeaturesOnly;
recursionGuard = 0;
findReferenced( vLayer, onlySaveSelected, findReferenced );
recursionGuard = 0;
findReferencing( vLayer, onlySaveSelected, findReferencing );
}
}

}
}

return true;
}
Expand All @@ -92,6 +276,7 @@ QVariantMap QgsPackageAlgorithm::processAlgorithm( const QVariantMap &parameters
const bool saveStyles = parameterAsBoolean( parameters, QStringLiteral( "SAVE_STYLES" ), context );
const bool saveMetadata = parameterAsBoolean( parameters, QStringLiteral( "SAVE_METADATA" ), context );
const bool selectedFeaturesOnly = parameterAsBoolean( parameters, QStringLiteral( "SELECTED_FEATURES_ONLY" ), context );

const QString packagePath = parameterAsString( parameters, QStringLiteral( "OUTPUT" ), context );
if ( packagePath.isEmpty() )
throw QgsProcessingException( QObject::tr( "No output file specified." ) );
Expand Down Expand Up @@ -133,6 +318,7 @@ QVariantMap QgsPackageAlgorithm::processAlgorithm( const QVariantMap &parameters
QgsProcessingMultiStepFeedback multiStepFeedback( mLayers.size(), feedback );

QStringList outputLayers;

int i = 0;
for ( const auto &layer : mLayers )
{
Expand Down Expand Up @@ -236,14 +422,27 @@ bool QgsPackageAlgorithm::packageVectorLayer( QgsVectorLayer *layer, const QStri
options.saveMetadata = true;
}

// remove any existing FID field, let this be completely recreated
// since many layer sources have fid fields which are not compatible with gpkg requirements
// Check FID compatibility with GPKG and remove any existing FID field if not compatible,
// let this be completely recreated since many layer sources have fid fields which are
// not compatible with gpkg requirements
const QgsFields fields = layer->fields();
const int fidIndex = fields.lookupField( QStringLiteral( "fid" ) );

options.attributes = fields.allAttributesList();
if ( fidIndex >= 0 )
options.attributes.removeAll( fidIndex );
{
const QVariant::Type fidType { layer->fields().field( fidIndex ).type() };
if ( ! layer->fieldConstraints( fidIndex ).testFlag( QgsFieldConstraints::Constraint::ConstraintUnique )
&& ! layer->fieldConstraints( fidIndex ).testFlag( QgsFieldConstraints::Constraint::ConstraintNotNull )
&& fidType != QVariant::Int
&& fidType != QVariant::UInt
&& fidType != QVariant::LongLong
&& fidType != QVariant::ULongLong )
{
options.attributes.removeAll( fidIndex );
}
}

if ( options.attributes.isEmpty() )
{
// fid was the only field
Expand Down
1 change: 1 addition & 0 deletions src/analysis/processing/qgsalgorithmpackage.h
Expand Up @@ -58,6 +58,7 @@ class QgsPackageAlgorithm : public QgsProcessingAlgorithm
bool saveStyles, bool saveMetadata, bool selectedFeaturesOnly );

std::vector< std::unique_ptr< QgsMapLayer> > mLayers;
QMap<QString, QString> mClonedLayerIds;

};

Expand Down
1 change: 1 addition & 0 deletions src/core/qgsvectorfilewriter.cpp
Expand Up @@ -342,6 +342,7 @@ void QgsVectorFileWriter::init( QString vectorFileName,
}
options[ datasourceOptions.size()] = nullptr;
}

mAttrIdxToOgrIdx.remove( 0 );

// create the data source
Expand Down
1 change: 1 addition & 0 deletions tests/src/python/CMakeLists.txt
Expand Up @@ -264,6 +264,7 @@ ADD_PYTHON_TEST(PyQgsProcessingAlgDecorator test_processing_alg_decorator.py)
ADD_PYTHON_TEST(PyQgsProcessingBatch test_qgsprocessingbatch.py)
ADD_PYTHON_TEST(PyQgsProcessingParameters test_qgsprocessingparameters.py)
ADD_PYTHON_TEST(PyQgsProcessingUtils test_qgsprocessingutils.py)
ADD_PYTHON_TEST(PyQgsProcessingPackageLayersAlgorithm test_processing_packagelayers.py)
ADD_PYTHON_TEST(PyQgsProfilePoint test_qgsprofilepoint.py)
ADD_PYTHON_TEST(PyQgsProfileRequest test_qgsprofilerequest.py)
ADD_PYTHON_TEST(PyQgsProjectionSelectionWidgets test_qgsprojectionselectionwidgets.py)
Expand Down

0 comments on commit aa5192f

Please sign in to comment.