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