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