QGIS API Documentation 4.3.0-Master (b46232e4f9a)
Loading...
Searching...
No Matches
qgsvectorfieldengine.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsvectorfieldengine.cpp
3 ---------------------
4 begin : September 2026
5 copyright : (C) 2026 by Stefanos Natsis
6 email : uclaros 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 "qgspainting.h"
19#include "qgsrendercontext.h"
22
23#include <QString>
24
25using namespace Qt::StringLiterals;
26
27#ifndef M_DEG2RAD
28#define M_DEG2RAD 0.0174532925
29#endif
30
31QgsVectorFieldEngine::QgsVectorFieldEngine( double datasetMagMaximumValue, double datasetMagMinimumValue, const QgsVectorFieldSettings &settings, QgsRenderContext &context, QSize size )
32 : mMinMag( datasetMagMinimumValue )
33 , mMaxMag( datasetMagMaximumValue )
34 , mContext( context )
35 , mCfg( settings )
36 , mVectorColoring( settings.vectorStrokeColoring() )
37 , mOutputSize( size )
38{
39 switch ( settings.symbology() )
40 {
41 case Qgis::VectorFieldSymbology::WindBarbs:
42 {
43 const QgsCoordinateReferenceSystem mapCrs = mContext.coordinateTransform().destinationCrs();
44 mGeographicTransform = std::make_unique<QgsCoordinateTransform>( mapCrs, mapCrs.toGeographicCrs(), mContext.coordinateTransform().context() );
45 break;
46 }
50 break;
51 }
52
53 // Set up the render configuration options
54 QPainter *painter = mContext.painter();
55
56 mScopedPainterState = std::make_unique<QgsScopedQPainterState>( painter );
57 mContext.setPainterFlagsUsingContext( painter );
58
59 QPen pen = painter->pen();
60 pen.setCapStyle( Qt::FlatCap );
61 pen.setJoinStyle( Qt::MiterJoin );
62
63 const double penWidth = mContext.convertToPainterUnits( mCfg.lineWidth(), Qgis::RenderUnit::Millimeters );
64 pen.setWidthF( penWidth );
65 painter->setPen( pen );
66}
67
69
70void QgsVectorFieldEngine::drawGlyph( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude )
71{
72 switch ( mCfg.symbology() )
73 {
75 drawArrow( lineStart, xVal, yVal, magnitude );
76 break;
78 drawWindBarb( lineStart, xVal, yVal, magnitude );
79 break;
82 // not drawn one glyph at a time, see drawStreamlines() and drawTraces()
83 break;
84 }
85}
86
87void QgsVectorFieldEngine::drawStreamlines( std::unique_ptr<QgsVectorFieldValueSource> source, QgsRasterBlockFeedback *feedback )
88{
89 if ( !source )
90 return;
91
92 auto field = std::make_unique<QgsVectorFieldStreamlinesField>( std::move( source ), mContext, mVectorColoring, feedback );
93
94 field->updateSize( mContext );
95 field->setPixelFillingDensity( mCfg.streamLinesSettings().seedingDensity() );
96 field->setLineWidth( mContext.convertToPainterUnits( mCfg.lineWidth(), Qgis::RenderUnit::Millimeters ) );
97 field->setColor( mCfg.color() );
98 field->setFilter( mCfg.filterMin(), mCfg.filterMax() );
99
100 switch ( mCfg.streamLinesSettings().seedingMethod() )
101 {
103 if ( mCfg.isOnUserDefinedGrid() )
104 field->addGriddedTraces( mCfg.userGridCellWidth(), mCfg.userGridCellHeight() );
105 else
106 field->addTracesOnDataPoints( mContext.mapExtent() );
107 break;
109 field->addRandomTraces();
110 break;
111 }
112
113 if ( mContext.renderingStopped() )
114 return;
115
116 field->compose();
117 mContext.painter()->drawImage( field->topLeft(), field->image() );
118}
119
120void QgsVectorFieldEngine::drawTraces( std::unique_ptr<QgsVectorFieldValueSource> source )
121{
122 if ( !source )
123 return;
124
125 auto field = std::make_unique<QgsVectorFieldParticleTracesField>( std::move( source ), mContext, mVectorColoring );
126
127 field->updateSize( mContext );
128 field->setParticleSize( mContext.convertToPainterUnits( mCfg.lineWidth(), Qgis::RenderUnit::Millimeters ) );
129 field->setParticlesCount( mCfg.tracesSettings().particlesCount() );
130 field->setTailFactor( 1 );
131 field->setStumpParticleWithLifeTime( false );
132
133 // as the particles go through 1 pixel for dt=1 and Vmax, the maximum tail length is the time step
134 field->setTimeStep( mContext.convertToPainterUnits( mCfg.tracesSettings().maximumTailLength(), mCfg.tracesSettings().maximumTailLengthUnit() ) );
135
136 field->addRandomParticles();
137 field->moveParticles();
138
139 if ( mContext.renderingStopped() )
140 return;
141
142 mContext.painter()->drawImage( field->topLeft(), field->image() );
143}
144
145bool QgsVectorFieldEngine::calcVectorLineEnd(
146 QgsPointXY &lineEnd,
147 double &vectorLength,
148 double &cosAlpha,
149 double &sinAlpha, //out
150 const QgsPointXY &lineStart,
151 double xVal,
152 double yVal,
153 double magnitude //in
154)
155{
156 // return true on error
157
158 if ( xVal == 0.0 && yVal == 0.0 )
159 return true;
160
161 // do not render if magnitude is outside of the filtered range (if filtering is enabled)
162 if ( mCfg.filterMin() >= 0 && magnitude < mCfg.filterMin() )
163 return true;
164 if ( mCfg.filterMax() >= 0 && magnitude > mCfg.filterMax() )
165 return true;
166
167 // Determine the angle of the vector, counter-clockwise, from east
168 // (and associated trigs)
169 const double vectorAngle = std::atan2( yVal, xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD;
170
171 cosAlpha = cos( vectorAngle );
172 sinAlpha = sin( vectorAngle );
173
174 // Now determine the X and Y distances of the end of the line from the start
175 double xDist = 0.0;
176 double yDist = 0.0;
177 switch ( mCfg.arrowSettings().shaftLengthMethod() )
178 {
180 {
181 const double minShaftLength = mContext.convertToPainterUnits( mCfg.arrowSettings().minShaftLength(), Qgis::RenderUnit::Millimeters );
182 const double maxShaftLength = mContext.convertToPainterUnits( mCfg.arrowSettings().maxShaftLength(), Qgis::RenderUnit::Millimeters );
183 const double minVal = mMinMag;
184 const double maxVal = mMaxMag;
185 const double k = ( magnitude - minVal ) / ( maxVal - minVal );
186 const double L = minShaftLength + k * ( maxShaftLength - minShaftLength );
187 xDist = cosAlpha * L;
188 yDist = sinAlpha * L;
189 break;
190 }
192 {
193 const double scaleFactor = mCfg.arrowSettings().scaleFactor();
194 xDist = scaleFactor * xVal;
195 yDist = scaleFactor * yVal;
196 break;
197 }
199 {
200 // We must be using a fixed length
201 const double fixedShaftLength = mContext.convertToPainterUnits( mCfg.arrowSettings().fixedShaftLength(), Qgis::RenderUnit::Millimeters );
202 xDist = cosAlpha * fixedShaftLength;
203 yDist = sinAlpha * fixedShaftLength;
204 break;
205 }
206 }
207
208 // Flip the Y axis (pixel vs real-world axis)
209 yDist *= -1.0;
210
211 if ( std::abs( xDist ) < 1 && std::abs( yDist ) < 1 )
212 return true;
213
214 // Determine the line coords
215 lineEnd = QgsPointXY( lineStart.x() + xDist, lineStart.y() + yDist );
216
217 vectorLength = sqrt( xDist * xDist + yDist * yDist );
218
219 // skip rendering if line bbox does not intersect the QImage area
220 if ( !QgsRectangle( lineStart, lineEnd ).intersects( QgsRectangle( 0, 0, mOutputSize.width(), mOutputSize.height() ) ) )
221 return true;
222
223 return false; //success
224}
225
226void QgsVectorFieldEngine::drawArrow( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude )
227{
228 QgsPointXY lineEnd;
229 double vectorLength;
230 double cosAlpha, sinAlpha;
231 if ( calcVectorLineEnd( lineEnd, vectorLength, cosAlpha, sinAlpha, lineStart, xVal, yVal, magnitude ) )
232 return;
233
234 // Make a set of vector head coordinates that we will place at the end of each vector,
235 // scale, translate and rotate.
236 QgsPointXY vectorHeadPoints[3];
237 QVector<QPointF> finalVectorHeadPoints( 3 );
238
239 const double vectorHeadWidthRatio = mCfg.arrowSettings().arrowHeadWidthRatio();
240 const double vectorHeadLengthRatio = mCfg.arrowSettings().arrowHeadLengthRatio();
241
242 // First head point: top of ->
243 vectorHeadPoints[0].setX( -1.0 * vectorHeadLengthRatio );
244 vectorHeadPoints[0].setY( vectorHeadWidthRatio * 0.5 );
245
246 // Second head point: right of ->
247 vectorHeadPoints[1].setX( 0.0 );
248 vectorHeadPoints[1].setY( 0.0 );
249
250 // Third head point: bottom of ->
251 vectorHeadPoints[2].setX( -1.0 * vectorHeadLengthRatio );
252 vectorHeadPoints[2].setY( -1.0 * vectorHeadWidthRatio * 0.5 );
253
254 // Determine the arrow head coords
255 for ( int j = 0; j < 3; j++ )
256 {
257 finalVectorHeadPoints[j].setX( lineEnd.x() + ( vectorHeadPoints[j].x() * cosAlpha * vectorLength ) - ( vectorHeadPoints[j].y() * sinAlpha * vectorLength ) );
258
259 finalVectorHeadPoints[j].setY( lineEnd.y() - ( vectorHeadPoints[j].x() * sinAlpha * vectorLength ) - ( vectorHeadPoints[j].y() * cosAlpha * vectorLength ) );
260 }
261
262 // Now actually draw the vector
263 QPen pen( mContext.painter()->pen() );
264 pen.setColor( mVectorColoring.color( magnitude ) );
265 mContext.painter()->setPen( pen );
266 mContext.painter()->drawLine( lineStart.toQPointF(), lineEnd.toQPointF() );
267 mContext.painter()->drawPolygon( finalVectorHeadPoints );
268}
269
270void QgsVectorFieldEngine::drawWindBarb( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude )
271{
272 // do not render if magnitude is outside of the filtered range (if filtering is enabled)
273 if ( mCfg.filterMin() >= 0 && magnitude < mCfg.filterMin() )
274 return;
275 if ( mCfg.filterMax() >= 0 && magnitude > mCfg.filterMax() )
276 return;
277
278 QPen pen( mContext.painter()->pen() );
279 pen.setColor( mVectorColoring.color( magnitude ) );
280 mContext.painter()->setPen( pen );
281
282 // we need a brush to fill center circle and pennants
283 QBrush brush( pen.color() );
284 mContext.painter()->setBrush( brush );
285
286 const double shaftLength = mContext.convertToPainterUnits( mCfg.windBarbSettings().shaftLength(), mCfg.windBarbSettings().shaftLengthUnits() );
287 if ( shaftLength < 1 )
288 return;
289
290 // Check if barb is above or below the equinox
291 const QgsPointXY mapPoint = mContext.mapToPixel().toMapCoordinates( lineStart.x(), lineStart.y() );
292 bool isNorthHemisphere = true;
293 try
294 {
295 const QgsPointXY geoPoint = mGeographicTransform->transform( mapPoint );
296 isNorthHemisphere = geoPoint.y() >= 0;
297 }
298 catch ( QgsCsException & )
299 {
300 QgsDebugError( u"Could not transform wind barb coordinates to geographic ones"_s );
301 }
302
303 const double d = shaftLength / 25; // this is a magic number ratio between shaft length and other barb dimensions
304 const double centerRadius = d;
305 const double zeroCircleRadius = 2 * d;
306 const double barbLength = 8 * d + pen.widthF();
307 const double barbAngle = 135;
308 const double barbOffset = 2 * d + pen.widthF();
309 const int sign = isNorthHemisphere ? 1 : -1;
310
311 // Determine the angle of the vector, counter-clockwise, from east
312 // (and associated trigs)
313 const double vectorAngle = std::atan2( yVal, xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD;
314
315 // Now determine the X and Y distances of the end of the line from the start
316 // Flip the Y axis (pixel vs real-world axis)
317 const double xDist = cos( vectorAngle ) * shaftLength;
318 const double yDist = -sin( vectorAngle ) * shaftLength;
319
320 // Determine the line coords
321 const QgsPointXY lineEnd = QgsPointXY( lineStart.x() - xDist, lineStart.y() - yDist );
322
323 // skip rendering if line bbox does not intersect the QImage area
324 if ( !QgsRectangle( lineStart, lineEnd ).intersects( QgsRectangle( 0, 0, mOutputSize.width(), mOutputSize.height() ) ) )
325 return;
326
327 // scale the magnitude to convert it to knots
328 double knots = magnitude * mCfg.windBarbSettings().magnitudeMultiplier();
329 QgsPointXY nextLineOrigin = lineEnd;
330
331 // special case for no wind, just an empty circle
332 if ( knots < 2.5 )
333 {
334 mContext.painter()->setBrush( Qt::NoBrush );
335 mContext.painter()->drawEllipse( lineStart.toQPointF(), zeroCircleRadius, zeroCircleRadius );
336 mContext.painter()->setBrush( brush );
337 return;
338 }
339
340 const double azimuth = lineEnd.azimuth( lineStart );
341
342 // conditionally draw the shaft
343 if ( knots < 47.5 && knots > 7.5 )
344 {
345 // When first barb is a '10', we want to draw the shaft and barb as a single polyline for a proper join
346 const QVector< QPointF > pts { lineStart.toQPointF(), lineEnd.toQPointF(), nextLineOrigin.project( barbLength, azimuth + barbAngle * sign ).toQPointF() };
347 mContext.painter()->drawPolyline( pts );
348 nextLineOrigin = nextLineOrigin.project( barbOffset, azimuth );
349 knots -= 10;
350 }
351 else
352 {
353 // draw just the shaft
354 mContext.painter()->drawLine( lineStart.toQPointF(), lineEnd.toQPointF() );
355 }
356
357 // draw the center circle
358 mContext.painter()->drawEllipse( lineStart.toQPointF(), centerRadius, centerRadius );
359
360 // draw pennants (50)
361 while ( knots > 47.5 )
362 {
363 const QVector< QPointF >
364 pts { nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength / 1.414, azimuth + 90 * sign ).toQPointF(), nextLineOrigin.project( barbLength / 1.414, azimuth ).toQPointF() };
365 mContext.painter()->drawPolygon( pts );
366 knots -= 50;
367
368 // don't use an offset for the next pennant
369 if ( knots > 47.5 )
370 nextLineOrigin = nextLineOrigin.project( barbLength / 1.414, azimuth );
371 else
372 nextLineOrigin = nextLineOrigin.project( barbLength / 1.414 + barbOffset, azimuth );
373 }
374
375 // draw large barbs (10)
376 while ( knots > 7.5 )
377 {
378 mContext.painter()->drawLine( nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength, azimuth + barbAngle * sign ).toQPointF() );
379 nextLineOrigin = nextLineOrigin.project( barbOffset, azimuth );
380 knots -= 10;
381 }
382
383 // draw small barb (5)
384 if ( knots > 2.5 )
385 {
386 // a single '5' barb should not start at the line end
387 if ( nextLineOrigin == lineEnd )
388 nextLineOrigin = nextLineOrigin.project( barbLength / 2, azimuth );
389
390 mContext.painter()->drawLine( nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength / 2, azimuth + barbAngle * sign ).toQPointF() );
391 }
392}
@ Gridded
Seeds start points on data grid or user regular grid.
Definition qgis.h:7251
@ Random
Seeds start points randomly.
Definition qgis.h:7252
@ WindBarbs
Displaying vector dataset with wind barbs.
Definition qgis.h:7238
@ Arrows
Displaying vector dataset with arrows.
Definition qgis.h:7235
@ Traces
Displaying vector dataset with particle traces.
Definition qgis.h:7237
@ Streamlines
Displaying vector dataset with streamlines.
Definition qgis.h:7236
@ Millimeters
Millimeters.
Definition qgis.h:5759
@ Fixed
Use fixed length fixedShaftLength() regardless of vector's magnitude.
Definition qgis.h:7222
@ Scaled
Scale vector magnitude by factor scaleFactor().
Definition qgis.h:7221
@ MinMax
Scale vector magnitude linearly to fit in range of vectorFilterMin() and vectorFilterMax().
Definition qgis.h:7220
double mapRotation() const
Returns the current map rotation in degrees (clockwise).
Represents a 2D point.
Definition qgspointxy.h:62
QgsPointXY project(double distance, double bearing) const
Returns a new point which corresponds to this point projected by a specified distance in a specified ...
void setY(double y)
Sets the y value of the point.
Definition qgspointxy.h:132
double azimuth(const QgsPointXY &other) const
Calculates azimuth between this point and other one (clockwise in degree, starting from north).
double y
Definition qgspointxy.h:66
double x
Definition qgspointxy.h:65
void setX(double x)
Sets the x value of the point.
Definition qgspointxy.h:122
QPointF toQPointF() const
Converts a point to a QPointF.
Definition qgspointxy.h:168
Feedback object tailored for raster block reading.
Contains information about the context of a rendering operation.
double convertToPainterUnits(double size, Qgis::RenderUnit unit, const QgsMapUnitScale &scale=QgsMapUnitScale(), Qgis::RenderSubcomponentProperty property=Qgis::RenderSubcomponentProperty::Generic) const
Converts a size from the specified units to painter units (pixels).
const QgsMapToPixel & mapToPixel() const
Returns the context's map to pixel transform, which transforms between map coordinates and device coo...
double minShaftLength() const
Returns mininimum shaft length (in millimeters).
Qgis::VectorFieldArrowScalingMethod shaftLengthMethod() const
Returns method used for drawing arrows.
double maxShaftLength() const
Returns maximum shaft length (in millimeters).
QgsVectorFieldEngine(double datasetMagMaximumValue, double datasetMagMinimumValue, const QgsVectorFieldSettings &settings, QgsRenderContext &context, QSize size)
Ctor.
void drawGlyph(const QgsPointXY &lineStart, double xVal, double yVal, double magnitude)
Draws a single glyph at lineStart, in painter coordinates, using the symbology of the settings the en...
void drawStreamlines(std::unique_ptr< QgsVectorFieldValueSource > source, QgsRasterBlockFeedback *feedback=nullptr)
Integrates and draws streamlines over the whole rendered extent, sampling the vector field from sourc...
void drawTraces(std::unique_ptr< QgsVectorFieldValueSource > source)
Seeds particles over the whole rendered extent, moves them one time step and draws their traces,...
Represents a renderer settings for vector datasets.
double filterMin() const
Returns filter value for vector magnitudes.
QgsVectorFieldArrowSettings arrowSettings() const
Returns settings for vector rendered with arrows.
Qgis::VectorFieldSymbology symbology() const
Returns the displaying method used to render vector datasets.
double filterMax() const
Returns filter value for vector magnitudes.
#define M_DEG2RAD
#define QgsDebugError(str)
Definition qgslogger.h:71