QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
qgstextrendererutils.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgstextrendererutils.h
3 -----------------
4 begin : May 2020
5 copyright : (C) Nyall Dawson
6 email : nyall dot dawson 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#include "qgsvectorlayer.h"
18
20{
22 const QString skind = string.trimmed();
23
24 if ( skind.compare( QLatin1String( "Square" ), Qt::CaseInsensitive ) == 0 )
25 {
27 }
28 else if ( skind.compare( QLatin1String( "Ellipse" ), Qt::CaseInsensitive ) == 0 )
29 {
31 }
32 else if ( skind.compare( QLatin1String( "Circle" ), Qt::CaseInsensitive ) == 0 )
33 {
35 }
36 else if ( skind.compare( QLatin1String( "SVG" ), Qt::CaseInsensitive ) == 0 )
37 {
39 }
40 else if ( skind.compare( QLatin1String( "marker" ), Qt::CaseInsensitive ) == 0 )
41 {
43 }
44 return shpkind;
45}
46
48{
49 const QString stype = string.trimmed();
50 // "Buffer"
52
53 if ( stype.compare( QLatin1String( "Fixed" ), Qt::CaseInsensitive ) == 0 )
54 {
56 }
57 return sizType;
58}
59
61{
62 const QString rotstr = string.trimmed();
63 // "Sync"
65
66 if ( rotstr.compare( QLatin1String( "Offset" ), Qt::CaseInsensitive ) == 0 )
67 {
69 }
70 else if ( rotstr.compare( QLatin1String( "Fixed" ), Qt::CaseInsensitive ) == 0 )
71 {
73 }
74 return rottype;
75}
76
78{
79 const QString str = string.trimmed();
80 // "Lowest"
82
83 if ( str.compare( QLatin1String( "Text" ), Qt::CaseInsensitive ) == 0 )
84 {
86 }
87 else if ( str.compare( QLatin1String( "Buffer" ), Qt::CaseInsensitive ) == 0 )
88 {
90 }
91 else if ( str.compare( QLatin1String( "Background" ), Qt::CaseInsensitive ) == 0 )
92 {
94 }
95 return shdwtype;
96}
97
99{
100 switch ( orientation )
101 {
102 case Qgis::TextOrientation::Horizontal:
103 return QStringLiteral( "horizontal" );
104 case Qgis::TextOrientation::Vertical:
105 return QStringLiteral( "vertical" );
106 case Qgis::TextOrientation::RotationBased:
107 return QStringLiteral( "rotation-based" );
108 }
109 return QString();
110}
111
113{
114 if ( ok )
115 *ok = true;
116
117 const QString cleaned = name.toLower().trimmed();
118
119 if ( cleaned == QLatin1String( "horizontal" ) )
120 return Qgis::TextOrientation::Horizontal;
121 else if ( cleaned == QLatin1String( "vertical" ) )
122 return Qgis::TextOrientation::Vertical;
123 else if ( cleaned == QLatin1String( "rotation-based" ) )
124 return Qgis::TextOrientation::RotationBased;
125
126 if ( ok )
127 *ok = false;
128 return Qgis::TextOrientation::Horizontal;
129}
130
132{
133 if ( val == 0 )
135 else if ( val == 1 )
137 else if ( val == 2 )
139 else if ( val == 3 )
141 else
143}
144
145QColor QgsTextRendererUtils::readColor( QgsVectorLayer *layer, const QString &property, const QColor &defaultColor, bool withAlpha )
146{
147 const int r = layer->customProperty( property + 'R', QVariant( defaultColor.red() ) ).toInt();
148 const int g = layer->customProperty( property + 'G', QVariant( defaultColor.green() ) ).toInt();
149 const int b = layer->customProperty( property + 'B', QVariant( defaultColor.blue() ) ).toInt();
150 const int a = withAlpha ? layer->customProperty( property + 'A', QVariant( defaultColor.alpha() ) ).toInt() : 255;
151 return QColor( r, g, b, a );
152}
153
154#if 0
155QgsTextRendererUtils::CurvePlacementProperties *QgsTextRendererUtils::generateCurvedTextPlacement( const QgsPrecalculatedTextMetrics &metrics, const QgsLineString *line, double offsetAlongLine, LabelLineDirection direction, double maxConcaveAngle, double maxConvexAngle, bool uprightOnly )
156{
157 const int numPoints = line->numPoints();
158 std::vector<double> pathDistances( numPoints );
159
160 const double *x = line->xData();
161 const double *y = line->yData();
162 double dx, dy;
163
164 pathDistances[0] = 0;
165 double prevX = *x++;
166 double prevY = *y++;
167
168 for ( int i = 1; i < numPoints; ++i )
169 {
170 dx = *x - prevX;
171 dy = *y - prevY;
172 pathDistances[i] = std::sqrt( dx * dx + dy * dy );
173
174 prevX = *x++;
175 prevY = *y++;
176 }
177
178 return generateCurvedTextPlacementPrivate( metrics, line->xData(), line->yData(), numPoints, pathDistances, offsetAlongLine, direction, maxConcaveAngle, maxConvexAngle, uprightOnly );
179}
180#endif
181
182QgsTextRendererUtils::CurvePlacementProperties *QgsTextRendererUtils::generateCurvedTextPlacement( const QgsPrecalculatedTextMetrics &metrics, const double *x, const double *y, int numPoints, const std::vector<double> &pathDistances, double offsetAlongLine, LabelLineDirection direction, double maxConcaveAngle, double maxConvexAngle, bool uprightOnly )
183{
184 return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, maxConcaveAngle, maxConvexAngle, uprightOnly );
185}
186
187QgsTextRendererUtils::CurvePlacementProperties *QgsTextRendererUtils::generateCurvedTextPlacementPrivate( const QgsPrecalculatedTextMetrics &metrics, const double *x, const double *y, int numPoints, const std::vector<double> &pathDistances, double offsetAlongLine, LabelLineDirection direction, double maxConcaveAngle, double maxConvexAngle, bool uprightOnly, bool isSecondAttempt )
188{
189 std::unique_ptr< CurvePlacementProperties > output = std::make_unique< CurvePlacementProperties >();
190 output->graphemePlacement.reserve( metrics.count() );
191
192 double offsetAlongSegment = offsetAlongLine;
193 int index = 1;
194 // Find index of segment corresponding to starting offset
195 while ( index < numPoints && offsetAlongSegment > pathDistances[index] )
196 {
197 offsetAlongSegment -= pathDistances[index];
198 index += 1;
199 }
200 if ( index >= numPoints )
201 {
202 return output.release();
203 }
204
205 const double segmentLength = pathDistances[index];
206 if ( qgsDoubleNear( segmentLength, 0.0 ) )
207 {
208 // Not allowed to place across on 0 length segments or discontinuities
209 return output.release();
210 }
211
212 const int characterCount = metrics.count();
213
214 if ( direction == RespectPainterOrientation && !isSecondAttempt )
215 {
216 // Calculate the orientation based on the angle of the path segment under consideration
217
218 double distance = offsetAlongSegment;
219 int endindex = index;
220
221 double startLabelX = 0;
222 double startLabelY = 0;
223 double endLabelX = 0;
224 double endLabelY = 0;
225 for ( int i = 0; i < characterCount; i++ )
226 {
227 const double characterWidth = metrics.characterWidth( i );
228 double characterStartX, characterStartY;
229 if ( !nextCharPosition( characterWidth, pathDistances[endindex], x, y, numPoints, endindex, distance, characterStartX, characterStartY, endLabelX, endLabelY ) )
230 {
231 return output.release();
232 }
233 if ( i == 0 )
234 {
235 startLabelX = characterStartX;
236 startLabelY = characterStartY;
237 }
238 }
239
240 // Determine the angle of the path segment under consideration
241 const double dx = endLabelX - startLabelX;
242 const double dy = endLabelY - startLabelY;
243 const double lineAngle = std::atan2( -dy, dx ) * 180 / M_PI;
244
245 if ( lineAngle > 90 || lineAngle < -90 )
246 {
247 output->labeledLineSegmentIsRightToLeft = true;
248 }
249 }
250
251 if ( isSecondAttempt )
252 {
253 // we know that treating the segment as running from right to left gave too many upside down characters, so try again treating the
254 // segment as left to right
255 output->labeledLineSegmentIsRightToLeft = false;
256 output->flippedCharacterPlacementToGetUprightLabels = true;
257 }
258
259 const double dx = x[index] - x[index - 1];
260 const double dy = y[index] - y[index - 1];
261
262 double angle = std::atan2( -dy, dx );
263
264 const double maxCharacterDescent = metrics.maximumCharacterDescent();
265 const double maxCharacterHeight = metrics.maximumCharacterHeight();
266
267 for ( int i = 0; i < characterCount; i++ )
268 {
269 const double lastCharacterAngle = angle;
270
271 // grab the next character according to the orientation
272 const double characterWidth = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterWidth( i ) : metrics.characterWidth( characterCount - i - 1 );
273 if ( qgsDoubleNear( characterWidth, 0.0 ) )
274 // Certain scripts rely on zero-width character, skip those to prevent failure (see #15801)
275 continue;
276
277 const double characterHeight = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterHeight( i ) : metrics.characterHeight( characterCount - i - 1 );
278 const double characterDescent = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterDescent( i ) : metrics.characterDescent( characterCount - i - 1 );
279
280 double characterStartX = 0;
281 double characterStartY = 0;
282 double characterEndX = 0;
283 double characterEndY = 0;
284 if ( !nextCharPosition( characterWidth, pathDistances[index], x, y, numPoints, index, offsetAlongSegment, characterStartX, characterStartY, characterEndX, characterEndY ) )
285 {
286 output->graphemePlacement.clear();
287 return output.release();
288 }
289
290 // Calculate angle from the start of the character to the end based on start/end of character
291 angle = std::atan2( characterStartY - characterEndY, characterEndX - characterStartX );
292
293 if ( maxConcaveAngle >= 0 || maxConvexAngle >= 0 )
294 {
295 // Test lastCharacterAngle vs angle
296 // since our rendering angle has changed then check against our
297 // max allowable angle change.
298 double angleDelta = lastCharacterAngle - angle;
299 // normalise between -180 and 180
300 while ( angleDelta > M_PI )
301 angleDelta -= 2 * M_PI;
302 while ( angleDelta < -M_PI )
303 angleDelta += 2 * M_PI;
304 if ( ( maxConcaveAngle >= 0 && angleDelta > 0 && angleDelta > maxConcaveAngle ) || ( maxConvexAngle >= 0 && angleDelta < 0 && angleDelta < -maxConvexAngle ) )
305 {
306 output->graphemePlacement.clear();
307 return output.release();
308 }
309 }
310
311 // Shift the character downwards since the draw position is specified at the baseline
312 // and we're calculating the mean line here
313 double dist = 0.9 * maxCharacterHeight / 2 - ( maxCharacterDescent - characterDescent );
314 if ( output->flippedCharacterPlacementToGetUprightLabels )
315 {
316 dist = -dist;
317 }
318 characterStartX += dist * std::cos( angle + M_PI_2 );
319 characterStartY -= dist * std::sin( angle + M_PI_2 );
320
321 double renderAngle = angle;
322 CurvedGraphemePlacement placement;
323 placement.graphemeIndex = !output->flippedCharacterPlacementToGetUprightLabels ? i : characterCount - i - 1;
324 placement.x = characterStartX;
325 placement.y = characterStartY;
326 placement.width = characterWidth;
327 placement.height = characterHeight;
328 if ( output->flippedCharacterPlacementToGetUprightLabels )
329 {
330 // rotate in place
331 placement.x += characterWidth * std::cos( renderAngle );
332 placement.y -= characterWidth * std::sin( renderAngle );
333 renderAngle += M_PI;
334 }
335 placement.angle = -renderAngle;
336 output->graphemePlacement.push_back( placement );
337
338 // Normalise to 0 <= angle < 2PI
339 while ( renderAngle >= 2 * M_PI )
340 renderAngle -= 2 * M_PI;
341 while ( renderAngle < 0 )
342 renderAngle += 2 * M_PI;
343
344 if ( renderAngle > M_PI_2 && renderAngle < 1.5 * M_PI )
345 output->upsideDownCharCount++;
346 }
347
348 if ( !isSecondAttempt && uprightOnly && output->upsideDownCharCount >= characterCount / 2.0 )
349 {
350 // more of text is upside down then right side up...
351 // if text should be shown upright then retry with the opposite orientation
352 return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, maxConcaveAngle, maxConvexAngle, uprightOnly, true );
353 }
354
355 return output.release();
356}
357
358bool QgsTextRendererUtils::nextCharPosition( double charWidth, double segmentLength, const double *x, const double *y, int numPoints, int &index, double &currentDistanceAlongSegment, double &characterStartX, double &characterStartY, double &characterEndX, double &characterEndY )
359{
360 // Coordinates this character will start at
361 if ( qgsDoubleNear( segmentLength, 0.0 ) )
362 {
363 // Not allowed to place across on 0 length segments or discontinuities
364 return false;
365 }
366
367 double segmentStartX = x[index - 1];
368 double segmentStartY = y[index - 1];
369
370 double segmentEndX = x[index];
371 double segmentEndY = y[index];
372
373 const double segmentDx = segmentEndX - segmentStartX;
374 const double segmentDy = segmentEndY - segmentStartY;
375
376 characterStartX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
377 characterStartY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
378
379 // Coordinates this character ends at, calculated below
380 characterEndX = 0;
381 characterEndY = 0;
382
383 if ( segmentLength - currentDistanceAlongSegment >= charWidth )
384 {
385 // if the distance remaining in this segment is enough, we just go further along the segment
386 currentDistanceAlongSegment += charWidth;
387 characterEndX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
388 characterEndY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
389 }
390 else
391 {
392 // If there isn't enough distance left on this segment
393 // then we need to search until we find the line segment that ends further than ci.width away
394 do
395 {
396 segmentStartX = segmentEndX;
397 segmentStartY = segmentEndY;
398 index++;
399 if ( index >= numPoints ) // Bail out if we run off the end of the shape
400 {
401 return false;
402 }
403 segmentEndX = x[index];
404 segmentEndY = y[index];
405 }
406 while ( std::sqrt( std::pow( characterStartX - segmentEndX, 2 ) + std::pow( characterStartY - segmentEndY, 2 ) ) < charWidth ); // Distance from character start to end
407
408 // Calculate the position to place the end of the character on
409 findLineCircleIntersection( characterStartX, characterStartY, charWidth, segmentStartX, segmentStartY, segmentEndX, segmentEndY, characterEndX, characterEndY );
410
411 // Need to calculate distance on the new segment
412 currentDistanceAlongSegment = std::sqrt( std::pow( segmentStartX - characterEndX, 2 ) + std::pow( segmentStartY - characterEndY, 2 ) );
413 }
414 return true;
415}
416
417void QgsTextRendererUtils::findLineCircleIntersection( double cx, double cy, double radius, double x1, double y1, double x2, double y2, double &xRes, double &yRes )
418{
419 double multiplier = 1;
420 if ( radius < 10 )
421 {
422 // these calculations get unstable for small coordinates differences, e.g. as a result of map labeling in a geographic
423 // CRS
424 multiplier = 10000;
425 x1 *= multiplier;
426 y1 *= multiplier;
427 x2 *= multiplier;
428 y2 *= multiplier;
429 cx *= multiplier;
430 cy *= multiplier;
431 radius *= multiplier;
432 }
433
434 const double dx = x2 - x1;
435 const double dy = y2 - y1;
436
437 const double A = dx * dx + dy * dy;
438 const double B = 2 * ( dx * ( x1 - cx ) + dy * ( y1 - cy ) );
439 const double C = ( x1 - cx ) * ( x1 - cx ) + ( y1 - cy ) * ( y1 - cy ) - radius * radius;
440
441 const double det = B * B - 4 * A * C;
442 if ( A <= 0.000000000001 || det < 0 )
443 // Should never happen, No real solutions.
444 return;
445
446 if ( qgsDoubleNear( det, 0.0 ) )
447 {
448 // Could potentially happen.... One solution.
449 const double t = -B / ( 2 * A );
450 xRes = x1 + t * dx;
451 yRes = y1 + t * dy;
452 }
453 else
454 {
455 // Two solutions.
456 // Always use the 1st one
457 // We only really have one solution here, as we know the line segment will start in the circle and end outside
458 const double t = ( -B + std::sqrt( det ) ) / ( 2 * A );
459 xRes = x1 + t * dx;
460 yRes = y1 + t * dy;
461 }
462
463 if ( multiplier != 1 )
464 {
465 xRes /= multiplier;
466 yRes /= multiplier;
467 }
468}
TextOrientation
Text orientations.
Definition: qgis.h:1430
Line string geometry type, with support for z-dimension and m-values.
Definition: qgslinestring.h:45
const double * yData() const
Returns a const pointer to the y vertex data.
const double * xData() const
Returns a const pointer to the x vertex data.
int numPoints() const override SIP_HOLDGIL
Returns the number of points in the curve.
Q_INVOKABLE QVariant customProperty(const QString &value, const QVariant &defaultValue=QVariant()) const
Read a custom property from layer.
Contains precalculated properties regarding text metrics for text to be renderered at a later stage.
double maximumCharacterHeight() const
Returns the maximum height of any character found in the text.
double characterDescent(int position) const
Returns the descent of the character at the specified position.
int count() const
Returns the total number of characters.
double maximumCharacterDescent() const
Returns the maximum descent of any character found in the text.
double characterWidth(int position) const
Returns the width of the character at the specified position.
double characterHeight(int position) const
Returns the character height of the character at the specified position (actually font metrics height...
SizeType
Methods for determining the background shape size.
@ SizeBuffer
Shape size is determined by adding a buffer margin around text.
ShapeType
Background shape types.
@ ShapeSquare
Square - buffered sizes only.
RotationType
Methods for determining the rotation of the background shape.
@ RotationOffset
Shape rotation is offset from text rotation.
@ RotationSync
Shape rotation is synced with text rotation.
@ RotationFixed
Shape rotation is a fixed angle.
Contains placement information for a curved text layout.
static QgsTextBackgroundSettings::ShapeType decodeShapeType(const QString &string)
Decodes a string representation of a background shape type to a type.
static Qgis::TextOrientation decodeTextOrientation(const QString &name, bool *ok=nullptr)
Attempts to decode a string representation of a text orientation.
LabelLineDirection
Controls behavior of curved text with respect to line directions.
@ RespectPainterOrientation
Curved text will be placed respecting the painter orientation, and the actual line direction will be ...
static QColor readColor(QgsVectorLayer *layer, const QString &property, const QColor &defaultColor=Qt::black, bool withAlpha=true)
Converts an encoded color value from a layer property.
static QgsTextShadowSettings::ShadowPlacement decodeShadowPlacementType(const QString &string)
Decodes a string representation of a shadow placement type to a type.
static CurvePlacementProperties * generateCurvedTextPlacement(const QgsPrecalculatedTextMetrics &metrics, const double *x, const double *y, int numPoints, const std::vector< double > &pathDistances, double offsetAlongLine, LabelLineDirection direction=RespectPainterOrientation, double maxConcaveAngle=-1, double maxConvexAngle=-1, bool uprightOnly=true)
Calculates curved text placement properties.
static QgsTextBackgroundSettings::RotationType decodeBackgroundRotationType(const QString &string)
Decodes a string representation of a background rotation type to a type.
static QString encodeTextOrientation(Qgis::TextOrientation orientation)
Encodes a text orientation.
static QgsTextBackgroundSettings::SizeType decodeBackgroundSizeType(const QString &string)
Decodes a string representation of a background size type to a type.
static QgsUnitTypes::RenderUnit convertFromOldLabelUnit(int val)
Converts a unit from an old (pre 3.0) label unit.
ShadowPlacement
Placement positions for text shadow.
@ ShadowBuffer
Draw shadow under buffer.
@ ShadowShape
Draw shadow under background shape.
@ ShadowLowest
Draw shadow below all text components.
@ ShadowText
Draw shadow under text.
RenderUnit
Rendering size units.
Definition: qgsunittypes.h:168
@ RenderPercentage
Percentage of another measurement (e.g., canvas size, feature size)
Definition: qgsunittypes.h:172
@ RenderPoints
Points (e.g., for font sizes)
Definition: qgsunittypes.h:173
@ RenderMillimeters
Millimeters.
Definition: qgsunittypes.h:169
@ RenderMapUnits
Map units.
Definition: qgsunittypes.h:170
Represents a vector layer which manages a vector based data sets.
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored)
Definition: MathUtils.cpp:786
#define str(x)
Definition: qgis.cpp:37
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2527