Skip to content

Commit

Permalink
Add tests for QgsElevationMap and for EDL of 2D point cloud rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
wonder-sk committed Sep 2, 2022
1 parent 7f66cb6 commit fc41309
Show file tree
Hide file tree
Showing 8 changed files with 173 additions and 0 deletions.
2 changes: 2 additions & 0 deletions python/core/auto_generated/qgselevationmap.sip.in
Expand Up @@ -9,6 +9,7 @@




class QgsElevationMap
{
%Docstring(signature="appended")
Expand Down Expand Up @@ -71,6 +72,7 @@ Converts elevation value to an actual color
Converts a color back to elevation value
%End


private:
QgsElevationMap( const QgsElevationMap & );
};
Expand Down
20 changes: 20 additions & 0 deletions src/core/qgselevationmap.cpp
Expand Up @@ -15,6 +15,8 @@

#include "qgselevationmap.h"

#include "qgsrasterblock.h"

#include <QPainter>
#include <algorithm>
#include <cmath>
Expand All @@ -41,6 +43,24 @@ float QgsElevationMap::decodeElevation( QRgb colorRaw )
return ( ( double ) zScaled ) / 1000 - 8000;
}

std::unique_ptr<QgsElevationMap> QgsElevationMap::fromRasterBlock( QgsRasterBlock *block )
{
std::unique_ptr<QgsElevationMap> elevMap( new QgsElevationMap( QSize( block->width(), block->height() ) ) );
QRgb *dataPtr = reinterpret_cast<QRgb *>( elevMap->mElevationImage.bits() );
for ( int row = 0; row < block->height(); ++row )
{
for ( int col = 0; col < block->width(); ++col )
{
bool isNoData;
double value = block->valueAndNoData( row, col, isNoData );
if ( !isNoData )
*dataPtr = encodeElevation( value );
++dataPtr;
}
}
return elevMap;
}

void QgsElevationMap::applyEyeDomeLighting( QImage &img, int distance, float strength, float rendererScale )
{
int imgWidth = img.width();
Expand Down
5 changes: 5 additions & 0 deletions src/core/qgselevationmap.h
Expand Up @@ -22,6 +22,8 @@
#include <QImage>
#include <memory>

class QgsRasterBlock;

/**
* \ingroup core
* \brief Stores digital elevation model in a raster image which may get updated
Expand Down Expand Up @@ -71,6 +73,9 @@ class CORE_EXPORT QgsElevationMap
//! Converts a color back to elevation value
static float decodeElevation( QRgb colorRaw );

//! Creates an elevation map based on data from the given raster block.
static std::unique_ptr<QgsElevationMap> fromRasterBlock( QgsRasterBlock *block ) SIP_SKIP;

private:

#ifdef SIP_RUN
Expand Down
1 change: 1 addition & 0 deletions tests/src/core/CMakeLists.txt
Expand Up @@ -51,6 +51,7 @@ set(TESTS
testqgsdiagram.cpp
testqgsdistancearea.cpp
testqgsdxfexport.cpp
testqgselevationmap.cpp
testqgsellipsemarker.cpp
testqgsexpression.cpp
testqgsexpressioncontext.cpp
Expand Down
107 changes: 107 additions & 0 deletions tests/src/core/testqgselevationmap.cpp
@@ -0,0 +1,107 @@
/***************************************************************************
testqgselevationmap.cpp
--------------------------------------
Date : August 2022
Copyright : (C) 2022 by Martin Dobias
Email : wonder dot sk at gmail dot com
***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgstest.h"

#include "qgsapplication.h"
#include "qgselevationmap.h"
#include "qgsrasterlayer.h"
#include "qgsrenderchecker.h"


static QString _fileNameForTest( const QString &testName )
{
return QDir::tempPath() + '/' + testName + ".png";
}

static bool _verifyImage( const QString &testName, QString &report )
{
QgsRenderChecker checker;
checker.setControlPathPrefix( QStringLiteral( "elevation_map" ) );
checker.setControlName( "expected_" + testName );
checker.setRenderedImage( _fileNameForTest( testName ) );
checker.setSizeTolerance( 3, 3 );
const bool equal = checker.compareImages( testName, 500 );
report += checker.report();
return equal;
}


class TestQgsElevationMap : public QObject
{
Q_OBJECT
private slots:
void initTestCase();
void cleanupTestCase();
void testRasterDemEdl();

private:
QString mReport;
};


void TestQgsElevationMap::initTestCase()
{
//
// Runs once before any tests are run
//
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();

mReport += QLatin1String( "<h1>Elevation Map Tests</h1>\n" );
}

void TestQgsElevationMap::cleanupTestCase()
{
const QString myReportFile = QDir::tempPath() + "/qgistest.html";
QFile myFile( myReportFile );
if ( myFile.open( QIODevice::WriteOnly | QIODevice::Append ) )
{
QTextStream myQTextStream( &myFile );
myQTextStream << mReport;
myFile.close();
}

QgsApplication::exitQgis();
}


void TestQgsElevationMap::testRasterDemEdl()
{

QString testDataDir = QStringLiteral( TEST_DATA_DIR ); //defined in CmakeLists.txt
QgsRasterLayer r( testDataDir + "/analysis/dem.tif" );
QVERIFY( r.isValid() );

const QgsRectangle fullExtent = r.extent();
const int width = r.width();
const int height = r.height();

std::unique_ptr<QgsRasterBlock> block( r.dataProvider()->block( 1, fullExtent, width, height ) );
QVERIFY( block );

QImage img( block->width(), block->height(), QImage::Format_RGB32 );
img.fill( Qt::cyan );

std::unique_ptr<QgsElevationMap> elevationMap( QgsElevationMap::fromRasterBlock( block.get() ) );
elevationMap->applyEyeDomeLighting( img, 2, 100, 10000 );

img.save( _fileNameForTest( QStringLiteral( "dem_edl" ) ) );
QVERIFY( _verifyImage( "dem_edl", mReport ) );
}


QGSTEST_MAIN( TestQgsElevationMap )
#include "testqgselevationmap.moc"
38 changes: 38 additions & 0 deletions tests/src/python/test_qgspointcloudclassifiedrenderer.py
Expand Up @@ -13,6 +13,7 @@
import qgis # NOQA

from qgis.core import (
QgsApplication,
QgsProviderRegistry,
QgsPointCloudLayer,
QgsPointCloudClassifiedRenderer,
Expand Down Expand Up @@ -329,6 +330,43 @@ def testRenderFiltered(self):
TestQgsPointCloudClassifiedRenderer.report += renderchecker.report()
self.assertTrue(result)

@unittest.skipIf('copc' not in QgsProviderRegistry.instance().providerList(), 'COPC provider not available')
def testRenderEyeDomeLighting(self):
layer = QgsPointCloudLayer(unitTestDataPath() + '/point_clouds/copc/extrabytes-dataset.copc.laz', 'test', 'copc')
self.assertTrue(layer.isValid())

# make sure that we are ready for rendering
while layer.statisticsCalculationState() == QgsPointCloudLayer.PointCloudStatisticsCalculationState.Calculating:
QgsApplication.processEvents()

# we don't have true CRS for the file, anything not lat/lon should be fine so that we get roughly correct scale
layer.setCrs(QgsCoordinateReferenceSystem("EPSG:3857"))

categories = QgsPointCloudRendererRegistry.classificationAttributeCategories(layer)
renderer = QgsPointCloudClassifiedRenderer('Classification', categories)
layer.setRenderer(renderer)

layer.renderer().setPointSize(1)
layer.renderer().setPointSizeUnit(QgsUnitTypes.RenderMillimeters)
layer.renderer().setDrawOrder2d(QgsPointCloudRenderer.DrawOrder.BottomToTop)

layer.renderer().setUseEyeDomeLighting(True)

mapsettings = QgsMapSettings()
mapsettings.setOutputSize(QSize(400, 400))
mapsettings.setOutputDpi(96)
mapsettings.setDestinationCrs(layer.crs())
mapsettings.setExtent(layer.extent())
mapsettings.setLayers([layer])

renderchecker = QgsMultiRenderChecker()
renderchecker.setMapSettings(mapsettings)
renderchecker.setControlPathPrefix('pointcloudrenderer')
renderchecker.setControlName('expected_eye_dome_lighting')
result = renderchecker.runTest('expected_eye_dome_lighting')
TestQgsPointCloudClassifiedRenderer.report += renderchecker.report()
self.assertTrue(result)


if __name__ == '__main__':
unittest.main()
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit fc41309

Please sign in to comment.