QGIS API Documentation  2.2.0-Valmiera
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Groups Pages
qgsatlascomposition.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgsatlascomposition.cpp
3  -----------------------
4  begin : October 2012
5  copyright : (C) 2005 by Hugo Mercier
6  email : hugo dot mercier at oslandia 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 <stdexcept>
18 
19 #include "qgsatlascomposition.h"
20 #include "qgsvectorlayer.h"
21 #include "qgscomposermap.h"
22 #include "qgscomposition.h"
23 #include "qgsvectordataprovider.h"
24 #include "qgsexpression.h"
25 #include "qgsgeometry.h"
26 #include "qgscomposerlabel.h"
27 #include "qgscomposershape.h"
28 #include "qgspaperitem.h"
29 #include "qgsmaplayerregistry.h"
30 
32  mComposition( composition ),
33  mEnabled( false ),
34  mHideCoverage( false ), mFilenamePattern( "'output_'||$feature" ),
35  mCoverageLayer( 0 ), mSingleFile( false ),
36  mSortFeatures( false ), mSortAscending( true ), mCurrentFeatureNo( 0 ),
37  mFilterFeatures( false ), mFeatureFilter( "" )
38 {
39 
40  // declare special columns with a default value
41  QgsExpression::setSpecialColumn( "$page", QVariant(( int )1 ) );
42  QgsExpression::setSpecialColumn( "$feature", QVariant(( int )0 ) );
43  QgsExpression::setSpecialColumn( "$numpages", QVariant(( int )1 ) );
44  QgsExpression::setSpecialColumn( "$numfeatures", QVariant(( int )0 ) );
45  QgsExpression::setSpecialColumn( "$atlasfeatureid", QVariant(( int )0 ) );
46  QgsExpression::setSpecialColumn( "$atlasgeometry", QVariant::fromValue( QgsGeometry() ) );
47 }
48 
50 {
51 }
52 
54 {
55  mEnabled = e;
57  emit toggled( e );
58 }
59 
61 {
62  mCoverageLayer = layer;
63 
64  // update the number of features
65  QgsExpression::setSpecialColumn( "$numfeatures", QVariant(( int )mFeatureIds.size() ) );
66 
67  // Grab the first feature so that user can use it to test the style in rules.
68  QgsFeature fet;
69  layer->getFeatures().nextFeature( fet );
70  QgsExpression::setSpecialColumn( "$atlasfeatureid", fet.id() );
71  QgsExpression::setSpecialColumn( "$atlasgeometry", QVariant::fromValue( *fet.geometry() ) );
72 
73  emit coverageLayerChanged( layer );
74 }
75 
77 {
78  //deprecated method. Until removed just return the first atlas-enabled composer map
79 
80  //build a list of composer maps
81  QList<QgsComposerMap*> maps;
82  mComposition->composerItems( maps );
83  for ( QList<QgsComposerMap*>::iterator mit = maps.begin(); mit != maps.end(); ++mit )
84  {
85  QgsComposerMap* currentMap = ( *mit );
86  if ( currentMap->atlasDriven() )
87  {
88  return currentMap;
89  }
90  }
91 
92  return 0;
93 }
94 
96 {
97  //deprecated
98 
99  if ( !map )
100  {
101  return;
102  }
103 
104  map->setAtlasDriven( true );
105 }
106 
108 {
109  //deprecated method. Until removed just return the property for the first atlas-enabled composer map
110  QgsComposerMap * map = composerMap();
111  if ( !map )
112  {
113  return false;
114  }
115 
116  return map->atlasFixedScale();
117 }
118 
120 {
121  //deprecated method. Until removed just set the property for the first atlas-enabled composer map
122  QgsComposerMap * map = composerMap();
123  if ( !map )
124  {
125  return;
126  }
127 
128  map->setAtlasFixedScale( fixed );
129 }
130 
132 {
133  //deprecated method. Until removed just return the property for the first atlas-enabled composer map
134  QgsComposerMap * map = composerMap();
135  if ( !map )
136  {
137  return 0;
138  }
139 
140  return map->atlasMargin();
141 }
142 
143 void QgsAtlasComposition::setMargin( float margin )
144 {
145  //deprecated method. Until removed just set the property for the first atlas-enabled composer map
146  QgsComposerMap * map = composerMap();
147  if ( !map )
148  {
149  return;
150  }
151 
152  map->setAtlasMargin(( double ) margin );
153 }
154 
155 //
156 // Private class only used for the sorting of features
158 {
159  public:
160  FieldSorter( QgsAtlasComposition::SorterKeys& keys, bool ascending = true ) : mKeys( keys ), mAscending( ascending ) {}
161 
162  bool operator()( const QgsFeatureId& id1, const QgsFeatureId& id2 )
163  {
164  bool result = true;
165 
166  if ( mKeys[ id1 ].type() == QVariant::Int )
167  {
168  result = mKeys[ id1 ].toInt() < mKeys[ id2 ].toInt();
169  }
170  else if ( mKeys[ id1 ].type() == QVariant::Double )
171  {
172  result = mKeys[ id1 ].toDouble() < mKeys[ id2 ].toDouble();
173  }
174  else if ( mKeys[ id1 ].type() == QVariant::String )
175  {
176  result = ( QString::localeAwareCompare( mKeys[ id1 ].toString(), mKeys[ id2 ].toString() ) < 0 );
177  }
178 
179  return mAscending ? result : !result;
180  }
181  private:
184 };
185 
187 {
188  //needs to be called when layer, filter, sort changes
189 
190  if ( !mCoverageLayer )
191  {
192  return 0;
193  }
194 
196 
197  // select all features with all attributes
199 
200  std::auto_ptr<QgsExpression> filterExpression;
201  if ( mFilterFeatures && !mFeatureFilter.isEmpty() )
202  {
203  filterExpression = std::auto_ptr<QgsExpression>( new QgsExpression( mFeatureFilter ) );
204  if ( filterExpression->hasParserError() )
205  {
206  throw std::runtime_error( tr( "Feature filter parser error: %1" ).arg( filterExpression->parserErrorString() ).toLocal8Bit().data() );
207  }
208  }
209 
210  // We cannot use nextFeature() directly since the feature pointer is rewinded by the rendering process
211  // We thus store the feature ids for future extraction
212  QgsFeature feat;
213  mFeatureIds.clear();
214  mFeatureKeys.clear();
215  while ( fit.nextFeature( feat ) )
216  {
217  if ( mFilterFeatures && !mFeatureFilter.isEmpty() )
218  {
219  QVariant result = filterExpression->evaluate( &feat, mCoverageLayer->pendingFields() );
220  if ( filterExpression->hasEvalError() )
221  {
222  throw std::runtime_error( tr( "Feature filter eval error: %1" ).arg( filterExpression->evalErrorString() ).toLocal8Bit().data() );
223  }
224 
225  // skip this feature if the filter evaluation if false
226  if ( !result.toBool() )
227  {
228  continue;
229  }
230  }
231  mFeatureIds.push_back( feat.id() );
232 
233  if ( mSortFeatures )
234  {
235  mFeatureKeys.insert( feat.id(), feat.attributes()[ mSortKeyAttributeIdx ] );
236  }
237  }
238 
239  // sort features, if asked for
240  if ( mSortFeatures )
241  {
243  qSort( mFeatureIds.begin(), mFeatureIds.end(), sorter );
244  }
245 
246  QgsExpression::setSpecialColumn( "$numfeatures", QVariant(( int )mFeatureIds.size() ) );
247 
248  //jump to first feature if currently using an atlas preview
249  //need to do this in case filtering/layer change has altered matching features
251  {
252  firstFeature();
253  }
254 
255  return mFeatureIds.size();
256 }
257 
258 
260 {
261  if ( !mCoverageLayer )
262  {
263  return false;
264  }
265 
266  bool featuresUpdated = updateFeatures();
267  if ( !featuresUpdated )
268  {
269  //no matching features found
270  return false;
271  }
272 
273  // special columns for expressions
274  QgsExpression::setSpecialColumn( "$numpages", QVariant( mComposition->numPages() ) );
275  QgsExpression::setSpecialColumn( "$numfeatures", QVariant(( int )mFeatureIds.size() ) );
276 
277  return true;
278 }
279 
281 {
282  if ( !mCoverageLayer )
283  {
284  return;
285  }
286 
287  // reset label expression contexts
288  QList<QgsComposerLabel*> labels;
289  mComposition->composerItems( labels );
290  for ( QList<QgsComposerLabel*>::iterator lit = labels.begin(); lit != labels.end(); ++lit )
291  {
292  ( *lit )->setExpressionContext( 0, 0 );
293  }
294 
295  updateAtlasMaps();
296 }
297 
299 {
300  //update atlas-enabled composer maps
301  QList<QgsComposerMap*> maps;
302  mComposition->composerItems( maps );
303  for ( QList<QgsComposerMap*>::iterator mit = maps.begin(); mit != maps.end(); ++mit )
304  {
305  QgsComposerMap* currentMap = ( *mit );
306  if ( !currentMap->atlasDriven() )
307  {
308  continue;
309  }
310 
311  currentMap->cache();
312  }
313 }
314 
316 {
317  return mFeatureIds.size();
318 }
319 
321 {
323  if ( mCurrentFeatureNo >= mFeatureIds.size() )
324  {
325  mCurrentFeatureNo = mFeatureIds.size() - 1;
326  }
327 
329 }
330 
332 {
334  if ( mCurrentFeatureNo < 0 )
335  {
336  mCurrentFeatureNo = 0;
337  }
338 
340 }
341 
343 {
344  mCurrentFeatureNo = 0;
346 }
347 
349 {
350  mCurrentFeatureNo = mFeatureIds.size() - 1;
352 }
353 
355 {
356  int featureI = mFeatureIds.indexOf( feat->id() );
357  prepareForFeature( featureI );
358 }
359 
361 {
362  if ( !mCoverageLayer )
363  {
364  return;
365  }
366 
367  if ( mFeatureIds.size() == 0 )
368  {
369  emit statusMsgChanged( tr( "No matching atlas features" ) );
370  return;
371  }
372 
373  // retrieve the next feature, based on its id
375 
376  QgsExpression::setSpecialColumn( "$atlasfeatureid", mCurrentFeature.id() );
377  QgsExpression::setSpecialColumn( "$atlasgeometry", QVariant::fromValue( *mCurrentFeature.geometry() ) );
378  QgsExpression::setSpecialColumn( "$feature", QVariant(( int )featureI + 1 ) );
379 
380  // generate filename for current feature
382 
383  // evaluate label expressions
384  QList<QgsComposerLabel*> labels;
385  mComposition->composerItems( labels );
386  for ( QList<QgsComposerLabel*>::iterator lit = labels.begin(); lit != labels.end(); ++lit )
387  {
388  ( *lit )->setExpressionContext( &mCurrentFeature, mCoverageLayer );
389  }
390 
391  // update shapes (in case they use data defined symbology with atlas properties)
392  QList<QgsComposerShape*> shapes;
393  mComposition->composerItems( shapes );
394  for ( QList<QgsComposerShape*>::iterator lit = shapes.begin(); lit != shapes.end(); ++lit )
395  {
396  ( *lit )->update();
397  }
398 
399  // update page background (in case it uses data defined symbology with atlas properties)
400  QList<QgsPaperItem*> pages;
401  mComposition->composerItems( pages );
402  for ( QList<QgsPaperItem*>::iterator pageIt = pages.begin(); pageIt != pages.end(); ++pageIt )
403  {
404  ( *pageIt )->update();
405  }
406 
407  emit statusMsgChanged( QString( tr( "Atlas feature %1 of %2" ) ).arg( featureI + 1 ).arg( mFeatureIds.size() ) );
408 
409  //update composer maps
410 
411  //build a list of atlas-enabled composer maps
412  QList<QgsComposerMap*> maps;
413  QList<QgsComposerMap*> atlasMaps;
414  mComposition->composerItems( maps );
415  for ( QList<QgsComposerMap*>::iterator mit = maps.begin(); mit != maps.end(); ++mit )
416  {
417  QgsComposerMap* currentMap = ( *mit );
418  if ( !currentMap->atlasDriven() )
419  {
420  continue;
421  }
422  atlasMaps << currentMap;
423  }
424 
425  //clear the transformed bounds of the previous feature
427 
428  if ( atlasMaps.isEmpty() )
429  {
430  //no atlas enabled maps
431  return;
432  }
433 
434  // compute extent of current feature in the map CRS. This should be set on a per-atlas map basis,
435  // but given that it's not currently possible to have maps with different CRSes we can just
436  // calculate it once based on the first atlas maps' CRS.
437  computeExtent( atlasMaps[0] );
438 
439  //update atlas bounds of every atlas enabled composer map
440  for ( QList<QgsComposerMap*>::iterator mit = atlasMaps.begin(); mit != atlasMaps.end(); ++mit )
441  {
442  prepareMap( *mit );
443  }
444 }
445 
447 {
448  // compute the extent of the current feature, in the crs of the specified map
449 
450  const QgsCoordinateReferenceSystem& coverage_crs = mCoverageLayer->crs();
451  // transformation needed for feature geometries
452  const QgsCoordinateReferenceSystem& destination_crs = map->mapRenderer()->destinationCrs();
453  mTransform.setSourceCrs( coverage_crs );
454  mTransform.setDestCRS( destination_crs );
455 
456  // QgsGeometry::boundingBox is expressed in the geometry"s native CRS
457  // We have to transform the grometry to the destination CRS and ask for the bounding box
458  // Note: we cannot directly take the transformation of the bounding box, since transformations are not linear
460  tgeom.transform( mTransform );
461  mTransformedFeatureBounds = tgeom.boundingBox();
462 }
463 
465 {
466  if ( !map->atlasDriven() )
467  {
468  return;
469  }
470 
472  {
473  //transformed extent of current feature hasn't been calculated yet. This can happen if
474  //a map has been set to be atlas controlled after prepare feature was called
475  computeExtent( map );
476  }
477 
478  double xa1 = mTransformedFeatureBounds.xMinimum();
479  double xa2 = mTransformedFeatureBounds.xMaximum();
480  double ya1 = mTransformedFeatureBounds.yMinimum();
481  double ya2 = mTransformedFeatureBounds.yMaximum();
483  QgsRectangle mOrigExtent = map->extent();
484 
485  if ( map->atlasFixedScale() )
486  {
487  // only translate, keep the original scale (i.e. width x height)
488 
489  double geom_center_x = ( xa1 + xa2 ) / 2.0;
490  double geom_center_y = ( ya1 + ya2 ) / 2.0;
491  double xx = geom_center_x - mOrigExtent.width() / 2.0;
492  double yy = geom_center_y - mOrigExtent.height() / 2.0;
493  new_extent = QgsRectangle( xx,
494  yy,
495  xx + mOrigExtent.width(),
496  yy + mOrigExtent.height() );
497  }
498  else
499  {
500  // auto scale
501 
503  double map_ratio = mOrigExtent.width() / mOrigExtent.height();
504 
505  // geometry height is too big
506  if ( geom_ratio < map_ratio )
507  {
508  // extent the bbox's width
509  double adj_width = ( map_ratio * mTransformedFeatureBounds.height() - mTransformedFeatureBounds.width() ) / 2.0;
510  xa1 -= adj_width;
511  xa2 += adj_width;
512  }
513  // geometry width is too big
514  else if ( geom_ratio > map_ratio )
515  {
516  // extent the bbox's height
517  double adj_height = ( mTransformedFeatureBounds.width() / map_ratio - mTransformedFeatureBounds.height() ) / 2.0;
518  ya1 -= adj_height;
519  ya2 += adj_height;
520  }
521  new_extent = QgsRectangle( xa1, ya1, xa2, ya2 );
522 
523  if ( map->atlasMargin() > 0.0 )
524  {
525  new_extent.scale( 1 + map->atlasMargin() );
526  }
527  }
528 
529  // set the new extent (and render)
530  map->setNewAtlasFeatureExtent( new_extent );
531 }
532 
534 {
535  return mCurrentFilename;
536 }
537 
538 void QgsAtlasComposition::writeXML( QDomElement& elem, QDomDocument& doc ) const
539 {
540  QDomElement atlasElem = doc.createElement( "Atlas" );
541  atlasElem.setAttribute( "enabled", mEnabled ? "true" : "false" );
542  if ( !mEnabled )
543  {
544  return;
545  }
546 
547  if ( mCoverageLayer )
548  {
549  atlasElem.setAttribute( "coverageLayer", mCoverageLayer->id() );
550  }
551  else
552  {
553  atlasElem.setAttribute( "coverageLayer", "" );
554  }
555 
556  atlasElem.setAttribute( "hideCoverage", mHideCoverage ? "true" : "false" );
557  atlasElem.setAttribute( "singleFile", mSingleFile ? "true" : "false" );
558  atlasElem.setAttribute( "filenamePattern", mFilenamePattern );
559 
560  atlasElem.setAttribute( "sortFeatures", mSortFeatures ? "true" : "false" );
561  if ( mSortFeatures )
562  {
563  atlasElem.setAttribute( "sortKey", QString::number( mSortKeyAttributeIdx ) );
564  atlasElem.setAttribute( "sortAscending", mSortAscending ? "true" : "false" );
565  }
566  atlasElem.setAttribute( "filterFeatures", mFilterFeatures ? "true" : "false" );
567  if ( mFilterFeatures )
568  {
569  atlasElem.setAttribute( "featureFilter", mFeatureFilter );
570  }
571 
572  elem.appendChild( atlasElem );
573 }
574 
575 void QgsAtlasComposition::readXML( const QDomElement& atlasElem, const QDomDocument& )
576 {
577  mEnabled = atlasElem.attribute( "enabled", "false" ) == "true" ? true : false;
578  emit toggled( mEnabled );
579  if ( !mEnabled )
580  {
581  emit parameterChanged();
582  return;
583  }
584 
585  // look for stored layer name
586  mCoverageLayer = 0;
587  QMap<QString, QgsMapLayer*> layers = QgsMapLayerRegistry::instance()->mapLayers();
588  for ( QMap<QString, QgsMapLayer*>::const_iterator it = layers.begin(); it != layers.end(); ++it )
589  {
590  if ( it.key() == atlasElem.attribute( "coverageLayer" ) )
591  {
592  mCoverageLayer = dynamic_cast<QgsVectorLayer*>( it.value() );
593  break;
594  }
595  }
596  //look for stored composer map, to upgrade pre 2.1 projects
597  int composerMapNo = atlasElem.attribute( "composerMap", "-1" ).toInt();
599  if ( composerMapNo != -1 )
600  {
601  QList<QgsComposerMap*> maps;
602  mComposition->composerItems( maps );
603  for ( QList<QgsComposerMap*>::iterator it = maps.begin(); it != maps.end(); ++it )
604  {
605  if (( *it )->id() == composerMapNo )
606  {
607  composerMap = ( *it );
608  composerMap->setAtlasDriven( true );
609  break;
610  }
611  }
612  }
613  mHideCoverage = atlasElem.attribute( "hideCoverage", "false" ) == "true" ? true : false;
614 
615  //upgrade pre 2.1 projects
616  double margin = atlasElem.attribute( "margin", "0.0" ).toDouble();
617  if ( composerMap && margin != 0 )
618  {
619  composerMap->setAtlasMargin( margin );
620  }
621  bool fixedScale = atlasElem.attribute( "fixedScale", "false" ) == "true" ? true : false;
622  if ( composerMap && fixedScale )
623  {
624  composerMap->setAtlasFixedScale( true );
625  }
626 
627  mSingleFile = atlasElem.attribute( "singleFile", "false" ) == "true" ? true : false;
628  mFilenamePattern = atlasElem.attribute( "filenamePattern", "" );
629 
630  mSortFeatures = atlasElem.attribute( "sortFeatures", "false" ) == "true" ? true : false;
631  if ( mSortFeatures )
632  {
633  mSortKeyAttributeIdx = atlasElem.attribute( "sortKey", "0" ).toInt();
634  mSortAscending = atlasElem.attribute( "sortAscending", "true" ) == "true" ? true : false;
635  }
636  mFilterFeatures = atlasElem.attribute( "filterFeatures", "false" ) == "true" ? true : false;
637  if ( mFilterFeatures )
638  {
639  mFeatureFilter = atlasElem.attribute( "featureFilter", "" );
640  }
641 
642  emit parameterChanged();
643 }
644 
646 {
647  mHideCoverage = hide;
648 
650  {
651  //an atlas preview is enabled, so reflect changes in coverage layer visibility immediately
652  updateAtlasMaps();
653  mComposition->update();
654  }
655 
656 }
657 
658 void QgsAtlasComposition::setFilenamePattern( const QString& pattern )
659 {
660  mFilenamePattern = pattern;
662 }
663 
665 {
666  const QgsFields& fields = mCoverageLayer->pendingFields();
667 
668  if ( !mSingleFile && mFilenamePattern.size() > 0 )
669  {
670  mFilenameExpr = std::auto_ptr<QgsExpression>( new QgsExpression( mFilenamePattern ) );
671  // expression used to evaluate each filename
672  // test for evaluation errors
673  if ( mFilenameExpr->hasParserError() )
674  {
675  throw std::runtime_error( tr( "Filename parsing error: %1" ).arg( mFilenameExpr->parserErrorString() ).toLocal8Bit().data() );
676  }
677 
678  // prepare the filename expression
679  mFilenameExpr->prepare( fields );
680  }
681 
682  //if atlas preview is currently enabled, regenerate filename for current feature
684  {
686  }
687 
688 }
689 
691 {
692  //generate filename for current atlas feature
693  if ( !mSingleFile && mFilenamePattern.size() > 0 )
694  {
695  QVariant filenameRes = mFilenameExpr->evaluate( &mCurrentFeature, mCoverageLayer->pendingFields() );
696  if ( mFilenameExpr->hasEvalError() )
697  {
698  throw std::runtime_error( tr( "Filename eval error: %1" ).arg( mFilenameExpr->evalErrorString() ).toLocal8Bit().data() );
699  }
700 
701  mCurrentFilename = filenameRes.toString();
702  }
703 }
704 
705