QGIS API Documentation 3.43.0-Master (32433f7016e)
qgsprofilerenderer.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsprofilerenderer.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 ***************************************************************************/
17#include "qgsprofilerenderer.h"
18#include "moc_qgsprofilerenderer.cpp"
21#include "qgscurve.h"
22#include "qgsprofilesnapping.h"
23#include "qgslinesymbollayer.h"
24
25#include <QtConcurrentMap>
26#include <QtConcurrentRun>
27
28QgsProfilePlotRenderer::QgsProfilePlotRenderer( const QList< QgsAbstractProfileSource * > &sources,
29 const QgsProfileRequest &request )
30 : mRequest( request )
31{
32 for ( QgsAbstractProfileSource *source : sources )
33 {
34 if ( source )
35 {
36 if ( std::unique_ptr< QgsAbstractProfileGenerator > generator{ source->createProfileGenerator( mRequest ) } )
37 mGenerators.emplace_back( std::move( generator ) );
38 }
39 }
40}
41
42QgsProfilePlotRenderer::QgsProfilePlotRenderer( std::vector<std::unique_ptr<QgsAbstractProfileGenerator> > generators, const QgsProfileRequest &request )
43 : mGenerators( std::move( generators ) )
44 , mRequest( request )
45{
46}
47
55
57{
58 QStringList res;
59 res.reserve( mGenerators.size() );
60 for ( const auto &it : mGenerators )
61 {
62 res.append( it->sourceId() );
63 }
64 return res;
65}
66
68{
69 if ( isActive() )
70 return;
71
72 mStatus = Generating;
73
74 Q_ASSERT( mJobs.empty() );
75
76 mJobs.reserve( mGenerators.size() );
77 for ( const auto &it : mGenerators )
78 {
79 auto job = std::make_unique< ProfileJob >();
80 job->generator = it.get();
81 job->context = mContext;
82 mJobs.emplace_back( std::move( job ) );
83 }
84
85 connect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsProfilePlotRenderer::onGeneratingFinished );
86
87 mFuture = QtConcurrent::map( mJobs, generateProfileStatic );
88 mFutureWatcher.setFuture( mFuture );
89}
90
92{
93 if ( isActive() )
94 return;
95
96 mStatus = Generating;
97
98 Q_ASSERT( mJobs.empty() );
99 mJobs.reserve( mGenerators.size() );
100
101 for ( const auto &it : mGenerators )
102 {
103 auto job = std::make_unique< ProfileJob >();
104 job->generator = it.get();
105 job->context = mContext;
106 it.get()->generateProfile( job->context );
107 job->results.reset( job->generator->takeResults() );
108 job->complete = true;
109 job->invalidatedResults.reset();
110 mJobs.emplace_back( std::move( job ) );
111 }
112
113 mStatus = Idle;
114}
115
117{
118 if ( !isActive() )
119 return;
120
121 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsProfilePlotRenderer::onGeneratingFinished );
122
123 for ( const auto &job : mJobs )
124 {
125 if ( job->generator )
126 {
127 if ( QgsFeedback *feedback = job->generator->feedback() )
128 {
129 feedback->cancel();
130 }
131 }
132 }
133
134 mFutureWatcher.waitForFinished();
135
136 onGeneratingFinished();
137}
138
140{
141 if ( !isActive() )
142 return;
143
144 for ( const auto &job : mJobs )
145 {
146 if ( job->generator )
147 {
148 if ( QgsFeedback *feedback = job->generator->feedback() )
149 {
150 feedback->cancel();
151 }
152 }
153 }
154}
155
157{
158 if ( !isActive() )
159 return;
160
161 disconnect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsProfilePlotRenderer::onGeneratingFinished );
162 mFutureWatcher.waitForFinished();
163
164 onGeneratingFinished();
165}
166
168{
169 return mStatus != Idle;
170}
171
173{
174 if ( mContext == context )
175 return;
176
177 const double maxErrorChanged = !qgsDoubleNear( context.maximumErrorMapUnits(), mContext.maximumErrorMapUnits() );
178 const double distanceRangeChanged = context.distanceRange() != mContext.distanceRange();
179 const double elevationRangeChanged = context.elevationRange() != mContext.elevationRange();
180 mContext = context;
181
182 for ( auto &job : mJobs )
183 {
184 // regenerate only those results which are refinable
185 const bool jobNeedsRegeneration = ( maxErrorChanged && ( job->generator->flags() & Qgis::ProfileGeneratorFlag::RespectsMaximumErrorMapUnit ) )
186 || ( distanceRangeChanged && ( job->generator->flags() & Qgis::ProfileGeneratorFlag::RespectsDistanceRange ) )
187 || ( elevationRangeChanged && ( job->generator->flags() & Qgis::ProfileGeneratorFlag::RespectsElevationRange ) );
188 if ( !jobNeedsRegeneration )
189 continue;
190
191 job->mutex.lock();
192 job->context = mContext;
193 if ( job->results && job->complete )
194 job->invalidatedResults = std::move( job->results );
195 job->results.reset();
196 job->complete = false;
197 job->mutex.unlock();
198 }
199}
200
202{
203 for ( auto &job : mJobs )
204 {
205 // regenerate only those results which are refinable
206 const bool jobNeedsRegeneration = ( job->generator->flags() & Qgis::ProfileGeneratorFlag::RespectsMaximumErrorMapUnit )
207 || ( job->generator->flags() & Qgis::ProfileGeneratorFlag::RespectsDistanceRange )
208 || ( job->generator->flags() & Qgis::ProfileGeneratorFlag::RespectsElevationRange );
209 if ( !jobNeedsRegeneration )
210 continue;
211
212 job->mutex.lock();
213 job->context = mContext;
214 if ( job->results && job->complete )
215 job->invalidatedResults = std::move( job->results );
216 job->results.reset();
217 job->complete = false;
218 job->mutex.unlock();
219 }
220}
221
223{
224 replaceSourceInternal( source, false );
225}
226
228{
229 return replaceSourceInternal( source, true );
230}
231
232bool QgsProfilePlotRenderer::replaceSourceInternal( QgsAbstractProfileSource *source, bool clearPreviousResults )
233{
234 if ( !source )
235 return false;
236
237 std::unique_ptr< QgsAbstractProfileGenerator > generator{ source->createProfileGenerator( mRequest ) };
238 if ( !generator )
239 return false;
240
241 QString sourceId = generator->sourceId();
242 bool res = false;
243 for ( auto &job : mJobs )
244 {
245 if ( job->generator && job->generator->sourceId() == sourceId )
246 {
247 job->mutex.lock();
248 res = true;
249 if ( clearPreviousResults )
250 {
251 job->results.reset();
252 job->complete = false;
253 }
254 else if ( job->results )
255 {
256 job->results->copyPropertiesFromGenerator( generator.get() );
257 }
258 job->generator = generator.get();
259 job->mutex.unlock();
260
261 for ( auto it = mGenerators.begin(); it != mGenerators.end(); )
262 {
263 if ( ( *it )->sourceId() == sourceId )
264 it = mGenerators.erase( it );
265 else
266 it++;
267 }
268 mGenerators.emplace_back( std::move( generator ) );
269 }
270 }
271 return res;
272}
273
275{
276 if ( isActive() )
277 return;
278
279 mStatus = Generating;
280
281 connect( &mFutureWatcher, &QFutureWatcher<void>::finished, this, &QgsProfilePlotRenderer::onGeneratingFinished );
282
283 mFuture = QtConcurrent::map( mJobs, generateProfileStatic );
284 mFutureWatcher.setFuture( mFuture );
285}
286
288{
289 double min = std::numeric_limits< double >::max();
290 double max = std::numeric_limits< double >::lowest();
291 for ( const auto &job : mJobs )
292 {
293 if ( job->complete && job->results )
294 {
295 const QgsDoubleRange jobRange = job->results->zRange();
296 min = std::min( min, jobRange.lower() );
297 max = std::max( max, jobRange.upper() );
298 }
299 }
300 return QgsDoubleRange( min, max );
301}
302
303QImage QgsProfilePlotRenderer::renderToImage( int width, int height, double distanceMin, double distanceMax, double zMin, double zMax, const QString &sourceId, double devicePixelRatio )
304{
305 QImage res( width, height, QImage::Format_ARGB32_Premultiplied );
306 res.setDotsPerMeterX( 96 / 25.4 * 1000 );
307 res.setDotsPerMeterY( 96 / 25.4 * 1000 );
308 res.fill( Qt::transparent );
309
310 QPainter p( &res );
311
314 context.setPainterFlagsUsingContext( &p );
315 context.setDevicePixelRatio( devicePixelRatio );
316 const double mapUnitsPerPixel = ( distanceMax - distanceMin ) / width;
317 context.setMapToPixel( QgsMapToPixel( mapUnitsPerPixel ) );
318
319 render( context, width, height, distanceMin, distanceMax, zMin, zMax, sourceId );
320 QRectF plotArea( QPointF( 0, 0 ), QPointF( width, height ) );
321 renderSubsectionsIndicator( context, plotArea, distanceMin, distanceMax, zMin, zMax );
322 p.end();
323
324 return res;
325}
326
327QTransform QgsProfilePlotRenderer::computeRenderTransform( double width, double height, double distanceMin, double distanceMax, double zMin, double zMax )
328{
329 QTransform transform;
330 transform.translate( 0, height );
331 transform.scale( width / ( distanceMax - distanceMin ), -height / ( zMax - zMin ) );
332 transform.translate( -distanceMin, -zMin );
333
334 return transform;
335}
336
337void QgsProfilePlotRenderer::render( QgsRenderContext &context, double width, double height, double distanceMin, double distanceMax, double zMin, double zMax, const QString &sourceId )
338{
339 QPainter *painter = context.painter();
340 if ( !painter )
341 return;
342
343 QgsProfileRenderContext profileRenderContext( context );
344 profileRenderContext.setWorldTransform( computeRenderTransform( width, height, distanceMin, distanceMax, zMin, zMax ) );
345
346 profileRenderContext.setDistanceRange( QgsDoubleRange( distanceMin, distanceMax ) );
347 profileRenderContext.setElevationRange( QgsDoubleRange( zMin, zMax ) );
348
349 for ( auto &job : mJobs )
350 {
351 if ( ( sourceId.isEmpty() || job->generator->sourceId() == sourceId ) )
352 {
353 job->mutex.lock();
354 if ( job->complete && job->results )
355 {
356 job->results->renderResults( profileRenderContext );
357 }
358 else if ( !job->complete && job->invalidatedResults )
359 {
360 // draw the outdated results while we wait for refinement to complete
361 job->invalidatedResults->renderResults( profileRenderContext );
362 }
363 job->mutex.unlock();
364 }
365 }
366}
367
369{
370 auto subSections = std::make_unique< QgsSimpleLineSymbolLayer >( QColor( 255, 0, 0, 255 ), 0.5 );
371 subSections->setPenCapStyle( Qt::FlatCap );
372 return std::make_unique<QgsLineSymbol>( QgsSymbolLayerList() << subSections.release() );
373}
374
376{
377 mSubsectionsSymbol.reset( symbol );
378}
379
381{
382 return mSubsectionsSymbol.get();
383}
384
385void QgsProfilePlotRenderer::renderSubsectionsIndicator( QgsRenderContext &context, const QRectF &plotArea, double distanceMin, double distanceMax, double zMin, double zMax )
386{
387 QgsCurve *profileCurve = mRequest.profileCurve();
388 if ( !profileCurve || profileCurve->length() < 3 || !mSubsectionsSymbol )
389 return;
390
391 QTransform transform = computeRenderTransform( plotArea.width(), plotArea.height(), distanceMin, distanceMax, zMin, zMax );
392
393 QgsPointSequence points;
394 profileCurve->points( points );
395 QgsPoint firstPoint = points.takeFirst();
396 points.removeLast();
397
398 mSubsectionsSymbol->startRender( context );
399 double accumulatedDistance = 0.;
400 for ( const QgsPoint &point : points )
401 {
402 accumulatedDistance += point.distance( firstPoint );
403 QPointF output = transform.map( QPointF( accumulatedDistance, 0. ) );
404 QPolygonF polyLine( QVector<QPointF> { QPointF( output.x() + plotArea.left(), plotArea.top() ), QPointF( output.x() + plotArea.left(), plotArea.bottom() ) } );
405 mSubsectionsSymbol->renderPolyline( polyLine, nullptr, context );
406 firstPoint = point;
407 }
408 mSubsectionsSymbol->stopRender( context );
409}
410
412{
413 QgsProfileSnapResult bestSnapResult;
414 if ( !mRequest.profileCurve() )
415 return bestSnapResult;
416
417 double bestSnapDistance = std::numeric_limits< double >::max();
418
419 for ( const auto &job : mJobs )
420 {
421 job->mutex.lock();
422 if ( job->complete && job->results )
423 {
424 const QgsProfileSnapResult jobSnapResult = job->results->snapPoint( point, context );
425 if ( jobSnapResult.isValid() )
426 {
427 const double snapDistance = std::pow( point.distance() - jobSnapResult.snappedPoint.distance(), 2 )
428 + std::pow( ( point.elevation() - jobSnapResult.snappedPoint.elevation() ) / context.displayRatioElevationVsDistance, 2 );
429
430 if ( snapDistance < bestSnapDistance )
431 {
432 bestSnapDistance = snapDistance;
433 bestSnapResult = jobSnapResult;
434 }
435 }
436 }
437 job->mutex.unlock();
438 }
439
440 return bestSnapResult;
441}
442
443QVector<QgsProfileIdentifyResults> QgsProfilePlotRenderer::identify( const QgsProfilePoint &point, const QgsProfileIdentifyContext &context )
444{
445 QVector<QgsProfileIdentifyResults> res;
446 if ( !mRequest.profileCurve() )
447 return res;
448
449 for ( const auto &job : mJobs )
450 {
451 job->mutex.lock();
452 if ( job->complete && job->results )
453 {
454 res.append( job->results->identify( point, context ) );
455 }
456 job->mutex.unlock();
457 }
458
459 return res;
460}
461
462QVector<QgsProfileIdentifyResults> QgsProfilePlotRenderer::identify( const QgsDoubleRange &distanceRange, const QgsDoubleRange &elevationRange, const QgsProfileIdentifyContext &context )
463{
464 QVector<QgsProfileIdentifyResults> res;
465 if ( !mRequest.profileCurve() )
466 return res;
467
468 for ( const auto &job : mJobs )
469 {
470 job->mutex.lock();
471 if ( job->complete && job->results )
472 {
473 res.append( job->results->identify( distanceRange, elevationRange, context ) );
474 }
475 job->mutex.unlock();
476 }
477
478 return res;
479}
480
481QVector<QgsAbstractProfileResults::Feature> QgsProfilePlotRenderer::asFeatures( Qgis::ProfileExportType type, QgsFeedback *feedback )
482{
483 QVector<QgsAbstractProfileResults::Feature > res;
484 for ( const auto &job : mJobs )
485 {
486 if ( feedback && feedback->isCanceled() )
487 break;
488
489 job->mutex.lock();
490 if ( job->complete && job->results )
491 {
492 res.append( job->results->asFeatures( type, feedback ) );
493 }
494 job->mutex.unlock();
495 }
496 return res;
497}
498
499void QgsProfilePlotRenderer::onGeneratingFinished()
500{
501 mStatus = Idle;
502 emit generationFinished();
503}
504
505void QgsProfilePlotRenderer::generateProfileStatic( std::unique_ptr< ProfileJob > &job )
506{
507 if ( job->results )
508 return;
509
510 Q_ASSERT( job->generator );
511
512 job->generator->generateProfile( job->context );
513 job->mutex.lock();
514 job->results.reset( job->generator->takeResults() );
515 job->complete = true;
516 job->invalidatedResults.reset();
517 job->mutex.unlock();
518}
@ RespectsMaximumErrorMapUnit
Generated profile respects the QgsProfileGenerationContext::maximumErrorMapUnits() property.
@ RespectsElevationRange
Generated profile respects the QgsProfileGenerationContext::elevationRange() property.
@ RespectsDistanceRange
Generated profile respects the QgsProfileGenerationContext::distanceRange() property.
@ Antialiasing
Use antialiasing while drawing.
ProfileExportType
Types of export for elevation profiles.
Definition qgis.h:4084
virtual double length() const
Returns the planar, 2-dimensional length of the geometry.
virtual QString sourceId() const =0
Returns a unique identifier representing the source of the profile.
Interface for classes which can generate elevation profiles.
virtual QgsAbstractProfileGenerator * createProfileGenerator(const QgsProfileRequest &request)=0
Given a profile request, returns a new profile generator ready for generating elevation profiles.
Abstract base class for curved geometry type.
Definition qgscurve.h:35
virtual void points(QgsPointSequence &pt) const =0
Returns a list of points within the curve.
QgsRange which stores a range of double values.
Definition qgsrange.h:233
Base class for feedback objects to be used for cancellation of something running in a worker thread.
Definition qgsfeedback.h:44
bool isCanceled() const
Tells whether the operation has been canceled already.
Definition qgsfeedback.h:53
A line symbol type, for rendering LineString and MultiLineString geometries.
Perform transforms between map coordinates and device coordinates.
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:49
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.
QgsDoubleRange elevationRange() const
Returns the range of elevations to include in the generation.
QgsDoubleRange distanceRange() const
Returns the range of distances to include in the generation.
Encapsulates the context of identifying profile results.
void setSubsectionsSymbol(QgsLineSymbol *symbol)
Sets the symbol used to draw the subsections.
QgsProfileSnapResult snapPoint(const QgsProfilePoint &point, const QgsProfileSnapContext &context)
Snap a point to the results.
void render(QgsRenderContext &context, double width, double height, double distanceMin, double distanceMax, double zMin, double zMax, const QString &sourceId=QString())
Renders a portion of the profile using the specified render context.
void renderSubsectionsIndicator(QgsRenderContext &context, const QRectF &plotArea, double distanceMin, double distanceMax, double zMin, double zMax)
Renders the vertices of the profile curve as vertical lines using the specified render context.
void regenerateInvalidatedResults()
Starts a background regeneration of any invalidated results and immediately returns.
QVector< QgsAbstractProfileResults::Feature > asFeatures(Qgis::ProfileExportType type, QgsFeedback *feedback=nullptr)
Exports the profile results as a set of features.
QImage renderToImage(int width, int height, double distanceMin, double distanceMax, double zMin, double zMax, const QString &sourceId=QString(), double devicePixelRatio=1.0)
Renders a portion of the profile to an image with the given width and height.
void cancelGenerationWithoutBlocking()
Triggers cancellation of the generation job without blocking.
QgsLineSymbol * subsectionsSymbol()
Returns the line symbol used to draw the subsections.
void invalidateAllRefinableSources()
Invalidates previous results from all refinable sources.
void cancelGeneration()
Stop the generation job - does not return until the job has terminated.
QgsProfilePlotRenderer(const QList< QgsAbstractProfileSource * > &sources, const QgsProfileRequest &request)
Constructor for QgsProfilePlotRenderer, using the provided list of profile sources to generate the re...
void generateSynchronously()
Generate the profile results synchronously in this thread.
void startGeneration()
Start the generation job and immediately return.
QgsDoubleRange zRange() const
Returns the limits of the retrieved elevation values.
QVector< QgsProfileIdentifyResults > identify(const QgsProfilePoint &point, const QgsProfileIdentifyContext &context)
Identify results visible at the specified profile point.
void waitForFinished()
Block until the current job has finished.
bool isActive() const
Returns true if the generation job is currently running in background.
QStringList sourceIds() const
Returns the ordered list of source IDs for the sources used by the renderer.
bool invalidateResults(QgsAbstractProfileSource *source)
Invalidates the profile results from the source with matching ID.
void replaceSource(QgsAbstractProfileSource *source)
Replaces the existing source with matching ID.
void setContext(const QgsProfileGenerationContext &context)
Sets the context in which the profile generation will occur.
void generationFinished()
Emitted when the profile generation is finished (or canceled).
static std::unique_ptr< QgsLineSymbol > defaultSubSectionsSymbol()
Returns the default line symbol to use for subsections lines.
Encapsulates a point on a distance-elevation profile.
double elevation() const
Returns the elevation of the point.
double distance() const
Returns the distance of the point.
Abstract base class for storage of elevation profiles.
void setWorldTransform(const QTransform &transform)
Sets the transform from world coordinates to painter coordinates.
void setDistanceRange(const QgsDoubleRange &range)
Sets the range of distances to include in the render.
void setElevationRange(const QgsDoubleRange &range)
Sets the range of elevations to include in the render.
Encapsulates properties and constraints relating to fetching elevation profiles from different source...
QgsCurve * profileCurve() const
Returns the cross section profile curve, which represents the line along which the profile should be ...
Encapsulates the context of snapping a profile point.
double displayRatioElevationVsDistance
Display ratio of elevation vs distance units.
Encapsulates results of snapping a profile point.
bool isValid() const
Returns true if the result is a valid point.
QgsProfilePoint snappedPoint
Snapped point.
T lower() const
Returns the lower bound of the range.
Definition qgsrange.h:78
T upper() const
Returns the upper bound of the range.
Definition qgsrange.h:85
Contains information about the context of a rendering operation.
void setDevicePixelRatio(float ratio)
Sets the device pixel ratio.
QPainter * painter()
Returns the destination QPainter for the render operation.
void setPainterFlagsUsingContext(QPainter *painter=nullptr) const
Sets relevant flags on a destination painter, using the flags and settings currently defined for the ...
void setFlag(Qgis::RenderContextFlag flag, bool on=true)
Enable or disable a particular flag (other flags are not affected)
void setMapToPixel(const QgsMapToPixel &mtp)
Sets the context's map to pixel transform, which transforms between map coordinates and device coordi...
static QgsRenderContext fromQPainter(QPainter *painter)
Creates a default render context given a pixel based QPainter destination.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition qgis.h:6286
QVector< QgsPoint > QgsPointSequence
QList< QgsSymbolLayer * > QgsSymbolLayerList
Definition qgssymbol.h:30