QGIS API Documentation 4.3.0-Master (6a28b93578f)
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"
19
20#include <QString>
21
22using namespace Qt::StringLiterals;
23
24#ifndef M_DEG2RAD
25#define M_DEG2RAD 0.0174532925
26#endif
27
28QgsVectorFieldEngine::QgsVectorFieldEngine( double datasetMagMaximumValue, double datasetMagMinimumValue, const QgsVectorFieldSettings &settings, QgsRenderContext &context, QSize size )
29 : mMinMag( datasetMagMinimumValue )
30 , mMaxMag( datasetMagMaximumValue )
31 , mContext( context )
32 , mCfg( settings )
33 , mVectorColoring( settings.vectorStrokeColoring() )
34 , mOutputSize( size )
35{
36 switch ( settings.symbology() )
37 {
38 case Qgis::VectorFieldSymbology::WindBarbs:
39 {
40 const QgsCoordinateReferenceSystem mapCrs = mContext.coordinateTransform().destinationCrs();
41 mGeographicTransform = std::make_unique<QgsCoordinateTransform>( mapCrs, mapCrs.toGeographicCrs(), mContext.coordinateTransform().context() );
42 break;
43 }
47 break;
48 }
49
50 // Set up the render configuration options
51 QPainter *painter = mContext.painter();
52
53 mScopedPainterState = std::make_unique<QgsScopedQPainterState>( painter );
54 mContext.setPainterFlagsUsingContext( painter );
55
56 QPen pen = painter->pen();
57 pen.setCapStyle( Qt::FlatCap );
58 pen.setJoinStyle( Qt::MiterJoin );
59
60 const double penWidth = mContext.convertToPainterUnits( mCfg.lineWidth(), Qgis::RenderUnit::Millimeters );
61 pen.setWidthF( penWidth );
62 painter->setPen( pen );
63}
64
66
67void QgsVectorFieldEngine::drawGlyph( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude )
68{
69 switch ( mCfg.symbology() )
70 {
72 drawArrow( lineStart, xVal, yVal, magnitude );
73 break;
75 drawWindBarb( lineStart, xVal, yVal, magnitude );
76 break;
79 // not drawn one glyph at a time, see drawStreamlines() and drawTraces()
80 break;
81 }
82}
83
84bool QgsVectorFieldEngine::calcVectorLineEnd(
85 QgsPointXY &lineEnd,
86 double &vectorLength,
87 double &cosAlpha,
88 double &sinAlpha, //out
89 const QgsPointXY &lineStart,
90 double xVal,
91 double yVal,
92 double magnitude //in
93)
94{
95 // return true on error
96
97 if ( xVal == 0.0 && yVal == 0.0 )
98 return true;
99
100 // do not render if magnitude is outside of the filtered range (if filtering is enabled)
101 if ( mCfg.filterMin() >= 0 && magnitude < mCfg.filterMin() )
102 return true;
103 if ( mCfg.filterMax() >= 0 && magnitude > mCfg.filterMax() )
104 return true;
105
106 // Determine the angle of the vector, counter-clockwise, from east
107 // (and associated trigs)
108 const double vectorAngle = std::atan2( yVal, xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD;
109
110 cosAlpha = cos( vectorAngle );
111 sinAlpha = sin( vectorAngle );
112
113 // Now determine the X and Y distances of the end of the line from the start
114 double xDist = 0.0;
115 double yDist = 0.0;
116 switch ( mCfg.arrowSettings().shaftLengthMethod() )
117 {
119 {
120 const double minShaftLength = mContext.convertToPainterUnits( mCfg.arrowSettings().minShaftLength(), Qgis::RenderUnit::Millimeters );
121 const double maxShaftLength = mContext.convertToPainterUnits( mCfg.arrowSettings().maxShaftLength(), Qgis::RenderUnit::Millimeters );
122 const double minVal = mMinMag;
123 const double maxVal = mMaxMag;
124 const double k = ( magnitude - minVal ) / ( maxVal - minVal );
125 const double L = minShaftLength + k * ( maxShaftLength - minShaftLength );
126 xDist = cosAlpha * L;
127 yDist = sinAlpha * L;
128 break;
129 }
131 {
132 const double scaleFactor = mCfg.arrowSettings().scaleFactor();
133 xDist = scaleFactor * xVal;
134 yDist = scaleFactor * yVal;
135 break;
136 }
138 {
139 // We must be using a fixed length
140 const double fixedShaftLength = mContext.convertToPainterUnits( mCfg.arrowSettings().fixedShaftLength(), Qgis::RenderUnit::Millimeters );
141 xDist = cosAlpha * fixedShaftLength;
142 yDist = sinAlpha * fixedShaftLength;
143 break;
144 }
145 }
146
147 // Flip the Y axis (pixel vs real-world axis)
148 yDist *= -1.0;
149
150 if ( std::abs( xDist ) < 1 && std::abs( yDist ) < 1 )
151 return true;
152
153 // Determine the line coords
154 lineEnd = QgsPointXY( lineStart.x() + xDist, lineStart.y() + yDist );
155
156 vectorLength = sqrt( xDist * xDist + yDist * yDist );
157
158 // skip rendering if line bbox does not intersect the QImage area
159 if ( !QgsRectangle( lineStart, lineEnd ).intersects( QgsRectangle( 0, 0, mOutputSize.width(), mOutputSize.height() ) ) )
160 return true;
161
162 return false; //success
163}
164
165void QgsVectorFieldEngine::drawArrow( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude )
166{
167 QgsPointXY lineEnd;
168 double vectorLength;
169 double cosAlpha, sinAlpha;
170 if ( calcVectorLineEnd( lineEnd, vectorLength, cosAlpha, sinAlpha, lineStart, xVal, yVal, magnitude ) )
171 return;
172
173 // Make a set of vector head coordinates that we will place at the end of each vector,
174 // scale, translate and rotate.
175 QgsPointXY vectorHeadPoints[3];
176 QVector<QPointF> finalVectorHeadPoints( 3 );
177
178 const double vectorHeadWidthRatio = mCfg.arrowSettings().arrowHeadWidthRatio();
179 const double vectorHeadLengthRatio = mCfg.arrowSettings().arrowHeadLengthRatio();
180
181 // First head point: top of ->
182 vectorHeadPoints[0].setX( -1.0 * vectorHeadLengthRatio );
183 vectorHeadPoints[0].setY( vectorHeadWidthRatio * 0.5 );
184
185 // Second head point: right of ->
186 vectorHeadPoints[1].setX( 0.0 );
187 vectorHeadPoints[1].setY( 0.0 );
188
189 // Third head point: bottom of ->
190 vectorHeadPoints[2].setX( -1.0 * vectorHeadLengthRatio );
191 vectorHeadPoints[2].setY( -1.0 * vectorHeadWidthRatio * 0.5 );
192
193 // Determine the arrow head coords
194 for ( int j = 0; j < 3; j++ )
195 {
196 finalVectorHeadPoints[j].setX( lineEnd.x() + ( vectorHeadPoints[j].x() * cosAlpha * vectorLength ) - ( vectorHeadPoints[j].y() * sinAlpha * vectorLength ) );
197
198 finalVectorHeadPoints[j].setY( lineEnd.y() - ( vectorHeadPoints[j].x() * sinAlpha * vectorLength ) - ( vectorHeadPoints[j].y() * cosAlpha * vectorLength ) );
199 }
200
201 // Now actually draw the vector
202 QPen pen( mContext.painter()->pen() );
203 pen.setColor( mVectorColoring.color( magnitude ) );
204 mContext.painter()->setPen( pen );
205 mContext.painter()->drawLine( lineStart.toQPointF(), lineEnd.toQPointF() );
206 mContext.painter()->drawPolygon( finalVectorHeadPoints );
207}
208
209void QgsVectorFieldEngine::drawWindBarb( const QgsPointXY &lineStart, double xVal, double yVal, double magnitude )
210{
211 // do not render if magnitude is outside of the filtered range (if filtering is enabled)
212 if ( mCfg.filterMin() >= 0 && magnitude < mCfg.filterMin() )
213 return;
214 if ( mCfg.filterMax() >= 0 && magnitude > mCfg.filterMax() )
215 return;
216
217 QPen pen( mContext.painter()->pen() );
218 pen.setColor( mVectorColoring.color( magnitude ) );
219 mContext.painter()->setPen( pen );
220
221 // we need a brush to fill center circle and pennants
222 QBrush brush( pen.color() );
223 mContext.painter()->setBrush( brush );
224
225 const double shaftLength = mContext.convertToPainterUnits( mCfg.windBarbSettings().shaftLength(), mCfg.windBarbSettings().shaftLengthUnits() );
226 if ( shaftLength < 1 )
227 return;
228
229 // Check if barb is above or below the equinox
230 const QgsPointXY mapPoint = mContext.mapToPixel().toMapCoordinates( lineStart.x(), lineStart.y() );
231 bool isNorthHemisphere = true;
232 try
233 {
234 const QgsPointXY geoPoint = mGeographicTransform->transform( mapPoint );
235 isNorthHemisphere = geoPoint.y() >= 0;
236 }
237 catch ( QgsCsException & )
238 {
239 QgsDebugError( u"Could not transform wind barb coordinates to geographic ones"_s );
240 }
241
242 const double d = shaftLength / 25; // this is a magic number ratio between shaft length and other barb dimensions
243 const double centerRadius = d;
244 const double zeroCircleRadius = 2 * d;
245 const double barbLength = 8 * d + pen.widthF();
246 const double barbAngle = 135;
247 const double barbOffset = 2 * d + pen.widthF();
248 const int sign = isNorthHemisphere ? 1 : -1;
249
250 // Determine the angle of the vector, counter-clockwise, from east
251 // (and associated trigs)
252 const double vectorAngle = std::atan2( yVal, xVal ) - mContext.mapToPixel().mapRotation() * M_DEG2RAD;
253
254 // Now determine the X and Y distances of the end of the line from the start
255 // Flip the Y axis (pixel vs real-world axis)
256 const double xDist = cos( vectorAngle ) * shaftLength;
257 const double yDist = -sin( vectorAngle ) * shaftLength;
258
259 // Determine the line coords
260 const QgsPointXY lineEnd = QgsPointXY( lineStart.x() - xDist, lineStart.y() - yDist );
261
262 // skip rendering if line bbox does not intersect the QImage area
263 if ( !QgsRectangle( lineStart, lineEnd ).intersects( QgsRectangle( 0, 0, mOutputSize.width(), mOutputSize.height() ) ) )
264 return;
265
266 // scale the magnitude to convert it to knots
267 double knots = magnitude * mCfg.windBarbSettings().magnitudeMultiplier();
268 QgsPointXY nextLineOrigin = lineEnd;
269
270 // special case for no wind, just an empty circle
271 if ( knots < 2.5 )
272 {
273 mContext.painter()->setBrush( Qt::NoBrush );
274 mContext.painter()->drawEllipse( lineStart.toQPointF(), zeroCircleRadius, zeroCircleRadius );
275 mContext.painter()->setBrush( brush );
276 return;
277 }
278
279 const double azimuth = lineEnd.azimuth( lineStart );
280
281 // conditionally draw the shaft
282 if ( knots < 47.5 && knots > 7.5 )
283 {
284 // When first barb is a '10', we want to draw the shaft and barb as a single polyline for a proper join
285 const QVector< QPointF > pts { lineStart.toQPointF(), lineEnd.toQPointF(), nextLineOrigin.project( barbLength, azimuth + barbAngle * sign ).toQPointF() };
286 mContext.painter()->drawPolyline( pts );
287 nextLineOrigin = nextLineOrigin.project( barbOffset, azimuth );
288 knots -= 10;
289 }
290 else
291 {
292 // draw just the shaft
293 mContext.painter()->drawLine( lineStart.toQPointF(), lineEnd.toQPointF() );
294 }
295
296 // draw the center circle
297 mContext.painter()->drawEllipse( lineStart.toQPointF(), centerRadius, centerRadius );
298
299 // draw pennants (50)
300 while ( knots > 47.5 )
301 {
302 const QVector< QPointF >
303 pts { nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength / 1.414, azimuth + 90 * sign ).toQPointF(), nextLineOrigin.project( barbLength / 1.414, azimuth ).toQPointF() };
304 mContext.painter()->drawPolygon( pts );
305 knots -= 50;
306
307 // don't use an offset for the next pennant
308 if ( knots > 47.5 )
309 nextLineOrigin = nextLineOrigin.project( barbLength / 1.414, azimuth );
310 else
311 nextLineOrigin = nextLineOrigin.project( barbLength / 1.414 + barbOffset, azimuth );
312 }
313
314 // draw large barbs (10)
315 while ( knots > 7.5 )
316 {
317 mContext.painter()->drawLine( nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength, azimuth + barbAngle * sign ).toQPointF() );
318 nextLineOrigin = nextLineOrigin.project( barbOffset, azimuth );
319 knots -= 10;
320 }
321
322 // draw small barb (5)
323 if ( knots > 2.5 )
324 {
325 // a single '5' barb should not start at the line end
326 if ( nextLineOrigin == lineEnd )
327 nextLineOrigin = nextLineOrigin.project( barbLength / 2, azimuth );
328
329 mContext.painter()->drawLine( nextLineOrigin.toQPointF(), nextLineOrigin.project( barbLength / 2, azimuth + barbAngle * sign ).toQPointF() );
330 }
331}
@ WindBarbs
Displaying vector dataset with wind barbs.
Definition qgis.h:7184
@ Arrows
Displaying vector dataset with arrows.
Definition qgis.h:7181
@ Traces
Displaying vector dataset with particle traces.
Definition qgis.h:7183
@ Streamlines
Displaying vector dataset with streamlines.
Definition qgis.h:7182
@ Millimeters
Millimeters.
Definition qgis.h:5717
@ Fixed
Use fixed length fixedShaftLength() regardless of vector's magnitude.
Definition qgis.h:7168
@ Scaled
Scale vector magnitude by factor scaleFactor().
Definition qgis.h:7167
@ MinMax
Scale vector magnitude linearly to fit in range of vectorFilterMin() and vectorFilterMax().
Definition qgis.h:7166
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
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...
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