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