QGIS API Documentation 3.99.0-Master (09f76ad7019)
Loading...
Searching...
No Matches
qgsrasterlayerprofilegenerator.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsrasterlayerprofilegenerator.cpp
3 ---------------
4 begin : March 2022
5 copyright : (C) 2022 by Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
18
19#include "qgscurve.h"
20#include "qgsfillsymbol.h"
21#include "qgsgeometryengine.h"
22#include "qgsgeos.h"
23#include "qgslinesymbol.h"
24#include "qgsprofilepoint.h"
25#include "qgsprofilerequest.h"
26#include "qgsrasteriterator.h"
27#include "qgsrasterlayer.h"
29#include "qgsthreadingutils.h"
30
31#include <QPolygonF>
32#include <QString>
33#include <QThread>
34
35using namespace Qt::StringLiterals;
36
37//
38// QgsRasterLayerProfileResults
39//
40
42{
43 return u"raster"_s;
44}
45
46QVector<QgsProfileIdentifyResults> QgsRasterLayerProfileResults::identify( const QgsProfilePoint &point, const QgsProfileIdentifyContext &context )
47{
48 const QVector<QgsProfileIdentifyResults> noLayerResults = QgsAbstractProfileSurfaceResults::identify( point, context );
49
50 // we have to make a new list, with the correct layer reference set
51 QVector<QgsProfileIdentifyResults> res;
52 res.reserve( noLayerResults.size() );
53 for ( const QgsProfileIdentifyResults &result : noLayerResults )
54 {
55 res.append( QgsProfileIdentifyResults( mLayer, result.results() ) );
56 }
57 return res;
58}
59
60
61
62//
63// QgsRasterLayerProfileGenerator
64//
65
68 , mId( layer->id() )
69 , mFeedback( std::make_unique< QgsRasterBlockFeedback >() )
70 , mProfileCurve( request.profileCurve() ? request.profileCurve()->clone() : nullptr )
71 , mSourceCrs( layer->crs() )
72 , mTargetCrs( request.crs() )
73 , mTransformContext( request.transformContext() )
74 , mOffset( layer->elevationProperties()->zOffset() )
75 , mScale( layer->elevationProperties()->zScale() )
76 , mLayer( layer )
77 , mBand( qgis::down_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() )->bandNumber() )
78 , mRasterUnitsPerPixelX( layer->rasterUnitsPerPixelX() )
79 , mRasterUnitsPerPixelY( layer->rasterUnitsPerPixelY() )
80 , mStepDistance( request.stepDistance() )
81{
82 mRasterProvider.reset( layer->dataProvider()->clone() );
83 mRasterProvider->moveToThread( nullptr );
84
85 mSymbology = qgis::down_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() )->profileSymbology();
86 mElevationLimit = qgis::down_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() )->elevationLimit();
87 mLineSymbol.reset( qgis::down_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() )->profileLineSymbol()->clone() );
88 mFillSymbol.reset( qgis::down_cast< QgsRasterLayerElevationProperties * >( layer->elevationProperties() )->profileFillSymbol()->clone() );
89}
90
92{
93 return mId;
94}
95
97{
98 return u"raster"_s;
99}
100
105
107
109{
110 if ( !mProfileCurve || mFeedback->isCanceled() )
111 return false;
112
113 QgsScopedAssignObjectToCurrentThread assignProviderToCurrentThread( mRasterProvider.get() );
114
115 const double startDistanceOffset = std::max( !context.distanceRange().isInfinite() ? context.distanceRange().lower() : 0, 0.0 );
116 const double endDistance = context.distanceRange().upper();
117
118 std::unique_ptr< QgsCurve > trimmedCurve;
119 QgsCurve *sourceCurve = nullptr;
120 if ( startDistanceOffset > 0 || endDistance < mProfileCurve->length() )
121 {
122 trimmedCurve.reset( mProfileCurve->curveSubstring( startDistanceOffset, endDistance ) );
123 sourceCurve = trimmedCurve.get();
124 }
125 else
126 {
127 sourceCurve = mProfileCurve.get();
128 }
129
130 // we need to transform the profile curve to the raster's CRS
131 std::unique_ptr< QgsCurve > transformedCurve( sourceCurve->clone() );
132 const QgsCoordinateTransform rasterToTargetTransform( mSourceCrs, mTargetCrs, mTransformContext );
133 try
134 {
135 transformedCurve->transform( rasterToTargetTransform, Qgis::TransformDirection::Reverse );
136 }
137 catch ( QgsCsException & )
138 {
139 QgsDebugError( u"Error transforming profile line to raster CRS"_s );
140 return false;
141 }
142
143 if ( mFeedback->isCanceled() )
144 return false;
145
146 const QgsRectangle profileCurveBoundingBox = transformedCurve->boundingBox();
147 if ( !profileCurveBoundingBox.intersects( mRasterProvider->extent() ) )
148 return false;
149
150 if ( mFeedback->isCanceled() )
151 return false;
152
153 mResults = std::make_unique< QgsRasterLayerProfileResults >();
154 mResults->mLayer = mLayer;
155 mResults->mId = mId;
156 mResults->copyPropertiesFromGenerator( this );
157
158 std::unique_ptr< QgsGeometryEngine > curveEngine( QgsGeometry::createGeometryEngine( transformedCurve.get() ) );
159 curveEngine->prepareGeometry();
160
161 if ( mFeedback->isCanceled() )
162 return false;
163
164 double stepDistance = mStepDistance;
165 if ( !std::isnan( context.maximumErrorMapUnits() ) )
166 {
167 // convert the maximum error in curve units to a step distance
168 // TODO -- there's no point in this being << pixel size!
169 if ( std::isnan( stepDistance ) || context.maximumErrorMapUnits() > stepDistance )
170 {
171 stepDistance = context.maximumErrorMapUnits();
172 }
173 }
174
175 QSet< QgsPointXY > profilePoints;
176 if ( !std::isnan( stepDistance ) )
177 {
178 // if specific step distance specified, use this to generate points along the curve
179 QgsGeometry densifiedCurve( sourceCurve->clone() );
180 densifiedCurve = densifiedCurve.densifyByDistance( stepDistance );
181 densifiedCurve.transform( rasterToTargetTransform, Qgis::TransformDirection::Reverse );
182 profilePoints.reserve( densifiedCurve.constGet()->nCoordinates() );
183 for ( auto it = densifiedCurve.vertices_begin(); it != densifiedCurve.vertices_end(); ++it )
184 {
185 profilePoints.insert( *it );
186 }
187 }
188
189 if ( mFeedback->isCanceled() )
190 return false;
191
192 // calculate the portion of the raster which actually covers the curve
193 int subRegionWidth = 0;
194 int subRegionHeight = 0;
195 int subRegionLeft = 0;
196 int subRegionTop = 0;
197 QgsRectangle rasterSubRegion = mRasterProvider->xSize() > 0 && mRasterProvider->ySize() > 0 ?
199 mRasterProvider->extent(),
200 mRasterProvider->xSize(),
201 mRasterProvider->ySize(),
202 transformedCurve->boundingBox(),
203 subRegionWidth,
204 subRegionHeight,
205 subRegionLeft,
206 subRegionTop ) : transformedCurve->boundingBox();
207
208 const bool zeroXYSize = mRasterProvider->xSize() == 0 || mRasterProvider->ySize() == 0;
209 if ( zeroXYSize )
210 {
211 const double curveLengthInPixels = sourceCurve->length() / context.mapUnitsPerDistancePixel();
212 const double conversionFactor = curveLengthInPixels / transformedCurve->length();
213 subRegionWidth = rasterSubRegion.width() * conversionFactor;
214 subRegionHeight = rasterSubRegion.height() * conversionFactor;
215
216 // ensure we fetch at least 1 pixel wide/high blocks, otherwise exactly vertical/horizontal profile lines will result in zero size blocks
217 // see https://github.com/qgis/QGIS/issues/51196
218 if ( subRegionWidth == 0 )
219 {
220 subRegionWidth = 1;
221 rasterSubRegion.setXMaximum( rasterSubRegion.xMinimum() + 1 / conversionFactor );
222 }
223 if ( subRegionHeight == 0 )
224 {
225 subRegionHeight = 1;
226 rasterSubRegion.setYMaximum( rasterSubRegion.yMinimum() + 1 / conversionFactor );
227 }
228 }
229
230 // iterate over the raster blocks, throwing away any which don't intersect the profile curve
231 QgsRasterIterator it( mRasterProvider.get() );
232 // we use smaller tile sizes vs the default, as we will be skipping over tiles which don't intersect the curve at all,
233 // and we expect that to be the VAST majority of the tiles in the raster.
234 // => Smaller tile sizes = more regions we can shortcut over = less pixels to iterate over = faster runtime
235 it.setMaximumTileHeight( 64 );
236 it.setMaximumTileWidth( 64 );
237
238 it.startRasterRead( mBand, subRegionWidth, subRegionHeight, rasterSubRegion );
239
240 const double halfPixelSizeX = mRasterUnitsPerPixelX / 2.0;
241 const double halfPixelSizeY = mRasterUnitsPerPixelY / 2.0;
242 int blockColumns = 0;
243 int blockRows = 0;
244 int blockTopLeftColumn = 0;
245 int blockTopLeftRow = 0;
246 QgsRectangle blockExtent;
247
248 while ( it.next( mBand, blockColumns, blockRows, blockTopLeftColumn, blockTopLeftRow, blockExtent ) )
249 {
250 if ( mFeedback->isCanceled() )
251 return false;
252
253 const QgsGeometry blockExtentGeom = QgsGeometry::fromRect( blockExtent );
254 if ( !curveEngine->intersects( blockExtentGeom.constGet() ) )
255 continue;
256
257 std::unique_ptr< QgsRasterBlock > block( mRasterProvider->block( mBand, blockExtent, blockColumns, blockRows, mFeedback.get() ) );
258 if ( mFeedback->isCanceled() )
259 return false;
260
261 if ( !block )
262 continue;
263
264 bool isNoData = false;
265
266 // there's two potential code paths we use here, depending on if we want to sample at every pixel, or if we only want to
267 // sample at specific points
268 if ( !std::isnan( stepDistance ) )
269 {
270 auto it = profilePoints.begin();
271 while ( it != profilePoints.end() )
272 {
273 if ( mFeedback->isCanceled() )
274 return false;
275
276 // convert point to a pixel and sample, if it's in this block
277 if ( blockExtent.contains( *it ) )
278 {
279 int row, col;
280 if ( zeroXYSize )
281 {
282 row = std::clamp( static_cast< int >( std::round( ( blockExtent.yMaximum() - it->y() ) * blockRows / blockExtent.height() ) ), 0, blockRows - 1 );
283 col = std::clamp( static_cast< int >( std::round( ( it->x() - blockExtent.xMinimum() ) * blockColumns / blockExtent.width() ) ), 0, blockColumns - 1 );
284 }
285 else
286 {
287 row = std::clamp( static_cast< int >( std::round( ( blockExtent.yMaximum() - it->y() ) / mRasterUnitsPerPixelY ) ), 0, blockRows - 1 );
288 col = std::clamp( static_cast< int >( std::round( ( it->x() - blockExtent.xMinimum() ) / mRasterUnitsPerPixelX ) ), 0, blockColumns - 1 );
289 }
290 double val = block->valueAndNoData( row, col, isNoData );
291 if ( !isNoData )
292 {
293 val = val * mScale + mOffset;
294 }
295 else
296 {
297 val = std::numeric_limits<double>::quiet_NaN();
298 }
299
300 QgsPoint pixel( it->x(), it->y(), val );
301 try
302 {
303 pixel.transform( rasterToTargetTransform );
304 }
305 catch ( QgsCsException & )
306 {
307 continue;
308 }
309 mResults->mRawPoints.append( pixel );
310
311 it = profilePoints.erase( it );
312 }
313 else
314 {
315 it++;
316 }
317 }
318
319 if ( profilePoints.isEmpty() )
320 break; // all done!
321 }
322 else
323 {
324 double currentY = blockExtent.yMaximum() - 0.5 * mRasterUnitsPerPixelY;
325 for ( int row = 0; row < blockRows; ++row )
326 {
327 if ( mFeedback->isCanceled() )
328 return false;
329
330 double currentX = blockExtent.xMinimum() + 0.5 * mRasterUnitsPerPixelX;
331 for ( int col = 0; col < blockColumns; ++col, currentX += mRasterUnitsPerPixelX )
332 {
333 const double val = block->valueAndNoData( row, col, isNoData );
334
335 // does pixel intersect curve?
336 QgsGeometry pixelRectGeometry = QgsGeometry::fromRect( QgsRectangle( currentX - halfPixelSizeX,
337 currentY - halfPixelSizeY,
338 currentX + halfPixelSizeX,
339 currentY + halfPixelSizeY ) );
340 if ( !curveEngine->intersects( pixelRectGeometry.constGet() ) )
341 continue;
342
343 QgsPoint pixel( currentX, currentY, isNoData ? std::numeric_limits<double>::quiet_NaN() : val * mScale + mOffset );
344 try
345 {
346 pixel.transform( rasterToTargetTransform );
347 }
348 catch ( QgsCsException & )
349 {
350 continue;
351 }
352 mResults->mRawPoints.append( pixel );
353 }
354 currentY -= mRasterUnitsPerPixelY;
355 }
356 }
357 }
358
359 if ( mFeedback->isCanceled() )
360 return false;
361
362 // convert x/y values back to distance/height values
363 QgsGeos originalCurveGeos( sourceCurve );
364 originalCurveGeos.prepareGeometry();
365 QString lastError;
366 for ( const QgsPoint &pixel : std::as_const( mResults->mRawPoints ) )
367 {
368 if ( mFeedback->isCanceled() )
369 return false;
370
371 const double distance = originalCurveGeos.lineLocatePoint( pixel, &lastError ) + startDistanceOffset;
372
373 if ( !std::isnan( pixel.z() ) )
374 {
375 mResults->minZ = std::min( pixel.z(), mResults->minZ );
376 mResults->maxZ = std::max( pixel.z(), mResults->maxZ );
377 }
378 mResults->mDistanceToHeightMap.insert( distance, pixel.z() );
379 }
380
381 return true;
382}
383
388
390{
391 return mFeedback.get();
392}
@ RespectsMaximumErrorMapUnit
Generated profile respects the QgsProfileGenerationContext::maximumErrorMapUnits() property.
Definition qgis.h:4313
@ RespectsDistanceRange
Generated profile respects the QgsProfileGenerationContext::distanceRange() property.
Definition qgis.h:4314
QFlags< ProfileGeneratorFlag > ProfileGeneratorFlags
Definition qgis.h:4318
@ Reverse
Reverse/inverse transform (from destination to source).
Definition qgis.h:2731
virtual int nCoordinates() const
Returns the number of nodes contained in the geometry.
virtual double length() const
Returns the planar, 2-dimensional length of the geometry.
Abstract base class for storage of elevation profiles.
QgsAbstractProfileSurfaceGenerator(const QgsProfileRequest &request)
Constructor for QgsAbstractProfileSurfaceGenerator.
QVector< QgsProfileIdentifyResults > identify(const QgsProfilePoint &point, const QgsProfileIdentifyContext &context) override
Identify results visible at the specified profile point.
Handles coordinate transforms between two coordinate systems.
Custom exception class for Coordinate Reference System related exceptions.
Abstract base class for curved geometry type.
Definition qgscurve.h:36
QgsCurve * clone() const override=0
Clones the geometry by performing a deep copy.
bool isInfinite() const
Returns true if the range consists of all possible values.
Definition qgsrange.h:290
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
A geometry is the spatial representation of a feature.
static QgsGeometry fromRect(const QgsRectangle &rect)
Creates a new geometry from a QgsRectangle.
QgsGeometry densifyByDistance(double distance) const
Densifies the geometry by adding regularly placed extra nodes inside each segment so that the maximum...
Qgis::GeometryOperationResult transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection direction=Qgis::TransformDirection::Forward, bool transformZ=false)
Transforms this geometry as described by the coordinate transform ct.
const QgsAbstractGeometry * constGet() const
Returns a non-modifiable (const) reference to the underlying abstract geometry primitive.
QgsAbstractGeometry::vertex_iterator vertices_begin() const
Returns STL-style iterator pointing to the first vertex of the geometry.
static QgsGeometryEngine * createGeometryEngine(const QgsAbstractGeometry *geometry, double precision=0.0, Qgis::GeosCreationFlags flags=Qgis::GeosCreationFlag::SkipEmptyInteriorRings)
Creates and returns a new geometry engine representing the specified geometry using precision on a gr...
QgsAbstractGeometry::vertex_iterator vertices_end() const
Returns STL-style iterator pointing to the imaginary vertex after the last vertex of the geometry.
Does vector analysis using the GEOS library and handles import, export, and exception handling.
Definition qgsgeos.h:141
void prepareGeometry() override
Prepares the geometry, so that subsequent calls to spatial relation methods are much faster.
Definition qgsgeos.cpp:288
double lineLocatePoint(const QgsPoint &point, QString *errorMsg=nullptr) const
Returns a distance representing the location along this linestring of the closest point on this lines...
Definition qgsgeos.cpp:3181
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
void transform(const QgsCoordinateTransform &ct, Qgis::TransformDirection d=Qgis::TransformDirection::Forward, bool transformZ=false) override
Transforms the geometry using a coordinate transform.
Definition qgspoint.cpp:408
Encapsulates the context in which an elevation profile is to be generated.
double maximumErrorMapUnits() const
Returns the maximum allowed error in the generated result, in profile curve map units.
double mapUnitsPerDistancePixel() const
Returns the number of map units per pixel in the distance dimension.
QgsDoubleRange distanceRange() const
Returns the range of distances to include in the generation.
Encapsulates the context of identifying profile results.
Stores identify results generated by a QgsAbstractProfileResults object.
Encapsulates a point on a distance-elevation profile.
Encapsulates properties and constraints relating to fetching elevation profiles from different source...
T lower() const
Returns the lower bound of the range.
Definition qgsrange.h:81
T upper() const
Returns the upper bound of the range.
Definition qgsrange.h:88
Feedback object tailored for raster block reading.
QgsRasterDataProvider * clone() const override=0
Clone itself, create deep copy.
Iterator for sequentially processing raster cells.
static QgsRectangle subRegion(const QgsRectangle &rasterExtent, int rasterWidth, int rasterHeight, const QgsRectangle &subRegion, int &subRegionWidth, int &subRegionHeight, int &subRegionLeft, int &subRegionTop, int resamplingFactor=1)
Given an overall raster extent and width and height in pixels, calculates the sub region of the raste...
bool next(int bandNumber, int &columns, int &rows, int &topLeftColumn, int &topLeftRow, QgsRectangle &blockExtent)
Fetches details of the next part of the raster data.
void setMaximumTileWidth(int w)
Sets the maximum tile width returned during iteration.
void startRasterRead(int bandNumber, qgssize nCols, qgssize nRows, const QgsRectangle &extent, QgsRasterBlockFeedback *feedback=nullptr)
Start reading of raster band.
void setMaximumTileHeight(int h)
Sets the minimum tile height returned during iteration.
Raster layer specific subclass of QgsMapLayerElevationProperties.
QgsAbstractProfileResults * takeResults() override
Takes results from the generator.
QgsRasterLayerProfileGenerator(QgsRasterLayer *layer, const QgsProfileRequest &request)
Constructor for QgsRasterLayerProfileGenerator.
QString type() const override
Returns the unique string identifier for the results type.
QgsFeedback * feedback() const override
Access to feedback object of the generator (may be nullptr).
bool generateProfile(const QgsProfileGenerationContext &context=QgsProfileGenerationContext()) override
Generate the profile (based on data stored in the class).
Qgis::ProfileGeneratorFlags flags() const override
Returns flags which reflect how the profile generator operates.
QString sourceId() const override
Returns a unique identifier representing the source of the profile.
~QgsRasterLayerProfileGenerator() override
QVector< QgsProfileIdentifyResults > identify(const QgsProfilePoint &point, const QgsProfileIdentifyContext &context) override
Identify results visible at the specified profile point.
QString type() const override
Returns the unique string identifier for the results type.
Represents a raster layer.
QgsRasterDataProvider * dataProvider() override
Returns the source data provider.
QgsMapLayerElevationProperties * elevationProperties() override
Returns the layer's elevation properties.
A rectangle specified with double values.
bool contains(const QgsRectangle &rect) const
Returns true when rectangle contains other rectangle.
double xMinimum
double yMinimum
bool intersects(const QgsRectangle &rect) const
Returns true when rectangle intersects with other rectangle.
void setYMaximum(double y)
Set the maximum y value.
void setXMaximum(double x)
Set the maximum x value.
double yMaximum
Temporarily moves a QObject to the current thread, then resets it back to nullptr thread on destructi...
#define QgsDebugError(str)
Definition qgslogger.h:59