QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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"
28 
30  : QObject( layout )
31  , mLayout( layout )
32  , mFilenameExpressionString( QStringLiteral( "'output_'||@atlas_featurenumber" ) )
33 {
34 
35  //listen out for layer removal
36  connect( mLayout->project(), static_cast < void ( QgsProject::* )( const QStringList & ) >( &QgsProject::layersWillBeRemoved ), this, &QgsLayoutAtlas::removeLayers );
37 }
38 
40 {
41  return QStringLiteral( "atlas" );
42 }
43 
45 {
46  return mLayout;
47 }
48 
50 {
51  return mLayout.data();
52 }
53 
54 bool QgsLayoutAtlas::writeXml( QDomElement &parentElement, QDomDocument &document, const QgsReadWriteContext & ) const
55 {
56  QDomElement atlasElem = document.createElement( QStringLiteral( "Atlas" ) );
57  atlasElem.setAttribute( QStringLiteral( "enabled" ), mEnabled ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
58 
59  if ( mCoverageLayer )
60  {
61  atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), mCoverageLayer.layerId );
62  atlasElem.setAttribute( QStringLiteral( "coverageLayerName" ), mCoverageLayer.name );
63  atlasElem.setAttribute( QStringLiteral( "coverageLayerSource" ), mCoverageLayer.source );
64  atlasElem.setAttribute( QStringLiteral( "coverageLayerProvider" ), mCoverageLayer.provider );
65  }
66  else
67  {
68  atlasElem.setAttribute( QStringLiteral( "coverageLayer" ), QString() );
69  }
70 
71  atlasElem.setAttribute( QStringLiteral( "hideCoverage" ), mHideCoverage ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
72  atlasElem.setAttribute( QStringLiteral( "filenamePattern" ), mFilenameExpressionString );
73  atlasElem.setAttribute( QStringLiteral( "pageNameExpression" ), mPageNameExpression );
74 
75  atlasElem.setAttribute( QStringLiteral( "sortFeatures" ), mSortFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
76  if ( mSortFeatures )
77  {
78  atlasElem.setAttribute( QStringLiteral( "sortKey" ), mSortExpression );
79  atlasElem.setAttribute( QStringLiteral( "sortAscending" ), mSortAscending ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
80  }
81  atlasElem.setAttribute( QStringLiteral( "filterFeatures" ), mFilterFeatures ? QStringLiteral( "1" ) : QStringLiteral( "0" ) );
82  if ( mFilterFeatures )
83  {
84  atlasElem.setAttribute( QStringLiteral( "featureFilter" ), mFilterExpression );
85  }
86 
87  parentElement.appendChild( atlasElem );
88 
89  return true;
90 }
91 
92 bool QgsLayoutAtlas::readXml( const QDomElement &atlasElem, const QDomDocument &, const QgsReadWriteContext & )
93 {
94  mEnabled = atlasElem.attribute( QStringLiteral( "enabled" ), QStringLiteral( "0" ) ).toInt();
95 
96  // look for stored layer name
97  QString layerId = atlasElem.attribute( QStringLiteral( "coverageLayer" ) );
98  QString layerName = atlasElem.attribute( QStringLiteral( "coverageLayerName" ) );
99  QString layerSource = atlasElem.attribute( QStringLiteral( "coverageLayerSource" ) );
100  QString layerProvider = atlasElem.attribute( QStringLiteral( "coverageLayerProvider" ) );
101 
102  mCoverageLayer = QgsVectorLayerRef( layerId, layerName, layerSource, layerProvider );
103  mCoverageLayer.resolveWeakly( mLayout->project() );
104  mLayout->reportContext().setLayer( mCoverageLayer.get() );
105 
106  mPageNameExpression = atlasElem.attribute( QStringLiteral( "pageNameExpression" ), QString() );
107  QString error;
108  setFilenameExpression( atlasElem.attribute( QStringLiteral( "filenamePattern" ), QString() ), error );
109 
110  mSortFeatures = atlasElem.attribute( QStringLiteral( "sortFeatures" ), QStringLiteral( "0" ) ).toInt();
111  mSortExpression = atlasElem.attribute( QStringLiteral( "sortKey" ) );
112  mSortAscending = atlasElem.attribute( QStringLiteral( "sortAscending" ), QStringLiteral( "1" ) ).toInt();
113  mFilterFeatures = atlasElem.attribute( QStringLiteral( "filterFeatures" ), QStringLiteral( "0" ) ).toInt();
114  mFilterExpression = atlasElem.attribute( QStringLiteral( "featureFilter" ) );
115 
116  mHideCoverage = atlasElem.attribute( QStringLiteral( "hideCoverage" ), QStringLiteral( "0" ) ).toInt();
117 
118  emit toggled( mEnabled );
119  emit changed();
120  return true;
121 }
122 
124 {
125  if ( enabled == mEnabled )
126  {
127  return;
128  }
129 
130  mEnabled = enabled;
131  emit toggled( enabled );
132  emit changed();
133 }
134 
135 void QgsLayoutAtlas::removeLayers( const QStringList &layers )
136 {
137  if ( !mCoverageLayer )
138  {
139  return;
140  }
141 
142  for ( const QString &layerId : layers )
143  {
144  if ( layerId == mCoverageLayer.layerId )
145  {
146  //current coverage layer removed
147  mCoverageLayer.setLayer( nullptr );
148  setEnabled( false );
149  break;
150  }
151  }
152 }
153 
155 {
156  if ( layer == mCoverageLayer.get() )
157  {
158  return;
159  }
160 
161  mCoverageLayer.setLayer( layer );
162  emit coverageLayerChanged( layer );
163 }
164 
165 QString QgsLayoutAtlas::nameForPage( int pageNumber ) const
166 {
167  if ( pageNumber < 0 || pageNumber >= mFeatureIds.count() )
168  return QString();
169 
170  return mFeatureIds.at( pageNumber ).second;
171 }
172 
173 bool QgsLayoutAtlas::setFilterExpression( const QString &expression, QString &errorString )
174 {
175  errorString.clear();
176  mFilterExpression = expression;
177 
178  QgsExpression filterExpression( mFilterExpression );
179  if ( filterExpression.hasParserError() )
180  {
181  errorString = filterExpression.parserErrorString();
182  return false;
183  }
184 
185  return true;
186 }
187 
188 
190 class AtlasFeatureSorter
191 {
192  public:
193  AtlasFeatureSorter( QgsLayoutAtlas::SorterKeys &keys, bool ascending = true )
194  : mKeys( keys )
195  , mAscending( ascending )
196  {}
197 
198  bool operator()( const QPair< QgsFeatureId, QString > &id1, const QPair< QgsFeatureId, QString > &id2 )
199  {
200  return mAscending ? qgsVariantLessThan( mKeys.value( id1.first ), mKeys.value( id2.first ) )
201  : qgsVariantGreaterThan( mKeys.value( id1.first ), mKeys.value( id2.first ) );
202  }
203 
204  private:
205  QgsLayoutAtlas::SorterKeys &mKeys;
206  bool mAscending;
207 };
208 
210 
212 {
213  mCurrentFeatureNo = -1;
214  if ( !mCoverageLayer )
215  {
216  return 0;
217  }
218 
219  QgsExpressionContext expressionContext = createExpressionContext();
220 
221  QString error;
222  updateFilenameExpression( error );
223 
224  // select all features with all attributes
225  QgsFeatureRequest req;
226 
227  req.setExpressionContext( expressionContext );
228 
229  mFilterParserError.clear();
230  if ( mFilterFeatures && !mFilterExpression.isEmpty() )
231  {
232  QgsExpression filterExpression( mFilterExpression );
233  if ( filterExpression.hasParserError() )
234  {
235  mFilterParserError = filterExpression.parserErrorString();
236  return 0;
237  }
238 
239  //filter good to go
240  req.setFilterExpression( mFilterExpression );
241  }
242 
243  QgsFeatureIterator fit = mCoverageLayer->getFeatures( req );
244 
245  std::unique_ptr<QgsExpression> nameExpression;
246  if ( !mPageNameExpression.isEmpty() )
247  {
248  nameExpression = qgis::make_unique< QgsExpression >( mPageNameExpression );
249  if ( nameExpression->hasParserError() )
250  {
251  nameExpression.reset( nullptr );
252  }
253  else
254  {
255  nameExpression->prepare( &expressionContext );
256  }
257  }
258 
259  // We cannot use nextFeature() directly since the feature pointer is rewinded by the rendering process
260  // We thus store the feature ids for future extraction
261  QgsFeature feat;
262  mFeatureIds.clear();
263  mFeatureKeys.clear();
264 
265  std::unique_ptr<QgsExpression> sortExpression;
266  if ( mSortFeatures && !mSortExpression.isEmpty() )
267  {
268  sortExpression = qgis::make_unique< QgsExpression >( mSortExpression );
269  if ( sortExpression->hasParserError() )
270  {
271  sortExpression.reset( nullptr );
272  }
273  else
274  {
275  sortExpression->prepare( &expressionContext );
276  }
277  }
278 
279  while ( fit.nextFeature( feat ) )
280  {
281  expressionContext.setFeature( feat );
282 
283  QString pageName;
284  if ( nameExpression )
285  {
286  QVariant result = nameExpression->evaluate( &expressionContext );
287  if ( nameExpression->hasEvalError() )
288  {
289  QgsMessageLog::logMessage( tr( "Atlas name eval error: %1" ).arg( nameExpression->evalErrorString() ), tr( "Layout" ) );
290  }
291  pageName = result.toString();
292  }
293 
294  mFeatureIds.push_back( qMakePair( feat.id(), pageName ) );
295 
296  if ( sortExpression )
297  {
298  QVariant result = sortExpression->evaluate( &expressionContext );
299  if ( sortExpression->hasEvalError() )
300  {
301  QgsMessageLog::logMessage( tr( "Atlas sort eval error: %1" ).arg( sortExpression->evalErrorString() ), tr( "Layout" ) );
302  }
303  mFeatureKeys.insert( feat.id(), result );
304  }
305  }
306 
307  // sort features, if asked for
308  if ( !mFeatureKeys.isEmpty() )
309  {
310  AtlasFeatureSorter sorter( mFeatureKeys, mSortAscending );
311  std::sort( mFeatureIds.begin(), mFeatureIds.end(), sorter ); // clazy:exclude=detaching-member
312  }
313 
314  emit numberFeaturesChanged( mFeatureIds.size() );
315  return mFeatureIds.size();
316 }
317 
319 {
320  if ( !mCoverageLayer )
321  {
322  return false;
323  }
324 
325  emit renderBegun();
326 
327  if ( !updateFeatures() )
328  {
329  //no matching features found
330  return false;
331  }
332 
333  return true;
334 }
335 
337 {
338  emit featureChanged( QgsFeature() );
339  emit renderEnded();
340  return true;
341 }
342 
344 {
345  return mFeatureIds.size();
346 }
347 
348 QString QgsLayoutAtlas::filePath( const QString &baseFilePath, const QString &extension )
349 {
350  QFileInfo fi( baseFilePath );
351  QDir dir = fi.dir(); // ignore everything except the directory
352  QString base = dir.filePath( mCurrentFilename );
353  if ( !extension.startsWith( '.' ) )
354  base += '.';
355  base += extension;
356  return base;
357 }
358 
360 {
361  int newFeatureNo = mCurrentFeatureNo + 1;
362  if ( newFeatureNo >= mFeatureIds.size() )
363  {
364  return false;
365  }
366 
367  return prepareForFeature( newFeatureNo );
368 }
369 
371 {
372  int newFeatureNo = mCurrentFeatureNo - 1;
373  if ( newFeatureNo < 0 )
374  {
375  return false;
376  }
377 
378  return prepareForFeature( newFeatureNo );
379 }
380 
382 {
383  return prepareForFeature( 0 );
384 }
385 
387 {
388  return prepareForFeature( mFeatureIds.size() - 1 );
389 }
390 
391 bool QgsLayoutAtlas::seekTo( int feature )
392 {
393  return prepareForFeature( feature );
394 }
395 
396 bool QgsLayoutAtlas::seekTo( const QgsFeature &feature )
397 {
398  int i = -1;
399  auto it = mFeatureIds.constBegin();
400  for ( int currentIdx = 0; it != mFeatureIds.constEnd(); ++it, ++currentIdx )
401  {
402  if ( ( *it ).first == feature.id() )
403  {
404  i = currentIdx;
405  break;
406  }
407  }
408 
409  if ( i < 0 )
410  {
411  //feature not found
412  return false;
413  }
414 
415  return seekTo( i );
416 }
417 
419 {
420  prepareForFeature( mCurrentFeatureNo );
421 }
422 
424 {
425  mHideCoverage = hide;
426 
427  mLayout->renderContext().setFlag( QgsLayoutRenderContext::FlagHideCoverageLayer, hide );
428  mLayout->refresh();
429 }
430 
431 bool QgsLayoutAtlas::setFilenameExpression( const QString &pattern, QString &errorString )
432 {
433  mFilenameExpressionString = pattern;
434  return updateFilenameExpression( errorString );
435 }
436 
438 {
439  return mCurrentFilename;
440 }
441 
442 QgsExpressionContext QgsLayoutAtlas::createExpressionContext()
443 {
444  QgsExpressionContext expressionContext;
445  expressionContext << QgsExpressionContextUtils::globalScope();
446  if ( mLayout )
447  expressionContext << QgsExpressionContextUtils::projectScope( mLayout->project() )
449 
450  expressionContext.appendScope( QgsExpressionContextUtils::atlasScope( this ) );
451 
452  if ( mCoverageLayer )
453  expressionContext.lastScope()->setFields( mCoverageLayer->fields() );
454 
455  if ( mLayout && mEnabled )
456  expressionContext.lastScope()->setFeature( mCurrentFeature );
457 
458  return expressionContext;
459 }
460 
461 bool QgsLayoutAtlas::updateFilenameExpression( QString &error )
462 {
463  if ( !mCoverageLayer )
464  {
465  return false;
466  }
467 
468  QgsExpressionContext expressionContext = createExpressionContext();
469 
470  if ( !mFilenameExpressionString.isEmpty() )
471  {
472  mFilenameExpression = QgsExpression( mFilenameExpressionString );
473  // expression used to evaluate each filename
474  // test for evaluation errors
475  if ( mFilenameExpression.hasParserError() )
476  {
477  error = mFilenameExpression.parserErrorString();
478  return false;
479  }
480 
481  // prepare the filename expression
482  mFilenameExpression.prepare( &expressionContext );
483  }
484 
485  // regenerate current filename
486  evalFeatureFilename( expressionContext );
487  return true;
488 }
489 
490 bool QgsLayoutAtlas::evalFeatureFilename( const QgsExpressionContext &context )
491 {
492  //generate filename for current atlas feature
493  if ( !mFilenameExpressionString.isEmpty() && mFilenameExpression.isValid() )
494  {
495  QVariant filenameRes = mFilenameExpression.evaluate( &context );
496  if ( mFilenameExpression.hasEvalError() )
497  {
498  QgsMessageLog::logMessage( tr( "Atlas filename evaluation error: %1" ).arg( mFilenameExpression.evalErrorString() ), tr( "Layout" ) );
499  return false;
500  }
501 
502  mCurrentFilename = filenameRes.toString();
503  }
504  return true;
505 }
506 
507 bool QgsLayoutAtlas::prepareForFeature( const int featureI )
508 {
509  if ( !mCoverageLayer )
510  {
511  return false;
512  }
513 
514  if ( mFeatureIds.isEmpty() )
515  {
516  emit messagePushed( tr( "No matching atlas features" ) );
517  return false;
518  }
519 
520  if ( featureI >= mFeatureIds.size() )
521  {
522  return false;
523  }
524 
525  mCurrentFeatureNo = featureI;
526 
527  // retrieve the next feature, based on its id
528  if ( !mCoverageLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeatureIds[ featureI ].first ) ).nextFeature( mCurrentFeature ) )
529  return false;
530 
531  mLayout->reportContext().blockSignals( true ); // setFeature emits changed, we don't want 2 signals
532  mLayout->reportContext().setLayer( mCoverageLayer.get() );
533  mLayout->reportContext().blockSignals( false );
534  mLayout->reportContext().setFeature( mCurrentFeature );
535 
536  // must come after we've set the report context feature, or the expression context will have an outdated atlas feature
537  QgsExpressionContext expressionContext = createExpressionContext();
538 
539  // generate filename for current feature
540  if ( !evalFeatureFilename( expressionContext ) )
541  {
542  //error evaluating filename
543  return false;
544  }
545 
546  emit featureChanged( mCurrentFeature );
547  emit messagePushed( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) );
548 
549  return mCurrentFeature.isValid();
550 }
551 
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
bool hasParserError() const
Returns true if an error occurred when parsing the input expression.
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.
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.
QString sortExpression() const
Returns the expression (or field name) to use for sorting features.
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.
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.
QString parserErrorString() const
Returns parser error.
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.
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.
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
QString filterExpression() const
Returns the expression used for filtering features in the coverage layer.
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)
Emitted when the current atlas feature changes.
_LayerRef< QgsVectorLayer > QgsVectorLayerRef
int updateFeatures()
Requeries the current atlas coverage layer and applies filtering and sorting.
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.
bool beginRender() override
Called when rendering begins, before iteration commences.
void messagePushed(const QString &message)
Emitted when the atlas has an updated status bar message.
bool nextFeature(QgsFeature &f)
Represents a vector layer which manages a vector based data sets.
TYPE * get() const
Returns a pointer to the layer, or nullptr if the reference has not yet been matched to a layer...
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.