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