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