87 changes: 80 additions & 7 deletions src/core/composer/qgsatlascomposition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ QgsAtlasComposition::QgsAtlasComposition( QgsComposition* composition ) :
mEnabled( false ),
mComposerMap( 0 ),
mHideCoverage( false ), mFixedScale( false ), mMargin( 0.10 ), mFilenamePattern( "'output_'||$feature" ),
mCoverageLayer( 0 ), mSingleFile( false )
mCoverageLayer( 0 ), mSingleFile( false ),
mSortFeatures( false ), mSortAscending( true ), mFeatureFilter( "" )
{

// declare special columns with a default value
Expand All @@ -52,6 +53,33 @@ void QgsAtlasComposition::setCoverageLayer( QgsVectorLayer* layer )
QgsExpression::setSpecialColumn( "$numfeatures", QVariant(( int )mFeatureIds.size() ) );
}

//
// Private class only used for the sorting of features
class FieldSorter
{
public:
FieldSorter( QgsAtlasComposition::SorterKeys& keys, bool ascending = true ) : mKeys( keys ), mAscending( ascending ) {}

bool operator()( const QgsFeatureId& id1, const QgsFeatureId& id2 )
{
bool result;
if ( mKeys[ id1 ].type() == QVariant::Int ) {
result = mKeys[ id1 ].toInt() < mKeys[ id2 ].toInt();
}
else if ( mKeys[ id1 ].type() == QVariant::Double ) {
result = mKeys[ id1 ].toDouble() < mKeys[ id2 ].toDouble();
}
else if ( mKeys[ id1 ].type() == QVariant::String ) {
result = (QString::localeAwareCompare(mKeys[ id1 ].toString(), mKeys[ id2 ].toString()) < 0);
}

return mAscending ? result : !result;
}
private:
QgsAtlasComposition::SorterKeys& mKeys;
bool mAscending;
};

void QgsAtlasComposition::beginRender()
{
if ( !mComposerMap || !mCoverageLayer )
Expand All @@ -67,14 +95,14 @@ void QgsAtlasComposition::beginRender()

const QgsFields& fields = mCoverageLayer->pendingFields();

if ( mFilenamePattern.size() > 0 )
if ( !mSingleFile && mFilenamePattern.size() > 0 )
{
mFilenameExpr = std::auto_ptr<QgsExpression>( new QgsExpression( mFilenamePattern ) );
// expression used to evaluate each filename
// test for evaluation errors
if ( mFilenameExpr->hasParserError() )
{
throw std::runtime_error( "Filename parsing error: " + mFilenameExpr->parserErrorString().toStdString() );
throw std::runtime_error( tr("Filename parsing error: %1").arg(mFilenameExpr->parserErrorString()).toLocal8Bit().data() );
}

// prepare the filename expression
Expand All @@ -84,13 +112,45 @@ void QgsAtlasComposition::beginRender()
// select all features with all attributes
QgsFeatureIterator fit = mCoverageLayer->getFeatures();

std::auto_ptr<QgsExpression> filterExpression;
if ( mFeatureFilter.size() > 0 ) {
filterExpression = std::auto_ptr<QgsExpression>(new QgsExpression( mFeatureFilter ));
if ( filterExpression->hasParserError() )
{
throw std::runtime_error( tr("Feature filter parser error: %1").arg( filterExpression->parserErrorString() ).toLocal8Bit().data() );
}
}

// We cannot use nextFeature() directly since the feature pointer is rewinded by the rendering process
// We thus store the feature ids for future extraction
QgsFeature feat;
mFeatureIds.clear();
mFeatureKeys.clear();
while ( fit.nextFeature( feat ) )
{
mFeatureIds.push_back( feat.id() );
if ( mFeatureFilter.size() > 0 ) {
QVariant result = filterExpression->evaluate( &feat, mCoverageLayer->pendingFields() );
if ( filterExpression->hasEvalError() )
{
throw std::runtime_error( tr("Feature filter eval error: %1").arg( filterExpression->evalErrorString()).toLocal8Bit().data() );
}

// skip this feature if the filter evaluation if false
if ( !result.toBool() ) {
continue;
}
}
mFeatureIds.push_back( feat.id() );

if ( mSortFeatures ) {
mFeatureKeys.insert( std::make_pair( feat.id(), feat.attributes()[ mSortKeyAttributeIdx ] ) );
}
}

// sort features, if asked for
if ( mSortFeatures ) {
FieldSorter sorter( mFeatureKeys, mSortAscending );
std::sort( mFeatureIds.begin(), mFeatureIds.end(), sorter );
}

mOrigExtent = mComposerMap->extent();
Expand Down Expand Up @@ -155,13 +215,13 @@ void QgsAtlasComposition::prepareForFeature( size_t featureI )
// retrieve the next feature, based on its id
mCoverageLayer->getFeatures( QgsFeatureRequest().setFilterFid( mFeatureIds[ featureI ] ) ).nextFeature( mCurrentFeature );

if ( mFilenamePattern.size() > 0 )
if ( !mSingleFile && mFilenamePattern.size() > 0 )
{
QgsExpression::setSpecialColumn( "$feature", QVariant(( int )featureI + 1 ) );
QVariant filenameRes = mFilenameExpr->evaluate( &mCurrentFeature );
if ( mFilenameExpr->hasEvalError() )
{
throw std::runtime_error( "Filename eval error: " + mFilenameExpr->evalErrorString().toStdString() );
throw std::runtime_error( tr("Filename eval error: %1").arg( mFilenameExpr->evalErrorString() ).toLocal8Bit().data() );
}

mCurrentFilename = filenameRes.toString();
Expand Down Expand Up @@ -284,6 +344,13 @@ void QgsAtlasComposition::writeXML( QDomElement& elem, QDomDocument& doc ) const
atlasElem.setAttribute( "margin", QString::number( mMargin ) );
atlasElem.setAttribute( "filenamePattern", mFilenamePattern );

atlasElem.setAttribute( "sortFeatures", mSortFeatures ? "true" : "false" );
if ( mSortFeatures ) {
atlasElem.setAttribute( "sortKey", QString::number(mSortKeyAttributeIdx) );
atlasElem.setAttribute( "sortAscending", mSortAscending ? "true" : "false" );
}
atlasElem.setAttribute( "featureFilter", mFeatureFilter );

elem.appendChild( atlasElem );
}

Expand Down Expand Up @@ -324,6 +391,12 @@ void QgsAtlasComposition::readXML( const QDomElement& atlasElem, const QDomDocum
mSingleFile = atlasElem.attribute( "singleFile", "false" ) == "true" ? true : false;
mFilenamePattern = atlasElem.attribute( "filenamePattern", "" );

std::cout << "emit parameter changed this = " << this << std::endl;
mSortFeatures = atlasElem.attribute( "sortFeatures", "false" ) == "true" ? true : false;
if ( mSortFeatures ) {
mSortKeyAttributeIdx = atlasElem.attribute( "sortKey", "0" ).toInt();
mSortAscending = atlasElem.attribute( "sortAscending", "true" ) == "true" ? true : false;
}
mFeatureFilter = atlasElem.attribute( "featureFilter", "" );

emit parameterChanged();
}
29 changes: 29 additions & 0 deletions src/core/composer/qgsatlascomposition.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ class CORE_EXPORT QgsAtlasComposition : public QObject
bool singleFile() const { return mSingleFile; }
void setSingleFile( bool single ) { mSingleFile = single; }

bool sortFeatures() const { return mSortFeatures; }
void setSortFeatures( bool doSort ) { mSortFeatures = doSort; }

bool sortAscending() const { return mSortAscending; }
void setSortAscending( bool ascending ) { mSortAscending = ascending; }

QString featureFilter() const { return mFeatureFilter; }
void setFeatureFilter( const QString& expression ) { mFeatureFilter = expression; }

size_t sortKeyAttributeIndex() const { return mSortKeyAttributeIdx; }
void setSortKeyAttributeIndex( size_t idx ) { mSortKeyAttributeIdx = idx; }

/** Begins the rendering. */
void beginRender();
/** Ends the rendering. Restores original extent */
Expand Down Expand Up @@ -103,7 +115,24 @@ class CORE_EXPORT QgsAtlasComposition : public QObject

QgsCoordinateTransform mTransform;
QString mCurrentFilename;
// feature ordering
bool mSortFeatures;
// sort direction
bool mSortAscending;
public:
typedef std::map< QgsFeatureId, QVariant > SorterKeys;
private:
// value of field that is used for ordering of features
SorterKeys mFeatureKeys;
// key (attribute index) used for ordering
size_t mSortKeyAttributeIdx;

// feature expression filter (or empty)
QString mFeatureFilter;

// id of each iterated feature (after filtering and sorting)
std::vector<QgsFeatureId> mFeatureIds;

QgsFeature mCurrentFeature;
QgsRectangle mOrigExtent;
bool mRestoreLayer;
Expand Down
67 changes: 59 additions & 8 deletions src/ui/qgsatlascompositionwidgetbase.ui
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,14 @@
<number>0</number>
</property>
<item>
<layout class="QGridLayout" name="gridLayout_7" rowstretch="0,0,0,0,0,0,0,0,0" columnstretch="0,0,0">
<layout class="QGridLayout" name="gridLayout_7" rowstretch="0,0,0,0,0,0,0,0,0,0" columnstretch="0,0,0">
<item row="4" column="0">
<widget class="QLabel" name="mAtlasFeatureFilterLabel">
<property name="text">
<string>Feature filter</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="mAtlasHideCoverageCheckBox">
<property name="toolTip">
Expand All @@ -86,21 +93,21 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Margin around coverage</string>
</property>
</widget>
</item>
<item row="7" column="0">
<item row="8" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Output filename expression</string>
</property>
</widget>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QSpinBox" name="mAtlasMarginSpinBox">
<property name="suffix">
<string> %</string>
Expand All @@ -113,14 +120,14 @@
</property>
</widget>
</item>
<item row="7" column="2">
<item row="8" column="2">
<widget class="QToolButton" name="mAtlasFilenameExpressionButton">
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="7" column="1">
<item row="8" column="1">
<widget class="QLineEdit" name="mAtlasFilenamePatternEdit"/>
</item>
<item row="1" column="0">
Expand All @@ -137,14 +144,14 @@
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="mAtlasFixedScaleCheckBox">
<property name="text">
<string>Fixed scale</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="mAtlasSingleFileCheckBox">
<property name="text">
<string>Single file export when possible</string>
Expand All @@ -161,6 +168,50 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="mAtlasSortFeatureKeyComboBox">
<property name="toolTip">
<string>Sort key</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QLineEdit" name="mAtlasFeatureFilterEdit">
<property name="toolTip">
<string>Feature filter</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="mAtlasSortFeatureCheckBox">
<property name="text">
<string>Sort features</string>
</property>
</widget>
</item>
<item row="4" column="2">
<widget class="QToolButton" name="mAtlasFeatureFilterButton">
<property name="toolTip">
<string>Open expression builder</string>
</property>
<property name="text">
<string>...</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QToolButton" name="mAtlasSortFeatureDirectionButton">
<property name="toolTip">
<string>Sort direction</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="arrowType">
<enum>Qt::UpArrow</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
Expand Down
56 changes: 56 additions & 0 deletions tests/src/core/testqgsatlascomposition.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class TestQgsAtlasComposition: public QObject
void fixedscale_render();
// test rendering with a hidden coverage
void hiding_render();
// test rendering with feature sorting
void sorting_render();
// test rendering with feature filtering
void filtering_render();
private:
QgsComposition* mComposition;
QgsComposerLabel* mLabel1;
Expand Down Expand Up @@ -223,5 +227,57 @@ void TestQgsAtlasComposition::hiding_render()
mAtlas->endRender();
}

void TestQgsAtlasComposition::sorting_render()
{
mAtlasMap->setNewExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) );
mAtlas->setFixedScale( true );
mAtlas->setHideCoverage( false );

mAtlas->setSortFeatures( true );
mAtlas->setSortKeyAttributeIndex( 4 ); // departement name
mAtlas->setSortAscending( false );

mAtlas->beginRender();

for ( size_t fit = 0; fit < 2; ++fit )
{
mAtlas->prepareForFeature( fit );
mLabel1->adjustSizeToText();

QgsCompositionChecker checker( "Atlas sorting test", mComposition,
QString( TEST_DATA_DIR ) + QDir::separator() + "control_images" + QDir::separator() +
"expected_composermapatlas" + QDir::separator() +
QString( "sorting_%1.png" ).arg(( int )fit ) );
QVERIFY( checker.testComposition( 0 ) );
}
mAtlas->endRender();
}

void TestQgsAtlasComposition::filtering_render()
{
mAtlasMap->setNewExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) );
mAtlas->setFixedScale( true );
mAtlas->setHideCoverage( false );

mAtlas->setSortFeatures( false );

mAtlas->setFeatureFilter( "substr(NAME_1,1,1)='P'" ); // select only 'Pays de la Loire'

mAtlas->beginRender();

for ( size_t fit = 0; fit < 1; ++fit )
{
mAtlas->prepareForFeature( fit );
mLabel1->adjustSizeToText();

QgsCompositionChecker checker( "Atlas filtering test", mComposition,
QString( TEST_DATA_DIR ) + QDir::separator() + "control_images" + QDir::separator() +
"expected_composermapatlas" + QDir::separator() +
QString( "filtering_%1.png" ).arg(( int )fit ) );
QVERIFY( checker.testComposition( 0 ) );
}
mAtlas->endRender();
}

QTEST_MAIN( TestQgsAtlasComposition )
#include "moc_testqgsatlascomposition.cxx"
48 changes: 48 additions & 0 deletions tests/src/python/test_qgsatlascomposition.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,54 @@ def hidden_render_test( self ):
assert res[0] == True
self.mAtlas.endRender()

def sorting_render_test( self ):
self.mAtlasMap.setNewExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) );
self.mAtlas.setFixedScale( True )
self.mAtlas.setHideCoverage( False )

self.mAtlas.setSortFeatures( True )
self.mAtlas.setSortKeyAttributeIndex( 4 ) # departement name
self.mAtlas.setSortAscending( False )

self.mAtlas.beginRender()

for i in range(0, 2):
self.mAtlas.prepareForFeature( i )
self.mLabel1.adjustSizeToText()

checker = QgsCompositionChecker()
res = checker.testComposition( "Atlas sorting test", self.mComposition, \
QString( self.TEST_DATA_DIR ) + QDir.separator() + \
"control_images" + QDir.separator() + \
"expected_composermapatlas" + QDir.separator() + \
QString( "sorting_%1.png" ).arg( i ) )
assert res[0] == True
self.mAtlas.endRender()

def filtering_render_test( self ):
self.mAtlasMap.setNewExtent( QgsRectangle( 209838.166, 6528781.020, 610491.166, 6920530.620 ) );
self.mAtlas.setFixedScale( True )
self.mAtlas.setHideCoverage( False )

self.mAtlas.setSortFeatures( False )

self.mAtlas.setFeatureFilter( "substr(NAME_1,1,1)='P'" ) # select only 'Pays de la loire'

self.mAtlas.beginRender()

for i in range(0, 1):
self.mAtlas.prepareForFeature( i )
self.mLabel1.adjustSizeToText()

checker = QgsCompositionChecker()
res = checker.testComposition( "Atlas filtering test", self.mComposition, \
QString( self.TEST_DATA_DIR ) + QDir.separator() + \
"control_images" + QDir.separator() + \
"expected_composermapatlas" + QDir.separator() + \
QString( "filtering_%1.png" ).arg( i ) )
assert res[0] == True
self.mAtlas.endRender()

if __name__ == '__main__':
unittest.main()

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.