QGIS API Documentation 3.41.0-Master (af5edcb665c)
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 "qgschunknode.h"
23#include "qgseventtracing.h"
24#include "qgsgeotransform.h"
26#include "qgsterrainentity.h"
29#include "qgsterraingenerator.h"
31
32#include <Qt3DRender/QGeometryRenderer>
33#include <Qt3DCore/QTransform>
34#include <QMutexLocker>
35
37
38static void _heightMapMinMax( const QByteArray &heightMap, float &zMin, float &zMax )
39{
40 const float *zBits = ( const float * ) heightMap.constData();
41 int zCount = heightMap.count() / sizeof( float );
42 bool first = true;
43
44 zMin = zMax = std::numeric_limits<float>::quiet_NaN();
45 for ( int i = 0; i < zCount; ++i )
46 {
47 float z = zBits[i];
48 if ( std::isnan( z ) )
49 continue;
50 if ( first )
51 {
52 zMin = zMax = z;
53 first = false;
54 }
55 zMin = std::min( zMin, z );
56 zMax = std::max( zMax, z );
57 }
58}
59
60
61QgsDemTerrainTileLoader::QgsDemTerrainTileLoader( QgsTerrainEntity *terrain, QgsChunkNode *node, QgsTerrainGenerator *terrainGenerator )
62 : QgsTerrainTileLoader( terrain, node )
63 , mResolution( 0 )
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->terrainSettings()->verticalScale(), 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 QgsGeoTransform *transform = new QgsGeoTransform;
117 transform->setGeoTranslation( QgsVector3D( extent.xMinimum(), extent.yMinimum(), 0 ) );
118 entity->addComponent( transform );
119
120 mNode->setExactBox3D( QgsBox3D( extent.xMinimum(), extent.yMinimum(), zMin * map->terrainSettings()->verticalScale(), extent.xMinimum() + side, extent.yMinimum() + side, zMax * map->terrainSettings()->verticalScale() ) );
121 mNode->updateParentBoundingBoxesRecursively();
122
123 entity->setParent( parent );
124 return entity;
125}
126
127void QgsDemTerrainTileLoader::onHeightMapReady( int jobId, const QByteArray &heightMap )
128{
129 if ( mHeightMapJobId == jobId )
130 {
131 this->mHeightMap = heightMap;
132 mHeightMapJobId = -1;
133
134 // continue loading - texture
135 loadTexture();
136 }
137}
138
139
140// ---------------------
141
142#include <qgsrasterlayer.h>
143#include <qgsrasterprojector.h>
144#include <QtConcurrent/QtConcurrentRun>
145#include <QFutureWatcher>
146#include "qgsterraindownloader.h"
147
148QgsDemHeightMapGenerator::QgsDemHeightMapGenerator( QgsRasterLayer *dtm, const QgsTilingScheme &tilingScheme, int resolution, const QgsCoordinateTransformContext &transformContext )
149 : mDtmExtent( dtm ? dtm->extent() : QgsRectangle() )
150 , mClonedProvider( dtm ? qgis::down_cast<QgsRasterDataProvider *>( dtm->dataProvider()->clone() ) : nullptr )
151 , mTilingScheme( tilingScheme )
152 , mResolution( resolution )
153 , mLastJobId( 0 )
154 , mDownloader( dtm ? nullptr : new QgsTerrainDownloader( transformContext ) )
155 , mTransformContext( transformContext )
156{
157}
158
159QgsDemHeightMapGenerator::~QgsDemHeightMapGenerator()
160{
161 delete mClonedProvider;
162}
163
164
165static QByteArray _readDtmData( QgsRasterDataProvider *provider, const QgsRectangle &extent, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsRectangle &clippingExtent )
166{
167 provider->moveToThread( QThread::currentThread() );
168
169 QgsEventTracing::ScopedEvent e( QStringLiteral( "3D" ), QStringLiteral( "DEM" ) );
170
171 // TODO: use feedback object? (but GDAL currently does not support cancellation anyway)
172 QgsRasterInterface *input = provider;
173 std::unique_ptr<QgsRasterProjector> projector;
174 if ( provider->crs() != destCrs )
175 {
176 projector.reset( new QgsRasterProjector );
177 projector->setCrs( provider->crs(), destCrs, provider->transformContext() );
178 projector->setInput( provider );
179 input = projector.get();
180 }
181 std::unique_ptr<QgsRasterBlock> block( input->block( 1, extent, res, res ) );
182
183 QByteArray data;
184 if ( block )
185 {
186 block->convert( Qgis::DataType::Float32 ); // currently we expect just floats
187
188 // set noData outside our clippingExtent
189 const QRect subRect = QgsRasterBlock::subRect( extent, block->width(), block->height(), clippingExtent );
190 if ( !block->hasNoDataValue() )
191 {
192 // QgsRasterBlock::setIsNoDataExcept() misbehaves without a defined no data value
193 // see https://github.com/qgis/QGIS/issues/51285
194 block->setNoDataValue( std::numeric_limits<float>::lowest() );
195 }
196 block->setIsNoDataExcept( subRect );
197
198 data = block->data();
199 data.detach(); // this should make a deep copy
200
201 if ( block->hasNoData() )
202 {
203 // turn all no-data values into NaN in the output array
204 float *floatData = reinterpret_cast<float *>( data.data() );
205 Q_ASSERT( data.count() % sizeof( float ) == 0 );
206 int count = data.count() / sizeof( float );
207 for ( int i = 0; i < count; ++i )
208 {
209 if ( block->isNoData( i ) )
210 floatData[i] = std::numeric_limits<float>::quiet_NaN();
211 }
212 }
213 }
214
215 delete provider;
216 return data;
217}
218
219static QByteArray _readOnlineDtm( QgsTerrainDownloader *downloader, const QgsRectangle &extent, int res, const QgsCoordinateReferenceSystem &destCrs, const QgsCoordinateTransformContext &context )
220{
221 return downloader->getHeightMap( extent, res, destCrs, context );
222}
223
224int QgsDemHeightMapGenerator::render( const QgsChunkNodeId &nodeId )
225{
226 QgsEventTracing::addEvent( QgsEventTracing::AsyncBegin, QStringLiteral( "3D" ), QStringLiteral( "DEM" ), nodeId.text() );
227
228 // extend the rect by half-pixel on each side? to get the values in "corners"
229 QgsRectangle extent = mTilingScheme.tileToExtent( nodeId );
230 float mapUnitsPerPixel = extent.width() / mResolution;
231 extent.grow( mapUnitsPerPixel / 2 );
232 // but make sure not to go beyond the root tile's full extent (returns invalid values)
233 QgsRectangle rootTileExtent = mTilingScheme.tileToExtent( 0, 0, 0 );
234 extent = extent.intersect( rootTileExtent );
235
236 JobData jd;
237 jd.jobId = ++mLastJobId;
238 jd.tileId = nodeId;
239 jd.extent = extent;
240 jd.timer.start();
241 QFutureWatcher<QByteArray> *fw = new QFutureWatcher<QByteArray>( nullptr );
242 connect( fw, &QFutureWatcher<QByteArray>::finished, this, &QgsDemHeightMapGenerator::onFutureFinished );
243 connect( fw, &QFutureWatcher<QByteArray>::finished, fw, &QObject::deleteLater );
244 if ( mClonedProvider )
245 {
246 // make a clone of the data provider so it is safe to use in worker thread
247 std::unique_ptr<QgsRasterDataProvider> clonedProviderClone( mClonedProvider->clone() );
248 clonedProviderClone->moveToThread( nullptr );
249 jd.future = QtConcurrent::run( _readDtmData, clonedProviderClone.release(), extent, mResolution, mTilingScheme.crs(), mTilingScheme.fullExtent() );
250 }
251 else
252 {
253 jd.future = QtConcurrent::run( _readOnlineDtm, mDownloader.get(), extent, mResolution, mTilingScheme.crs(), mTransformContext );
254 }
255
256 fw->setFuture( jd.future );
257
258 mJobs.insert( fw, jd );
259
260 return jd.jobId;
261}
262
263void QgsDemHeightMapGenerator::waitForFinished()
264{
265 for ( auto it = mJobs.keyBegin(); it != mJobs.keyEnd(); it++ )
266 {
267 QFutureWatcher<QByteArray> *fw = *it;
268 disconnect( fw, &QFutureWatcher<QByteArray>::finished, this, &QgsDemHeightMapGenerator::onFutureFinished );
269 disconnect( fw, &QFutureWatcher<QByteArray>::finished, fw, &QObject::deleteLater );
270 }
271 QVector<QFutureWatcher<QByteArray> *> toBeDeleted;
272 for ( auto it = mJobs.keyBegin(); it != mJobs.keyEnd(); it++ )
273 {
274 QFutureWatcher<QByteArray> *fw = *it;
275 fw->waitForFinished();
276 JobData jobData = mJobs.value( fw );
277 toBeDeleted.push_back( fw );
278
279 QByteArray data = jobData.future.result();
280 emit heightMapReady( jobData.jobId, data );
281 }
282
283 for ( QFutureWatcher<QByteArray> *fw : toBeDeleted )
284 {
285 mJobs.remove( fw );
286 fw->deleteLater();
287 }
288}
289
290void QgsDemHeightMapGenerator::lazyLoadDtmCoarseData( int res, const QgsRectangle &rect )
291{
292 QMutexLocker locker( &mLazyLoadDtmCoarseDataMutex );
293 if ( mDtmCoarseData.isEmpty() )
294 {
295 std::unique_ptr<QgsRasterBlock> block( mClonedProvider->block( 1, rect, res, res ) );
296 block->convert( Qgis::DataType::Float32 );
297 mDtmCoarseData = block->data();
298 mDtmCoarseData.detach(); // make a deep copy
299 }
300}
301
302float QgsDemHeightMapGenerator::heightAt( double x, double y )
303{
304 if ( !mClonedProvider )
305 return 0; // TODO: calculate heights for online DTM
306
307 // TODO: this is quite a primitive implementation: better to use heightmaps currently in use
308 int res = 1024;
309 lazyLoadDtmCoarseData( res, mDtmExtent );
310
311 int cellX = ( int ) ( ( x - mDtmExtent.xMinimum() ) / mDtmExtent.width() * res + .5f );
312 int cellY = ( int ) ( ( mDtmExtent.yMaximum() - y ) / mDtmExtent.height() * res + .5f );
313 cellX = std::clamp( cellX, 0, res - 1 );
314 cellY = std::clamp( cellY, 0, res - 1 );
315
316 const float *data = ( const float * ) mDtmCoarseData.constData();
317 return data[cellX + cellY * res];
318}
319
320void QgsDemHeightMapGenerator::onFutureFinished()
321{
322 QFutureWatcher<QByteArray> *fw = static_cast<QFutureWatcher<QByteArray> *>( sender() );
323 Q_ASSERT( fw );
324 Q_ASSERT( mJobs.contains( fw ) );
325 JobData jobData = mJobs.value( fw );
326
327 mJobs.remove( fw );
328 fw->deleteLater();
329
330 QgsEventTracing::addEvent( QgsEventTracing::AsyncEnd, QStringLiteral( "3D" ), QStringLiteral( "DEM" ), jobData.tileId.text() );
331
332 QByteArray data = jobData.future.result();
333 emit heightMapReady( jobData.jobId, data );
334}
335
@ Float32
Thirty two bit floating point (float)
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: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