QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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
18#include "qgsvectorlayer.h"
19
21{
23 const QString skind = string.trimmed();
24
25 if ( skind.compare( QLatin1String( "Square" ), Qt::CaseInsensitive ) == 0 )
26 {
28 }
29 else if ( skind.compare( QLatin1String( "Ellipse" ), Qt::CaseInsensitive ) == 0 )
30 {
32 }
33 else if ( skind.compare( QLatin1String( "Circle" ), Qt::CaseInsensitive ) == 0 )
34 {
36 }
37 else if ( skind.compare( QLatin1String( "SVG" ), Qt::CaseInsensitive ) == 0 )
38 {
40 }
41 else if ( skind.compare( QLatin1String( "marker" ), Qt::CaseInsensitive ) == 0 )
42 {
44 }
45 return shpkind;
46}
47
49{
50 const QString stype = string.trimmed();
51 // "Buffer"
53
54 if ( stype.compare( QLatin1String( "Fixed" ), Qt::CaseInsensitive ) == 0 )
55 {
57 }
58 return sizType;
59}
60
62{
63 const QString rotstr = string.trimmed();
64 // "Sync"
66
67 if ( rotstr.compare( QLatin1String( "Offset" ), Qt::CaseInsensitive ) == 0 )
68 {
70 }
71 else if ( rotstr.compare( QLatin1String( "Fixed" ), Qt::CaseInsensitive ) == 0 )
72 {
74 }
75 return rottype;
76}
77
79{
80 const QString str = string.trimmed();
81 // "Lowest"
83
84 if ( str.compare( QLatin1String( "Text" ), Qt::CaseInsensitive ) == 0 )
85 {
87 }
88 else if ( str.compare( QLatin1String( "Buffer" ), Qt::CaseInsensitive ) == 0 )
89 {
91 }
92 else if ( str.compare( QLatin1String( "Background" ), Qt::CaseInsensitive ) == 0 )
93 {
95 }
96 return shdwtype;
97}
98
100{
101 switch ( orientation )
102 {
104 return QStringLiteral( "horizontal" );
106 return QStringLiteral( "vertical" );
108 return QStringLiteral( "rotation-based" );
109 }
110 return QString();
111}
112
114{
115 if ( ok )
116 *ok = true;
117
118 const QString cleaned = name.toLower().trimmed();
119
120 if ( cleaned == QLatin1String( "horizontal" ) )
122 else if ( cleaned == QLatin1String( "vertical" ) )
124 else if ( cleaned == QLatin1String( "rotation-based" ) )
126
127 if ( ok )
128 *ok = false;
130}
131
133{
134 if ( val == 0 )
136 else if ( val == 1 )
138 else if ( val == 2 )
140 else if ( val == 3 )
142 else
144}
145
146QColor QgsTextRendererUtils::readColor( QgsVectorLayer *layer, const QString &property, const QColor &defaultColor, bool withAlpha )
147{
148 const int r = layer->customProperty( property + 'R', QVariant( defaultColor.red() ) ).toInt();
149 const int g = layer->customProperty( property + 'G', QVariant( defaultColor.green() ) ).toInt();
150 const int b = layer->customProperty( property + 'B', QVariant( defaultColor.blue() ) ).toInt();
151 const int a = withAlpha ? layer->customProperty( property + 'A', QVariant( defaultColor.alpha() ) ).toInt() : 255;
152 return QColor( r, g, b, a );
153}
154
155std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > QgsTextRendererUtils::generateCurvedTextPlacement( const QgsPrecalculatedTextMetrics &metrics, const QPolygonF &line, double offsetAlongLine, LabelLineDirection direction, double maxConcaveAngle, double maxConvexAngle, CurvedTextFlags flags )
156{
157 const int numPoints = line.size();
158 std::vector<double> pathDistances( numPoints );
159
160 const QPointF *p = line.data();
161 double dx, dy;
162
163 pathDistances[0] = 0;
164 double prevX = p->x();
165 double prevY = p->y();
166 p++;
167
168 std::vector< double > x( numPoints );
169 std::vector< double > y( numPoints );
170 x[0] = prevX;
171 y[0] = prevY;
172
173 for ( int i = 1; i < numPoints; ++i )
174 {
175 dx = p->x() - prevX;
176 dy = p->y() - prevY;
177 pathDistances[i] = std::sqrt( dx * dx + dy * dy );
178
179 prevX = p->x();
180 prevY = p->y();
181 p++;
182 x[i] = prevX;
183 y[i] = prevY;
184 }
185
186 return generateCurvedTextPlacementPrivate( metrics, x.data(), y.data(), numPoints, pathDistances, offsetAlongLine, direction, flags, maxConcaveAngle, maxConvexAngle, false );
187}
188
189std::unique_ptr< QgsTextRendererUtils::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, CurvedTextFlags flags )
190{
191 return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, flags, maxConcaveAngle, maxConvexAngle );
192}
193
194std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > QgsTextRendererUtils::generateCurvedTextPlacementPrivate( const QgsPrecalculatedTextMetrics &metrics, const double *x, const double *y, int numPoints, const std::vector<double> &pathDistances, double offsetAlongLine, LabelLineDirection direction, CurvedTextFlags flags, double maxConcaveAngle, double maxConvexAngle, bool isSecondAttempt )
195{
196 std::unique_ptr< CurvePlacementProperties > output = std::make_unique< CurvePlacementProperties >();
197 output->graphemePlacement.reserve( metrics.count() );
198
199 double offsetAlongSegment = offsetAlongLine;
200 int index = 1;
201 // Find index of segment corresponding to starting offset
202 while ( index < numPoints && offsetAlongSegment > pathDistances[index] )
203 {
204 offsetAlongSegment -= pathDistances[index];
205 index += 1;
206 }
207 if ( index >= numPoints )
208 {
209 return output;
210 }
211
212 const double segmentLength = pathDistances[index];
213 if ( qgsDoubleNear( segmentLength, 0.0 ) )
214 {
215 // Not allowed to place across on 0 length segments or discontinuities
216 return output;
217 }
218
219 int characterCount = metrics.count();
220
221 if ( direction == RespectPainterOrientation && !isSecondAttempt )
222 {
223 // Calculate the orientation based on the angle of the path segment under consideration
224
225 double distance = offsetAlongSegment;
226 int endindex = index;
227
228 double startLabelX = 0;
229 double startLabelY = 0;
230 double endLabelX = 0;
231 double endLabelY = 0;
232 for ( int i = 0; i < characterCount; i++ )
233 {
234 const double characterWidth = metrics.characterWidth( i );
235 double characterStartX, characterStartY;
236 if ( !nextCharPosition( characterWidth, pathDistances[endindex], x, y, numPoints, endindex, distance, characterStartX, characterStartY, endLabelX, endLabelY ) )
237 {
239 {
240 characterCount = i + 1;
241 break;
242 }
243 else
244 {
245 return output;
246 }
247 }
248 if ( i == 0 )
249 {
250 startLabelX = characterStartX;
251 startLabelY = characterStartY;
252 }
253 }
254
255 // Determine the angle of the path segment under consideration
256 const double dx = endLabelX - startLabelX;
257 const double dy = endLabelY - startLabelY;
258 const double lineAngle = std::atan2( -dy, dx ) * 180 / M_PI;
259
260 if ( lineAngle > 90 || lineAngle < -90 )
261 {
262 output->labeledLineSegmentIsRightToLeft = true;
263 }
264 }
265
266 if ( isSecondAttempt )
267 {
268 // we know that treating the segment as running from right to left gave too many upside down characters, so try again treating the
269 // segment as left to right
270 output->labeledLineSegmentIsRightToLeft = false;
271 output->flippedCharacterPlacementToGetUprightLabels = true;
272 }
273
274 const double dx = x[index] - x[index - 1];
275 const double dy = y[index] - y[index - 1];
276
277 double angle = std::atan2( -dy, dx );
278
279 const double maxCharacterDescent = metrics.maximumCharacterDescent();
280 const double maxCharacterHeight = metrics.maximumCharacterHeight();
281
282 for ( int i = 0; i < characterCount; i++ )
283 {
284 const double lastCharacterAngle = angle;
285
286 // grab the next character according to the orientation
287 const double characterWidth = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterWidth( i ) : metrics.characterWidth( characterCount - i - 1 );
288 if ( qgsDoubleNear( characterWidth, 0.0 ) )
289 // Certain scripts rely on zero-width character, skip those to prevent failure (see #15801)
290 continue;
291
292 const double characterHeight = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterHeight( i ) : metrics.characterHeight( characterCount - i - 1 );
293 const double characterDescent = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterDescent( i ) : metrics.characterDescent( characterCount - i - 1 );
294
295 double characterStartX = 0;
296 double characterStartY = 0;
297 double characterEndX = 0;
298 double characterEndY = 0;
299 if ( !nextCharPosition( characterWidth, pathDistances[index], x, y, numPoints, index, offsetAlongSegment, characterStartX, characterStartY, characterEndX, characterEndY ) )
300 {
302 {
303 characterCount = i + 1;
304 break;
305 }
306 else
307 {
308 output->graphemePlacement.clear();
309 return output;
310 }
311 }
312
313 // Calculate angle from the start of the character to the end based on start/end of character
314 angle = std::atan2( characterStartY - characterEndY, characterEndX - characterStartX );
315
316 if ( maxConcaveAngle >= 0 || maxConvexAngle >= 0 )
317 {
318 // Test lastCharacterAngle vs angle
319 // since our rendering angle has changed then check against our
320 // max allowable angle change.
321 double angleDelta = lastCharacterAngle - angle;
322 // normalise between -180 and 180
323 while ( angleDelta > M_PI )
324 angleDelta -= 2 * M_PI;
325 while ( angleDelta < -M_PI )
326 angleDelta += 2 * M_PI;
327 if ( ( maxConcaveAngle >= 0 && angleDelta > 0 && angleDelta > maxConcaveAngle ) || ( maxConvexAngle >= 0 && angleDelta < 0 && angleDelta < -maxConvexAngle ) )
328 {
329 output->graphemePlacement.clear();
330 return output;
331 }
332 }
333
334 if ( !( flags & CurvedTextFlag::UseBaselinePlacement ) )
335 {
336 // Shift the character downwards since the draw position is specified at the baseline
337 // and we're calculating the mean line here
338 double dist = 0.9 * maxCharacterHeight / 2 - ( maxCharacterDescent - characterDescent );
339 if ( output->flippedCharacterPlacementToGetUprightLabels )
340 {
341 dist = -dist;
342 }
343 characterStartX += dist * std::cos( angle + M_PI_2 );
344 characterStartY -= dist * std::sin( angle + M_PI_2 );
345 }
346
347 double renderAngle = angle;
348 CurvedGraphemePlacement placement;
349 placement.graphemeIndex = !output->flippedCharacterPlacementToGetUprightLabels ? i : characterCount - i - 1;
350 placement.x = characterStartX;
351 placement.y = characterStartY;
352 placement.width = characterWidth;
353 placement.height = characterHeight;
354 if ( output->flippedCharacterPlacementToGetUprightLabels )
355 {
356 // rotate in place
357 placement.x += characterWidth * std::cos( renderAngle );
358 placement.y -= characterWidth * std::sin( renderAngle );
359 renderAngle += M_PI;
360 }
361 placement.angle = -renderAngle;
362 output->graphemePlacement.push_back( placement );
363
364 // Normalise to 0 <= angle < 2PI
365 while ( renderAngle >= 2 * M_PI )
366 renderAngle -= 2 * M_PI;
367 while ( renderAngle < 0 )
368 renderAngle += 2 * M_PI;
369
370 if ( renderAngle > M_PI_2 && renderAngle < 1.5 * M_PI )
371 output->upsideDownCharCount++;
372 }
373
374 if ( !isSecondAttempt && ( flags & QgsTextRendererUtils::CurvedTextFlag::UprightCharactersOnly ) && output->upsideDownCharCount >= characterCount / 2.0 )
375 {
376 // more of text is upside down then right side up...
377 // if text should be shown upright then retry with the opposite orientation
378 return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, flags, maxConcaveAngle, maxConvexAngle, true );
379 }
380
381 return output;
382}
383
384bool 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 )
385{
386 // Coordinates this character will start at
387 if ( qgsDoubleNear( segmentLength, 0.0 ) )
388 {
389 // Not allowed to place across on 0 length segments or discontinuities
390 return false;
391 }
392
393 double segmentStartX = x[index - 1];
394 double segmentStartY = y[index - 1];
395
396 double segmentEndX = x[index];
397 double segmentEndY = y[index];
398
399 const double segmentDx = segmentEndX - segmentStartX;
400 const double segmentDy = segmentEndY - segmentStartY;
401
402 characterStartX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
403 characterStartY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
404
405 // Coordinates this character ends at, calculated below
406 characterEndX = 0;
407 characterEndY = 0;
408
409 if ( segmentLength - currentDistanceAlongSegment >= charWidth )
410 {
411 // if the distance remaining in this segment is enough, we just go further along the segment
412 currentDistanceAlongSegment += charWidth;
413 characterEndX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
414 characterEndY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
415 }
416 else
417 {
418 // If there isn't enough distance left on this segment
419 // then we need to search until we find the line segment that ends further than ci.width away
420 do
421 {
422 segmentStartX = segmentEndX;
423 segmentStartY = segmentEndY;
424 index++;
425 if ( index >= numPoints ) // Bail out if we run off the end of the shape
426 {
427 return false;
428 }
429 segmentEndX = x[index];
430 segmentEndY = y[index];
431 }
432 while ( std::sqrt( std::pow( characterStartX - segmentEndX, 2 ) + std::pow( characterStartY - segmentEndY, 2 ) ) < charWidth ); // Distance from character start to end
433
434 // Calculate the position to place the end of the character on
435 findLineCircleIntersection( characterStartX, characterStartY, charWidth, segmentStartX, segmentStartY, segmentEndX, segmentEndY, characterEndX, characterEndY );
436
437 // Need to calculate distance on the new segment
438 currentDistanceAlongSegment = std::sqrt( std::pow( segmentStartX - characterEndX, 2 ) + std::pow( segmentStartY - characterEndY, 2 ) );
439 }
440 return true;
441}
442
443void QgsTextRendererUtils::findLineCircleIntersection( double cx, double cy, double radius, double x1, double y1, double x2, double y2, double &xRes, double &yRes )
444{
445 double multiplier = 1;
446 if ( radius < 10 )
447 {
448 // these calculations get unstable for small coordinates differences, e.g. as a result of map labeling in a geographic
449 // CRS
450 multiplier = 10000;
451 x1 *= multiplier;
452 y1 *= multiplier;
453 x2 *= multiplier;
454 y2 *= multiplier;
455 cx *= multiplier;
456 cy *= multiplier;
457 radius *= multiplier;
458 }
459
460 const double dx = x2 - x1;
461 const double dy = y2 - y1;
462
463 const double A = dx * dx + dy * dy;
464 const double B = 2 * ( dx * ( x1 - cx ) + dy * ( y1 - cy ) );
465 const double C = QgsGeometryUtilsBase::sqrDistance2D( x1, y1, cx, cy ) - radius * radius;
466
467 const double det = B * B - 4 * A * C;
468 if ( A <= 0.000000000001 || det < 0 )
469 // Should never happen, No real solutions.
470 return;
471
472 if ( qgsDoubleNear( det, 0.0 ) )
473 {
474 // Could potentially happen.... One solution.
475 const double t = -B / ( 2 * A );
476 xRes = x1 + t * dx;
477 yRes = y1 + t * dy;
478 }
479 else
480 {
481 // Two solutions.
482 // Always use the 1st one
483 // We only really have one solution here, as we know the line segment will start in the circle and end outside
484 const double t = ( -B + std::sqrt( det ) ) / ( 2 * A );
485 xRes = x1 + t * dx;
486 yRes = y1 + t * dy;
487 }
488
489 if ( multiplier != 1 )
490 {
491 xRes /= multiplier;
492 yRes /= multiplier;
493 }
494}
TextOrientation
Text orientations.
Definition: qgis.h:2368
@ Vertical
Vertically oriented text.
@ RotationBased
Horizontally or vertically oriented text based on rotation (only available for map labeling)
@ Horizontal
Horizontally oriented text.
RenderUnit
Rendering size units.
Definition: qgis.h:4255
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size)
@ Millimeters
Millimeters.
@ Points
Points (e.g., for font sizes)
@ MapUnits
Map units.
static double sqrDistance2D(double x1, double y1, double x2, double y2)
Returns the squared 2D distance between (x1, y1) and (x2, y2).
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.
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 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.
@ TruncateStringWhenLineIsTooShort
When a string is too long for the line, truncate characters instead of aborting the placement.
@ UprightCharactersOnly
Permit upright characters only. If not present then upside down text placement is permitted.
@ UseBaselinePlacement
Generate placement based on the character baselines instead of centers.
static Qgis::RenderUnit convertFromOldLabelUnit(int val)
Converts a unit from an old (pre 3.0) label unit.
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
static std::unique_ptr< CurvePlacementProperties > generateCurvedTextPlacement(const QgsPrecalculatedTextMetrics &metrics, const QPolygonF &line, double offsetAlongLine, LabelLineDirection direction=RespectPainterOrientation, double maxConcaveAngle=-1, double maxConvexAngle=-1, CurvedTextFlags flags=CurvedTextFlags())
Calculates curved text placement properties.
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.
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:716
#define str(x)
Definition: qgis.cpp:38
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:5207