QGIS API Documentation  3.4.15-Madeira (e83d02e274)
qgslayoutatlas.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgslayoutatlas.cpp
3  ----------------
4  begin : December 2017
5  copyright : (C) 2017 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 #include <algorithm>
18 #include <stdexcept>
19 #include <QtAlgorithms>
20 
21 #include "qgslayoutatlas.h"
22 #include "qgslayout.h"
23 #include "qgsmessagelog.h"
24 #include "qgsfeaturerequest.h"
25 #include "qgsfeatureiterator.h"
26 #include "qgsvectorlayer.h"
27 
29  : QObject( layout )
30  , mLayout( layout )
31  , mFilenameExpressionString( QStringLiteral( "'output_'||@atlas_featurenumber" ) )
32 {
33 
34  //listen out for layer removal
35  connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QStringList & ) >( &QgsProject::layersWillBeRemoved ), this, &QgsLayoutAtlas::removeLayers );
36 }
37 
39 {
40  return QStringLiteral( "atlas" );
41 }
42 
44 {
45  return mLayout;
46 }
47 
49 {
50  return mLayout.data();
51 }
52 
53 bool QgsLayoutAtlas::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
54 {
55  QDomElement atlasElem = document.createElement( QStringLiteral( "Atlas" ) );
56  atlasElem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
57 
58  if ( mCoverageLayer )
59  {
60  atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId );
61  atlasElem.setAttribute( QStringLiteral( "coverageLayerName" ), mCoverageLayer.name );
62  atlasElem.setAttribute( QStringLiteral( "coverageLayerSource" ), mCoverageLayer.source );
63  atlasElem.setAttribute( QStringLiteral( "coverageLayerProvider" ), mCoverageLayer.provider );
64  }
65  else
66  {
67  atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), QString() );
68  }
69 
70  atlasElem.setAttribute( QStringLiteral( "hideCoverage" ), mHideCoverage ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
71  atlasElem.setAttribute( QStringLiteral( "filenamePattern" ), mFilenameExpressionString );
72  atlasElem.setAttribute( QStringLiteral( "pageNameExpression" ), mPageNameExpression );
73 
74  atlasElem.setAttribute( QStringLiteral( "sortFeatures" ), mSortFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
75  if ( mSortFeatures )
76  {
77  atlasElem.setAttribute( QStringLiteral( "sortKey" ), mSortExpression );
78  atlasElem.setAttribute( QStringLiteral( "sortAscending" ), mSortAscending ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
79  }
80  atlasElem.setAttribute( QStringLiteral( "filterFeatures" ), mFilterFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
81  if ( mFilterFeatures )
82  {
83  atlasElem.setAttribute( QStringLiteral( "featureFilter" ), mFilterExpression );
84  }
85 
86  parentElement.appendChild( atlasElem );
87 
88  return true;
89 }
90 
91 bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument &, const QgsReadWriteContext & )
92 {
93  mEnabled = atlasElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt();
94 
95  // look for stored layer name
96  QString layerId = atlasElem.attribute( QStringLiteral( "coverageLayer" ) );
97  QString layerName = atlasElem.attribute( QStringLiteral( "coverageLayerName" ) );
98  QString layerSource = atlasElem.attribute( QStringLiteral( "coverageLayerSource" ) );
99  QString layerProvider = atlasElem.attribute( QStringLiteral( "coverageLayerProvider" ) );
100 
101  mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider );
102  mCoverageLayer.resolveWeakly( mLayout->project() );
103  mLayout->reportContext().setLayer( mCoverageLayer.get() );
104 
105  mPageNameExpression = atlasElem.attribute( QStringLiteral( "pageNameExpression" ), QString() );
106  QString error;
107  setFilenameExpression( atlasElem.attribute( QStringLiteral( "filenamePattern" ), QString() ), error );
108 
109  mSortFeatures = atlasElem.attribute( QStringLiteral( "sortFeatures" ), QStringLiteral( "0" ) ).toInt();
110  mSortExpression = atlasElem.attribute( QStringLiteral( "sortKey" ) );
111  mSortAscending = atlasElem.attribute( QStringLiteral( "sortAscending" ), QStringLiteral( "1" ) ).toInt();
112  mFilterFeatures = atlasElem.attribute( QStringLiteral( "filterFeatures" ), QStringLiteral( "0" ) ).toInt();
113  mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) );
114 
115  mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt();
116 
117  emit toggled( mEnabled );
118  emit changed();
119  return true;
120 }
121 
123 {
124  if ( enabled == mEnabled )
125  {
126  return;
127  }
128 
129  mEnabled = enabled;
130  emit toggled( enabled );
131  emit changed();
132 }
133 
134 void QgsLayoutAtlas::removeLayers( const QStringList &layers )
135 {
136  if ( !mCoverageLayer )
137  {
138  return;
139  }
140 
141  for ( const QString &layerId : layers )
142  {
143  if ( layerId == mCoverageLayer.layerId )
144  {
145  //current coverage layer removed
146  mCoverageLayer.setLayer( nullptr );
147  setEnabled( false );
148  break;
149  }
150  }
151 }
152 
154 {
155  if ( layer == mCoverageLayer.get() )
156  {
157  return;
158  }
159 
160  mCoverageLayer.setLayer( layer );
161  emit coverageLayerChanged( layer );
162 }
163 
164 QString QgsLayoutAtlas::nameForPage( int pageNumber ) const
165 {
166  if ( pageNumber < 0 || pageNumber >= mFeatureIds.count() )
167  return QString();
168 
169  return mFeatureIds.at( pageNumber ).second;
170 }
171 
172 bool QgsLayoutAtlas::setFilterExpression( const QString &expression, QString &errorString )
173 {
174  errorString.clear();
175  mFilterExpression = expression;
176 
177  QgsExpression filterExpression( mFilterExpression );
178  if ( filterExpression.hasParserError() )
179  {
180  errorString = filterExpression.parserErrorString();
181  return false;
182  }
183 
184  return true;
185 }
186 
187 
189 class AtlasFeatureSorter
190 {
191  public:
192  AtlasFeatureSorter( QgsLayoutAtlas::SorterKeys &keys, bool ascending = true )
193  : mKeys( keys )
194  , mAscending( ascending )
195  {}
196 
197  bool operator()( const QPair< QgsFeatureId, QString > &id1, const QPair< QgsFeatureId, QString > &id2 )
198  {
199  return mAscending ? qgsVariantLessThan( mKeys.value( id1.first ), mKeys.value( id2.first ) )
200  : qgsVariantGreaterThan( mKeys.value( id1.first ), mKeys.value( id2.first ) );
201  }
202 
203  private:
204  QgsLayoutAtlas::SorterKeys &mKeys;
205  bool mAscending;
206 };
207 
209 
211 {
212  mCurrentFeatureNo = -1;
213  if ( !mCoverageLayer )
214  {
215  return 0;
216  }
217 
218  QgsExpressionContext expressionContext = createExpressionContext();
219 
220  QString error;
221  updateFilenameExpression( error );
222 
223  // select all features with all attributes
224  QgsFeatureRequest req;
225 
226  req.setExpressionContext( expressionContext );
227 
228  mFilterParserError.clear();
229  if ( mFilterFeatures && !mFilterExpression.isEmpty() )
230  {
231  QgsExpression filterExpression( mFilterExpression );
232  if ( filterExpression.hasParserError() )
233  {
234  mFilterParserError = filterExpression.parserErrorString();
235  return 0;
236  }
237 
238  //filter good to go
239  req.setFilterExpression( mFilterExpression );
240  }
241 
242  QgsFeatureIterator fit = mCoverageLayer->getFeatures( req );
243 
244  std::unique_ptr<QgsExpression> nameExpression;
245  if ( !mPageNameExpression.isEmpty() )
246  {
247  nameExpression = qgis::make_unique< QgsExpression >( mPageNameExpression );
248  if ( nameExpression->hasParserError() )
249  {
250  nameExpression.reset( nullptr );
251  }
252  else
253  {
254  nameExpression->prepare( &expressionContext );
255  }
256  }
257 
258  // We cannot use nextFeature() directly since the feature pointer is rewinded by the rendering process
259  // We thus store the feature ids for future extraction
260  QgsFeature feat;
261  mFeatureIds.clear();
262  mFeatureKeys.clear();
263 
264  std::unique_ptr<QgsExpression> sortExpression;
265  if ( mSortFeatures && !mSortExpression.isEmpty() )
266  {
267  sortExpression = qgis::make_unique< QgsExpression >( mSortExpression );
268  if ( sortExpression->hasParserError() )
269  {
270  sortExpression.reset( nullptr );
271  }
272  else
273  {
274  sortExpression->prepare( &expressionContext );
275  }
276  }
277 
278  while ( fit.nextFeature( feat ) )
279  {
280  expressionContext.setFeature( feat );
281 
282  QString pageName;
283  if ( nameExpression )
284  {
285  QVariant result = nameExpression->evaluate( &expressionContext );
286  if ( nameExpression->hasEvalError() )
287  {
288  QgsMessageLog::logMessage( tr( "Atlas name eval error: %1" ).arg( nameExpression->evalErrorString() ), tr( "Layout" ) );
289  }
290  pageName = result.toString();
291  }
292 
293  mFeatureIds.push_back( qMakePair( feat.id(), pageName ) );
294 
295  if ( sortExpression )
296  {
297  QVariant result = sortExpression->evaluate( &expressionContext );
298  if ( sortExpression->hasEvalError() )
299  {
300  QgsMessageLog::logMessage( tr( "Atlas sort eval error: %1" ).arg( sortExpression->evalErrorString() ), tr( "Layout" ) );
301  }
302  mFeatureKeys.insert( feat.id(), result );
303  }
304  }
305 
306  // sort features, if asked for
307  if ( !mFeatureKeys.isEmpty() )
308  {
309  AtlasFeatureSorter sorter( mFeatureKeys, mSortAscending );
310  std::sort( mFeatureIds.begin(), mFeatureIds.end(), sorter ); // clazy:exclude=detaching-member
311  }
312 
313  emit numberFeaturesChanged( mFeatureIds.size() );
314  return mFeatureIds.size();
315 }
316 
318 {
319  if ( !mCoverageLayer )
320  {
321  return false;
322  }
323 
324  emit renderBegun();
325 
326  if ( !updateFeatures() )
327  {
328  //no matching features found
329  return false;
330  }
331 
332  return true;
333 }
334 
336 {
337  emit featureChanged( QgsFeature() );
338  emit renderEnded();
339  return true;
340 }
341 
343 {
344  return mFeatureIds.size();
345 }
346 
347 QString QgsLayoutAtlas::filePath( const QString &baseFilePath, const QString &extension )
348 {
349  QFileInfo fi( baseFilePath );
350  QDir dir = fi.dir(); // ignore everything except the directory
351  QString base = dir.filePath( mCurrentFilename );
352  if ( !extension.startsWith( '.' ) )
353  base += '.';
354  base += extension;
355  return base;
356 }
357 
359 {
360  int newFeatureNo = mCurrentFeatureNo + 1;
361  if ( newFeatureNo >= mFeatureIds.size() )
362  {
363  return false;
364  }
365 
366  return prepareForFeature( newFeatureNo );
367 }
368 
370 {
371  int newFeatureNo = mCurrentFeatureNo - 1;
372  if ( newFeatureNo < 0 )
373  {
374  return false;
375  }
376 
377  return prepareForFeature( newFeatureNo );
378 }
379 
381 {
382  return prepareForFeature( 0 );
383 }
384 
386 {
387  return prepareForFeature( mFeatureIds.size() - 1 );
388 }
389 
390 bool QgsLayoutAtlas::seekTo( int feature )
391 {
392  return prepareForFeature( feature );
393 }
394 
395 bool QgsLayoutAtlas::seekTo( const QgsFeature &feature )
396 {
397  int i = -1;
398  auto it = mFeatureIds.constBegin();
399  for ( int currentIdx = 0; it != mFeatureIds.constEnd(); ++it, ++currentIdx )
400  {
401  if ( ( *it ).first == feature.id() )
402  {
403  i = currentIdx;
404  break;
405  }
406  }
407 
408  if ( i < 0 )
409  {
410  //feature not found
411  return false;
412  }
413 
414  return seekTo( i );
415 }
416 
418 {
419  prepareForFeature( mCurrentFeatureNo );
420 }
421 
423 {
424  mHideCoverage = hide;
425 
426  mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, hide );
427  mLayout->refresh();
428 }
429 
430 bool QgsLayoutAtlas::setFilenameExpression( const QString &pattern, QString &errorString )
431 {
432  mFilenameExpressionString = pattern;
433  return updateFilenameExpression( errorString );
434 }
435 
437 {
438  return mCurrentFilename;
439 }
440 
441 QgsExpressionContext QgsLayoutAtlas::createExpressionContext()
442 {
443  QgsExpressionContext expressionContext;
444  expressionContext << QgsExpressionContextUtils::globalScope();
445  if ( mLayout )
446  expressionContext << QgsExpressionContextUtils::projectScope( mLayout->project() )
448 
449  expressionContext.appendScope( QgsExpressionContextUtils::atlasScope( this ) );
450 
451  if ( mCoverageLayer )
452  expressionContext.lastScope()->setFields( mCoverageLayer->fields() );
453 
454  if ( mLayout && mEnabled )
455  expressionContext.lastScope()->setFeature( mCurrentFeature );
456 
457  return expressionContext;
458 }
459 
460 bool QgsLayoutAtlas::updateFilenameExpression( QString &error )
461 {
462  if ( !mCoverageLayer )
463  {
464  return false;
465  }
466 
467  QgsExpressionContext expressionContext = createExpressionContext();
468 
469  if ( !mFilenameExpressionString.isEmpty() )
470  {
471  mFilenameExpression = QgsExpression( mFilenameExpressionString );
472  // expression used to evaluate each filename
473  // test for evaluation errors
474  if ( mFilenameExpression.hasParserError() )
475  {
476  error = mFilenameExpression.parserErrorString();
477  return false;
478  }
479 
480  // prepare the filename expression
481  mFilenameExpression.prepare( &expressionContext );
482  }
483 
484  // regenerate current filename
485  evalFeatureFilename( expressionContext );
486  return true;
487 }
488 
489 bool QgsLayoutAtlas::evalFeatureFilename( const QgsExpressionContext &context )
490 {
491  //generate filename for current atlas feature
492  if ( !mFilenameExpressionString.isEmpty() && mFilenameExpression.isValid() )
493  {
494  QVariant filenameRes = mFilenameExpression.evaluate( &context );
495  if ( mFilenameExpression.hasEvalError() )
496  {
497  QgsMessageLog::logMessage( tr( "Atlas filename evaluation error: %1" ).arg( mFilenameExpression.evalErrorString() ), tr( "Layout" ) );
498  return false;
499  }
500 
501  mCurrentFilename = filenameRes.toString();
502  }
503  return true;
504 }
505 
506 bool QgsLayoutAtlas::prepareForFeature( const int featureI )
507 {
508  if ( !mCoverageLayer )
509  {
510  return false;
511  }
512 
513  if ( mFeatureIds.isEmpty() )
514  {
515  emit messagePushed( tr( "No matching atlas features" ) );
516  return false;
517  }
518 
519  if ( featureI >= mFeatureIds.size() )
520  {
521  return false;
522  }
523 
524  mCurrentFeatureNo = featureI;
525 
526  // retrieve the next feature, based on its id
527  if ( !mCoverageLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeatureIds[ featureI ].first ) ).nextFeature( mCurrentFeature ) )
528  return false;
529 
530  mLayout->reportContext().blockSignals( true ); // setFeature emits changed, we don't want 2 signals
531  mLayout->reportContext().setLayer( mCoverageLayer.get() );
532  mLayout->reportContext().blockSignals( false );
533  mLayout->reportContext().setFeature( mCurrentFeature );
534 
535  // must come after we've set the report context feature, or the expression context will have an outdated atlas feature
536  QgsExpressionContext expressionContext = createExpressionContext();
537 
538  // generate filename for current feature
539  if ( !evalFeatureFilename( expressionContext ) )
540  {
541  //error evaluating filename
542  return false;
543  }
544 
545  emit featureChanged( mCurrentFeature );
546  emit messagePushed( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) );
547 
548  return mCurrentFeature.isValid();
549 }
550 
void setCoverageLayer(QgsVectorLayer *layer)
Sets the coverage layer to use for the atlas features.
Class for parsing and evaluation of expressions (formerly called "search strings").
QgsFeatureId id
Definition: qgsfeature.h:64
The class is used as a container of context for various read/write operations on other objects...
Wrapper for iterator of features from vector data provider or vector layer.
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
QString filePath(const QString &baseFilePath, const QString &extension) override
Returns the file path for the current feature, based on a specified base file path and extension...
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the context.
TYPE * resolveWeakly(const QgsProject *project)
Resolves the map layer by attempting to find a matching layer in a project using a weak match...
QString stringType() const override
Returns the object type as a string.
void setFields(const QgsFields &fields)
Convenience function for setting a fields for the scope.
TYPE * get() const
Returns a pointer to the layer, or nullptr if the reference has not yet been matched to a layer...
void toggled(bool)
Emitted when atlas is enabled or disabled.
static QgsExpressionContextScope * projectScope(const QgsProject *project)
Creates a new scope which contains variables and functions relating to a QGIS project.
The feature class encapsulates a single feature including its id, geometry and a list of field/values...
Definition: qgsfeature.h:55
bool qgsVariantGreaterThan(const QVariant &lhs, const QVariant &rhs)
Compares two QVariant values and returns whether the first is greater than the second.
Definition: qgis.cpp:221
bool writeXml(QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext &context) const override
Stores the objects&#39;s state in a DOM element.
bool qgsVariantLessThan(const QVariant &lhs, const QVariant &rhs)
Compares two QVariant values and returns whether the first is less than the second.
Definition: qgis.cpp:153
QgsLayoutAtlas(QgsLayout *layout)
Constructor for new QgsLayoutAtlas.
QgsLayout * layout() override
Returns the layout associated with the iterator.
QString filterExpression() const
Returns the expression used for filtering features in the coverage layer.
bool endRender() override
Ends the render, performing any required cleanup tasks.
bool setFilenameExpression(const QString &expression, QString &errorString)
Sets the filename expression used for generating output filenames for each atlas page.
QgsFeatureRequest & setExpressionContext(const QgsExpressionContext &context)
Sets the expression context used to evaluate filter expressions.
void refreshCurrentFeature()
Refreshes the current atlas feature, by refetching its attributes from the vector layer provider...
void numberFeaturesChanged(int numFeatures)
Emitted when the number of features for the atlas changes.
QgsExpressionContextScope * lastScope()
Returns the last scope added to the context.
QgsFeatureRequest & setFilterExpression(const QString &expression)
Set the filter expression.
QString provider
Weak reference to layer provider.
QString layerId
Original layer ID.
static QgsExpressionContextScope * globalScope()
Creates a new scope which contains variables and functions relating to the global QGIS context...
bool readXml(const QDomElement &element, const QDomDocument &document, const QgsReadWriteContext &context) override
Sets the objects&#39;s state from a DOM element.
QString nameForPage(int page) const
Returns the calculated name for a specified atlas page number.
bool enabled() const
Returns whether the atlas generation is enabled.
Expression contexts are used to encapsulate the parameters around which a QgsExpression should be eva...
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
QString name
Weak reference to layer name.
bool last()
Seeks to the last feature, returning false if no feature was found.
This class wraps a request for features to a vector layer (or directly its vector data provider)...
Reads and writes project states.
Definition: qgsproject.h:89
QString currentFilename() const
Returns the current feature filename.
void setHideCoverage(bool hide)
Sets whether the coverage layer should be hidden in map items in the layouts.
int count() override
Returns the number of features to iterate over.
bool first()
Seeks to the first feature, returning false if no feature was found.
void setFeature(const QgsFeature &feature)
Convenience function for setting a feature for the scope.
void setLayer(TYPE *l)
Sets the reference to point to a specified layer.
void renderBegun()
Emitted when atlas rendering has begun.
QString source
Weak reference to layer public source.
Base class for layouts, which can contain items such as maps, labels, scalebars, etc.
Definition: qgslayout.h:49
bool previous()
Iterates to the previous feature, returning false if no previous feature exists.
bool next() override
bool setFilterExpression(const QString &expression, QString &errorString)
Sets the expression used for filtering features in the coverage layer.
void renderEnded()
Emitted when atlas rendering has ended.
static QgsExpressionContextScope * atlasScope(QgsLayoutAtlas *atlas)
Creates a new scope which contains variables and functions relating to a QgsLayoutAtlas.
void layersWillBeRemoved(const QStringList &layerIds)
Emitted when one or more layers are about to be removed from the registry.
static QgsExpressionContextScope * layoutScope(const QgsLayout *layout)
Creates a new scope which contains variables and functions relating to a QgsLayout layout...
void setEnabled(bool enabled)
Sets whether the atlas is enabled.
friend class AtlasFeatureSorter
void appendScope(QgsExpressionContextScope *scope)
Appends a scope to the end of the context.
void featureChanged(const QgsFeature &feature)
Is emitted when the current atlas feature changes.
_LayerRef< QgsVectorLayer > QgsVectorLayerRef
int updateFeatures()
Requeries the current atlas coverage layer and applies filtering and sorting.
bool beginRender() override
Called when rendering begins, before iteration commences.
void messagePushed(const QString &message)
Is emitted when the atlas has an updated status bar message.
bool nextFeature(QgsFeature &f)
QString sortExpression() const
Returns the expression (or field name) to use for sorting features.
Represents a vector layer which manages a vector based data sets.
QString parserErrorString() const
Returns parser error.
void changed()
Emitted when one of the atlas parameters changes.
bool seekTo(int feature)
Seeks to the specified feature number.
void coverageLayerChanged(QgsVectorLayer *layer)
Emitted when the coverage layer for the atlas changes.