Quantum GIS API Documentation  master-693a1fe
src/core/raster/qgsrasterinterface.cpp
Go to the documentation of this file.
00001 /***************************************************************************
00002     qgsrasterface.cpp - Internal raster processing modules interface
00003      --------------------------------------
00004     Date                 : Jun 21, 2012
00005     Copyright            : (C) 2012 by Radim Blazek
00006     email                : radim dot blazek at gmail dot com
00007  ***************************************************************************/
00008 
00009 /***************************************************************************
00010  *                                                                         *
00011  *   This program is free software; you can redistribute it and/or modify  *
00012  *   it under the terms of the GNU General Public License as published by  *
00013  *   the Free Software Foundation; either version 2 of the License, or     *
00014  *   (at your option) any later version.                                   *
00015  *                                                                         *
00016  ***************************************************************************/
00017 
00018 #include <limits>
00019 #include <typeinfo>
00020 
00021 #include <QByteArray>
00022 #include <QTime>
00023 
00024 #include <qmath.h>
00025 
00026 #include "qgslogger.h"
00027 #include "qgsrasterbandstats.h"
00028 #include "qgsrasterhistogram.h"
00029 #include "qgsrasterinterface.h"
00030 #include "qgsrectangle.h"
00031 
00032 QgsRasterInterface::QgsRasterInterface( QgsRasterInterface * input )
00033     : mInput( input )
00034     , mOn( true )
00035 {
00036 }
00037 
00038 QgsRasterInterface::~QgsRasterInterface()
00039 {
00040 }
00041 
00042 void QgsRasterInterface::initStatistics( QgsRasterBandStats &theStatistics,
00043     int theBandNo,
00044     int theStats,
00045     const QgsRectangle & theExtent,
00046     int theSampleSize )
00047 {
00048   QgsDebugMsg( QString( "theBandNo = %1 theSampleSize = %2" ).arg( theBandNo ).arg( theSampleSize ) );
00049 
00050   theStatistics.bandNumber = theBandNo;
00051   theStatistics.statsGathered = theStats;
00052 
00053   QgsRectangle myExtent;
00054   if ( theExtent.isEmpty() )
00055   {
00056     myExtent = extent();
00057   }
00058   else
00059   {
00060     myExtent = extent().intersect( &theExtent );
00061   }
00062   theStatistics.extent = myExtent;
00063 
00064   if ( theSampleSize > 0 )
00065   {
00066     // Calc resolution from theSampleSize
00067     double xRes, yRes;
00068     xRes = yRes = sqrt(( myExtent.width() * myExtent.height() ) / theSampleSize );
00069 
00070     // But limit by physical resolution
00071     if ( capabilities() & Size )
00072     {
00073       double srcXRes = extent().width() / xSize();
00074       double srcYRes = extent().height() / ySize();
00075       if ( xRes < srcXRes ) xRes = srcXRes;
00076       if ( yRes < srcYRes ) yRes = srcYRes;
00077     }
00078     QgsDebugMsg( QString( "xRes = %1 yRes = %2" ).arg( xRes ).arg( yRes ) );
00079 
00080     theStatistics.width = static_cast <int>( myExtent.width() / xRes );
00081     theStatistics.height = static_cast <int>( myExtent.height() / yRes );
00082   }
00083   else
00084   {
00085     if ( capabilities() & Size )
00086     {
00087       theStatistics.width = xSize();
00088       theStatistics.height = ySize();
00089     }
00090     else
00091     {
00092       theStatistics.width = 1000;
00093       theStatistics.height = 1000;
00094     }
00095   }
00096   QgsDebugMsg( QString( "theStatistics.width = %1 theStatistics.height = %2" ).arg( theStatistics.width ).arg( theStatistics.height ) );
00097 }
00098 
00099 bool QgsRasterInterface::hasStatistics( int theBandNo,
00100                                         int theStats,
00101                                         const QgsRectangle & theExtent,
00102                                         int theSampleSize )
00103 {
00104   QgsDebugMsg( QString( "theBandNo = %1 theStats = %2 theSampleSize = %3" ).arg( theBandNo ).arg( theStats ).arg( theSampleSize ) );
00105   if ( mStatistics.size() == 0 ) return false;
00106 
00107   QgsRasterBandStats myRasterBandStats;
00108   initStatistics( myRasterBandStats, theBandNo, theStats, theExtent, theSampleSize );
00109 
00110   foreach ( QgsRasterBandStats stats, mStatistics )
00111   {
00112     if ( stats.contains( myRasterBandStats ) )
00113     {
00114       QgsDebugMsg( "Has cached statistics." );
00115       return true;
00116     }
00117   }
00118   return false;
00119 }
00120 
00121 QgsRasterBandStats QgsRasterInterface::bandStatistics( int theBandNo,
00122     int theStats,
00123     const QgsRectangle & theExtent,
00124     int theSampleSize )
00125 {
00126   QgsDebugMsg( QString( "theBandNo = %1 theStats = %2 theSampleSize = %3" ).arg( theBandNo ).arg( theStats ).arg( theSampleSize ) );
00127 
00128   // TODO: null values set on raster layer!!!
00129 
00130   QgsRasterBandStats myRasterBandStats;
00131   initStatistics( myRasterBandStats, theBandNo, theStats, theExtent, theSampleSize );
00132 
00133   foreach ( QgsRasterBandStats stats, mStatistics )
00134   {
00135     if ( stats.contains( myRasterBandStats ) )
00136     {
00137       QgsDebugMsg( "Using cached statistics." );
00138       return stats;
00139     }
00140   }
00141 
00142   QgsRectangle myExtent = myRasterBandStats.extent;
00143   int myWidth = myRasterBandStats.width;
00144   int myHeight = myRasterBandStats.height;
00145 
00146   //int myDataType = dataType( theBandNo );
00147 
00148   int myXBlockSize = xBlockSize();
00149   int myYBlockSize = yBlockSize();
00150   if ( myXBlockSize == 0 ) // should not happen, but happens
00151   {
00152     myXBlockSize = 500;
00153   }
00154   if ( myYBlockSize == 0 ) // should not happen, but happens
00155   {
00156     myYBlockSize = 500;
00157   }
00158 
00159   int myNXBlocks = ( myWidth + myXBlockSize - 1 ) / myXBlockSize;
00160   int myNYBlocks = ( myHeight + myYBlockSize - 1 ) / myYBlockSize;
00161 
00162   double myXRes = myExtent.width() / myWidth;
00163   double myYRes = myExtent.height() / myHeight;
00164   // TODO: progress signals
00165 
00166   // used by single pass stdev
00167   double myMean = 0;
00168   double mySumOfSquares = 0;
00169 
00170   bool myFirstIterationFlag = true;
00171   for ( int myYBlock = 0; myYBlock < myNYBlocks; myYBlock++ )
00172   {
00173     for ( int myXBlock = 0; myXBlock < myNXBlocks; myXBlock++ )
00174     {
00175       QgsDebugMsg( QString( "myYBlock = %1 myXBlock = %2" ).arg( myYBlock ).arg( myXBlock ) );
00176       int myBlockWidth = qMin( myXBlockSize, myWidth - myXBlock * myXBlockSize );
00177       int myBlockHeight = qMin( myYBlockSize, myHeight - myYBlock * myYBlockSize );
00178 
00179       double xmin = myExtent.xMinimum() + myXBlock * myXBlockSize * myXRes;
00180       double xmax = xmin + myBlockWidth * myXRes;
00181       double ymin = myExtent.yMaximum() - myYBlock * myYBlockSize * myYRes;
00182       double ymax = ymin - myBlockHeight * myYRes;
00183 
00184       QgsRectangle myPartExtent( xmin, ymin, xmax, ymax );
00185 
00186       QgsRasterBlock* blk = block( theBandNo, myPartExtent, myBlockWidth, myBlockHeight );
00187 
00188       // Collect the histogram counts.
00189       for ( size_t i = 0; i < (( size_t ) myBlockHeight ) * myBlockWidth; i++ )
00190       {
00191         if ( blk->isNoData( i ) ) continue; // NULL
00192 
00193         double myValue = blk->value( i );
00194 
00195         myRasterBandStats.sum += myValue;
00196         myRasterBandStats.elementCount++;
00197 
00198         if ( myFirstIterationFlag )
00199         {
00200           myFirstIterationFlag = false;
00201           myRasterBandStats.minimumValue = myValue;
00202           myRasterBandStats.maximumValue = myValue;
00203         }
00204         else
00205         {
00206           if ( myValue < myRasterBandStats.minimumValue )
00207           {
00208             myRasterBandStats.minimumValue = myValue;
00209           }
00210           if ( myValue > myRasterBandStats.maximumValue )
00211           {
00212             myRasterBandStats.maximumValue = myValue;
00213           }
00214         }
00215 
00216         // Single pass stdev
00217         double myDelta = myValue - myMean;
00218         myMean += myDelta / myRasterBandStats.elementCount;
00219         mySumOfSquares += myDelta * ( myValue - myMean );
00220       }
00221       delete blk;
00222     }
00223   }
00224 
00225   myRasterBandStats.range = myRasterBandStats.maximumValue - myRasterBandStats.minimumValue;
00226   myRasterBandStats.mean = myRasterBandStats.sum / myRasterBandStats.elementCount;
00227 
00228   myRasterBandStats.sumOfSquares = mySumOfSquares; // OK with single pass?
00229 
00230   // stdDev may differ  from GDAL stats, because GDAL is using naive single pass
00231   // algorithm which is more error prone (because of rounding errors)
00232   // Divide result by sample size - 1 and get square root to get stdev
00233   myRasterBandStats.stdDev = sqrt( mySumOfSquares / ( myRasterBandStats.elementCount - 1 ) );
00234 
00235   QgsDebugMsg( "************ STATS **************" );
00236   QgsDebugMsg( QString( "MIN %1" ).arg( myRasterBandStats.minimumValue ) );
00237   QgsDebugMsg( QString( "MAX %1" ).arg( myRasterBandStats.maximumValue ) );
00238   QgsDebugMsg( QString( "RANGE %1" ).arg( myRasterBandStats.range ) );
00239   QgsDebugMsg( QString( "MEAN %1" ).arg( myRasterBandStats.mean ) );
00240   QgsDebugMsg( QString( "STDDEV %1" ).arg( myRasterBandStats.stdDev ) );
00241 
00242   myRasterBandStats.statsGathered = QgsRasterBandStats::All;
00243   mStatistics.append( myRasterBandStats );
00244 
00245   return myRasterBandStats;
00246 }
00247 
00248 void QgsRasterInterface::initHistogram( QgsRasterHistogram &theHistogram,
00249                                         int theBandNo,
00250                                         int theBinCount,
00251                                         double theMinimum, double theMaximum,
00252                                         const QgsRectangle & theExtent,
00253                                         int theSampleSize,
00254                                         bool theIncludeOutOfRange )
00255 {
00256   theHistogram.bandNumber = theBandNo;
00257   theHistogram.minimum = theMinimum;
00258   theHistogram.maximum = theMaximum;
00259   theHistogram.includeOutOfRange = theIncludeOutOfRange;
00260 
00261   int mySrcDataType = srcDataType( theBandNo );
00262 
00263   if ( qIsNaN( theHistogram.minimum ) )
00264   {
00265     // TODO: this was OK when stats/histogram were calced in provider,
00266     // but what TODO in other interfaces? Check for mInput for now.
00267     if ( !mInput && mySrcDataType == QGis::Byte )
00268     {
00269       theHistogram.minimum = 0; // see histogram() for shift for rounding
00270     }
00271     else
00272     {
00273       // We need statistics -> avoid histogramDefaults in hasHistogram if possible
00274       // TODO: use approximated statistics if aproximated histogram is requested
00275       // (theSampleSize > 0)
00276       QgsRasterBandStats stats =  bandStatistics( theBandNo, QgsRasterBandStats::Min, theExtent, theSampleSize );
00277       theHistogram.minimum = stats.minimumValue;
00278     }
00279   }
00280   if ( qIsNaN( theHistogram.maximum ) )
00281   {
00282     if ( !mInput && mySrcDataType == QGis::Byte )
00283     {
00284       theHistogram.maximum = 255;
00285     }
00286     else
00287     {
00288       QgsRasterBandStats stats =  bandStatistics( theBandNo, QgsRasterBandStats::Max, theExtent, theSampleSize );
00289       theHistogram.maximum = stats.maximumValue;
00290     }
00291   }
00292 
00293   QgsRectangle myExtent;
00294   if ( theExtent.isEmpty() )
00295   {
00296     myExtent = extent();
00297   }
00298   else
00299   {
00300     myExtent = extent().intersect( &theExtent );
00301   }
00302   theHistogram.extent = myExtent;
00303 
00304   if ( theSampleSize > 0 )
00305   {
00306     // Calc resolution from theSampleSize
00307     double xRes, yRes;
00308     xRes = yRes = sqrt(( myExtent.width() * myExtent.height() ) / theSampleSize );
00309 
00310     // But limit by physical resolution
00311     if ( capabilities() & Size )
00312     {
00313       double srcXRes = extent().width() / xSize();
00314       double srcYRes = extent().height() / ySize();
00315       if ( xRes < srcXRes ) xRes = srcXRes;
00316       if ( yRes < srcYRes ) yRes = srcYRes;
00317     }
00318     QgsDebugMsg( QString( "xRes = %1 yRes = %2" ).arg( xRes ).arg( yRes ) );
00319 
00320     theHistogram.width = static_cast <int>( myExtent.width() / xRes );
00321     theHistogram.height = static_cast <int>( myExtent.height() / yRes );
00322   }
00323   else
00324   {
00325     if ( capabilities() & Size )
00326     {
00327       theHistogram.width = xSize();
00328       theHistogram.height = ySize();
00329     }
00330     else
00331     {
00332       theHistogram.width = 1000;
00333       theHistogram.height = 1000;
00334     }
00335   }
00336   QgsDebugMsg( QString( "theHistogram.width = %1 theHistogram.height = %2" ).arg( theHistogram.width ).arg( theHistogram.height ) );
00337 
00338   int myBinCount = theBinCount;
00339   if ( myBinCount == 0 )
00340   {
00341     // TODO: this was OK when stats/histogram were calced in provider,
00342     // but what TODO in other interfaces? Check for mInput for now.
00343     if ( !mInput && mySrcDataType == QGis::Byte )
00344     {
00345       myBinCount = 256; // Cannot store more values in byte
00346     }
00347     else
00348     {
00349       // There is no best default value, to display something reasonable in histogram chart, binCount should be small, OTOH, to get precise data for cumulative cut, the number should be big. Because it is easier to define fixed lower value for the chart, we calc optimum binCount for higher resolution (to avoid calculating that where histogram() is used. In any any case, it does not make sense to use more than width*height;
00350       myBinCount = theHistogram.width * theHistogram.height;
00351       if ( myBinCount > 1000 )  myBinCount = 1000;
00352     }
00353   }
00354   theHistogram.binCount = myBinCount;
00355   QgsDebugMsg( QString( "theHistogram.binCount = %1" ).arg( theHistogram.binCount ) );
00356 }
00357 
00358 
00359 bool QgsRasterInterface::hasHistogram( int theBandNo,
00360                                        int theBinCount,
00361                                        double theMinimum, double theMaximum,
00362                                        const QgsRectangle & theExtent,
00363                                        int theSampleSize,
00364                                        bool theIncludeOutOfRange )
00365 {
00366   QgsDebugMsg( QString( "theBandNo = %1 theBinCount = %2 theMinimum = %3 theMaximum = %4 theSampleSize = %5" ).arg( theBandNo ).arg( theBinCount ).arg( theMinimum ).arg( theMaximum ).arg( theSampleSize ) );
00367   // histogramDefaults() needs statistics if theMinimum or theMaximum is NaN ->
00368   // do other checks which don't need statistics before histogramDefaults()
00369   if ( mHistograms.size() == 0 ) return false;
00370 
00371   QgsRasterHistogram myHistogram;
00372   initHistogram( myHistogram, theBandNo, theBinCount, theMinimum, theMaximum, theExtent, theSampleSize, theIncludeOutOfRange );
00373 
00374   foreach ( QgsRasterHistogram histogram, mHistograms )
00375   {
00376     if ( histogram == myHistogram )
00377     {
00378       QgsDebugMsg( "Has cached histogram." );
00379       return true;
00380     }
00381   }
00382   return false;
00383 }
00384 
00385 QgsRasterHistogram QgsRasterInterface::histogram( int theBandNo,
00386     int theBinCount,
00387     double theMinimum, double theMaximum,
00388     const QgsRectangle & theExtent,
00389     int theSampleSize,
00390     bool theIncludeOutOfRange )
00391 {
00392   QgsDebugMsg( QString( "theBandNo = %1 theBinCount = %2 theMinimum = %3 theMaximum = %4 theSampleSize = %5" ).arg( theBandNo ).arg( theBinCount ).arg( theMinimum ).arg( theMaximum ).arg( theSampleSize ) );
00393 
00394   QgsRasterHistogram myHistogram;
00395   initHistogram( myHistogram, theBandNo, theBinCount, theMinimum, theMaximum, theExtent, theSampleSize, theIncludeOutOfRange );
00396 
00397   // Find cached
00398   foreach ( QgsRasterHistogram histogram, mHistograms )
00399   {
00400     if ( histogram == myHistogram )
00401     {
00402       QgsDebugMsg( "Using cached histogram." );
00403       return histogram;
00404     }
00405   }
00406 
00407   int myBinCount = myHistogram.binCount;
00408   int myWidth = myHistogram.width;
00409   int myHeight = myHistogram.height;
00410   QgsRectangle myExtent = myHistogram.extent;
00411   myHistogram.histogramVector.resize( myBinCount );
00412 
00413   int myXBlockSize = xBlockSize();
00414   int myYBlockSize = yBlockSize();
00415   if ( myXBlockSize == 0 ) // should not happen, but happens
00416   {
00417     myXBlockSize = 500;
00418   }
00419   if ( myYBlockSize == 0 ) // should not happen, but happens
00420   {
00421     myYBlockSize = 500;
00422   }
00423 
00424   int myNXBlocks = ( myWidth + myXBlockSize - 1 ) / myXBlockSize;
00425   int myNYBlocks = ( myHeight + myYBlockSize - 1 ) / myYBlockSize;
00426 
00427   double myXRes = myExtent.width() / myWidth;
00428   double myYRes = myExtent.height() / myHeight;
00429 
00430   double myMinimum = myHistogram.minimum;
00431   double myMaximum = myHistogram.maximum;
00432 
00433   // To avoid rounding errors
00434   // TODO: check this
00435   double myerval = ( myMaximum - myMinimum ) / myHistogram.binCount;
00436   myMinimum -= 0.1 * myerval;
00437   myMaximum += 0.1 * myerval;
00438 
00439   QgsDebugMsg( QString( "binCount = %1 myMinimum = %2 myMaximum = %3" ).arg( myHistogram.binCount ).arg( myMinimum ).arg( myMaximum ) );
00440 
00441   double myBinSize = ( myMaximum - myMinimum ) / myBinCount;
00442 
00443   // TODO: progress signals
00444   for ( int myYBlock = 0; myYBlock < myNYBlocks; myYBlock++ )
00445   {
00446     for ( int myXBlock = 0; myXBlock < myNXBlocks; myXBlock++ )
00447     {
00448       int myBlockWidth = qMin( myXBlockSize, myWidth - myXBlock * myXBlockSize );
00449       int myBlockHeight = qMin( myYBlockSize, myHeight - myYBlock * myYBlockSize );
00450 
00451       double xmin = myExtent.xMinimum() + myXBlock * myXBlockSize * myXRes;
00452       double xmax = xmin + myBlockWidth * myXRes;
00453       double ymin = myExtent.yMaximum() - myYBlock * myYBlockSize * myYRes;
00454       double ymax = ymin - myBlockHeight * myYRes;
00455 
00456       QgsRectangle myPartExtent( xmin, ymin, xmax, ymax );
00457 
00458       QgsRasterBlock* blk = block( theBandNo, myPartExtent, myBlockWidth, myBlockHeight );
00459 
00460       // Collect the histogram counts.
00461       for ( size_t i = 0; i < (( size_t ) myBlockHeight ) * myBlockWidth; i++ )
00462       {
00463         if ( blk->isNoData( i ) )
00464         {
00465           continue; // NULL
00466         }
00467         double myValue = blk->value( i );
00468 
00469         int myBinIndex = static_cast <int>( qFloor(( myValue - myMinimum ) /  myBinSize ) ) ;
00470 
00471         if (( myBinIndex < 0 || myBinIndex > ( myBinCount - 1 ) ) && !theIncludeOutOfRange )
00472         {
00473           continue;
00474         }
00475         if ( myBinIndex < 0 ) myBinIndex = 0;
00476         if ( myBinIndex > ( myBinCount - 1 ) ) myBinIndex = myBinCount - 1;
00477 
00478         myHistogram.histogramVector[myBinIndex] += 1;
00479         myHistogram.nonNullCount++;
00480       }
00481       delete blk;
00482     }
00483   }
00484 
00485   myHistogram.valid = true;
00486   mHistograms.append( myHistogram );
00487 
00488 #ifdef QGISDEBUG
00489   QString hist;
00490   for ( int i = 0; i < qMin( myHistogram.histogramVector.size(), 500 ); i++ )
00491   {
00492     hist += QString::number( myHistogram.histogramVector.value( i ) ) + " ";
00493   }
00494   QgsDebugMsg( "Histogram (max first 500 bins): " + hist );
00495 #endif
00496 
00497   return myHistogram;
00498 }
00499 
00500 void QgsRasterInterface::cumulativeCut( int theBandNo,
00501                                         double theLowerCount, double theUpperCount,
00502                                         double &theLowerValue, double &theUpperValue,
00503                                         const QgsRectangle & theExtent,
00504                                         int theSampleSize )
00505 {
00506   QgsDebugMsg( QString( "theBandNo = %1 theLowerCount = %2 theUpperCount = %3 theSampleSize = %4" ).arg( theBandNo ).arg( theLowerCount ).arg( theUpperCount ).arg( theSampleSize ) );
00507 
00508   QgsRasterHistogram myHistogram = histogram( theBandNo, 0, std::numeric_limits<double>::quiet_NaN(), std::numeric_limits<double>::quiet_NaN(), theExtent, theSampleSize );
00509 
00510   // Init to NaN is better than histogram min/max to catch errors
00511   theLowerValue = std::numeric_limits<double>::quiet_NaN();
00512   theUpperValue = std::numeric_limits<double>::quiet_NaN();
00513 
00514   double myBinXStep = ( myHistogram.maximum - myHistogram.minimum ) / myHistogram.binCount;
00515   int myCount = 0;
00516   int myMinCount = ( int ) qRound( theLowerCount * myHistogram.nonNullCount );
00517   int myMaxCount = ( int ) qRound( theUpperCount * myHistogram.nonNullCount );
00518   bool myLowerFound = false;
00519   QgsDebugMsg( QString( "binCount = %1 minimum = %2 maximum = %3 myBinXStep = %4" ).arg( myHistogram.binCount ).arg( myHistogram.minimum ).arg( myHistogram.maximum ).arg( myBinXStep ) );
00520   QgsDebugMsg( QString( "myMinCount = %1 myMaxCount = %2" ).arg( myMinCount ).arg( myMaxCount ) );
00521 
00522   for ( int myBin = 0; myBin < myHistogram.histogramVector.size(); myBin++ )
00523   {
00524     int myBinValue = myHistogram.histogramVector.value( myBin );
00525     myCount += myBinValue;
00526     if ( !myLowerFound && myCount > myMinCount )
00527     {
00528       theLowerValue = myHistogram.minimum + myBin * myBinXStep;
00529       myLowerFound = true;
00530     }
00531     if ( myCount >= myMaxCount )
00532     {
00533       theUpperValue = myHistogram.minimum + myBin * myBinXStep;
00534       break;
00535     }
00536   }
00537 }
00538 
00539 QString QgsRasterInterface::capabilitiesString() const
00540 {
00541   QStringList abilitiesList;
00542 
00543   int abilities = capabilities();
00544 
00545   // Not all all capabilities are here (Size, IdentifyValue, IdentifyText,
00546   // IdentifyHtml, IdentifyFeature) because those are quite technical and probably
00547   // would be confusing for users
00548 
00549   if ( abilities & QgsRasterInterface::Identify )
00550   {
00551     abilitiesList += tr( "Identify" );
00552   }
00553 
00554   if ( abilities & QgsRasterInterface::Create )
00555   {
00556     abilitiesList += tr( "Create Datasources" );
00557   }
00558 
00559   if ( abilities & QgsRasterInterface::Remove )
00560   {
00561     abilitiesList += tr( "Remove Datasources" );
00562   }
00563 
00564   if ( abilities & QgsRasterInterface::BuildPyramids )
00565   {
00566     abilitiesList += tr( "Build Pyramids" );
00567   }
00568 
00569   QgsDebugMsg( "Capability: " + abilitiesList.join( ", " ) );
00570 
00571   return abilitiesList.join( ", " );
00572 }
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Defines