1 /***************************************************************************
2                          qgsalgorithmexporttospreadsheet.cpp
3                          ------------------
4     begin                : December 2020
5     copyright            : (C) 2020 by Nyall Dawson
6     email                : nyall dot dawson at gmail dot com
7  ***************************************************************************/
8 
9 /***************************************************************************
10  *                                                                         *
11  *   This program is free software; you can redistribute it and/or modify  *
12  *   it under the terms of the GNU General Public License as published by  *
13  *   the Free Software Foundation; either version 2 of the License, or     *
14  *   (at your option) any later version.                                   *
15  *                                                                         *
16  ***************************************************************************/
17 
18 #include "qgsalgorithmexporttospreadsheet.h"
19 #include "qgsogrutils.h"
20 #include "qgsvectorfilewriter.h"
21 #include "qgsvectorlayer.h"
22 #include "qgsapplication.h"
23 #include "qgsfieldformatterregistry.h"
24 #include "qgsfieldformatter.h"
25 
26 ///@cond PRIVATE
27 
28 class FieldValueConverter : public QgsVectorFileWriter::FieldValueConverter
29 {
30   public:
FieldValueConverter(QgsVectorLayer * vl)31     FieldValueConverter( QgsVectorLayer *vl )
32       : mLayer( vl )
33     {
34       const QStringList formattersAllowList{ QStringLiteral( "KeyValue" ),
35                                              QStringLiteral( "List" ),
36                                              QStringLiteral( "ValueRelation" ),
37                                              QStringLiteral( "ValueMap" ) };
38 
39       for ( int i = 0; i < mLayer->fields().count(); ++i )
40       {
41         const QgsEditorWidgetSetup setup = mLayer->fields().at( i ).editorWidgetSetup();
42         const QgsFieldFormatter *fieldFormatter = QgsApplication::fieldFormatterRegistry()->fieldFormatter( setup.type() );
43         if ( formattersAllowList.contains( fieldFormatter->id() ) )
44         {
45           mFormatters[i] = fieldFormatter;
46           mConfig[i] = setup.config();
47         }
48       }
49     }
50 
fieldDefinition(const QgsField & field)51     QgsField fieldDefinition( const QgsField &field ) override
52     {
53       if ( !mLayer )
54         return field;
55 
56       const int idx = mLayer->fields().indexFromName( field.name() );
57       if ( mFormatters.contains( idx ) )
58       {
59         return QgsField( field.name(), QVariant::String );
60       }
61       return field;
62     }
63 
convert(int i,const QVariant & value)64     QVariant convert( int i, const QVariant &value ) override
65     {
66       const QgsFieldFormatter *formatter = mFormatters.value( i );
67       if ( !formatter )
68         return value;
69 
70       QVariant cache;
71       if ( mCaches.contains( i ) )
72       {
73         cache = mCaches.value( i );
74       }
75       else
76       {
77         cache = formatter->createCache( mLayer.data(), i, mConfig.value( i ) );
78         mCaches[ i ] = cache;
79       }
80 
81       return formatter->representValue( mLayer.data(), i, mConfig.value( i ), cache, value );
82     }
83 
clone() const84     FieldValueConverter *clone() const override
85     {
86       return new FieldValueConverter( *this );
87     }
88 
89   private:
90     QPointer< QgsVectorLayer > mLayer;
91     QMap< int, const QgsFieldFormatter * > mFormatters;
92     QMap< int, QVariantMap > mConfig;
93     QMap< int, QVariant > mCaches;
94 };
95 
name() const96 QString QgsExportToSpreadsheetAlgorithm::name() const
97 {
98   return QStringLiteral( "exporttospreadsheet" );
99 }
100 
displayName() const101 QString QgsExportToSpreadsheetAlgorithm::displayName() const
102 {
103   return QObject::tr( "Export to spreadsheet" );
104 }
105 
tags() const106 QStringList QgsExportToSpreadsheetAlgorithm::tags() const
107 {
108   return QObject::tr( "microsoft,excel,xls,xlsx,calc,open,office,libre,ods" ).split( ',' );
109 }
110 
group() const111 QString QgsExportToSpreadsheetAlgorithm::group() const
112 {
113   return QObject::tr( "Layer tools" );
114 }
115 
groupId() const116 QString QgsExportToSpreadsheetAlgorithm::groupId() const
117 {
118   return QStringLiteral( "layertools" );
119 }
120 
initAlgorithm(const QVariantMap &)121 void QgsExportToSpreadsheetAlgorithm::initAlgorithm( const QVariantMap & )
122 {
123   addParameter( new QgsProcessingParameterMultipleLayers( QStringLiteral( "LAYERS" ), QObject::tr( "Input layers" ), QgsProcessing::TypeVector ) );
124   addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "USE_ALIAS" ), QObject::tr( "Use field aliases as column headings" ), false ) );
125   addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "FORMATTED_VALUES" ), QObject::tr( "Export formatted values instead of raw values" ), false ) );
126   QgsProcessingParameterFileDestination *outputParameter = new QgsProcessingParameterFileDestination( QStringLiteral( "OUTPUT" ), QObject::tr( "Destination spreadsheet" ), QObject::tr( "Microsoft Excel (*.xlsx);;Open Document Spreadsheet (*.ods)" ) );
127   outputParameter->setMetadata( QVariantMap( {{QStringLiteral( "widget_wrapper" ), QVariantMap( {{QStringLiteral( "dontconfirmoverwrite" ), true }} ) }} ) );
128   addParameter( outputParameter );
129   addParameter( new QgsProcessingParameterBoolean( QStringLiteral( "OVERWRITE" ), QObject::tr( "Overwrite existing spreadsheet" ), true ) );
130   addOutput( new QgsProcessingOutputMultipleLayers( QStringLiteral( "OUTPUT_LAYERS" ), QObject::tr( "Layers within spreadsheet" ) ) );
131 }
132 
shortHelpString() const133 QString QgsExportToSpreadsheetAlgorithm::shortHelpString() const
134 {
135   return QObject::tr( "This algorithm collects a number of existing layers and exports them into a spreadsheet document.\n\n"
136                       "Optionally the layers can be appended to an existing spreadsheet as additional sheets.\n\n" );
137 }
138 
createInstance() const139 QgsExportToSpreadsheetAlgorithm *QgsExportToSpreadsheetAlgorithm::createInstance() const
140 {
141   return new QgsExportToSpreadsheetAlgorithm();
142 }
143 
prepareAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)144 bool QgsExportToSpreadsheetAlgorithm::prepareAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
145 {
146   const QList< QgsMapLayer * > layers = parameterAsLayerList( parameters, QStringLiteral( "LAYERS" ), context );
147   for ( QgsMapLayer *layer : layers )
148   {
149     mLayers.emplace_back( layer->clone() );
150   }
151 
152   if ( mLayers.empty() )
153     feedback->reportError( QObject::tr( "No layers selected, spreadsheet will be empty" ), false );
154 
155   return true;
156 }
157 
processAlgorithm(const QVariantMap & parameters,QgsProcessingContext & context,QgsProcessingFeedback * feedback)158 QVariantMap QgsExportToSpreadsheetAlgorithm::processAlgorithm( const QVariantMap &parameters, QgsProcessingContext &context, QgsProcessingFeedback *feedback )
159 {
160   const bool overwrite = parameterAsBoolean( parameters, QStringLiteral( "OVERWRITE" ), context );
161   const QString outputPath = parameterAsString( parameters, QStringLiteral( "OUTPUT" ), context );
162   if ( outputPath.isEmpty() )
163     throw QgsProcessingException( QObject::tr( "No output file specified." ) );
164 
165   const bool useAlias = parameterAsBoolean( parameters, QStringLiteral( "USE_ALIAS" ), context );
166   const bool formattedValues = parameterAsBoolean( parameters, QStringLiteral( "FORMATTED_VALUES" ), context );
167   bool createNew = true;
168   // delete existing spreadsheet if it exists
169   if ( overwrite && QFile::exists( outputPath ) )
170   {
171     feedback->pushInfo( QObject::tr( "Removing existing file '%1'" ).arg( outputPath ) );
172     if ( !QFile( outputPath ).remove() )
173     {
174       throw QgsProcessingException( QObject::tr( "Could not remove existing file '%1'" ).arg( outputPath ) );
175     }
176   }
177   else if ( QFile::exists( outputPath ) )
178   {
179     createNew = false;
180   }
181 
182   const QFileInfo fi( outputPath );
183   const QString driverName = QgsVectorFileWriter::driverForExtension( fi.suffix() );
184 
185   OGRSFDriverH hDriver = OGRGetDriverByName( driverName.toLocal8Bit().constData() );
186   if ( !hDriver )
187   {
188     if ( driverName == QLatin1String( "ods" ) )
189       throw QgsProcessingException( QObject::tr( "Open Document Spreadsheet driver not found." ) );
190     else
191       throw QgsProcessingException( QObject::tr( "Microsoft Excel driver not found." ) );
192   }
193 
194   const gdal::ogr_datasource_unique_ptr hDS;
195 #if 0
196   if ( !QFile::exists( outputPath ) )
197   {
198     hDS = gdal::ogr_datasource_unique_ptr( OGR_Dr_CreateDataSource( hDriver, outputPath.toUtf8().constData(), nullptr ) );
199     if ( !hDS )
200       throw QgsProcessingException( QObject::tr( "Creation of spreadsheet %1 failed (OGR error: %2)" ).arg( outputPath, QString::fromUtf8( CPLGetLastErrorMsg() ) ) );
201   }
202 #endif
203   bool errored = false;
204 
205   QgsProcessingMultiStepFeedback multiStepFeedback( mLayers.size(), feedback );
206 
207   QStringList outputLayers;
208   int i = 0;
209   for ( const auto &layer : mLayers )
210   {
211     if ( feedback->isCanceled() )
212       break;
213 
214     multiStepFeedback.setCurrentStep( i );
215     i++;
216 
217     if ( !layer )
218     {
219       // don't throw immediately - instead do what we can and error out later
220       feedback->pushDebugInfo( QObject::tr( "Error retrieving map layer." ) );
221       errored = true;
222       continue;
223     }
224 
225     feedback->pushInfo( QObject::tr( "Exporting layer %1/%2: %3" ).arg( i ).arg( mLayers.size() ).arg( layer ? layer->name() : QString() ) );
226 
227     FieldValueConverter converter( qobject_cast< QgsVectorLayer * >( layer.get() ) );
228 
229     if ( !exportVectorLayer( qobject_cast< QgsVectorLayer * >( layer.get() ), outputPath,
230                              context, &multiStepFeedback, driverName, createNew, useAlias, formattedValues ? &converter : nullptr ) )
231       errored = true;
232     else
233     {
234       outputLayers.append( QStringLiteral( "%1|layername=%2" ).arg( outputPath, layer->name() ) );
235       createNew = false;
236     }
237   }
238 
239   if ( errored )
240     throw QgsProcessingException( QObject::tr( "Error obtained while exporting one or more layers." ) );
241 
242   QVariantMap outputs;
243   outputs.insert( QStringLiteral( "OUTPUT" ), outputPath );
244   outputs.insert( QStringLiteral( "OUTPUT_LAYERS" ), outputLayers );
245   return outputs;
246 }
247 
exportVectorLayer(QgsVectorLayer * layer,const QString & path,QgsProcessingContext & context,QgsProcessingFeedback * feedback,const QString & driverName,bool createNew,bool preferAlias,QgsVectorFileWriter::FieldValueConverter * converter)248 bool QgsExportToSpreadsheetAlgorithm::exportVectorLayer( QgsVectorLayer *layer, const QString &path, QgsProcessingContext &context,
249     QgsProcessingFeedback *feedback, const QString &driverName, bool createNew, bool preferAlias, QgsVectorFileWriter::FieldValueConverter *converter )
250 {
251   QgsVectorFileWriter::SaveVectorOptions options;
252   options.driverName = driverName;
253   options.layerName = layer->name();
254   options.actionOnExistingFile = createNew ? QgsVectorFileWriter::CreateOrOverwriteFile : QgsVectorFileWriter::CreateOrOverwriteLayer;
255   options.fileEncoding = context.defaultEncoding();
256   options.feedback = feedback;
257   options.fieldNameSource = preferAlias ? QgsVectorFileWriter::PreferAlias : QgsVectorFileWriter::Original;
258   options.fieldValueConverter = converter;
259 
260 
261   QString error;
262   QString newFilename;
263   QString newLayer;
264   if ( QgsVectorFileWriter::writeAsVectorFormatV3( layer, path, context.transformContext(), options, &error, &newFilename, &newLayer ) != QgsVectorFileWriter::NoError )
265   {
266     feedback->reportError( QObject::tr( "Exporting layer failed: %1" ).arg( error ) );
267     return false;
268   }
269   else
270   {
271     return true;
272   }
273 }
274 
275 ///@endcond
276