QGIS API Documentation  3.24.2-Tisler (13c1a02865)
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 
16 #include "qgstextrendererutils.h"
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 
146 QColor 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
156 QgsTextRendererUtils::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 
183 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, bool uprightOnly )
184 {
185  return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, maxConcaveAngle, maxConvexAngle, uprightOnly );
186 }
187 
188 QgsTextRendererUtils::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 
355 bool 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 
414 void 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:44
const double * yData() const
Returns a const pointer to the y vertex data.
int numPoints() const override SIP_HOLDGIL
Returns the number of points in the curve.
const double * xData() const
Returns a const pointer to the x vertex data.
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:1578