QGIS API Documentation 3.34.0-Prizren (ffbdd678812)
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#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 {
103 return QStringLiteral( "horizontal" );
105 return QStringLiteral( "vertical" );
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" ) )
121 else if ( cleaned == QLatin1String( "vertical" ) )
123 else if ( cleaned == QLatin1String( "rotation-based" ) )
125
126 if ( ok )
127 *ok = false;
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
154std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > QgsTextRendererUtils::generateCurvedTextPlacement( const QgsPrecalculatedTextMetrics &metrics, const QPolygonF &line, double offsetAlongLine, LabelLineDirection direction, double maxConcaveAngle, double maxConvexAngle, CurvedTextFlags flags )
155{
156 const int numPoints = line.size();
157 std::vector<double> pathDistances( numPoints );
158
159 const QPointF *p = line.data();
160 double dx, dy;
161
162 pathDistances[0] = 0;
163 double prevX = p->x();
164 double prevY = p->y();
165 p++;
166
167 std::vector< double > x( numPoints );
168 std::vector< double > y( numPoints );
169 x[0] = prevX;
170 y[0] = prevY;
171
172 for ( int i = 1; i < numPoints; ++i )
173 {
174 dx = p->x() - prevX;
175 dy = p->y() - prevY;
176 pathDistances[i] = std::sqrt( dx * dx + dy * dy );
177
178 prevX = p->x();
179 prevY = p->y();
180 p++;
181 x[i] = prevX;
182 y[i] = prevY;
183 }
184
185 return generateCurvedTextPlacementPrivate( metrics, x.data(), y.data(), numPoints, pathDistances, offsetAlongLine, direction, flags, maxConcaveAngle, maxConvexAngle, false );
186}
187
188std::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 )
189{
190 return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, flags, maxConcaveAngle, maxConvexAngle );
191}
192
193std::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 )
194{
195 std::unique_ptr< CurvePlacementProperties > output = std::make_unique< CurvePlacementProperties >();
196 output->graphemePlacement.reserve( metrics.count() );
197
198 double offsetAlongSegment = offsetAlongLine;
199 int index = 1;
200 // Find index of segment corresponding to starting offset
201 while ( index < numPoints && offsetAlongSegment > pathDistances[index] )
202 {
203 offsetAlongSegment -= pathDistances[index];
204 index += 1;
205 }
206 if ( index >= numPoints )
207 {
208 return output;
209 }
210
211 const double segmentLength = pathDistances[index];
212 if ( qgsDoubleNear( segmentLength, 0.0 ) )
213 {
214 // Not allowed to place across on 0 length segments or discontinuities
215 return output;
216 }
217
218 int characterCount = metrics.count();
219
220 if ( direction == RespectPainterOrientation && !isSecondAttempt )
221 {
222 // Calculate the orientation based on the angle of the path segment under consideration
223
224 double distance = offsetAlongSegment;
225 int endindex = index;
226
227 double startLabelX = 0;
228 double startLabelY = 0;
229 double endLabelX = 0;
230 double endLabelY = 0;
231 for ( int i = 0; i < characterCount; i++ )
232 {
233 const double characterWidth = metrics.characterWidth( i );
234 double characterStartX, characterStartY;
235 if ( !nextCharPosition( characterWidth, pathDistances[endindex], x, y, numPoints, endindex, distance, characterStartX, characterStartY, endLabelX, endLabelY ) )
236 {
238 {
239 characterCount = i + 1;
240 break;
241 }
242 else
243 {
244 return output;
245 }
246 }
247 if ( i == 0 )
248 {
249 startLabelX = characterStartX;
250 startLabelY = characterStartY;
251 }
252 }
253
254 // Determine the angle of the path segment under consideration
255 const double dx = endLabelX - startLabelX;
256 const double dy = endLabelY - startLabelY;
257 const double lineAngle = std::atan2( -dy, dx ) * 180 / M_PI;
258
259 if ( lineAngle > 90 || lineAngle < -90 )
260 {
261 output->labeledLineSegmentIsRightToLeft = true;
262 }
263 }
264
265 if ( isSecondAttempt )
266 {
267 // we know that treating the segment as running from right to left gave too many upside down characters, so try again treating the
268 // segment as left to right
269 output->labeledLineSegmentIsRightToLeft = false;
270 output->flippedCharacterPlacementToGetUprightLabels = true;
271 }
272
273 const double dx = x[index] - x[index - 1];
274 const double dy = y[index] - y[index - 1];
275
276 double angle = std::atan2( -dy, dx );
277
278 const double maxCharacterDescent = metrics.maximumCharacterDescent();
279 const double maxCharacterHeight = metrics.maximumCharacterHeight();
280
281 for ( int i = 0; i < characterCount; i++ )
282 {
283 const double lastCharacterAngle = angle;
284
285 // grab the next character according to the orientation
286 const double characterWidth = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterWidth( i ) : metrics.characterWidth( characterCount - i - 1 );
287 if ( qgsDoubleNear( characterWidth, 0.0 ) )
288 // Certain scripts rely on zero-width character, skip those to prevent failure (see #15801)
289 continue;
290
291 const double characterHeight = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterHeight( i ) : metrics.characterHeight( characterCount - i - 1 );
292 const double characterDescent = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterDescent( i ) : metrics.characterDescent( characterCount - i - 1 );
293
294 double characterStartX = 0;
295 double characterStartY = 0;
296 double characterEndX = 0;
297 double characterEndY = 0;
298 if ( !nextCharPosition( characterWidth, pathDistances[index], x, y, numPoints, index, offsetAlongSegment, characterStartX, characterStartY, characterEndX, characterEndY ) )
299 {
301 {
302 characterCount = i + 1;
303 break;
304 }
305 else
306 {
307 output->graphemePlacement.clear();
308 return output;
309 }
310 }
311
312 // Calculate angle from the start of the character to the end based on start/end of character
313 angle = std::atan2( characterStartY - characterEndY, characterEndX - characterStartX );
314
315 if ( maxConcaveAngle >= 0 || maxConvexAngle >= 0 )
316 {
317 // Test lastCharacterAngle vs angle
318 // since our rendering angle has changed then check against our
319 // max allowable angle change.
320 double angleDelta = lastCharacterAngle - angle;
321 // normalise between -180 and 180
322 while ( angleDelta > M_PI )
323 angleDelta -= 2 * M_PI;
324 while ( angleDelta < -M_PI )
325 angleDelta += 2 * M_PI;
326 if ( ( maxConcaveAngle >= 0 && angleDelta > 0 && angleDelta > maxConcaveAngle ) || ( maxConvexAngle >= 0 && angleDelta < 0 && angleDelta < -maxConvexAngle ) )
327 {
328 output->graphemePlacement.clear();
329 return output;
330 }
331 }
332
333 if ( !( flags & CurvedTextFlag::UseBaselinePlacement ) )
334 {
335 // Shift the character downwards since the draw position is specified at the baseline
336 // and we're calculating the mean line here
337 double dist = 0.9 * maxCharacterHeight / 2 - ( maxCharacterDescent - characterDescent );
338 if ( output->flippedCharacterPlacementToGetUprightLabels )
339 {
340 dist = -dist;
341 }
342 characterStartX += dist * std::cos( angle + M_PI_2 );
343 characterStartY -= dist * std::sin( angle + M_PI_2 );
344 }
345
346 double renderAngle = angle;
347 CurvedGraphemePlacement placement;
348 placement.graphemeIndex = !output->flippedCharacterPlacementToGetUprightLabels ? i : characterCount - i - 1;
349 placement.x = characterStartX;
350 placement.y = characterStartY;
351 placement.width = characterWidth;
352 placement.height = characterHeight;
353 if ( output->flippedCharacterPlacementToGetUprightLabels )
354 {
355 // rotate in place
356 placement.x += characterWidth * std::cos( renderAngle );
357 placement.y -= characterWidth * std::sin( renderAngle );
358 renderAngle += M_PI;
359 }
360 placement.angle = -renderAngle;
361 output->graphemePlacement.push_back( placement );
362
363 // Normalise to 0 <= angle < 2PI
364 while ( renderAngle >= 2 * M_PI )
365 renderAngle -= 2 * M_PI;
366 while ( renderAngle < 0 )
367 renderAngle += 2 * M_PI;
368
369 if ( renderAngle > M_PI_2 && renderAngle < 1.5 * M_PI )
370 output->upsideDownCharCount++;
371 }
372
373 if ( !isSecondAttempt && ( flags & QgsTextRendererUtils::CurvedTextFlag::UprightCharactersOnly ) && output->upsideDownCharCount >= characterCount / 2.0 )
374 {
375 // more of text is upside down then right side up...
376 // if text should be shown upright then retry with the opposite orientation
377 return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, flags, maxConcaveAngle, maxConvexAngle, true );
378 }
379
380 return output;
381}
382
383bool 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 )
384{
385 // Coordinates this character will start at
386 if ( qgsDoubleNear( segmentLength, 0.0 ) )
387 {
388 // Not allowed to place across on 0 length segments or discontinuities
389 return false;
390 }
391
392 double segmentStartX = x[index - 1];
393 double segmentStartY = y[index - 1];
394
395 double segmentEndX = x[index];
396 double segmentEndY = y[index];
397
398 const double segmentDx = segmentEndX - segmentStartX;
399 const double segmentDy = segmentEndY - segmentStartY;
400
401 characterStartX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
402 characterStartY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
403
404 // Coordinates this character ends at, calculated below
405 characterEndX = 0;
406 characterEndY = 0;
407
408 if ( segmentLength - currentDistanceAlongSegment >= charWidth )
409 {
410 // if the distance remaining in this segment is enough, we just go further along the segment
411 currentDistanceAlongSegment += charWidth;
412 characterEndX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
413 characterEndY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
414 }
415 else
416 {
417 // If there isn't enough distance left on this segment
418 // then we need to search until we find the line segment that ends further than ci.width away
419 do
420 {
421 segmentStartX = segmentEndX;
422 segmentStartY = segmentEndY;
423 index++;
424 if ( index >= numPoints ) // Bail out if we run off the end of the shape
425 {
426 return false;
427 }
428 segmentEndX = x[index];
429 segmentEndY = y[index];
430 }
431 while ( std::sqrt( std::pow( characterStartX - segmentEndX, 2 ) + std::pow( characterStartY - segmentEndY, 2 ) ) < charWidth ); // Distance from character start to end
432
433 // Calculate the position to place the end of the character on
434 findLineCircleIntersection( characterStartX, characterStartY, charWidth, segmentStartX, segmentStartY, segmentEndX, segmentEndY, characterEndX, characterEndY );
435
436 // Need to calculate distance on the new segment
437 currentDistanceAlongSegment = std::sqrt( std::pow( segmentStartX - characterEndX, 2 ) + std::pow( segmentStartY - characterEndY, 2 ) );
438 }
439 return true;
440}
441
442void QgsTextRendererUtils::findLineCircleIntersection( double cx, double cy, double radius, double x1, double y1, double x2, double y2, double &xRes, double &yRes )
443{
444 double multiplier = 1;
445 if ( radius < 10 )
446 {
447 // these calculations get unstable for small coordinates differences, e.g. as a result of map labeling in a geographic
448 // CRS
449 multiplier = 10000;
450 x1 *= multiplier;
451 y1 *= multiplier;
452 x2 *= multiplier;
453 y2 *= multiplier;
454 cx *= multiplier;
455 cy *= multiplier;
456 radius *= multiplier;
457 }
458
459 const double dx = x2 - x1;
460 const double dy = y2 - y1;
461
462 const double A = dx * dx + dy * dy;
463 const double B = 2 * ( dx * ( x1 - cx ) + dy * ( y1 - cy ) );
464 const double C = ( x1 - cx ) * ( x1 - cx ) + ( y1 - cy ) * ( y1 - cy ) - radius * radius;
465
466 const double det = B * B - 4 * A * C;
467 if ( A <= 0.000000000001 || det < 0 )
468 // Should never happen, No real solutions.
469 return;
470
471 if ( qgsDoubleNear( det, 0.0 ) )
472 {
473 // Could potentially happen.... One solution.
474 const double t = -B / ( 2 * A );
475 xRes = x1 + t * dx;
476 yRes = y1 + t * dy;
477 }
478 else
479 {
480 // Two solutions.
481 // Always use the 1st one
482 // We only really have one solution here, as we know the line segment will start in the circle and end outside
483 const double t = ( -B + std::sqrt( det ) ) / ( 2 * A );
484 xRes = x1 + t * dx;
485 yRes = y1 + t * dy;
486 }
487
488 if ( multiplier != 1 )
489 {
490 xRes /= multiplier;
491 yRes /= multiplier;
492 }
493}
TextOrientation
Text orientations.
Definition qgis.h:2114
@ 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:3627
@ Percentage
Percentage of another measurement (e.g., canvas size, feature size)
@ Millimeters
Millimeters.
@ Points
Points (e.g., for font sizes)
@ MapUnits
Map units.
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.
@ 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.
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())
Flags controlling behavior of curved text generation.
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)
#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:4332