26 #include "cpl_string.h" 
   33 #include <QTextStream> 
   36   : mInputFile( inputFile )
 
   37   , mOutputFile( outputFile )
 
   38   , mOutputFormat( outputFormat )
 
   39   , mSlopeFilter( std::make_unique< 
QgsSlopeFilter >( inputFile, outputFile, outputFormat ) )
 
   40   , mAspectFilter( std::make_unique< 
QgsAspectFilter > ( inputFile, outputFile, outputFormat ) )
 
   41   , mHillshadeFilter285( std::make_unique< 
QgsHillshadeFilter >( inputFile, outputFile, outputFormat, 285, 30 ) )
 
   42   , mHillshadeFilter300( std::make_unique< 
QgsHillshadeFilter >( inputFile, outputFile, outputFormat, 300, 30 ) )
 
   43   , mHillshadeFilter315( std::make_unique< 
QgsHillshadeFilter >( inputFile, outputFile, outputFormat, 315, 30 ) )
 
   53   mReliefColors.clear();
 
   58   mReliefColors.push_back( color );
 
   61 void QgsRelief::setDefaultReliefColors()
 
   83   GDALDriverH outputDriver = openOutputDriver();
 
   96   mHillshadeFilter285->setCellSizeX( mCellSizeX );
 
   97   mHillshadeFilter285->setCellSizeY( mCellSizeY );
 
   98   mHillshadeFilter285->setZFactor( mZFactor );
 
   99   mHillshadeFilter300->setCellSizeX( mCellSizeX );
 
  100   mHillshadeFilter300->setCellSizeY( mCellSizeY );
 
  101   mHillshadeFilter300->setZFactor( mZFactor );
 
  102   mHillshadeFilter315->setCellSizeX( mCellSizeX );
 
  103   mHillshadeFilter315->setCellSizeY( mCellSizeY );
 
  104   mHillshadeFilter315->setZFactor( mZFactor );
 
  105   mSlopeFilter->setCellSizeX( mCellSizeX );
 
  106   mSlopeFilter->setCellSizeY( mCellSizeY );
 
  107   mSlopeFilter->setZFactor( mZFactor );
 
  108   mAspectFilter->setCellSizeX( mCellSizeX );
 
  109   mAspectFilter->setCellSizeY( mCellSizeY );
 
  110   mAspectFilter->setZFactor( mZFactor );
 
  113   GDALRasterBandH rasterBand = GDALGetRasterBand( inputDataset.get(), 1 );
 
  118   mInputNodataValue = GDALGetRasterNoDataValue( rasterBand, 
nullptr );
 
  119   mSlopeFilter->setInputNodataValue( mInputNodataValue );
 
  120   mAspectFilter->setInputNodataValue( mInputNodataValue );
 
  121   mHillshadeFilter285->setInputNodataValue( mInputNodataValue );
 
  122   mHillshadeFilter300->setInputNodataValue( mInputNodataValue );
 
  123   mHillshadeFilter315->setInputNodataValue( mInputNodataValue );
 
  125   GDALRasterBandH outputRedBand = GDALGetRasterBand( outputDataset.get(), 1 );
 
  126   GDALRasterBandH outputGreenBand = GDALGetRasterBand( outputDataset.get(), 2 );
 
  127   GDALRasterBandH outputBlueBand = GDALGetRasterBand( outputDataset.get(), 3 );
 
  129   if ( !outputRedBand || !outputGreenBand || !outputBlueBand )
 
  134   GDALSetRasterNoDataValue( outputRedBand, -9999 );
 
  135   GDALSetRasterNoDataValue( outputGreenBand, -9999 );
 
  136   GDALSetRasterNoDataValue( outputBlueBand, -9999 );
 
  137   mOutputNodataValue = GDALGetRasterNoDataValue( outputRedBand, 
nullptr );
 
  138   mSlopeFilter->setOutputNodataValue( mOutputNodataValue );
 
  139   mAspectFilter->setOutputNodataValue( mOutputNodataValue );
 
  140   mHillshadeFilter285->setOutputNodataValue( mOutputNodataValue );
 
  141   mHillshadeFilter300->setOutputNodataValue( mOutputNodataValue );
 
  142   mHillshadeFilter315->setOutputNodataValue( mOutputNodataValue );
 
  150   float *scanLine1 = ( 
float * ) CPLMalloc( 
sizeof( 
float ) * xSize );
 
  151   float *scanLine2 = ( 
float * ) CPLMalloc( 
sizeof( 
float ) * xSize );
 
  152   float *scanLine3 = ( 
float * ) CPLMalloc( 
sizeof( 
float ) * xSize );
 
  154   unsigned char *resultRedLine = ( 
unsigned char * ) CPLMalloc( 
sizeof( 
unsigned char ) * xSize );
 
  155   unsigned char *resultGreenLine = ( 
unsigned char * ) CPLMalloc( 
sizeof( 
unsigned char ) * xSize );
 
  156   unsigned char *resultBlueLine = ( 
unsigned char * ) CPLMalloc( 
sizeof( 
unsigned char ) * xSize );
 
  161   for ( 
int i = 0; i < ySize; ++i )
 
  165       feedback->
setProgress( 100.0 * i / 
static_cast< double >( ySize ) );
 
  176       for ( 
int a = 0; a < xSize; ++a )
 
  178         scanLine1[a] = mInputNodataValue;
 
  180       if ( GDALRasterIO( rasterBand, GF_Read, 0, 0, xSize, 1, scanLine2, xSize, 1, GDT_Float32, 0, 0 )  != CE_None )
 
  182         QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  188       CPLFree( scanLine1 );
 
  189       scanLine1 = scanLine2;
 
  190       scanLine2 = scanLine3;
 
  191       scanLine3 = ( 
float * ) CPLMalloc( 
sizeof( 
float ) * xSize );
 
  194     if ( i == ySize - 1 ) 
 
  196       for ( 
int a = 0; a < xSize; ++a )
 
  198         scanLine3[a] = mInputNodataValue;
 
  203       if ( GDALRasterIO( rasterBand, GF_Read, 0, i + 1, xSize, 1, scanLine3, xSize, 1, GDT_Float32, 0, 0 ) != CE_None )
 
  205         QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  209     for ( 
int j = 0; j < xSize; ++j )
 
  213         resultOk = processNineCellWindow( &mInputNodataValue, &scanLine1[j], &scanLine1[j + 1], &mInputNodataValue, &scanLine2[j], \
 
  214                                           &scanLine2[j + 1], &mInputNodataValue, &scanLine3[j], &scanLine3[j + 1], \
 
  215                                           &resultRedLine[j], &resultGreenLine[j], &resultBlueLine[j] );
 
  217       else if ( j == xSize - 1 )
 
  219         resultOk = processNineCellWindow( &scanLine1[j - 1], &scanLine1[j], &mInputNodataValue, &scanLine2[j - 1], &scanLine2[j], \
 
  220                                           &mInputNodataValue, &scanLine3[j - 1], &scanLine3[j], &mInputNodataValue, \
 
  221                                           &resultRedLine[j], &resultGreenLine[j], &resultBlueLine[j] );
 
  225         resultOk = processNineCellWindow( &scanLine1[j - 1], &scanLine1[j], &scanLine1[j + 1], &scanLine2[j - 1], &scanLine2[j], \
 
  226                                           &scanLine2[j + 1], &scanLine3[j - 1], &scanLine3[j], &scanLine3[j + 1], \
 
  227                                           &resultRedLine[j], &resultGreenLine[j], &resultBlueLine[j] );
 
  232         resultRedLine[j] = mOutputNodataValue;
 
  233         resultGreenLine[j] = mOutputNodataValue;
 
  234         resultBlueLine[j] = mOutputNodataValue;
 
  238     if ( GDALRasterIO( outputRedBand, GF_Write, 0, i, xSize, 1, resultRedLine, xSize, 1, GDT_Byte, 0, 0 ) != CE_None )
 
  240       QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  242     if ( GDALRasterIO( outputGreenBand, GF_Write, 0, i, xSize, 1, resultGreenLine, xSize, 1, GDT_Byte, 0, 0 ) != CE_None )
 
  244       QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  246     if ( GDALRasterIO( outputBlueBand, GF_Write, 0, i, xSize, 1, resultBlueLine, xSize, 1, GDT_Byte, 0, 0 ) != CE_None )
 
  248       QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  257   CPLFree( resultRedLine );
 
  258   CPLFree( resultBlueLine );
 
  259   CPLFree( resultGreenLine );
 
  260   CPLFree( scanLine1 );
 
  261   CPLFree( scanLine2 );
 
  262   CPLFree( scanLine3 );
 
  274 bool QgsRelief::processNineCellWindow( 
float *x1, 
float *x2, 
float *x3, 
float *x4, 
float *x5, 
float *x6, 
float *x7, 
float *x8, 
float *x9,
 
  275                                        unsigned char *red, 
unsigned char *green, 
unsigned char *blue )
 
  282   float hillShadeValue300 = mHillshadeFilter300->processNineCellWindow( x1, x2, x3, x4, x5, x6, x7, x8, x9 );
 
  283   if ( hillShadeValue300 != mOutputNodataValue )
 
  285     if ( !setElevationColor( *x5, &r, &g, &b ) )
 
  287       r = hillShadeValue300;
 
  288       g = hillShadeValue300;
 
  289       b = hillShadeValue300;
 
  293       r = r / 2.0 + hillShadeValue300 / 2.0;
 
  294       g = g / 2.0 + hillShadeValue300 / 2.0;
 
  295       b = b / 2.0 + hillShadeValue300 / 2.0;
 
  300   float hillShadeValue315 = mHillshadeFilter315->processNineCellWindow( x1, x2, x3, x4, x5, x6, x7, x8, x9 );
 
  301   float slope = mSlopeFilter->processNineCellWindow( x1, x2, x3, x4, x5, x6, x7, x8, x9 );
 
  302   if ( hillShadeValue315 != mOutputNodataValue && slope != mOutputNodataValue )
 
  307       r2 = 0 / 2.0 + hillShadeValue315 / 2.0;
 
  308       g2 = 0 / 2.0 + hillShadeValue315 / 2.0;
 
  309       b2 = 0 / 2.0 + hillShadeValue315 / 2.0;
 
  311     else if ( slope >= 1 )
 
  313       int slopeValue = 255 - ( slope / 15.0 * 255.0 );
 
  314       r2 = slopeValue / 2.0 + hillShadeValue315 / 2.0;
 
  315       g2 = slopeValue / 2.0 + hillShadeValue315 / 2.0;
 
  316       b2 = slopeValue / 2.0 + hillShadeValue315 / 2.0;
 
  320       r2 = hillShadeValue315;
 
  321       g2 = hillShadeValue315;
 
  322       b2 = hillShadeValue315;
 
  326     r = r * 0.7 + r2 * 0.3;
 
  327     g = g * 0.7 + g2 * 0.3;
 
  328     b = b * 0.7 + b2 * 0.3;
 
  332   float hillShadeValue285 = mHillshadeFilter285->processNineCellWindow( x1, x2, x3, x4, x5, x6, x7, x8, x9 );
 
  333   float aspect = mAspectFilter->processNineCellWindow( x1, x2, x3, x4, x5, x6, x7, x8, x9 );
 
  334   if ( hillShadeValue285 != mOutputNodataValue && aspect != mOutputNodataValue )
 
  336     double angle_diff = std::fabs( 285 - aspect );
 
  337     if ( angle_diff > 180 )
 
  343     if ( angle_diff < 90 )
 
  345       int aspectVal = ( 1 - std::cos( angle_diff * M_PI / 180 ) ) * 255;
 
  346       r3 = 0.5 * 255 + hillShadeValue315 * 0.5;
 
  347       g3 = 0.5 * 255 + hillShadeValue315 * 0.5;
 
  348       b3 = 0.5 * aspectVal + hillShadeValue315 * 0.5;
 
  352       r3 = 0.5 * 255 + hillShadeValue315 * 0.5;
 
  353       g3 = 0.5 * 255 + hillShadeValue315 * 0.5;
 
  354       b3 = 0.5 * 255 + hillShadeValue315 * 0.5;
 
  357     r = r3 * 0.1 + r * 0.9;
 
  358     g = g3 * 0.1 + g * 0.9;
 
  359     b = b3 * 0.1 + b * 0.9;
 
  362   *red = ( 
unsigned char )r;
 
  363   *green = ( 
unsigned char )g;
 
  364   *blue = ( 
unsigned char )b;
 
  368 bool QgsRelief::setElevationColor( 
double elevation, 
int *red, 
int *green, 
int *blue )
 
  370   QList< ReliefColor >::const_iterator reliefColorIt = mReliefColors.constBegin();
 
  371   for ( ; reliefColorIt != mReliefColors.constEnd(); ++reliefColorIt )
 
  373     if ( elevation >= reliefColorIt->minElevation && elevation <= reliefColorIt->maxElevation )
 
  375       const QColor &
c = reliefColorIt->color;
 
  392     nCellsX = GDALGetRasterXSize( inputDataset.get() );
 
  393     nCellsY = GDALGetRasterYSize( inputDataset.get() );
 
  396     if ( GDALGetRasterCount( inputDataset.get() ) < 1 )
 
  404 GDALDriverH QgsRelief::openOutputDriver()
 
  407   GDALDriverH outputDriver = GDALGetDriverByName( mOutputFormat.toLocal8Bit().data() );
 
  429   int xSize = GDALGetRasterXSize( inputDataset );
 
  430   int ySize = GDALGetRasterYSize( inputDataset );
 
  433   char **papszOptions = 
nullptr;
 
  436   papszOptions = CSLSetNameValue( papszOptions, 
"COMPRESS", 
"PACKBITS" );
 
  439   gdal::dataset_unique_ptr outputDataset( GDALCreate( outputDriver, mOutputFile.toUtf8().constData(), xSize, ySize, 3, GDT_Byte, papszOptions ) );
 
  440   CSLDestroy( papszOptions );
 
  441   papszOptions = 
nullptr;
 
  443   if ( !outputDataset )
 
  449   double geotransform[6];
 
  450   if ( GDALGetGeoTransform( inputDataset, geotransform ) != CE_None )
 
  454   GDALSetGeoTransform( outputDataset.get(), geotransform );
 
  457   mCellSizeX = geotransform[1];
 
  458   if ( mCellSizeX < 0 )
 
  460     mCellSizeX = -mCellSizeX;
 
  462   mCellSizeY = geotransform[5];
 
  463   if ( mCellSizeY < 0 )
 
  465     mCellSizeY = -mCellSizeY;
 
  468   const char *projection = GDALGetProjectionRef( inputDataset );
 
  469   GDALSetProjection( outputDataset.get(), projection );
 
  471   return outputDataset;
 
  477   int nCellsX, nCellsY;
 
  485   GDALRasterBandH elevationBand = GDALGetRasterBand( inputDataset.get(), 1 );
 
  486   if ( !elevationBand )
 
  494   minMax[0] = GDALGetRasterMinimum( elevationBand, &minOk );
 
  495   minMax[1] = GDALGetRasterMaximum( elevationBand, &maxOk );
 
  497   if ( !minOk || !maxOk )
 
  499     GDALComputeRasterMinMax( elevationBand, 
true, minMax );
 
  505   double frequency[252] = {0};
 
  506   double frequencyClassRange = ( minMax[1] - minMax[0] ) / 252.0;
 
  508   float *scanLine = ( 
float * ) CPLMalloc( 
sizeof( 
float ) *  nCellsX );
 
  509   int elevationClass = -1;
 
  511   for ( 
int i = 0; i < nCellsY; ++i )
 
  513     if ( GDALRasterIO( elevationBand, GF_Read, 0, i, nCellsX, 1,
 
  514                        scanLine, nCellsX, 1, GDT_Float32,
 
  517       QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  520     for ( 
int j = 0; j < nCellsX; ++j )
 
  522       elevationClass = frequencyClassForElevation( scanLine[j], minMax[0], frequencyClassRange );
 
  523       if ( elevationClass >= 0 && elevationClass < 252 )
 
  525         frequency[elevationClass] += 1.0;
 
  533   for ( 
int i = 0; i < 252; ++i )
 
  535     frequency[i] = std::log10( frequency[i] );
 
  539   QFile outFile( file );
 
  540   if ( !outFile.open( QIODevice::WriteOnly | QIODevice::Truncate ) )
 
  545   QTextStream outstream( &outFile );
 
  546 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) 
  547   outstream.setCodec( 
"UTF-8" );
 
  549   for ( 
int i = 0; i < 252; ++i )
 
  551     outstream << QString::number( i ) + 
',' + QString::number( frequency[i] ) << endl;
 
  559   QList< QgsRelief::ReliefColor > resultList;
 
  561   int nCellsX, nCellsY;
 
  569   GDALRasterBandH elevationBand = GDALGetRasterBand( inputDataset.get(), 1 );
 
  570   if ( !elevationBand )
 
  578   minMax[0] = GDALGetRasterMinimum( elevationBand, &minOk );
 
  579   minMax[1] = GDALGetRasterMaximum( elevationBand, &maxOk );
 
  581   if ( !minOk || !maxOk )
 
  583     GDALComputeRasterMinMax( elevationBand, 
true, minMax );
 
  589   double frequency[252] = {0};
 
  590   double frequencyClassRange = ( minMax[1] - minMax[0] ) / 252.0;
 
  592   float *scanLine = ( 
float * ) CPLMalloc( 
sizeof( 
float ) *  nCellsX );
 
  593   int elevationClass = -1;
 
  595   for ( 
int i = 0; i < nCellsY; ++i )
 
  597     if ( GDALRasterIO( elevationBand, GF_Read, 0, i, nCellsX, 1,
 
  598                        scanLine, nCellsX, 1, GDT_Float32,
 
  601       QgsDebugMsg( QStringLiteral( 
"Raster IO Error" ) );
 
  603     for ( 
int j = 0; j < nCellsX; ++j )
 
  605       elevationClass = frequencyClassForElevation( scanLine[j], minMax[0], frequencyClassRange );
 
  606       elevationClass = std::max( std::min( elevationClass, 251 ), 0 );
 
  607       frequency[elevationClass] += 1.0;
 
  614   for ( 
int i = 0; i < 252; ++i )
 
  616     frequency[i] = std::log10( frequency[i] );
 
  620   QList<int> classBreaks;
 
  621   classBreaks.append( 0 );
 
  622   classBreaks.append( 28 );
 
  623   classBreaks.append( 56 );
 
  624   classBreaks.append( 84 );
 
  625   classBreaks.append( 112 );
 
  626   classBreaks.append( 140 );
 
  627   classBreaks.append( 168 );
 
  628   classBreaks.append( 196 );
 
  629   classBreaks.append( 224 );
 
  630   classBreaks.append( 252 );
 
  632   for ( 
int i = 0; i < 10; ++i )
 
  634     optimiseClassBreaks( classBreaks, frequency );
 
  638   for ( 
int i = 0; i < classBreaks.size(); ++i )
 
  640     qWarning( 
"%d", classBreaks[i] );
 
  644   QVector<QColor> colorList;
 
  645   colorList.reserve( 9 );
 
  646   colorList.push_back( QColor( 7, 165, 144 ) );
 
  647   colorList.push_back( QColor( 12, 221, 162 ) );
 
  648   colorList.push_back( QColor( 33, 252, 183 ) );
 
  649   colorList.push_back( QColor( 247, 252, 152 ) );
 
  650   colorList.push_back( QColor( 252, 196, 8 ) );
 
  651   colorList.push_back( QColor( 252, 166, 15 ) );
 
  652   colorList.push_back( QColor( 175, 101, 15 ) );
 
  653   colorList.push_back( QColor( 255, 133, 92 ) );
 
  654   colorList.push_back( QColor( 204, 204, 204 ) );
 
  656   resultList.reserve( classBreaks.size() );
 
  657   for ( 
int i = 1; i < classBreaks.size(); ++i )
 
  659     double minElevation = minMax[0] + classBreaks[i - 1] * frequencyClassRange;
 
  660     double maxElevation = minMax[0] + classBreaks[i] * frequencyClassRange;
 
  667 void QgsRelief::optimiseClassBreaks( QList<int> &breaks, 
double *frequencies )
 
  669   int nClasses = breaks.size() - 1;
 
  670   double *a = 
new double[nClasses]; 
 
  671   double *b = 
new double[nClasses]; 
 
  673   for ( 
int i = 0; i < nClasses; ++i )
 
  676     QList< QPair < int, double > > regressionInput;
 
  677     regressionInput.reserve( breaks.at( i + 1 ) - breaks.at( i ) );
 
  678     for ( 
int j = breaks.at( i ); j < breaks.at( i + 1 ); ++j )
 
  680       regressionInput.push_back( qMakePair( j, frequencies[j] ) );
 
  683     double aParam, bParam;
 
  684     if ( !regressionInput.isEmpty() && calculateRegression( regressionInput, aParam, bParam ) )
 
  696   QList<int> classesToRemove;
 
  699   for ( 
int i = 1; i < nClasses ; ++i )
 
  701     if ( breaks[i] == breaks[ i - 1 ] )
 
  712       int newX = ( b[i - 1] - b[ i ] ) / ( a[ i ] - a[ i - 1 ] );
 
  714       if ( newX <= breaks[i - 1] )
 
  716         newX = breaks[i - 1];
 
  719       else if ( i < nClasses - 1 && newX >= breaks[i + 1] )
 
  721         newX = breaks[i + 1];
 
  729   for ( 
int i = classesToRemove.size() - 1; i >= 0; --i )
 
  731     breaks.removeAt( classesToRemove.at( i ) );
 
  738 int QgsRelief::frequencyClassForElevation( 
double elevation, 
double minElevation, 
double elevationClassRange )
 
  740   return ( elevation - minElevation ) / elevationClassRange;
 
  743 bool QgsRelief::calculateRegression( 
const QList< QPair < int, double > > &input, 
double &a, 
double &b )
 
  748   QList< QPair < int, double > >::const_iterator inputIt = input.constBegin();
 
  749   for ( ; inputIt != input.constEnd(); ++inputIt )
 
  751     xSum += inputIt->first;
 
  752     ySum += inputIt->second;
 
  754   xMean = xSum / input.size();
 
  755   yMean = ySum / input.size();
 
  757   double sumCounter = 0;
 
  758   double sumDenominator = 0;
 
  759   inputIt = input.constBegin();
 
  760   for ( ; inputIt != input.constEnd(); ++inputIt )
 
  762     sumCounter += ( ( inputIt->first - xMean ) * ( inputIt->second - yMean ) );
 
  763     sumDenominator += ( ( inputIt->first - xMean ) * ( inputIt->first - xMean ) );
 
  766   a = sumCounter / sumDenominator;
 
  767   b = yMean - a * xMean;
 
Calculates aspect values in a window of 3x3 cells based on first order derivatives in x- and y- direc...
Base class for feedback objects to be used for cancellation of something running in a worker thread.
bool isCanceled() const SIP_HOLDGIL
Tells whether the operation has been canceled already.
void setProgress(double progress)
Sets the current progress for the feedback object.
static bool supportsRasterCreate(GDALDriverH driver)
Reads whether a driver supports GDALCreate() for raster purposes.
bool exportFrequencyDistributionToCsv(const QString &file)
Write frequency of elevation values to file for manual inspection.
QgsRelief(const QString &inputFile, const QString &outputFile, const QString &outputFormat)
QList< QgsRelief::ReliefColor > calculateOptimizedReliefClasses()
Calculates class breaks according with the method of Buenzli (2011) using an iterative algorithm for ...
int processRaster(QgsFeedback *feedback=nullptr)
Starts the calculation, reads from mInputFile and stores the result in mOutputFile.
void addReliefColorClass(const QgsRelief::ReliefColor &color)
Calculates slope values in a window of 3x3 cells based on first order derivatives in x- and y- direct...
void CORE_EXPORT fast_delete_and_close(dataset_unique_ptr &dataset, GDALDriverH driver, const QString &path)
Performs a fast close of an unwanted GDAL dataset handle by deleting the underlying data store.
std::unique_ptr< std::remove_pointer< GDALDatasetH >::type, GDALDatasetCloser > dataset_unique_ptr
Scoped GDAL dataset.
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)