Skip to content

Commit f85d494

Browse files
authoredMar 20, 2019
Merge pull request #9549 from wonder-sk/online-3d-terrain
[3d] Add option to use terrain data from online service
2 parents 13a74ae + 1010522 commit f85d494

16 files changed

+872
-83
lines changed
 

‎src/3d/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ SET(QGIS_3D_SRCS
4646
terrain/qgsdemterraintilegeometry_p.cpp
4747
terrain/qgsdemterraintileloader_p.cpp
4848
terrain/qgsflatterraingenerator.cpp
49+
terrain/qgsonlineterraingenerator.cpp
50+
terrain/qgsterraindownloader.cpp
4951
terrain/qgsterrainentity_p.cpp
5052
terrain/qgsterraingenerator.cpp
5153
terrain/qgsterraintexturegenerator_p.cpp
@@ -130,6 +132,8 @@ SET(QGIS_3D_HDRS
130132
terrain/qgsdemterraingenerator.h
131133
terrain/qgsdemterraintilegeometry_p.h
132134
terrain/qgsdemterraintileloader_p.h
135+
terrain/qgsonlineterraingenerator.h
136+
terrain/qgsterraindownloader.h
133137
terrain/qgsterrainentity_p.h
134138
terrain/qgsterraingenerator.h
135139
terrain/qgsterraintexturegenerator_p.h

‎src/3d/qgs3dmapsettings.cpp

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#include "qgs3dutils.h"
1919
#include "qgsflatterraingenerator.h"
2020
#include "qgsdemterraingenerator.h"
21-
//#include "quantizedmeshterraingenerator.h"
21+
#include "qgsonlineterraingenerator.h"
2222
#include "qgsvectorlayer3drenderer.h"
2323
#include "qgsmeshlayer3drenderer.h"
2424

@@ -141,12 +141,11 @@ void Qgs3DMapSettings::readXml( const QDomElement &elem, const QgsReadWriteConte
141141
demTerrainGenerator->setCrs( mCrs, mTransformContext );
142142
mTerrainGenerator.reset( demTerrainGenerator );
143143
}
144-
else if ( terrainGenType == QLatin1String( "quantized-mesh" ) )
144+
else if ( terrainGenType == QLatin1String( "online" ) )
145145
{
146-
#if 0
147-
terrainGenerator.reset( new QuantizedMeshTerrainGenerator );
148-
#endif
149-
Q_ASSERT( false ); // currently disabled
146+
QgsOnlineTerrainGenerator *onlineTerrainGenerator = new QgsOnlineTerrainGenerator;
147+
onlineTerrainGenerator->setCrs( mCrs, mTransformContext );
148+
mTerrainGenerator.reset( onlineTerrainGenerator );
150149
}
151150
else // "flat"
152151
{

‎src/3d/terrain/qgsdemterraintileloader_p.cpp

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include "qgschunknode_p.h"
2020
#include "qgsdemterraingenerator.h"
2121
#include "qgsdemterraintilegeometry_p.h"
22+
#include "qgsonlineterraingenerator.h"
2223
#include "qgsterrainentity_p.h"
2324
#include "qgsterraintexturegenerator_p.h"
2425
#include "qgsterraintileentity_p.h"
@@ -56,13 +57,27 @@ QgsDemTerrainTileLoader::QgsDemTerrainTileLoader( QgsTerrainEntity *terrain, Qgs
5657
{
5758

5859
const Qgs3DMapSettings &map = terrain->map3D();
59-
QgsDemTerrainGenerator *generator = static_cast<QgsDemTerrainGenerator *>( map.terrainGenerator() );
60+
61+
QgsDemHeightMapGenerator *heightMapGenerator = nullptr;
62+
if ( map.terrainGenerator()->type() == QgsTerrainGenerator::Dem )
63+
{
64+
QgsDemTerrainGenerator *generator = static_cast<QgsDemTerrainGenerator *>( map.terrainGenerator() );
65+
heightMapGenerator = generator->heightMapGenerator();
66+
mSkirtHeight = generator->skirtHeight();
67+
}
68+
else if ( map.terrainGenerator()->type() == QgsTerrainGenerator::Online )
69+
{
70+
QgsOnlineTerrainGenerator *generator = static_cast<QgsOnlineTerrainGenerator *>( map.terrainGenerator() );
71+
heightMapGenerator = generator->heightMapGenerator();
72+
mSkirtHeight = generator->skirtHeight();
73+
}
74+
else
75+
Q_ASSERT( false );
6076

6177
// get heightmap asynchronously
62-
connect( generator->heightMapGenerator(), &QgsDemHeightMapGenerator::heightMapReady, this, &QgsDemTerrainTileLoader::onHeightMapReady );
63-
mHeightMapJobId = generator->heightMapGenerator()->render( node->tileX(), node->tileY(), node->tileZ() );
64-
mResolution = generator->heightMapGenerator()->resolution();
65-
mSkirtHeight = generator->skirtHeight();
78+
connect( heightMapGenerator, &QgsDemHeightMapGenerator::heightMapReady, this, &QgsDemTerrainTileLoader::onHeightMapReady );
79+
mHeightMapJobId = heightMapGenerator->render( node->tileX(), node->tileY(), node->tileZ() );
80+
mResolution = heightMapGenerator->resolution();
6681
}
6782

6883
Qt3DCore::QEntity *QgsDemTerrainTileLoader::createEntity( Qt3DCore::QEntity *parent )
@@ -131,13 +146,15 @@ void QgsDemTerrainTileLoader::onHeightMapReady( int jobId, const QByteArray &hei
131146
#include <qgsrasterprojector.h>
132147
#include <QtConcurrent/QtConcurrentRun>
133148
#include <QFutureWatcher>
149+
#include "qgsterraindownloader.h"
134150

135151
QgsDemHeightMapGenerator::QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution )
136152
: mDtm( dtm )
137-
, mClonedProvider( ( QgsRasterDataProvider * )dtm->dataProvider()->clone() )
153+
, mClonedProvider( dtm ? ( QgsRasterDataProvider * )dtm->dataProvider()->clone() : nullptr )
138154
, mTilingScheme( tilingScheme )
139155
, mResolution( resolution )
140156
, mLastJobId( 0 )
157+
, mDownloader( dtm ? nullptr : new QgsTerrainDownloader )
141158
{
142159
}
143160

@@ -188,6 +205,12 @@ static QByteArray _readDtmData( QgsRasterDataProvider *provider, const QgsRectan
188205
return data;
189206
}
190207

208+
209+
static QByteArray _readOnlineDtm( QgsTerrainDownloader *downloader, const QgsRectangle &extent, int res, const QgsCoordinateReferenceSystem &destCrs )
210+
{
211+
return downloader->getHeightMap( extent, res, destCrs );
212+
}
213+
191214
int QgsDemHeightMapGenerator::render( int x, int y, int z )
192215
{
193216
Q_ASSERT( mJobs.isEmpty() ); // should be always just one active job...
@@ -205,7 +228,10 @@ int QgsDemHeightMapGenerator::render( int x, int y, int z )
205228
jd.extent = extent;
206229
jd.timer.start();
207230
// make a clone of the data provider so it is safe to use in worker thread
208-
jd.future = QtConcurrent::run( _readDtmData, mClonedProvider, extent, mResolution, mTilingScheme.crs() );
231+
if ( mDtm )
232+
jd.future = QtConcurrent::run( _readDtmData, mClonedProvider, extent, mResolution, mTilingScheme.crs() );
233+
else
234+
jd.future = QtConcurrent::run( _readOnlineDtm, mDownloader.get(), extent, mResolution, mTilingScheme.crs() );
209235

210236
QFutureWatcher<QByteArray> *fw = new QFutureWatcher<QByteArray>( nullptr );
211237
fw->setFuture( jd.future );
@@ -241,6 +267,9 @@ QByteArray QgsDemHeightMapGenerator::renderSynchronously( int x, int y, int z )
241267

242268
float QgsDemHeightMapGenerator::heightAt( double x, double y )
243269
{
270+
if ( !mDtm )
271+
return 0; // TODO: calculate heights for online DTM
272+
244273
// TODO: this is quite a primitive implementation: better to use heightmaps currently in use
245274
int res = 1024;
246275
QgsRectangle rect = mDtm->extent();

‎src/3d/terrain/qgsdemterraintileloader_p.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ class QgsDemTerrainTileLoader : public QgsTerrainTileLoader
6464
};
6565

6666

67+
class QgsTerrainDownloader;
6768

6869
/**
6970
* \ingroup 3d
@@ -114,6 +115,8 @@ class QgsDemHeightMapGenerator : public QObject
114115

115116
int mLastJobId;
116117

118+
std::unique_ptr<QgsTerrainDownloader> mDownloader;
119+
117120
struct JobData
118121
{
119122
int jobId;
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/***************************************************************************
2+
qgsonlineterraingenerator.cpp
3+
--------------------------------------
4+
Date : March 2019
5+
Copyright : (C) 2019 by Martin Dobias
6+
Email : wonder dot sk at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgsonlineterraingenerator.h"
17+
18+
#include "qgsdemterraintileloader_p.h"
19+
20+
21+
QgsOnlineTerrainGenerator::QgsOnlineTerrainGenerator() = default;
22+
23+
QgsOnlineTerrainGenerator::~QgsOnlineTerrainGenerator() = default;
24+
25+
QgsChunkLoader *QgsOnlineTerrainGenerator::createChunkLoader( QgsChunkNode *node ) const
26+
{
27+
return new QgsDemTerrainTileLoader( mTerrain, node );
28+
}
29+
30+
QgsTerrainGenerator *QgsOnlineTerrainGenerator::clone() const
31+
{
32+
QgsOnlineTerrainGenerator *cloned = new QgsOnlineTerrainGenerator;
33+
cloned->mCrs = mCrs;
34+
cloned->mExtent = mExtent;
35+
cloned->mResolution = mResolution;
36+
cloned->mSkirtHeight = mSkirtHeight;
37+
cloned->updateGenerator();
38+
return cloned;
39+
}
40+
41+
QgsTerrainGenerator::Type QgsOnlineTerrainGenerator::type() const
42+
{
43+
return QgsTerrainGenerator::Online;
44+
}
45+
46+
QgsRectangle QgsOnlineTerrainGenerator::extent() const
47+
{
48+
return mTerrainTilingScheme.tileToExtent( 0, 0, 0 );
49+
}
50+
51+
float QgsOnlineTerrainGenerator::heightAt( double x, double y, const Qgs3DMapSettings &map ) const
52+
{
53+
Q_UNUSED( map );
54+
if ( mHeightMapGenerator )
55+
return mHeightMapGenerator->heightAt( x, y );
56+
else
57+
return 0;
58+
}
59+
60+
void QgsOnlineTerrainGenerator::writeXml( QDomElement &elem ) const
61+
{
62+
QgsRectangle r = mExtent;
63+
QDomElement elemExtent = elem.ownerDocument().createElement( QStringLiteral( "extent" ) );
64+
elemExtent.setAttribute( QStringLiteral( "xmin" ), QString::number( r.xMinimum() ) );
65+
elemExtent.setAttribute( QStringLiteral( "xmax" ), QString::number( r.xMaximum() ) );
66+
elemExtent.setAttribute( QStringLiteral( "ymin" ), QString::number( r.yMinimum() ) );
67+
elemExtent.setAttribute( QStringLiteral( "ymax" ), QString::number( r.yMaximum() ) );
68+
69+
elem.setAttribute( QStringLiteral( "resolution" ), mResolution );
70+
elem.setAttribute( QStringLiteral( "skirt-height" ), mSkirtHeight );
71+
72+
// crs is not read/written - it should be the same as destination crs of the map
73+
}
74+
75+
void QgsOnlineTerrainGenerator::readXml( const QDomElement &elem )
76+
{
77+
QDomElement elemExtent = elem.firstChildElement( QStringLiteral( "extent" ) );
78+
double xmin = elemExtent.attribute( QStringLiteral( "xmin" ) ).toDouble();
79+
double xmax = elemExtent.attribute( QStringLiteral( "xmax" ) ).toDouble();
80+
double ymin = elemExtent.attribute( QStringLiteral( "ymin" ) ).toDouble();
81+
double ymax = elemExtent.attribute( QStringLiteral( "ymax" ) ).toDouble();
82+
83+
setExtent( QgsRectangle( xmin, ymin, xmax, ymax ) );
84+
85+
mResolution = elem.attribute( QStringLiteral( "resolution" ) ).toInt();
86+
mSkirtHeight = elem.attribute( QStringLiteral( "skirt-height" ) ).toFloat();
87+
88+
// crs is not read/written - it should be the same as destination crs of the map
89+
}
90+
91+
void QgsOnlineTerrainGenerator::setCrs( const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context )
92+
{
93+
mCrs = crs;
94+
mTransformContext = context;
95+
updateGenerator();
96+
}
97+
98+
void QgsOnlineTerrainGenerator::setExtent( const QgsRectangle &extent )
99+
{
100+
mExtent = extent;
101+
updateGenerator();
102+
}
103+
104+
void QgsOnlineTerrainGenerator::updateGenerator()
105+
{
106+
if ( mExtent.isNull() )
107+
{
108+
mTerrainTilingScheme = QgsTilingScheme();
109+
}
110+
else
111+
{
112+
// the real extent will be a square where the given extent fully fits
113+
mTerrainTilingScheme = QgsTilingScheme( mExtent, mCrs );
114+
}
115+
116+
mHeightMapGenerator.reset( new QgsDemHeightMapGenerator( nullptr, mTerrainTilingScheme, mResolution ) );
117+
}
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/***************************************************************************
2+
qgsonlineterraingenerator.h
3+
--------------------------------------
4+
Date : March 2019
5+
Copyright : (C) 2019 by Martin Dobias
6+
Email : wonder dot sk at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSONLINETERRAINGENERATOR_H
17+
#define QGSONLINETERRAINGENERATOR_H
18+
19+
#include "qgis_3d.h"
20+
21+
#include "qgsterraingenerator.h"
22+
23+
#include "qgscoordinatetransformcontext.h"
24+
25+
class QgsDemHeightMapGenerator;
26+
27+
/**
28+
* \ingroup 3d
29+
* Implementation of terrain generator that uses online resources to download heightmaps.
30+
* \since QGIS 3.8
31+
*/
32+
class _3D_EXPORT QgsOnlineTerrainGenerator : public QgsTerrainGenerator
33+
{
34+
public:
35+
//! Constructor for QgsOnlineTerrainGenerator
36+
QgsOnlineTerrainGenerator();
37+
~QgsOnlineTerrainGenerator() override;
38+
39+
//! Sets extent of the terrain
40+
void setExtent( const QgsRectangle &extent );
41+
42+
//! Sets CRS of the terrain
43+
void setCrs( const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context );
44+
//! Returns CRS of the terrain
45+
QgsCoordinateReferenceSystem crs() const { return mCrs; }
46+
47+
//! Sets resolution of the generator (how many elevation samples on one side of a terrain tile)
48+
void setResolution( int resolution ) { mResolution = resolution; updateGenerator(); }
49+
//! Returns resolution of the generator (how many elevation samples on one side of a terrain tile)
50+
int resolution() const { return mResolution; }
51+
52+
//! Sets skirt height (in world units). Skirts at the edges of terrain tiles help hide cracks between adjacent tiles.
53+
void setSkirtHeight( float skirtHeight ) { mSkirtHeight = skirtHeight; }
54+
//! Returns skirt height (in world units). Skirts at the edges of terrain tiles help hide cracks between adjacent tiles.
55+
float skirtHeight() const { return mSkirtHeight; }
56+
57+
//! Returns height map generator object - takes care of extraction of elevations from the layer)
58+
QgsDemHeightMapGenerator *heightMapGenerator() { return mHeightMapGenerator.get(); }
59+
60+
QgsTerrainGenerator *clone() const override SIP_FACTORY;
61+
Type type() const override;
62+
QgsRectangle extent() const override;
63+
float heightAt( double x, double y, const Qgs3DMapSettings &map ) const override;
64+
void writeXml( QDomElement &elem ) const override;
65+
void readXml( const QDomElement &elem ) override;
66+
//void resolveReferences( const QgsProject &project ) override;
67+
68+
QgsChunkLoader *createChunkLoader( QgsChunkNode *node ) const override SIP_FACTORY;
69+
70+
private:
71+
72+
void updateGenerator();
73+
74+
QgsRectangle mExtent;
75+
QgsCoordinateReferenceSystem mCrs;
76+
QgsCoordinateTransformContext mTransformContext;
77+
78+
//! how many vertices to place on one side of the tile
79+
int mResolution = 16;
80+
//! height of the "skirts" at the edges of tiles to hide cracks between adjacent cracks
81+
float mSkirtHeight = 10.f;
82+
83+
std::unique_ptr<QgsDemHeightMapGenerator> mHeightMapGenerator;
84+
};
85+
86+
#endif // QGSONLINETERRAINGENERATOR_H
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/***************************************************************************
2+
qgsterraindownloader.cpp
3+
--------------------------------------
4+
Date : March 2019
5+
Copyright : (C) 2019 by Martin Dobias
6+
Email : wonder dot sk at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgsterraindownloader.h"
17+
18+
#include "qgslogger.h"
19+
#include "qgsrasterlayer.h"
20+
21+
#include "qgsgdalutils.h"
22+
23+
24+
QgsTerrainDownloader::QgsTerrainDownloader()
25+
{
26+
setDataSource( defaultDataSource() );
27+
28+
// the whole world is projected to a square:
29+
// X going from 180 W to 180 E
30+
// Y going from ~85 N to ~85 S (=atan(sinh(pi)) ... to get a square)
31+
Q_NOWARN_DEPRECATED_PUSH
32+
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), QgsCoordinateReferenceSystem( "EPSG:3857" ) );
33+
Q_NOWARN_DEPRECATED_POP
34+
QgsPointXY topLeftLonLat( -180, 180.0 / M_PI * std::atan( std::sinh( M_PI ) ) );
35+
QgsPointXY bottomRightLonLat( 180, 180.0 / M_PI * std::atan( std::sinh( -M_PI ) ) );
36+
QgsPointXY topLeft = ct.transform( topLeftLonLat );
37+
QgsPointXY bottomRight = ct.transform( bottomRightLonLat );
38+
mXSpan = ( bottomRight.x() - topLeft.x() );
39+
}
40+
41+
QgsTerrainDownloader::~QgsTerrainDownloader() = default;
42+
43+
QgsTerrainDownloader::DataSource QgsTerrainDownloader::defaultDataSource()
44+
{
45+
// using terrain tiles stored on AWS and listed within Registry of Open Data on AWS
46+
// see https://registry.opendata.aws/terrain-tiles/
47+
//
48+
// tiles are generated using a variety of sources (SRTM, ETOPO1 and more detailed data for some countries)
49+
// for more details and attribution see https://github.com/tilezen/joerd/blob/master/docs/data-sources.md
50+
51+
DataSource ds;
52+
ds.uri = "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png";
53+
ds.zMin = 0;
54+
ds.zMax = 15;
55+
return ds;
56+
}
57+
58+
void QgsTerrainDownloader::setDataSource( const QgsTerrainDownloader::DataSource &ds )
59+
{
60+
mDataSource = ds;
61+
QString uri = QString( "type=xyz&url=%1&zmin=%2&zmax=%3" ).arg( mDataSource.uri ).arg( mDataSource.zMin ).arg( mDataSource.zMax );
62+
mOnlineDtm.reset( new QgsRasterLayer( uri, "terrarium", "wms" ) );
63+
}
64+
65+
66+
void QgsTerrainDownloader::adjustExtentAndResolution( double mupp, const QgsRectangle &extentOrig, QgsRectangle &extent, int &res )
67+
{
68+
double xMin = floor( extentOrig.xMinimum() / mupp ) * mupp;
69+
double xMax = ceil( extentOrig.xMaximum() / mupp ) * mupp;
70+
71+
double yMin = floor( extentOrig.yMinimum() / mupp ) * mupp;
72+
double yMax = ceil( extentOrig.yMaximum() / mupp ) * mupp;
73+
74+
extent = QgsRectangle( xMin, yMin, xMax, yMax );
75+
res = round( ( xMax - xMin ) / mupp );
76+
}
77+
78+
79+
double QgsTerrainDownloader::findBestTileResolution( double requestedMupp )
80+
{
81+
int zoom = 0;
82+
for ( ; zoom <= 15; ++zoom )
83+
{
84+
double tileMupp = mXSpan / ( 256 * ( 1 << zoom ) );
85+
if ( tileMupp <= requestedMupp )
86+
break;
87+
}
88+
89+
if ( zoom > 15 ) zoom = 15;
90+
double finalMupp = mXSpan / ( 256 * ( 1 << zoom ) );
91+
return finalMupp;
92+
}
93+
94+
95+
void QgsTerrainDownloader::tileImageToHeightMap( const QImage &img, QByteArray &heightMap )
96+
{
97+
// for description of the "terrarium" format:
98+
// https://github.com/tilezen/joerd/blob/master/docs/formats.md
99+
100+
// assuming ARGB premultiplied but with alpha 255
101+
const QRgb *rgb = reinterpret_cast<const QRgb *>( img.constBits() );
102+
int count = img.width() * img.height();
103+
heightMap.resize( sizeof( float ) * count );
104+
float *hData = reinterpret_cast<float *>( heightMap.data() );
105+
for ( int i = 0; i < count; ++i )
106+
{
107+
QRgb c = rgb[i];
108+
if ( qAlpha( c ) == 255 )
109+
{
110+
float h = qRed( c ) * 256 + qGreen( c ) + qBlue( c ) / 256.f - 32768;
111+
*hData++ = h;
112+
}
113+
else
114+
{
115+
*hData++ = std::numeric_limits<float>::quiet_NaN();
116+
}
117+
}
118+
}
119+
120+
121+
QByteArray QgsTerrainDownloader::getHeightMap( const QgsRectangle &extentOrig, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context, QString tmpFilenameImg, QString tmpFilenameTif )
122+
{
123+
if ( !mOnlineDtm || !mOnlineDtm->isValid() )
124+
{
125+
QgsDebugMsg( "missing a valid data source" );
126+
return QByteArray();
127+
}
128+
129+
QgsRectangle extentTr = extentOrig;
130+
if ( destCrs != mOnlineDtm->crs() )
131+
{
132+
// if in different CRS - need to reproject extent and resolution
133+
QgsCoordinateTransform ct( destCrs, mOnlineDtm->crs(), context );
134+
extentTr = ct.transformBoundingBox( extentOrig );
135+
}
136+
137+
double requestedMupp = extentTr.width() / res;
138+
double finalMupp = findBestTileResolution( requestedMupp );
139+
140+
// adjust extent to match native resolution of terrain tiles
141+
142+
QgsRectangle extent;
143+
int resOrig = res;
144+
adjustExtentAndResolution( finalMupp, extentTr, extent, res );
145+
146+
// request tile
147+
148+
QgsRasterBlock *b = mOnlineDtm->dataProvider()->block( 1, extent, res, res );
149+
QImage img = b->image();
150+
delete b;
151+
if ( !tmpFilenameImg.isEmpty() )
152+
img.save( tmpFilenameImg );
153+
154+
// convert to height data
155+
156+
QByteArray heightMap;
157+
tileImageToHeightMap( img, heightMap );
158+
159+
// prepare source/destination datasets for resampling
160+
161+
gdal::dataset_unique_ptr hSrcDS( QgsGdalUtils::createSingleBandMemoryDataset( GDT_Float32, extent, res, res, mOnlineDtm->crs() ) );
162+
gdal::dataset_unique_ptr hDstDS;
163+
if ( !tmpFilenameTif.isEmpty() )
164+
hDstDS = QgsGdalUtils::createSingleBandTiffDataset( tmpFilenameTif, GDT_Float32, extentOrig, resOrig, resOrig, destCrs );
165+
else
166+
hDstDS = QgsGdalUtils::createSingleBandMemoryDataset( GDT_Float32, extentOrig, resOrig, resOrig, destCrs );
167+
168+
if ( !hSrcDS || !hDstDS )
169+
{
170+
QgsDebugMsg( "failed to create GDAL dataset for heightmap" );
171+
return QByteArray();
172+
}
173+
174+
CPLErr err = GDALRasterIO( GDALGetRasterBand( hSrcDS.get(), 1 ), GF_Write, 0, 0, res, res, heightMap.data(), res, res, GDT_Float32, 0, 0 );
175+
if ( err != CE_None )
176+
{
177+
QgsDebugMsg( "failed to write heightmap data to GDAL dataset" );
178+
return QByteArray();
179+
}
180+
181+
// resample to the desired extent + resolution
182+
183+
QgsGdalUtils::resampleSingleBandRaster( hSrcDS.get(), hDstDS.get(), GRA_Bilinear );
184+
185+
QByteArray heightMapOut;
186+
heightMapOut.resize( resOrig * resOrig * sizeof( float ) );
187+
char *data = heightMapOut.data();
188+
189+
// read the data back
190+
191+
CPLErr err2 = GDALRasterIO( GDALGetRasterBand( hDstDS.get(), 1 ), GF_Read, 0, 0, resOrig, resOrig, data, resOrig, resOrig, GDT_Float32, 0, 0 );
192+
if ( err2 != CE_None )
193+
{
194+
QgsDebugMsg( "failed to read heightmap data from GDAL dataset" );
195+
return QByteArray();
196+
}
197+
198+
return heightMapOut;
199+
}

‎src/3d/terrain/qgsterraindownloader.h

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/***************************************************************************
2+
qgsterraindownloader.h
3+
--------------------------------------
4+
Date : March 2019
5+
Copyright : (C) 2019 by Martin Dobias
6+
Email : wonder dot sk at gmail dot com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#ifndef QGSTERRAINDOWNLOADER_H
17+
#define QGSTERRAINDOWNLOADER_H
18+
19+
#include "qgis_3d.h"
20+
21+
#include <memory>
22+
#include <QByteArray>
23+
#include <QImage>
24+
25+
#include "qgscoordinatetransformcontext.h"
26+
27+
class QgsRectangle;
28+
class QgsCoordinateReferenceSystem;
29+
class QgsRasterLayer;
30+
31+
/**
32+
* \ingroup 3d
33+
* Takes care of downloading terrain data from a publicly available data source.
34+
*
35+
* Currently using terrain tiles in Terrarium format hosted on AWS. More info:
36+
* - data format: https://github.com/tilezen/joerd/blob/master/docs/formats.md
37+
* - data sources: https://github.com/tilezen/joerd/blob/master/docs/data-sources.md
38+
* - hosting: https://registry.opendata.aws/terrain-tiles/
39+
*
40+
* \since QGIS 3.8
41+
*/
42+
class _3D_EXPORT QgsTerrainDownloader
43+
{
44+
45+
public:
46+
QgsTerrainDownloader();
47+
~QgsTerrainDownloader();
48+
49+
//! Definition of data source for terrain tiles (assuming "terrarium" data encoding with usual XYZ tiling scheme)
50+
typedef struct
51+
{
52+
QString uri; //!< HTTP(S) template for XYZ tiles requests (e.g. http://example.com/{z}/{x}/{y}.png)
53+
int zMin = 0; //!< Minimum zoom level (Z) with valid data
54+
int zMax = 0; //!< Maximum zoom level (Z) with valid data
55+
} DataSource;
56+
57+
//! Returns the data source used by default
58+
static DataSource defaultDataSource();
59+
60+
//! Configures data source to be used for download of terrain tiles
61+
void setDataSource( const DataSource &ds );
62+
63+
//! Returns currently configured data source
64+
DataSource dataSource() const { return mDataSource; }
65+
66+
/**
67+
* For given extent and resolution (number of pixels for width/height) in specified CRS, download necessary
68+
* tile images (if not cached already) and produce height map out of them (byte array of res*res float values)
69+
*/
70+
QByteArray getHeightMap( const QgsRectangle &extentOrig, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context = QgsCoordinateTransformContext(), QString tmpFilenameImg = QString(), QString tmpFilenameTif = QString() );
71+
72+
private:
73+
74+
/**
75+
* For the requested resolution given as map units per pixel, find out the best native tile resolution
76+
* (higher resolution = fewer map units per pixel)
77+
*/
78+
double findBestTileResolution( double requestedMupp );
79+
80+
/**
81+
* Given extent and map units per pixels, adjust the extent and resolution
82+
*/
83+
static void adjustExtentAndResolution( double mupp, const QgsRectangle &extentOrig, QgsRectangle &extent, int &res );
84+
85+
/**
86+
* Takes an image tile with heights encoded using "terrarium" encoding and converts it to
87+
* an array of 32-bit floats with decoded elevations.
88+
*/
89+
static void tileImageToHeightMap( const QImage &img, QByteArray &heightMap );
90+
91+
private:
92+
DataSource mDataSource;
93+
std::unique_ptr<QgsRasterLayer> mOnlineDtm;
94+
double mXSpan = 0; //!< Width of the tile at zoom level 0 in map units
95+
};
96+
97+
#endif // QGSTERRAINDOWNLOADER_H

‎src/3d/terrain/qgsterraingenerator.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ QString QgsTerrainGenerator::typeToString( QgsTerrainGenerator::Type type )
6464
return QStringLiteral( "flat" );
6565
case QgsTerrainGenerator::Dem:
6666
return QStringLiteral( "dem" );
67-
case QgsTerrainGenerator::QuantizedMesh:
68-
return QStringLiteral( "quantized-mesh" );
67+
case QgsTerrainGenerator::Online:
68+
return QStringLiteral( "online" );
6969
}
7070
return QString();
7171
}

‎src/3d/terrain/qgsterraingenerator.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class _3D_EXPORT QgsTerrainGenerator : public QgsChunkLoaderFactory
4848
{
4949
Flat, //!< The whole terrain is flat area
5050
Dem, //!< Terrain is built from raster layer with digital elevation model
51-
QuantizedMesh, //!< Terrain is built from downloaded tiles in quantized mesh format
51+
Online, //!< Terrain is built from downloaded tiles with digital elevation model
5252
};
5353

5454
//! Sets terrain entity for the generator (does not transfer ownership)

‎src/app/3d/qgs3dmapconfigwidget.cpp

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "qgs3dmapsettings.h"
1919
#include "qgsdemterraingenerator.h"
2020
#include "qgsflatterraingenerator.h"
21+
#include "qgsonlineterraingenerator.h"
2122
#include "qgs3dutils.h"
2223

2324
#include "qgsmapcanvas.h"
@@ -46,18 +47,30 @@ Qgs3DMapConfigWidget::Qgs3DMapConfigWidget( Qgs3DMapSettings *map, QgsMapCanvas
4647
cboTerrainLayer->setAllowEmptyLayer( true );
4748
cboTerrainLayer->setFilters( QgsMapLayerProxyModel::RasterLayer );
4849

50+
cboTerrainType->addItem( tr( "Flat terrain" ), QgsTerrainGenerator::Flat );
51+
cboTerrainType->addItem( tr( "DEM (Raster layer)" ), QgsTerrainGenerator::Dem );
52+
cboTerrainType->addItem( tr( "Online" ), QgsTerrainGenerator::Online );
53+
4954
QgsTerrainGenerator *terrainGen = mMap->terrainGenerator();
5055
if ( terrainGen && terrainGen->type() == QgsTerrainGenerator::Dem )
5156
{
57+
cboTerrainType->setCurrentIndex( cboTerrainType->findData( QgsTerrainGenerator::Dem ) );
5258
QgsDemTerrainGenerator *demTerrainGen = static_cast<QgsDemTerrainGenerator *>( terrainGen );
5359
spinTerrainResolution->setValue( demTerrainGen->resolution() );
5460
spinTerrainSkirtHeight->setValue( demTerrainGen->skirtHeight() );
5561
cboTerrainLayer->setLayer( demTerrainGen->layer() );
5662
}
63+
else if ( terrainGen && terrainGen->type() == QgsTerrainGenerator::Online )
64+
{
65+
cboTerrainType->setCurrentIndex( cboTerrainType->findData( QgsTerrainGenerator::Online ) );
66+
QgsOnlineTerrainGenerator *onlineTerrainGen = static_cast<QgsOnlineTerrainGenerator *>( terrainGen );
67+
spinTerrainResolution->setValue( onlineTerrainGen->resolution() );
68+
spinTerrainSkirtHeight->setValue( onlineTerrainGen->skirtHeight() );
69+
}
5770
else
5871
{
72+
cboTerrainType->setCurrentIndex( cboTerrainType->findData( QgsTerrainGenerator::Flat ) );
5973
cboTerrainLayer->setLayer( nullptr );
60-
spinTerrainResolution->setEnabled( false );
6174
spinTerrainResolution->setValue( 16 );
6275
spinTerrainSkirtHeight->setValue( 10 );
6376
}
@@ -86,21 +99,24 @@ Qgs3DMapConfigWidget::Qgs3DMapConfigWidget( Qgs3DMapSettings *map, QgsMapCanvas
8699

87100
widgetLights->setPointLights( mMap->pointLights() );
88101

102+
connect( cboTerrainType, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &Qgs3DMapConfigWidget::onTerrainTypeChanged );
89103
connect( cboTerrainLayer, static_cast<void ( QComboBox::* )( int )>( &QgsMapLayerComboBox::currentIndexChanged ), this, &Qgs3DMapConfigWidget::onTerrainLayerChanged );
90104
connect( spinMapResolution, static_cast<void ( QSpinBox::* )( int )>( &QSpinBox::valueChanged ), this, &Qgs3DMapConfigWidget::updateMaxZoomLevel );
91105
connect( spinGroundError, static_cast<void ( QDoubleSpinBox::* )( double )>( &QDoubleSpinBox::valueChanged ), this, &Qgs3DMapConfigWidget::updateMaxZoomLevel );
92106

93-
updateMaxZoomLevel();
107+
onTerrainTypeChanged();
94108
}
95109

96110
void Qgs3DMapConfigWidget::apply()
97111
{
98-
QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() );
99-
100112
bool needsUpdateOrigin = false;
101113

102-
if ( demLayer )
114+
QgsTerrainGenerator::Type terrainType = static_cast<QgsTerrainGenerator::Type>( cboTerrainType->currentData().toInt() );
115+
116+
if ( terrainType == QgsTerrainGenerator::Dem ) // DEM from raster layer
103117
{
118+
QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() );
119+
104120
bool tGenNeedsUpdate = true;
105121
if ( mMap->terrainGenerator()->type() == QgsTerrainGenerator::Dem )
106122
{
@@ -123,7 +139,29 @@ void Qgs3DMapConfigWidget::apply()
123139
needsUpdateOrigin = true;
124140
}
125141
}
126-
else if ( !demLayer && mMap->terrainGenerator()->type() != QgsTerrainGenerator::Flat )
142+
else if ( terrainType == QgsTerrainGenerator::Online )
143+
{
144+
bool tGenNeedsUpdate = true;
145+
if ( mMap->terrainGenerator()->type() == QgsTerrainGenerator::Online )
146+
{
147+
QgsOnlineTerrainGenerator *oldOnlineTerrainGen = static_cast<QgsOnlineTerrainGenerator *>( mMap->terrainGenerator() );
148+
if ( oldOnlineTerrainGen->resolution() == spinTerrainResolution->value() &&
149+
oldOnlineTerrainGen->skirtHeight() == spinTerrainSkirtHeight->value() )
150+
tGenNeedsUpdate = false;
151+
}
152+
153+
if ( tGenNeedsUpdate )
154+
{
155+
QgsOnlineTerrainGenerator *onlineTerrainGen = new QgsOnlineTerrainGenerator;
156+
onlineTerrainGen->setCrs( mMap->crs(), QgsProject::instance()->transformContext() );
157+
onlineTerrainGen->setExtent( mMainCanvas->fullExtent() );
158+
onlineTerrainGen->setResolution( spinTerrainResolution->value() );
159+
onlineTerrainGen->setSkirtHeight( spinTerrainSkirtHeight->value() );
160+
mMap->setTerrainGenerator( onlineTerrainGen );
161+
needsUpdateOrigin = true;
162+
}
163+
}
164+
else if ( terrainType == QgsTerrainGenerator::Flat )
127165
{
128166
QgsFlatTerrainGenerator *flatTerrainGen = new QgsFlatTerrainGenerator;
129167
flatTerrainGen->setCrs( mMap->crs() );
@@ -161,33 +199,44 @@ void Qgs3DMapConfigWidget::apply()
161199
mMap->setPointLights( widgetLights->pointLights() );
162200
}
163201

202+
void Qgs3DMapConfigWidget::onTerrainTypeChanged()
203+
{
204+
bool isFlat = cboTerrainType->currentIndex() == 0;
205+
bool isDem = cboTerrainType->currentIndex() == 1;
206+
labelTerrainResolution->setVisible( !isFlat );
207+
spinTerrainResolution->setVisible( !isFlat );
208+
labelTerrainSkirtHeight->setVisible( !isFlat );
209+
spinTerrainSkirtHeight->setVisible( !isFlat );
210+
labelTerrainLayer->setVisible( isDem );
211+
cboTerrainLayer->setVisible( isDem );
212+
213+
updateMaxZoomLevel();
214+
}
215+
164216
void Qgs3DMapConfigWidget::onTerrainLayerChanged()
165217
{
166-
spinTerrainResolution->setEnabled( cboTerrainLayer->currentLayer() );
218+
updateMaxZoomLevel();
167219
}
168220

169221
void Qgs3DMapConfigWidget::updateMaxZoomLevel()
170222
{
171-
// TODO: tidy up, less duplication with apply()
172-
std::unique_ptr<QgsTerrainGenerator> tGen;
173-
QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() );
174-
if ( demLayer )
223+
QgsRectangle te;
224+
QgsTerrainGenerator::Type terrainType = static_cast<QgsTerrainGenerator::Type>( cboTerrainType->currentData().toInt() );
225+
if ( terrainType == QgsTerrainGenerator::Dem )
175226
{
176-
QgsDemTerrainGenerator *demTerrainGen = new QgsDemTerrainGenerator;
177-
demTerrainGen->setCrs( mMap->crs(), QgsProject::instance()->transformContext() );
178-
demTerrainGen->setLayer( demLayer );
179-
demTerrainGen->setResolution( spinTerrainResolution->value() );
180-
tGen.reset( demTerrainGen );
227+
if ( QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() ) )
228+
{
229+
te = demLayer->extent();
230+
QgsCoordinateTransform terrainToMapTransform( demLayer->crs(), mMap->crs(), QgsProject::instance()->transformContext() );
231+
te = terrainToMapTransform.transformBoundingBox( te );
232+
}
181233
}
182-
else
234+
else // flat or online
183235
{
184-
QgsFlatTerrainGenerator *flatTerrainGen = new QgsFlatTerrainGenerator;
185-
flatTerrainGen->setCrs( mMap->crs() );
186-
flatTerrainGen->setExtent( mMainCanvas->fullExtent() );
187-
tGen.reset( flatTerrainGen );
236+
te = mMainCanvas->fullExtent();
188237
}
189238

190-
double tile0width = tGen->extent().width();
239+
double tile0width = std::max( te.width(), te.height() );
191240
int zoomLevel = Qgs3DUtils::maxZoomLevel( tile0width, spinMapResolution->value(), spinGroundError->value() );
192241
labelZoomLevels->setText( QStringLiteral( "0 - %1" ).arg( zoomLevel ) );
193242
}

‎src/app/3d/qgs3dmapconfigwidget.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class Qgs3DMapConfigWidget : public QWidget, private Ui::Map3DConfigWidget
3737
signals:
3838

3939
private slots:
40+
void onTerrainTypeChanged();
4041
void onTerrainLayerChanged();
4142
void updateMaxZoomLevel();
4243

‎src/core/qgsgdalutils.cpp

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,89 @@ bool QgsGdalUtils::supportsRasterCreate( GDALDriverH driver )
3333
return CSLFetchBoolean( driverMetadata, GDAL_DCAP_CREATE, false ) &&
3434
CSLFetchBoolean( driverMetadata, GDAL_DCAP_RASTER, false );
3535
}
36+
37+
gdal::dataset_unique_ptr QgsGdalUtils::createSingleBandMemoryDataset( GDALDataType dataType, QgsRectangle extent, int width, int height, const QgsCoordinateReferenceSystem &crs )
38+
{
39+
GDALDriverH hDriverMem = GDALGetDriverByName( "MEM" );
40+
if ( !hDriverMem )
41+
{
42+
return gdal::dataset_unique_ptr();
43+
}
44+
45+
gdal::dataset_unique_ptr hSrcDS( GDALCreate( hDriverMem, "", width, height, 1, dataType, nullptr ) );
46+
47+
double cellSizeX = extent.width() / width;
48+
double cellSizeY = extent.height() / height;
49+
double geoTransform[6];
50+
geoTransform[0] = extent.xMinimum();
51+
geoTransform[1] = cellSizeX;
52+
geoTransform[2] = 0;
53+
geoTransform[3] = extent.yMinimum() + ( cellSizeY * height );
54+
geoTransform[4] = 0;
55+
geoTransform[5] = -cellSizeY;
56+
57+
GDALSetProjection( hSrcDS.get(), crs.toWkt().toLatin1().constData() );
58+
GDALSetGeoTransform( hSrcDS.get(), geoTransform );
59+
return hSrcDS;
60+
}
61+
62+
gdal::dataset_unique_ptr QgsGdalUtils::createSingleBandTiffDataset( QString filename, GDALDataType dataType, QgsRectangle extent, int width, int height, const QgsCoordinateReferenceSystem &crs )
63+
{
64+
double cellSizeX = extent.width() / width;
65+
double cellSizeY = extent.height() / height;
66+
double geoTransform[6];
67+
geoTransform[0] = extent.xMinimum();
68+
geoTransform[1] = cellSizeX;
69+
geoTransform[2] = 0;
70+
geoTransform[3] = extent.yMinimum() + ( cellSizeY * height );
71+
geoTransform[4] = 0;
72+
geoTransform[5] = -cellSizeY;
73+
74+
GDALDriverH hDriver = GDALGetDriverByName( "GTiff" );
75+
if ( !hDriver )
76+
{
77+
return gdal::dataset_unique_ptr();
78+
}
79+
80+
// Create the output file.
81+
gdal::dataset_unique_ptr hDstDS( GDALCreate( hDriver, filename.toLocal8Bit().constData(), width, height, 1, dataType, nullptr ) );
82+
if ( !hDstDS )
83+
{
84+
return gdal::dataset_unique_ptr();
85+
}
86+
87+
// Write out the projection definition.
88+
GDALSetProjection( hDstDS.get(), crs.toWkt().toLatin1().constData() );
89+
GDALSetGeoTransform( hDstDS.get(), geoTransform );
90+
return hDstDS;
91+
}
92+
93+
void QgsGdalUtils::resampleSingleBandRaster( GDALDatasetH hSrcDS, GDALDatasetH hDstDS, GDALResampleAlg resampleAlg )
94+
{
95+
gdal::warp_options_unique_ptr psWarpOptions( GDALCreateWarpOptions() );
96+
psWarpOptions->hSrcDS = hSrcDS;
97+
psWarpOptions->hDstDS = hDstDS;
98+
99+
psWarpOptions->nBandCount = 1;
100+
psWarpOptions->panSrcBands = ( int * ) CPLMalloc( sizeof( int ) * 1 );
101+
psWarpOptions->panDstBands = ( int * ) CPLMalloc( sizeof( int ) * 1 );
102+
psWarpOptions->panSrcBands[0] = 1;
103+
psWarpOptions->panDstBands[0] = 1;
104+
105+
psWarpOptions->eResampleAlg = resampleAlg;
106+
107+
// Establish reprojection transformer.
108+
psWarpOptions->pTransformerArg =
109+
GDALCreateGenImgProjTransformer( hSrcDS, GDALGetProjectionRef( hSrcDS ),
110+
hDstDS, GDALGetProjectionRef( hDstDS ),
111+
FALSE, 0.0, 1 );
112+
psWarpOptions->pfnTransformer = GDALGenImgProjTransform;
113+
114+
// Initialize and execute the warp operation.
115+
GDALWarpOperation oOperation;
116+
oOperation.Initialize( psWarpOptions.get() );
117+
118+
oOperation.ChunkAndWarpImage( 0, 0, GDALGetRasterXSize( hDstDS ), GDALGetRasterYSize( hDstDS ) );
119+
120+
GDALDestroyGenImgProjTransformer( psWarpOptions->pTransformerArg );
121+
}

‎src/core/qgsgdalutils.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include "qgis_core.h"
2222
#include <gdal.h>
2323

24+
#include "qgsogrutils.h"
25+
2426
/**
2527
* \ingroup core
2628
* \class QgsGdalUtils
@@ -39,6 +41,25 @@ class CORE_EXPORT QgsGdalUtils
3941
* \returns TRUE if a driver supports GDALCreate() for raster purposes.
4042
*/
4143
static bool supportsRasterCreate( GDALDriverH driver );
44+
45+
/**
46+
* Creates a new single band memory dataset with given parameters
47+
* \since QGIS 3.8
48+
*/
49+
static gdal::dataset_unique_ptr createSingleBandMemoryDataset( GDALDataType dataType, QgsRectangle extent, int width, int height, const QgsCoordinateReferenceSystem &crs );
50+
51+
/**
52+
* Creates a new single band TIFF dataset with given parameters
53+
* \since QGIS 3.8
54+
*/
55+
static gdal::dataset_unique_ptr createSingleBandTiffDataset( QString filename, GDALDataType dataType, QgsRectangle extent, int width, int height, const QgsCoordinateReferenceSystem &crs );
56+
57+
/**
58+
* Resamples a single band raster to the destination dataset with different resolution (and possibly with different CRS).
59+
* Ideally the source dataset should cover the whole area or the destination dataset.
60+
* \since QGIS 3.8
61+
*/
62+
static void resampleSingleBandRaster( GDALDatasetH hSrcDS, GDALDatasetH hDstDS, GDALResampleAlg resampleAlg );
4263
};
4364

4465
#endif // QGSGDALUTILS_H

‎src/ui/3d/map3dconfigwidget.ui

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
<rect>
77
<x>0</x>
88
<y>0</y>
9-
<width>465</width>
10-
<height>565</height>
9+
<width>623</width>
10+
<height>661</height>
1111
</rect>
1212
</property>
1313
<property name="windowTitle">
1414
<string>Configure 3D Map Rendering</string>
15-
</property>
15+
</property>
1616
<layout class="QVBoxLayout" name="verticalLayout">
1717
<item>
1818
<widget class="QgsScrollArea" name="scrollArea">
@@ -23,9 +23,9 @@
2323
<property name="geometry">
2424
<rect>
2525
<x>0</x>
26-
<y>-303</y>
26+
<y>0</y>
2727
<width>557</width>
28-
<height>1004</height>
28+
<height>1298</height>
2929
</rect>
3030
</property>
3131
<layout class="QVBoxLayout" name="verticalInnerLayout">
@@ -47,9 +47,6 @@
4747
<property name="suffix">
4848
<string>°</string>
4949
</property>
50-
<property name="maximum">
51-
<number>0</number>
52-
</property>
5350
<property name="maximum">
5451
<number>180</number>
5552
</property>
@@ -64,38 +61,47 @@
6461
<string>Terrain</string>
6562
</property>
6663
<layout class="QGridLayout" name="gridLayout1">
67-
<item row="2" column="0">
68-
<widget class="QLabel" name="label_10">
64+
<item row="1" column="0">
65+
<widget class="QLabel" name="labelTerrainLayer">
6966
<property name="text">
70-
<string>Tile resolution</string>
67+
<string>Elevation</string>
7168
</property>
7269
</widget>
7370
</item>
74-
<item row="0" column="0">
75-
<widget class="QLabel" name="label">
71+
<item row="4" column="0">
72+
<widget class="QLabel" name="labelTerrainSkirtHeight">
7673
<property name="text">
77-
<string>Elevation</string>
74+
<string>Skirt height</string>
7875
</property>
7976
</widget>
8077
</item>
81-
<item row="0" column="1" colspan="2">
78+
<item row="1" column="1" colspan="2">
8279
<widget class="QgsMapLayerComboBox" name="cboTerrainLayer"/>
8380
</item>
84-
<item row="1" column="0">
85-
<widget class="QLabel" name="label_2">
86-
<property name="text">
87-
<string>Vertical scale</string>
81+
<item row="4" column="1" colspan="2">
82+
<widget class="QgsDoubleSpinBox" name="spinTerrainSkirtHeight">
83+
<property name="suffix">
84+
<string> map units</string>
85+
</property>
86+
<property name="decimals">
87+
<number>1</number>
88+
</property>
89+
<property name="maximum">
90+
<double>10000.000000000000000</double>
91+
</property>
92+
<property name="singleStep">
93+
<double>10.000000000000000</double>
8894
</property>
8995
</widget>
9096
</item>
91-
<item row="1" column="1" colspan="2">
92-
<widget class="QgsDoubleSpinBox" name="spinTerrainScale">
93-
<property name="value">
94-
<double>1.000000000000000</double>
97+
<item row="3" column="0">
98+
<widget class="QLabel" name="labelTerrainResolution">
99+
<property name="text">
100+
<string>Tile resolution</string>
95101
</property>
96102
</widget>
97103
</item>
98-
<item row="2" column="1" colspan="2">
104+
<item row="3" column="1" colspan="2">
99105
<widget class="QgsSpinBox" name="spinTerrainResolution">
100106
<property name="suffix">
101107
<string> px</string>
@@ -105,43 +111,44 @@
105111
</property>
106112
</widget>
107113
</item>
108-
<item row="3" column="0">
109-
<widget class="QLabel" name="label_8">
114+
<item row="5" column="0">
115+
<widget class="QLabel" name="labelTerrainMapTheme">
110116
<property name="text">
111-
<string>Skirt height</string>
117+
<string>Map theme</string>
112118
</property>
113119
</widget>
114120
</item>
115-
<item row="3" column="1" colspan="2">
116-
<widget class="QgsDoubleSpinBox" name="spinTerrainSkirtHeight">
117-
<property name="suffix">
118-
<string> map units</string>
119-
</property>
120-
<property name="decimals">
121-
<number>1</number>
122-
</property>
123-
<property name="maximum">
124-
<double>10000.000000000000000</double>
125-
</property>
126-
<property name="singleStep">
127-
<double>10.000000000000000</double>
121+
<item row="2" column="0">
122+
<widget class="QLabel" name="labelTerrainScale">
123+
<property name="text">
124+
<string>Vertical scale</string>
128125
</property>
129126
</widget>
130127
</item>
131-
<item row="4" column="0">
132-
<widget class="QLabel" name="label_9">
133-
<property name="text">
134-
<string>Map theme</string>
128+
<item row="2" column="1" colspan="2">
129+
<widget class="QgsDoubleSpinBox" name="spinTerrainScale">
130+
<property name="value">
131+
<double>1.000000000000000</double>
135132
</property>
136133
</widget>
137134
</item>
138-
<item row="4" column="1" colspan="2">
135+
<item row="5" column="1" colspan="2">
139136
<widget class="QComboBox" name="cboTerrainMapTheme">
140137
<property name="editable">
141138
<bool>false</bool>
142139
</property>
143140
</widget>
144141
</item>
142+
<item row="0" column="0">
143+
<widget class="QLabel" name="labelTerrainType">
144+
<property name="text">
145+
<string>Type</string>
146+
</property>
147+
</widget>
148+
</item>
149+
<item row="0" column="1" colspan="2">
150+
<widget class="QComboBox" name="cboTerrainType"/>
151+
</item>
145152
</layout>
146153
</widget>
147154
</item>
@@ -339,6 +346,9 @@
339346
</customwidget>
340347
</customwidgets>
341348
<tabstops>
349+
<tabstop>scrollArea</tabstop>
350+
<tabstop>spinCameraFieldOfView</tabstop>
351+
<tabstop>cboTerrainType</tabstop>
342352
<tabstop>cboTerrainLayer</tabstop>
343353
<tabstop>spinTerrainScale</tabstop>
344354
<tabstop>spinTerrainResolution</tabstop>

‎tests/src/core/testqgsgdalutils.cpp

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
#include "qgsgdalutils.h"
2424
#include "qgsapplication.h"
25+
#include "qgsrasterlayer.h"
26+
2527

2628
class TestQgsGdalUtils: public QObject
2729
{
@@ -33,6 +35,9 @@ class TestQgsGdalUtils: public QObject
3335
void init();// will be called before each testfunction is executed.
3436
void cleanup();// will be called after every testfunction.
3537
void supportsRasterCreate();
38+
void testCreateSingleBandMemoryDataset();
39+
void testCreateSingleBandTiffDataset();
40+
void testResampleSingleBandRaster();
3641

3742
private:
3843
};
@@ -74,5 +79,88 @@ void TestQgsGdalUtils::supportsRasterCreate()
7479
QVERIFY( !QgsGdalUtils::supportsRasterCreate( GDALGetDriverByName( "ESRI Shapefile" ) ) );
7580
}
7681

82+
#define EPSG_4326_WKT \
83+
"GEOGCS[\"WGS 84\",DATUM[\"WGS_1984\",SPHEROID[\"WGS 84\",6378137,298.257223563,AUTHORITY[\"EPSG\",\"7030\"]]," \
84+
"AUTHORITY[\"EPSG\",\"6326\"]],PRIMEM[\"Greenwich\",0,AUTHORITY[\"EPSG\",\"8901\"]],UNIT[\"degree\",0.0174532925199433," \
85+
"AUTHORITY[\"EPSG\",\"9122\"]],AUTHORITY[\"EPSG\",\"4326\"]]"
86+
87+
void TestQgsGdalUtils::testCreateSingleBandMemoryDataset()
88+
{
89+
gdal::dataset_unique_ptr ds1 = QgsGdalUtils::createSingleBandMemoryDataset( GDT_Float32, QgsRectangle( 1, 1, 21, 11 ), 40, 20, QgsCoordinateReferenceSystem( "EPSG:4326" ) );
90+
QVERIFY( ds1 );
91+
92+
QCOMPARE( GDALGetRasterCount( ds1.get() ), 1 );
93+
QCOMPARE( GDALGetRasterXSize( ds1.get() ), 40 );
94+
QCOMPARE( GDALGetRasterYSize( ds1.get() ), 20 );
95+
96+
QCOMPARE( GDALGetProjectionRef( ds1.get() ), EPSG_4326_WKT );
97+
double geoTransform[6];
98+
double geoTransformExpected[] = { 1, 0.5, 0, 11, 0, -0.5 };
99+
QCOMPARE( GDALGetGeoTransform( ds1.get(), geoTransform ), CE_None );
100+
QVERIFY( memcmp( geoTransform, geoTransformExpected, sizeof( double ) * 6 ) == 0 );
101+
102+
QCOMPARE( GDALGetRasterDataType( GDALGetRasterBand( ds1.get(), 1 ) ), GDT_Float32 );
103+
}
104+
105+
void TestQgsGdalUtils::testCreateSingleBandTiffDataset()
106+
{
107+
QString filename = QDir::tempPath() + "/qgis_test_single_band_raster.tif";
108+
QFile::remove( filename );
109+
QVERIFY( !QFile::exists( filename ) );
110+
111+
gdal::dataset_unique_ptr ds1 = QgsGdalUtils::createSingleBandTiffDataset( filename, GDT_Float32, QgsRectangle( 1, 1, 21, 11 ), 40, 20, QgsCoordinateReferenceSystem( "EPSG:4326" ) );
112+
QVERIFY( ds1 );
113+
114+
QCOMPARE( GDALGetRasterCount( ds1.get() ), 1 );
115+
QCOMPARE( GDALGetRasterXSize( ds1.get() ), 40 );
116+
QCOMPARE( GDALGetRasterYSize( ds1.get() ), 20 );
117+
118+
QCOMPARE( GDALGetProjectionRef( ds1.get() ), EPSG_4326_WKT );
119+
double geoTransform[6];
120+
double geoTransformExpected[] = { 1, 0.5, 0, 11, 0, -0.5 };
121+
QCOMPARE( GDALGetGeoTransform( ds1.get(), geoTransform ), CE_None );
122+
QVERIFY( memcmp( geoTransform, geoTransformExpected, sizeof( double ) * 6 ) == 0 );
123+
124+
QCOMPARE( GDALGetRasterDataType( GDALGetRasterBand( ds1.get(), 1 ) ), GDT_Float32 );
125+
126+
ds1.reset(); // makes sure the file is fully written
127+
128+
QVERIFY( QFile::exists( filename ) );
129+
130+
std::unique_ptr<QgsRasterLayer> layer( new QgsRasterLayer( filename, "test", "gdal" ) );
131+
QVERIFY( layer->isValid() );
132+
QCOMPARE( layer->extent(), QgsRectangle( 1, 1, 21, 11 ) );
133+
QCOMPARE( layer->width(), 40 );
134+
QCOMPARE( layer->height(), 20 );
135+
136+
layer.reset(); // let's clean up before removing the file
137+
QFile::remove( filename );
138+
}
139+
140+
void TestQgsGdalUtils::testResampleSingleBandRaster()
141+
{
142+
QString inputFilename = QString( TEST_DATA_DIR ) + "/float1-16.tif";
143+
gdal::dataset_unique_ptr srcDS( GDALOpen( inputFilename.toUtf8().constData(), GA_ReadOnly ) );
144+
QVERIFY( srcDS );
145+
146+
QString outputFilename = QDir::tempPath() + "/qgis_test_float1-16_resampled.tif";
147+
QgsRectangle outputExtent( 106.25, -6.75, 106.55, -6.45 );
148+
gdal::dataset_unique_ptr dstDS = QgsGdalUtils::createSingleBandTiffDataset( outputFilename, GDT_Float32, outputExtent, 2, 2, QgsCoordinateReferenceSystem( "EPSG:4326" ) );
149+
QVERIFY( dstDS );
150+
151+
QgsGdalUtils::resampleSingleBandRaster( srcDS.get(), dstDS.get(), GRA_NearestNeighbour );
152+
dstDS.reset();
153+
154+
std::unique_ptr<QgsRasterLayer> layer( new QgsRasterLayer( outputFilename, "test", "gdal" ) );
155+
QVERIFY( layer );
156+
std::unique_ptr<QgsRasterBlock> block( layer->dataProvider()->block( 1, outputExtent, 2, 2 ) );
157+
QVERIFY( block );
158+
QCOMPARE( block->value( 0, 0 ), 6. );
159+
QCOMPARE( block->value( 1, 1 ), 11. );
160+
161+
layer.reset();
162+
QFile::remove( outputFilename );
163+
}
164+
77165
QGSTEST_MAIN( TestQgsGdalUtils )
78166
#include "testqgsgdalutils.moc"

0 commit comments

Comments
 (0)
Please sign in to comment.