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