Skip to content

Commit c9ab24f

Browse files
committedMar 18, 2019
[FEATURE][3d] Add option to use terrain data from online service
This adds support for elevation tiles (using web mercator tiling) in "terrarium" format produced by Mapzen tools and publicly hosted by AWS. Terrain tiles are downloaded just like ordinary XYZ tiles, then the elevations are decoded from RGB colors and finally resampled to whatever terrain tile resolution and CRS is used by the project.
1 parent 8b621b6 commit c9ab24f

15 files changed

+715
-67
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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
#include "qgsonlineterraingenerator.h"
2+
3+
#include "qgsdemterraintileloader_p.h"
4+
5+
6+
QgsOnlineTerrainGenerator::~QgsOnlineTerrainGenerator()
7+
{
8+
delete mHeightMapGenerator;
9+
}
10+
11+
QgsChunkLoader *QgsOnlineTerrainGenerator::createChunkLoader( QgsChunkNode *node ) const
12+
{
13+
return new QgsDemTerrainTileLoader( mTerrain, node );
14+
}
15+
16+
QgsTerrainGenerator *QgsOnlineTerrainGenerator::clone() const
17+
{
18+
QgsOnlineTerrainGenerator *cloned = new QgsOnlineTerrainGenerator;
19+
cloned->mCrs = mCrs;
20+
cloned->mExtent = mExtent;
21+
cloned->mResolution = mResolution;
22+
cloned->mSkirtHeight = mSkirtHeight;
23+
cloned->updateGenerator();
24+
return cloned;
25+
}
26+
27+
QgsTerrainGenerator::Type QgsOnlineTerrainGenerator::type() const
28+
{
29+
return QgsTerrainGenerator::Online;
30+
}
31+
32+
QgsRectangle QgsOnlineTerrainGenerator::extent() const
33+
{
34+
return mTerrainTilingScheme.tileToExtent( 0, 0, 0 );
35+
}
36+
37+
float QgsOnlineTerrainGenerator::heightAt( double x, double y, const Qgs3DMapSettings &map ) const
38+
{
39+
Q_UNUSED( map );
40+
if ( mHeightMapGenerator )
41+
return mHeightMapGenerator->heightAt( x, y );
42+
else
43+
return 0;
44+
}
45+
46+
void QgsOnlineTerrainGenerator::writeXml( QDomElement &elem ) const
47+
{
48+
QgsRectangle r = mExtent;
49+
QDomElement elemExtent = elem.ownerDocument().createElement( QStringLiteral( "extent" ) );
50+
elemExtent.setAttribute( QStringLiteral( "xmin" ), QString::number( r.xMinimum() ) );
51+
elemExtent.setAttribute( QStringLiteral( "xmax" ), QString::number( r.xMaximum() ) );
52+
elemExtent.setAttribute( QStringLiteral( "ymin" ), QString::number( r.yMinimum() ) );
53+
elemExtent.setAttribute( QStringLiteral( "ymax" ), QString::number( r.yMaximum() ) );
54+
55+
elem.setAttribute( QStringLiteral( "resolution" ), mResolution );
56+
elem.setAttribute( QStringLiteral( "skirt-height" ), mSkirtHeight );
57+
58+
// crs is not read/written - it should be the same as destination crs of the map
59+
}
60+
61+
void QgsOnlineTerrainGenerator::readXml( const QDomElement &elem )
62+
{
63+
QDomElement elemExtent = elem.firstChildElement( QStringLiteral( "extent" ) );
64+
double xmin = elemExtent.attribute( QStringLiteral( "xmin" ) ).toDouble();
65+
double xmax = elemExtent.attribute( QStringLiteral( "xmax" ) ).toDouble();
66+
double ymin = elemExtent.attribute( QStringLiteral( "ymin" ) ).toDouble();
67+
double ymax = elemExtent.attribute( QStringLiteral( "ymax" ) ).toDouble();
68+
69+
setExtent( QgsRectangle( xmin, ymin, xmax, ymax ) );
70+
71+
mResolution = elem.attribute( QStringLiteral( "resolution" ) ).toInt();
72+
mSkirtHeight = elem.attribute( QStringLiteral( "skirt-height" ) ).toFloat();
73+
74+
// crs is not read/written - it should be the same as destination crs of the map
75+
}
76+
77+
void QgsOnlineTerrainGenerator::setCrs( const QgsCoordinateReferenceSystem &crs, const QgsCoordinateTransformContext &context )
78+
{
79+
mCrs = crs;
80+
mTransformContext = context;
81+
updateGenerator();
82+
}
83+
84+
void QgsOnlineTerrainGenerator::setExtent( const QgsRectangle &extent )
85+
{
86+
mExtent = extent;
87+
updateGenerator();
88+
}
89+
90+
void QgsOnlineTerrainGenerator::updateGenerator()
91+
{
92+
if ( mExtent.isNull() )
93+
{
94+
mTerrainTilingScheme = QgsTilingScheme();
95+
}
96+
else
97+
{
98+
// the real extent will be a square where the given extent fully fits
99+
mTerrainTilingScheme = QgsTilingScheme( mExtent, mCrs );
100+
}
101+
102+
delete mHeightMapGenerator;
103+
mHeightMapGenerator = new QgsDemHeightMapGenerator( nullptr, mTerrainTilingScheme, mResolution );
104+
}
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() = default;
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; }
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+
QgsDemHeightMapGenerator *mHeightMapGenerator = nullptr;
84+
};
85+
86+
#endif // QGSONLINETERRAINGENERATOR_H
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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+
QString uri = "type=xyz&url=http://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png&zmax=15&zmin=0";
27+
onlineDtm.reset( new QgsRasterLayer( uri, "terrarium", "wms" ) );
28+
29+
// the whole world is projected to a square:
30+
// X going from 180 W to 180 E
31+
// Y going from ~85 N to ~85 S (=atan(sinh(pi)) ... to get a square)
32+
Q_NOWARN_DEPRECATED_PUSH
33+
QgsCoordinateTransform ct( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:4326" ) ), QgsCoordinateReferenceSystem( "EPSG:3857" ) );
34+
Q_NOWARN_DEPRECATED_POP
35+
QgsPointXY topLeftLonLat( -180, 180.0 / M_PI * std::atan( std::sinh( M_PI ) ) );
36+
QgsPointXY bottomRightLonLat( 180, 180.0 / M_PI * std::atan( std::sinh( -M_PI ) ) );
37+
QgsPointXY topLeft = ct.transform( topLeftLonLat );
38+
QgsPointXY bottomRight = ct.transform( bottomRightLonLat );
39+
mXSpan = ( bottomRight.x() - topLeft.x() );
40+
}
41+
42+
QgsTerrainDownloader::~QgsTerrainDownloader() = default;
43+
44+
45+
void QgsTerrainDownloader::adjustExtentAndResolution( double mupp, const QgsRectangle &extentOrig, QgsRectangle &extent, int &res )
46+
{
47+
double xMin = floor( extentOrig.xMinimum() / mupp ) * mupp;
48+
double xMax = ceil( extentOrig.xMaximum() / mupp ) * mupp;
49+
50+
double yMin = floor( extentOrig.yMinimum() / mupp ) * mupp;
51+
double yMax = ceil( extentOrig.yMaximum() / mupp ) * mupp;
52+
53+
extent = QgsRectangle( xMin, yMin, xMax, yMax );
54+
res = round( ( xMax - xMin ) / mupp );
55+
}
56+
57+
58+
double QgsTerrainDownloader::findBestTileResolution( double requestedMupp )
59+
{
60+
int zoom = 0;
61+
for ( ; zoom <= 15; ++zoom )
62+
{
63+
double tileMupp = mXSpan / ( 256 * ( 1 << zoom ) );
64+
if ( tileMupp <= requestedMupp )
65+
break;
66+
}
67+
68+
if ( zoom > 15 ) zoom = 15;
69+
double finalMupp = mXSpan / ( 256 * ( 1 << zoom ) );
70+
return finalMupp;
71+
}
72+
73+
74+
void QgsTerrainDownloader::tileImageToHeightMap( const QImage &img, QByteArray &heightMap )
75+
{
76+
// assuming ARGB preformatted but with alpha 255
77+
const QRgb *rgb = reinterpret_cast<const QRgb *>( img.constBits() );
78+
int count = img.width() * img.height();
79+
heightMap.resize( sizeof( float ) * count );
80+
float *hData = reinterpret_cast<float *>( heightMap.data() );
81+
for ( int i = 0; i < count; ++i )
82+
{
83+
QRgb c = rgb[i];
84+
if ( qAlpha( c ) == 255 )
85+
{
86+
float h = qRed( c ) * 256 + qGreen( c ) + qBlue( c ) / 256.f - 32768;
87+
*hData++ = h;
88+
}
89+
else
90+
{
91+
*hData++ = std::numeric_limits<float>::quiet_NaN();
92+
}
93+
}
94+
}
95+
96+
97+
QByteArray QgsTerrainDownloader::getHeightMap( const QgsRectangle &extentOrig, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context, QString tmpFilenameImg, QString tmpFilenameTif )
98+
{
99+
QgsRectangle extentTr = extentOrig;
100+
if ( destCrs != onlineDtm->crs() )
101+
{
102+
// if in different CRS - need to reproject extent and resolution
103+
QgsCoordinateTransform ct( destCrs, onlineDtm->crs(), context );
104+
extentTr = ct.transformBoundingBox( extentOrig );
105+
}
106+
107+
double requestedMupp = extentTr.width() / res;
108+
double finalMupp = findBestTileResolution( requestedMupp );
109+
110+
// adjust extent to match native resolution of terrain tiles
111+
112+
QgsRectangle extent;
113+
int resOrig = res;
114+
adjustExtentAndResolution( finalMupp, extentTr, extent, res );
115+
116+
// request tile
117+
118+
QgsRasterBlock *b = onlineDtm->dataProvider()->block( 1, extent, res, res );
119+
QImage img = b->image();
120+
delete b;
121+
if ( !tmpFilenameImg.isEmpty() )
122+
img.save( tmpFilenameImg );
123+
124+
// convert to height data
125+
126+
QByteArray heightMap;
127+
tileImageToHeightMap( img, heightMap );
128+
129+
// prepare source/destination datasets for resampling
130+
131+
gdal::dataset_unique_ptr hSrcDS( QgsGdalUtils::createSingleBandMemoryDataset( GDT_Float32, extent, res, res, onlineDtm->crs() ) );
132+
gdal::dataset_unique_ptr hDstDS;
133+
if ( !tmpFilenameTif.isEmpty() )
134+
hDstDS = QgsGdalUtils::createSingleBandTiffDataset( tmpFilenameTif, GDT_Float32, extentOrig, resOrig, resOrig, destCrs );
135+
else
136+
hDstDS = QgsGdalUtils::createSingleBandMemoryDataset( GDT_Float32, extentOrig, resOrig, resOrig, destCrs );
137+
138+
if ( !hSrcDS || !hDstDS )
139+
{
140+
QgsDebugMsg( "failed to create GDAL dataset for heightmap" );
141+
return QByteArray();
142+
}
143+
144+
CPLErr err = GDALRasterIO( GDALGetRasterBand( hSrcDS.get(), 1 ), GF_Write, 0, 0, res, res, heightMap.data(), res, res, GDT_Float32, 0, 0 );
145+
if ( err != CE_None )
146+
{
147+
QgsDebugMsg( "failed to write heightmap data to GDAL dataset" );
148+
return QByteArray();
149+
}
150+
151+
// resample to the desired extent + resolution
152+
153+
QgsGdalUtils::resampleSingleBandRaster( hSrcDS.get(), hDstDS.get(), GRA_Bilinear );
154+
155+
QByteArray heightMapOut;
156+
heightMapOut.resize( resOrig * resOrig * sizeof( float ) );
157+
char *data = heightMapOut.data();
158+
159+
// read the data back
160+
161+
CPLErr err2 = GDALRasterIO( GDALGetRasterBand( hDstDS.get(), 1 ), GF_Read, 0, 0, resOrig, resOrig, data, resOrig, resOrig, GDT_Float32, 0, 0 );
162+
if ( err2 != CE_None )
163+
{
164+
QgsDebugMsg( "failed to read heightmap data from GDAL dataset" );
165+
return QByteArray();
166+
}
167+
168+
return heightMapOut;
169+
}

‎src/3d/terrain/qgsterraindownloader.h

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 GeoTIFF format hosted on AWS.
36+
*
37+
* \since QGIS 3.8
38+
*/
39+
class _3D_EXPORT QgsTerrainDownloader
40+
{
41+
42+
public:
43+
QgsTerrainDownloader();
44+
~QgsTerrainDownloader();
45+
46+
/**
47+
* For given extent and resolution (number of pixels for width/height) in specified CRS, download necessary
48+
* tile images (if not cached already) and produce height map out of them (byte array of res*res float values)
49+
*/
50+
QByteArray getHeightMap( const QgsRectangle &extentOrig, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context = QgsCoordinateTransformContext(), QString tmpFilenameImg = QString(), QString tmpFilenameTif = QString() );
51+
52+
private:
53+
54+
/**
55+
* For the requested resolution given as map units per pixel, find out the best native tile resolution
56+
* (higher resolution = fewer map units per pixel)
57+
*/
58+
double findBestTileResolution( double requestedMupp );
59+
60+
/**
61+
* Given extent and map units per pixels, adjust the extent and resolution
62+
*/
63+
static void adjustExtentAndResolution( double mupp, const QgsRectangle &extentOrig, QgsRectangle &extent, int &res );
64+
65+
/**
66+
* Takes an image tile with heights encoded using "terrarium" encoding and converts it to
67+
* an array of 32-bit floats with decoded elevations.
68+
*/
69+
static void tileImageToHeightMap( const QImage &img, QByteArray &heightMap );
70+
71+
private:
72+
std::unique_ptr<QgsRasterLayer> onlineDtm;
73+
double mXSpan = 0; //!< Width of the tile at zoom level 0 in map units
74+
};
75+
76+
#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: 50 additions & 6 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"
@@ -49,15 +50,23 @@ Qgs3DMapConfigWidget::Qgs3DMapConfigWidget( Qgs3DMapSettings *map, QgsMapCanvas
4950
QgsTerrainGenerator *terrainGen = mMap->terrainGenerator();
5051
if ( terrainGen && terrainGen->type() == QgsTerrainGenerator::Dem )
5152
{
53+
cboTerrainType->setCurrentIndex( 1 );
5254
QgsDemTerrainGenerator *demTerrainGen = static_cast<QgsDemTerrainGenerator *>( terrainGen );
5355
spinTerrainResolution->setValue( demTerrainGen->resolution() );
5456
spinTerrainSkirtHeight->setValue( demTerrainGen->skirtHeight() );
5557
cboTerrainLayer->setLayer( demTerrainGen->layer() );
5658
}
59+
else if ( terrainGen && terrainGen->type() == QgsTerrainGenerator::Online )
60+
{
61+
cboTerrainType->setCurrentIndex( 2 );
62+
QgsOnlineTerrainGenerator *onlineTerrainGen = static_cast<QgsOnlineTerrainGenerator *>( terrainGen );
63+
spinTerrainResolution->setValue( onlineTerrainGen->resolution() );
64+
spinTerrainSkirtHeight->setValue( onlineTerrainGen->skirtHeight() );
65+
}
5766
else
5867
{
68+
cboTerrainType->setCurrentIndex( 0 );
5969
cboTerrainLayer->setLayer( nullptr );
60-
spinTerrainResolution->setEnabled( false );
6170
spinTerrainResolution->setValue( 16 );
6271
spinTerrainSkirtHeight->setValue( 10 );
6372
}
@@ -86,21 +95,23 @@ Qgs3DMapConfigWidget::Qgs3DMapConfigWidget( Qgs3DMapSettings *map, QgsMapCanvas
8695

8796
widgetLights->setPointLights( mMap->pointLights() );
8897

98+
connect( cboTerrainType, static_cast<void ( QComboBox::* )( int )>( &QComboBox::currentIndexChanged ), this, &Qgs3DMapConfigWidget::onTerrainTypeChanged );
8999
connect( cboTerrainLayer, static_cast<void ( QComboBox::* )( int )>( &QgsMapLayerComboBox::currentIndexChanged ), this, &Qgs3DMapConfigWidget::onTerrainLayerChanged );
90100
connect( spinMapResolution, static_cast<void ( QSpinBox::* )( int )>( &QSpinBox::valueChanged ), this, &Qgs3DMapConfigWidget::updateMaxZoomLevel );
91101
connect( spinGroundError, static_cast<void ( QDoubleSpinBox::* )( double )>( &QDoubleSpinBox::valueChanged ), this, &Qgs3DMapConfigWidget::updateMaxZoomLevel );
92102

93103
updateMaxZoomLevel();
104+
onTerrainTypeChanged();
94105
}
95106

96107
void Qgs3DMapConfigWidget::apply()
97108
{
98-
QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() );
99-
100109
bool needsUpdateOrigin = false;
101110

102-
if ( demLayer )
111+
if ( cboTerrainType->currentIndex() == 1 ) // DEM from raster layer
103112
{
113+
QgsRasterLayer *demLayer = qobject_cast<QgsRasterLayer *>( cboTerrainLayer->currentLayer() );
114+
104115
bool tGenNeedsUpdate = true;
105116
if ( mMap->terrainGenerator()->type() == QgsTerrainGenerator::Dem )
106117
{
@@ -123,7 +134,29 @@ void Qgs3DMapConfigWidget::apply()
123134
needsUpdateOrigin = true;
124135
}
125136
}
126-
else if ( !demLayer && mMap->terrainGenerator()->type() != QgsTerrainGenerator::Flat )
137+
else if ( cboTerrainType->currentIndex() == 2 ) // Online
138+
{
139+
bool tGenNeedsUpdate = true;
140+
if ( mMap->terrainGenerator()->type() == QgsTerrainGenerator::Online )
141+
{
142+
QgsOnlineTerrainGenerator *oldOnlineTerrainGen = static_cast<QgsOnlineTerrainGenerator *>( mMap->terrainGenerator() );
143+
if ( oldOnlineTerrainGen->resolution() == spinTerrainResolution->value() &&
144+
oldOnlineTerrainGen->skirtHeight() == spinTerrainSkirtHeight->value() )
145+
tGenNeedsUpdate = false;
146+
}
147+
148+
if ( tGenNeedsUpdate )
149+
{
150+
QgsOnlineTerrainGenerator *onlineTerrainGen = new QgsOnlineTerrainGenerator;
151+
onlineTerrainGen->setCrs( mMap->crs(), QgsProject::instance()->transformContext() );
152+
onlineTerrainGen->setExtent( mMainCanvas->fullExtent() );
153+
onlineTerrainGen->setResolution( spinTerrainResolution->value() );
154+
onlineTerrainGen->setSkirtHeight( spinTerrainSkirtHeight->value() );
155+
mMap->setTerrainGenerator( onlineTerrainGen );
156+
needsUpdateOrigin = true;
157+
}
158+
}
159+
else if ( cboTerrainType->currentIndex() == 0 ) // flat terrain
127160
{
128161
QgsFlatTerrainGenerator *flatTerrainGen = new QgsFlatTerrainGenerator;
129162
flatTerrainGen->setCrs( mMap->crs() );
@@ -161,9 +194,20 @@ void Qgs3DMapConfigWidget::apply()
161194
mMap->setPointLights( widgetLights->pointLights() );
162195
}
163196

197+
void Qgs3DMapConfigWidget::onTerrainTypeChanged()
198+
{
199+
bool isFlat = cboTerrainType->currentIndex() == 0;
200+
bool isDem = cboTerrainType->currentIndex() == 1;
201+
labelTerrainResolution->setVisible( !isFlat );
202+
spinTerrainResolution->setVisible( !isFlat );
203+
labelTerrainSkirtHeight->setVisible( !isFlat );
204+
spinTerrainSkirtHeight->setVisible( !isFlat );
205+
labelTerrainLayer->setVisible( isDem );
206+
cboTerrainLayer->setVisible( isDem );
207+
}
208+
164209
void Qgs3DMapConfigWidget::onTerrainLayerChanged()
165210
{
166-
spinTerrainResolution->setEnabled( cboTerrainLayer->currentLayer() );
167211
}
168212

169213
void Qgs3DMapConfigWidget::updateMaxZoomLevel()

‎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: 71 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,60 @@
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>
152+
<property name="text">
153+
<string>Flat terrain</string>
154+
</property>
155+
</item>
156+
<item>
157+
<property name="text">
158+
<string>DEM (Raster layer)</string>
159+
</property>
160+
</item>
161+
<item>
162+
<property name="text">
163+
<string>Online</string>
164+
</property>
165+
</item>
166+
</widget>
167+
</item>
145168
</layout>
146169
</widget>
147170
</item>
@@ -339,6 +362,9 @@
339362
</customwidget>
340363
</customwidgets>
341364
<tabstops>
365+
<tabstop>scrollArea</tabstop>
366+
<tabstop>spinCameraFieldOfView</tabstop>
367+
<tabstop>cboTerrainType</tabstop>
342368
<tabstop>cboTerrainLayer</tabstop>
343369
<tabstop>spinTerrainScale</tabstop>
344370
<tabstop>spinTerrainResolution</tabstop>

0 commit comments

Comments
 (0)
Please sign in to comment.