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