QGIS API Documentation 3.99.0-Master (09f76ad7019)
Loading...
Searching...
No Matches
qgsdemterraintileloader_p.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsdemterraintileloader_p.cpp
3 --------------------------------------
4 Date : July 2017
5 Copyright : (C) 2017 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
17
18#include "qgs3dmapsettings.h"
20#include "qgschunknode.h"
23#include "qgseventtracing.h"
24#include "qgsgeotransform.h"
26#include "qgsterrainentity.h"
27#include "qgsterraingenerator.h"
30
31#include <QMutexLocker>
32#include <QString>
33#include <Qt3DCore/QTransform>
34#include <Qt3DRender/QGeometryRenderer>
35
36#include "moc_qgsdemterraintileloader_p.cpp"
37
38using namespace Qt::StringLiterals;
39
41
42static void _heightMapMinMax( const QByteArray &heightMap, float &zMin, float &zMax )
43{
44 const float *zBits = ( const float * ) heightMap.constData();
45 int zCount = heightMap.count() / sizeof( float );
46 bool first = true;
47
48 zMin = zMax = std::numeric_limits<float>::quiet_NaN();
49 for ( int i = 0; i < zCount; ++i )
50 {
51 float z = zBits[i];
52 if ( std::isnan( z ) )
53 continue;
54 if ( first )
55 {
56 zMin = zMax = z;
57 first = false;
58 }
59 zMin = std::min( zMin, z );
60 zMax = std::max( zMax, z );
61 }
62}
63
64
65QgsDemTerrainTileLoader::QgsDemTerrainTileLoader( QgsTerrainEntity *terrain, QgsChunkNode *node, QgsTerrainGenerator *terrainGenerator )
66 : QgsTerrainTileLoader( terrain, node )
67 , mTerrainGenerator( terrainGenerator )
68{}
69
70void QgsDemTerrainTileLoader::start()
71{
72 QgsChunkNode *node = chunk();
73
74 QgsDemHeightMapGenerator *heightMapGenerator = nullptr;
75 if ( mTerrainGenerator->type() == QgsTerrainGenerator::Dem )
76 {
77 QgsDemTerrainGenerator *generator = static_cast<QgsDemTerrainGenerator *>( mTerrainGenerator );
78 heightMapGenerator = generator->heightMapGenerator();
79 mSkirtHeight = generator->skirtHeight();
80 }
81 else if ( mTerrainGenerator->type() == QgsTerrainGenerator::Online )
82 {
83 QgsOnlineTerrainGenerator *generator = static_cast<QgsOnlineTerrainGenerator *>( mTerrainGenerator );
84 heightMapGenerator = generator->heightMapGenerator();
85 mSkirtHeight = generator->skirtHeight();
86 }
87 else
88 Q_ASSERT( false );
89
90 // get heightmap asynchronously
91 connect( heightMapGenerator, &QgsDemHeightMapGenerator::heightMapReady, this, &QgsDemTerrainTileLoader::onHeightMapReady );
92 mHeightMapJobId = heightMapGenerator->render( node->tileId() );
93 mResolution = heightMapGenerator->resolution();
94}
95
96Qt3DCore::QEntity *QgsDemTerrainTileLoader::createEntity( Qt3DCore::QEntity *parent )
97{
98 float zMin, zMax;
99 _heightMapMinMax( mHeightMap, zMin, zMax );
100
101 if ( std::isnan( zMin ) || std::isnan( zMax ) )
102 {
103 // no data available for this tile
104 return nullptr;
105 }
106
107 Qgs3DMapSettings *map = terrain()->mapSettings();
108 QgsChunkNodeId nodeId = mNode->tileId();
109 QgsRectangle extent = map->terrainGenerator()->tilingScheme().tileToExtent( nodeId );
110 double side = extent.width();
111
112 QgsTerrainTileEntity *entity = new QgsTerrainTileEntity( nodeId );
113
114 // create geometry renderer
115
116 Qt3DRender::QGeometryRenderer *mesh = new Qt3DRender::QGeometryRenderer;
117 mesh->setGeometry( new DemTerrainTileGeometry( mResolution, side, map->terrainSettings()->verticalScale(), mSkirtHeight, mHeightMap, mesh ) );
118 entity->addComponent( mesh ); // takes ownership if the component has no parent
119
120 // create material
121
122 createTextureComponent( entity, map->isTerrainShadingEnabled(), map->terrainShadingMaterial(), !map->layers().empty() );
123
124 // create transform
125 QgsGeoTransform *transform = new QgsGeoTransform;
126 transform->setGeoTranslation( QgsVector3D( extent.xMinimum(), extent.yMinimum(), 0 ) );
127 entity->addComponent( transform );
128
129 mNode->setExactBox3D( QgsBox3D( extent.xMinimum(), extent.yMinimum(), zMin * map->terrainSettings()->verticalScale(), extent.xMinimum() + side, extent.yMinimum() + side, zMax * map->terrainSettings()->verticalScale() ) );
130 mNode->updateParentBoundingBoxesRecursively();
131
132 entity->setParent( parent );
133 return entity;
134}
135
136void QgsDemTerrainTileLoader::onHeightMapReady( int jobId, const QByteArray &heightMap )
137{
138 if ( mHeightMapJobId == jobId )
139 {
140 this->mHeightMap = heightMap;
141 mHeightMapJobId = -1;
142
143 // continue loading - texture
144 loadTexture();
145 }
146}
147
148
149// ---------------------
150
151#include "qgsrasterlayer.h"
152#include "qgsrasterprojector.h"
153#include <QtConcurrentRun>
154#include <QFutureWatcher>
155#include <memory>
156#include "qgsterraindownloader.h"
157
158QgsDemHeightMapGenerator::QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution, const QgsCoordinateTransformContext &transformContext )
159 : mDtmExtent( dtm ? dtm->extent() : QgsRectangle() )
160 , mClonedProvider( dtm ? qgis::down_cast<QgsRasterDataProvider *>( dtm->dataProvider()->clone() ) : nullptr )
161 , mTilingScheme( tilingScheme )
162 , mResolution( resolution )
163 , mDownloader( dtm ? nullptr : new QgsTerrainDownloader( transformContext ) )
164 , mTransformContext( transformContext )
165{
166}
167
168QgsDemHeightMapGenerator::~QgsDemHeightMapGenerator()
169{
170 delete mClonedProvider;
171}
172
173
174static QByteArray _readDtmData( QgsRasterDataProvider *provider, const QgsRectangle &extent, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsRectangle &clippingExtent )
175{
176 provider->moveToThread( QThread::currentThread() );
177
178 QgsEventTracing::ScopedEvent e( u"3D"_s, u"DEM"_s );
179
180 // TODO: use feedback object? (but GDAL currently does not support cancellation anyway)
181 QgsRasterInterface *input = provider;
182 std::unique_ptr<QgsRasterProjector> projector;
183 if ( provider->crs() != destCrs )
184 {
185 projector = std::make_unique<QgsRasterProjector>();
186 projector->setCrs( provider->crs(), destCrs, provider->transformContext() );
187 projector->setInput( provider );
188 input = projector.get();
189 }
190 std::unique_ptr<QgsRasterBlock> block( input->block( 1, extent, res, res ) );
191
192 QByteArray data;
193 if ( block )
194 {
195 block->convert( Qgis::DataType::Float32 ); // currently we expect just floats
196
197 // set noData outside our clippingExtent
198 const QRect subRect = QgsRasterBlock::subRect( extent, block->width(), block->height(), clippingExtent );
199 if ( !block->hasNoDataValue() )
200 {
201 // QgsRasterBlock::setIsNoDataExcept() misbehaves without a defined no data value
202 // see https://github.com/qgis/QGIS/issues/51285
203 block->setNoDataValue( std::numeric_limits<float>::lowest() );
204 }
205 block->setIsNoDataExcept( subRect );
206
207 data = block->data();
208 data.detach(); // this should make a deep copy
209
210 if ( block->hasNoData() )
211 {
212 // turn all no-data values into NaN in the output array
213 float *floatData = reinterpret_cast<float *>( data.data() );
214 Q_ASSERT( data.count() % sizeof( float ) == 0 );
215 int count = data.count() / sizeof( float );
216 for ( int i = 0; i < count; ++i )
217 {
218 if ( block->isNoData( i ) )
219 floatData[i] = std::numeric_limits<float>::quiet_NaN();
220 }
221 }
222 }
223
224 delete provider;
225 return data;
226}
227
228static QByteArray _readOnlineDtm( QgsTerrainDownloader *downloader, const QgsRectangle &extent, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context )
229{
230 return downloader->getHeightMap( extent, res, destCrs, context );
231}
232
233int QgsDemHeightMapGenerator::render( const QgsChunkNodeId &nodeId )
234{
235 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, u"3D"_s, u"DEM"_s, nodeId.text() );
236
237 // extend the rect by half-pixel on each side? to get the values in "corners"
238 QgsRectangle extent = mTilingScheme.tileToExtent( nodeId );
239 float mapUnitsPerPixel = extent.width() / mResolution;
240 extent.grow( mapUnitsPerPixel / 2 );
241 // but make sure not to go beyond the root tile's full extent (returns invalid values)
242 QgsRectangle rootTileExtent = mTilingScheme.tileToExtent( 0, 0, 0 );
243 extent = extent.intersect( rootTileExtent );
244
245 JobData jd;
246 jd.jobId = ++mLastJobId;
247 jd.tileId = nodeId;
248 jd.extent = extent;
249 jd.timer.start();
250 QFutureWatcher<QByteArray> *fw = new QFutureWatcher<QByteArray>( nullptr );
251 connect( fw, &QFutureWatcher<QByteArray>::finished, this, &QgsDemHeightMapGenerator::onFutureFinished );
252 connect( fw, &QFutureWatcher<QByteArray>::finished, fw, &QObject::deleteLater );
253 if ( mClonedProvider )
254 {
255 // make a clone of the data provider so it is safe to use in worker thread
256 std::unique_ptr<QgsRasterDataProvider> clonedProviderClone( mClonedProvider->clone() );
257 clonedProviderClone->moveToThread( nullptr );
258 jd.future = QtConcurrent::run( _readDtmData, clonedProviderClone.release(), extent, mResolution, mTilingScheme.crs(), mTilingScheme.fullExtent() );
259 }
260 else
261 {
262 jd.future = QtConcurrent::run( _readOnlineDtm, mDownloader.get(), extent, mResolution, mTilingScheme.crs(), mTransformContext );
263 }
264
265 fw->setFuture( jd.future );
266
267 mJobs.insert( fw, jd );
268
269 return jd.jobId;
270}
271
272void QgsDemHeightMapGenerator::waitForFinished()
273{
274 for ( auto it = mJobs.keyBegin(); it != mJobs.keyEnd(); it++ )
275 {
276 QFutureWatcher<QByteArray> *fw = *it;
277 disconnect( fw, &QFutureWatcher<QByteArray>::finished, this, &QgsDemHeightMapGenerator::onFutureFinished );
278 disconnect( fw, &QFutureWatcher<QByteArray>::finished, fw, &QObject::deleteLater );
279 }
280 QVector<QFutureWatcher<QByteArray> *> toBeDeleted;
281 for ( auto it = mJobs.keyBegin(); it != mJobs.keyEnd(); it++ )
282 {
283 QFutureWatcher<QByteArray> *fw = *it;
284 fw->waitForFinished();
285 JobData jobData = mJobs.value( fw );
286 toBeDeleted.push_back( fw );
287
288 QByteArray data = jobData.future.result();
289 emit heightMapReady( jobData.jobId, data );
290 }
291
292 for ( QFutureWatcher<QByteArray> *fw : toBeDeleted )
293 {
294 mJobs.remove( fw );
295 fw->deleteLater();
296 }
297}
298
299void QgsDemHeightMapGenerator::lazyLoadDtmCoarseData( int res, const QgsRectangle &rect )
300{
301 QMutexLocker locker( &mLazyLoadDtmCoarseDataMutex );
302 if ( mDtmCoarseData.isEmpty() )
303 {
304 std::unique_ptr<QgsRasterBlock> block( mClonedProvider->block( 1, rect, res, res ) );
305 block->convert( Qgis::DataType::Float32 );
306 mDtmCoarseData = block->data();
307 mDtmCoarseData.detach(); // make a deep copy
308 }
309}
310
311float QgsDemHeightMapGenerator::heightAt( double x, double y )
312{
313 if ( !mClonedProvider )
314 return 0; // TODO: calculate heights for online DTM
315
316 // TODO: this is quite a primitive implementation: better to use heightmaps currently in use
317 int res = 1024;
318 lazyLoadDtmCoarseData( res, mDtmExtent );
319
320 int cellX = ( int ) ( ( x - mDtmExtent.xMinimum() ) / mDtmExtent.width() * res + .5f );
321 int cellY = ( int ) ( ( mDtmExtent.yMaximum() - y ) / mDtmExtent.height() * res + .5f );
322 cellX = std::clamp( cellX, 0, res - 1 );
323 cellY = std::clamp( cellY, 0, res - 1 );
324
325 const float *data = ( const float * ) mDtmCoarseData.constData();
326 return data[cellX + cellY * res];
327}
328
329void QgsDemHeightMapGenerator::onFutureFinished()
330{
331 QFutureWatcher<QByteArray> *fw = static_cast<QFutureWatcher<QByteArray> *>( sender() );
332 Q_ASSERT( fw );
333 Q_ASSERT( mJobs.contains( fw ) );
334 JobData jobData = mJobs.value( fw );
335
336 mJobs.remove( fw );
337 fw->deleteLater();
338
339 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, u"3D"_s, u"DEM"_s, jobData.tileId.text() );
340
341 QByteArray data = jobData.future.result();
342 emit heightMapReady( jobData.jobId, data );
343}
344
@ Float32
Thirty two bit floating point (float).
Definition qgis.h:387
Definition of the world.
const QgsAbstractTerrainSettings * terrainSettings() const
Returns the terrain settings.
QgsTerrainGenerator * terrainGenerator() const
Returns the terrain generator.
bool isTerrainShadingEnabled() const
Returns whether terrain shading is enabled.
QgsPhongMaterialSettings terrainShadingMaterial() const
Returns terrain shading material.
QList< QgsMapLayer * > layers() const
Returns the list of 3D map layers to be rendered in the scene.
double verticalScale() const
Returns the vertical scale (exaggeration) for terrain.
A 3-dimensional box composed of x, y, z coordinates.
Definition qgsbox3d.h:45
Represents a coordinate reference system (CRS).
Contains information about the context in which a coordinate transform is executed.
QgsCoordinateTransformContext transformContext() const
Returns data provider coordinate transform context.
virtual QgsCoordinateReferenceSystem crs() const =0
Returns the coordinate system for the data source.
Implementation of terrain generator that uses a raster layer with DEM to build terrain.
QgsDemHeightMapGenerator * heightMapGenerator()
Returns height map generator object - takes care of extraction of elevations from the layer).
float skirtHeight() const
Returns skirt height (in world units). Skirts at the edges of terrain tiles help hide cracks between ...
Implementation of terrain generator that uses online resources to download heightmaps.
QgsDemHeightMapGenerator * heightMapGenerator()
Returns height map generator object - takes care of extraction of elevations from the layer).
float skirtHeight() const
Returns skirt height (in world units). Skirts at the edges of terrain tiles help hide cracks between ...
static QRect subRect(const QgsRectangle &extent, int width, int height, const QgsRectangle &subExtent)
For extent and width, height find rectangle covered by subextent.
Base class for raster data providers.
Base class for processing filters like renderers, reprojector, resampler etc.
virtual QgsRasterBlock * block(int bandNo, const QgsRectangle &extent, int width, int height, QgsRasterBlockFeedback *feedback=nullptr)=0
Read block of data using given extent and size.
Represents a raster layer.
A rectangle specified with double values.
double xMinimum
double yMinimum
void grow(double delta)
Grows the rectangle in place by the specified amount.
QgsRectangle intersect(const QgsRectangle &rect) const
Returns the intersection with the given rectangle.
Takes care of downloading terrain data from a publicly available data source.
QByteArray getHeightMap(const QgsRectangle &extentOrig, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context=QgsCoordinateTransformContext(), QString tmpFilenameImg=QString(), QString tmpFilenameTif=QString())
For given extent and resolution (number of pixels for width/height) in specified CRS,...
Base class for generators of terrain.
@ Dem
Terrain is built from raster layer with digital elevation model.
@ Online
Terrain is built from downloaded tiles with digital elevation model.
const QgsTilingScheme & tilingScheme() const
Returns tiling scheme of the terrain.
Encapsulates tiling schemes (just like with WMTS / TMS / XYZ layers).
QgsRectangle tileToExtent(int x, int y, int z) const
Returns map coordinates of the extent of a tile.
A 3D vector (similar to QVector3D) with the difference that it uses double precision instead of singl...
Definition qgsvector3d.h:33