Branch data Line data Source code
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 : :
20 : 0 : QgsTextBackgroundSettings::ShapeType QgsTextRendererUtils::decodeShapeType( const QString &string )
21 : : {
22 : 0 : QgsTextBackgroundSettings::ShapeType shpkind = QgsTextBackgroundSettings::ShapeRectangle;
23 : 0 : const QString skind = string.trimmed();
24 : :
25 : 0 : if ( skind.compare( QLatin1String( "Square" ), Qt::CaseInsensitive ) == 0 )
26 : : {
27 : 0 : shpkind = QgsTextBackgroundSettings::ShapeSquare;
28 : 0 : }
29 : 0 : else if ( skind.compare( QLatin1String( "Ellipse" ), Qt::CaseInsensitive ) == 0 )
30 : : {
31 : 0 : shpkind = QgsTextBackgroundSettings::ShapeEllipse;
32 : 0 : }
33 : 0 : else if ( skind.compare( QLatin1String( "Circle" ), Qt::CaseInsensitive ) == 0 )
34 : : {
35 : 0 : shpkind = QgsTextBackgroundSettings::ShapeCircle;
36 : 0 : }
37 : 0 : else if ( skind.compare( QLatin1String( "SVG" ), Qt::CaseInsensitive ) == 0 )
38 : : {
39 : 0 : shpkind = QgsTextBackgroundSettings::ShapeSVG;
40 : 0 : }
41 : 0 : else if ( skind.compare( QLatin1String( "marker" ), Qt::CaseInsensitive ) == 0 )
42 : : {
43 : 0 : shpkind = QgsTextBackgroundSettings::ShapeMarkerSymbol;
44 : 0 : }
45 : 0 : return shpkind;
46 : 0 : }
47 : :
48 : 0 : QgsTextBackgroundSettings::SizeType QgsTextRendererUtils::decodeBackgroundSizeType( const QString &string )
49 : : {
50 : 0 : const QString stype = string.trimmed();
51 : : // "Buffer"
52 : 0 : QgsTextBackgroundSettings::SizeType sizType = QgsTextBackgroundSettings::SizeBuffer;
53 : :
54 : 0 : if ( stype.compare( QLatin1String( "Fixed" ), Qt::CaseInsensitive ) == 0 )
55 : : {
56 : 0 : sizType = QgsTextBackgroundSettings::SizeFixed;
57 : 0 : }
58 : 0 : return sizType;
59 : 0 : }
60 : :
61 : 0 : QgsTextBackgroundSettings::RotationType QgsTextRendererUtils::decodeBackgroundRotationType( const QString &string )
62 : : {
63 : 0 : const QString rotstr = string.trimmed();
64 : : // "Sync"
65 : 0 : QgsTextBackgroundSettings::RotationType rottype = QgsTextBackgroundSettings::RotationSync;
66 : :
67 : 0 : if ( rotstr.compare( QLatin1String( "Offset" ), Qt::CaseInsensitive ) == 0 )
68 : : {
69 : 0 : rottype = QgsTextBackgroundSettings::RotationOffset;
70 : 0 : }
71 : 0 : else if ( rotstr.compare( QLatin1String( "Fixed" ), Qt::CaseInsensitive ) == 0 )
72 : : {
73 : 0 : rottype = QgsTextBackgroundSettings::RotationFixed;
74 : 0 : }
75 : 0 : return rottype;
76 : 0 : }
77 : :
78 : 0 : QgsTextShadowSettings::ShadowPlacement QgsTextRendererUtils::decodeShadowPlacementType( const QString &string )
79 : : {
80 : 0 : const QString str = string.trimmed();
81 : : // "Lowest"
82 : 0 : QgsTextShadowSettings::ShadowPlacement shdwtype = QgsTextShadowSettings::ShadowLowest;
83 : :
84 : 0 : if ( str.compare( QLatin1String( "Text" ), Qt::CaseInsensitive ) == 0 )
85 : : {
86 : 0 : shdwtype = QgsTextShadowSettings::ShadowText;
87 : 0 : }
88 : 0 : else if ( str.compare( QLatin1String( "Buffer" ), Qt::CaseInsensitive ) == 0 )
89 : : {
90 : 0 : shdwtype = QgsTextShadowSettings::ShadowBuffer;
91 : 0 : }
92 : 0 : else if ( str.compare( QLatin1String( "Background" ), Qt::CaseInsensitive ) == 0 )
93 : : {
94 : 0 : shdwtype = QgsTextShadowSettings::ShadowShape;
95 : 0 : }
96 : 0 : return shdwtype;
97 : 0 : }
98 : :
99 : 0 : QString QgsTextRendererUtils::encodeTextOrientation( QgsTextFormat::TextOrientation orientation )
100 : : {
101 : 0 : switch ( orientation )
102 : : {
103 : : case QgsTextFormat::HorizontalOrientation:
104 : 0 : return QStringLiteral( "horizontal" );
105 : : case QgsTextFormat::VerticalOrientation:
106 : 0 : return QStringLiteral( "vertical" );
107 : : case QgsTextFormat::RotationBasedOrientation:
108 : 0 : return QStringLiteral( "rotation-based" );
109 : : }
110 : 0 : return QString();
111 : 0 : }
112 : :
113 : 0 : QgsTextFormat::TextOrientation QgsTextRendererUtils::decodeTextOrientation( const QString &name, bool *ok )
114 : : {
115 : 0 : if ( ok )
116 : 0 : *ok = true;
117 : :
118 : 0 : QString cleaned = name.toLower().trimmed();
119 : :
120 : 0 : if ( cleaned == QLatin1String( "horizontal" ) )
121 : 0 : return QgsTextFormat::HorizontalOrientation;
122 : 0 : else if ( cleaned == QLatin1String( "vertical" ) )
123 : 0 : return QgsTextFormat::VerticalOrientation;
124 : 0 : else if ( cleaned == QLatin1String( "rotation-based" ) )
125 : 0 : return QgsTextFormat::RotationBasedOrientation;
126 : :
127 : 0 : if ( ok )
128 : 0 : *ok = false;
129 : 0 : return QgsTextFormat::HorizontalOrientation;
130 : 0 : }
131 : :
132 : 0 : QgsUnitTypes::RenderUnit QgsTextRendererUtils::convertFromOldLabelUnit( int val )
133 : : {
134 : 0 : if ( val == 0 )
135 : 0 : return QgsUnitTypes::RenderPoints;
136 : 0 : else if ( val == 1 )
137 : 0 : return QgsUnitTypes::RenderMillimeters;
138 : 0 : else if ( val == 2 )
139 : 0 : return QgsUnitTypes::RenderMapUnits;
140 : 0 : else if ( val == 3 )
141 : 0 : return QgsUnitTypes::RenderPercentage;
142 : : else
143 : 0 : return QgsUnitTypes::RenderMillimeters;
144 : 0 : }
145 : :
146 : 0 : QColor QgsTextRendererUtils::readColor( QgsVectorLayer *layer, const QString &property, const QColor &defaultColor, bool withAlpha )
147 : : {
148 : 0 : int r = layer->customProperty( property + 'R', QVariant( defaultColor.red() ) ).toInt();
149 : 0 : int g = layer->customProperty( property + 'G', QVariant( defaultColor.green() ) ).toInt();
150 : 0 : int b = layer->customProperty( property + 'B', QVariant( defaultColor.blue() ) ).toInt();
151 : 0 : int a = withAlpha ? layer->customProperty( property + 'A', QVariant( defaultColor.alpha() ) ).toInt() : 255;
152 : 0 : return QColor( r, g, b, a );
153 : 0 : }
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 : 0 : 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 : 0 : return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, maxConcaveAngle, maxConvexAngle, uprightOnly );
186 : : }
187 : :
188 : 0 : 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 : 0 : std::unique_ptr< CurvePlacementProperties > output = std::make_unique< CurvePlacementProperties >();
191 : 0 : output->graphemePlacement.reserve( metrics.count() );
192 : :
193 : 0 : double offsetAlongSegment = offsetAlongLine;
194 : 0 : int index = 1;
195 : : // Find index of segment corresponding to starting offset
196 : 0 : while ( index < numPoints && offsetAlongSegment > pathDistances[index] )
197 : : {
198 : 0 : offsetAlongSegment -= pathDistances[index];
199 : 0 : index += 1;
200 : : }
201 : 0 : if ( index >= numPoints )
202 : : {
203 : 0 : return output.release();
204 : : }
205 : :
206 : 0 : const double characterHeight = metrics.characterHeight();
207 : :
208 : 0 : const double segmentLength = pathDistances[index];
209 : 0 : if ( qgsDoubleNear( segmentLength, 0.0 ) )
210 : : {
211 : : // Not allowed to place across on 0 length segments or discontinuities
212 : 0 : return output.release();
213 : : }
214 : :
215 : 0 : const int characterCount = metrics.count();
216 : :
217 : 0 : if ( direction == RespectPainterOrientation && !isSecondAttempt )
218 : : {
219 : : // Calculate the orientation based on the angle of the path segment under consideration
220 : :
221 : 0 : double distance = offsetAlongSegment;
222 : 0 : int endindex = index;
223 : :
224 : 0 : double startLabelX = 0;
225 : 0 : double startLabelY = 0;
226 : 0 : double endLabelX = 0;
227 : 0 : double endLabelY = 0;
228 : 0 : for ( int i = 0; i < characterCount; i++ )
229 : : {
230 : 0 : const double characterWidth = metrics.characterWidth( i );
231 : : double characterStartX, characterStartY;
232 : 0 : if ( !nextCharPosition( characterWidth, pathDistances[endindex], x, y, numPoints, endindex, distance, characterStartX, characterStartY, endLabelX, endLabelY ) )
233 : : {
234 : 0 : return output.release();
235 : : }
236 : 0 : if ( i == 0 )
237 : : {
238 : 0 : startLabelX = characterStartX;
239 : 0 : startLabelY = characterStartY;
240 : 0 : }
241 : 0 : }
242 : :
243 : : // Determine the angle of the path segment under consideration
244 : 0 : double dx = endLabelX - startLabelX;
245 : 0 : double dy = endLabelY - startLabelY;
246 : 0 : const double lineAngle = std::atan2( -dy, dx ) * 180 / M_PI;
247 : :
248 : 0 : if ( lineAngle > 90 || lineAngle < -90 )
249 : : {
250 : 0 : output->labeledLineSegmentIsRightToLeft = true;
251 : 0 : }
252 : 0 : }
253 : :
254 : 0 : 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 : 0 : output->labeledLineSegmentIsRightToLeft = false;
259 : 0 : output->flippedCharacterPlacementToGetUprightLabels = true;
260 : 0 : }
261 : :
262 : 0 : double dx = x[index] - x[index - 1];
263 : 0 : double dy = y[index] - y[index - 1];
264 : :
265 : 0 : double angle = std::atan2( -dy, dx );
266 : :
267 : 0 : for ( int i = 0; i < characterCount; i++ )
268 : : {
269 : 0 : double lastCharacterAngle = angle;
270 : :
271 : : // grab the next character according to the orientation
272 : 0 : const double characterWidth = !output->flippedCharacterPlacementToGetUprightLabels ? metrics.characterWidth( i ) : metrics.characterWidth( characterCount - i - 1 );
273 : 0 : if ( qgsDoubleNear( characterWidth, 0.0 ) )
274 : : // Certain scripts rely on zero-width character, skip those to prevent failure (see #15801)
275 : 0 : continue;
276 : :
277 : 0 : double characterStartX = 0;
278 : 0 : double characterStartY = 0;
279 : 0 : double characterEndX = 0;
280 : 0 : double characterEndY = 0;
281 : 0 : if ( !nextCharPosition( characterWidth, pathDistances[index], x, y, numPoints, index, offsetAlongSegment, characterStartX, characterStartY, characterEndX, characterEndY ) )
282 : : {
283 : 0 : output->graphemePlacement.clear();
284 : 0 : return output.release();
285 : : }
286 : :
287 : : // Calculate angle from the start of the character to the end based on start/end of character
288 : 0 : angle = std::atan2( characterStartY - characterEndY, characterEndX - characterStartX );
289 : :
290 : 0 : 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 : 0 : double angleDelta = lastCharacterAngle - angle;
296 : : // normalise between -180 and 180
297 : 0 : while ( angleDelta > M_PI )
298 : 0 : angleDelta -= 2 * M_PI;
299 : 0 : while ( angleDelta < -M_PI )
300 : 0 : angleDelta += 2 * M_PI;
301 : 0 : if ( ( maxConcaveAngle >= 0 && angleDelta > 0 && angleDelta > maxConcaveAngle ) || ( maxConvexAngle >= 0 && angleDelta < 0 && angleDelta < -maxConvexAngle ) )
302 : : {
303 : 0 : output->graphemePlacement.clear();
304 : 0 : return output.release();
305 : : }
306 : 0 : }
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 : 0 : double dist = 0.9 * metrics.characterHeight() / 2;
311 : 0 : if ( output->flippedCharacterPlacementToGetUprightLabels )
312 : : {
313 : 0 : dist = -dist;
314 : 0 : }
315 : 0 : characterStartX += dist * std::cos( angle + M_PI_2 );
316 : 0 : characterStartY -= dist * std::sin( angle + M_PI_2 );
317 : :
318 : 0 : double renderAngle = angle;
319 : 0 : CurvedGraphemePlacement placement;
320 : 0 : placement.graphemeIndex = !output->flippedCharacterPlacementToGetUprightLabels ? i : characterCount - i - 1;
321 : 0 : placement.x = characterStartX;
322 : 0 : placement.y = characterStartY;
323 : 0 : placement.width = characterWidth;
324 : 0 : placement.height = characterHeight;
325 : 0 : if ( output->flippedCharacterPlacementToGetUprightLabels )
326 : : {
327 : : // rotate in place
328 : 0 : placement.x += characterWidth * std::cos( renderAngle );
329 : 0 : placement.y -= characterWidth * std::sin( renderAngle );
330 : 0 : renderAngle += M_PI;
331 : 0 : }
332 : 0 : placement.angle = -renderAngle;
333 : 0 : output->graphemePlacement.push_back( placement );
334 : :
335 : : // Normalise to 0 <= angle < 2PI
336 : 0 : while ( renderAngle >= 2 * M_PI )
337 : 0 : renderAngle -= 2 * M_PI;
338 : 0 : while ( renderAngle < 0 )
339 : 0 : renderAngle += 2 * M_PI;
340 : :
341 : 0 : if ( renderAngle > M_PI_2 && renderAngle < 1.5 * M_PI )
342 : 0 : output->upsideDownCharCount++;
343 : 0 : }
344 : :
345 : 0 : 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 : 0 : return generateCurvedTextPlacementPrivate( metrics, x, y, numPoints, pathDistances, offsetAlongLine, direction, maxConcaveAngle, maxConvexAngle, uprightOnly, true );
350 : : }
351 : :
352 : 0 : return output.release();
353 : 0 : }
354 : :
355 : 0 : bool QgsTextRendererUtils::nextCharPosition( double charWidth, double segmentLength, const double *x, const double *y, int numPoints, int &index, double ¤tDistanceAlongSegment, double &characterStartX, double &characterStartY, double &characterEndX, double &characterEndY )
356 : : {
357 : : // Coordinates this character will start at
358 : 0 : if ( qgsDoubleNear( segmentLength, 0.0 ) )
359 : : {
360 : : // Not allowed to place across on 0 length segments or discontinuities
361 : 0 : return false;
362 : : }
363 : :
364 : 0 : double segmentStartX = x[index - 1];
365 : 0 : double segmentStartY = y[index - 1];
366 : :
367 : 0 : double segmentEndX = x[index];
368 : 0 : double segmentEndY = y[index];
369 : :
370 : 0 : double segmentDx = segmentEndX - segmentStartX;
371 : 0 : double segmentDy = segmentEndY - segmentStartY;
372 : :
373 : 0 : characterStartX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
374 : 0 : characterStartY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
375 : :
376 : : // Coordinates this character ends at, calculated below
377 : 0 : characterEndX = 0;
378 : 0 : characterEndY = 0;
379 : :
380 : 0 : if ( segmentLength - currentDistanceAlongSegment >= charWidth )
381 : : {
382 : : // if the distance remaining in this segment is enough, we just go further along the segment
383 : 0 : currentDistanceAlongSegment += charWidth;
384 : 0 : characterEndX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
385 : 0 : characterEndY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
386 : 0 : }
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 : 0 : do
392 : : {
393 : 0 : segmentStartX = segmentEndX;
394 : 0 : segmentStartY = segmentEndY;
395 : 0 : index++;
396 : 0 : if ( index >= numPoints ) // Bail out if we run off the end of the shape
397 : : {
398 : 0 : return false;
399 : : }
400 : 0 : segmentEndX = x[index];
401 : 0 : segmentEndY = y[index];
402 : 0 : }
403 : 0 : 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 : 0 : findLineCircleIntersection( characterStartX, characterStartY, charWidth, segmentStartX, segmentStartY, segmentEndX, segmentEndY, characterEndX, characterEndY );
407 : :
408 : : // Need to calculate distance on the new segment
409 : 0 : currentDistanceAlongSegment = std::sqrt( std::pow( segmentStartX - characterEndX, 2 ) + std::pow( segmentStartY - characterEndY, 2 ) );
410 : : }
411 : 0 : return true;
412 : 0 : }
413 : :
414 : 0 : void QgsTextRendererUtils::findLineCircleIntersection( double cx, double cy, double radius, double x1, double y1, double x2, double y2, double &xRes, double &yRes )
415 : : {
416 : 0 : double multiplier = 1;
417 : 0 : 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 : 0 : multiplier = 10000;
422 : 0 : x1 *= multiplier;
423 : 0 : y1 *= multiplier;
424 : 0 : x2 *= multiplier;
425 : 0 : y2 *= multiplier;
426 : 0 : cx *= multiplier;
427 : 0 : cy *= multiplier;
428 : 0 : radius *= multiplier;
429 : 0 : }
430 : :
431 : 0 : double dx = x2 - x1;
432 : 0 : double dy = y2 - y1;
433 : :
434 : 0 : double A = dx * dx + dy * dy;
435 : 0 : double B = 2 * ( dx * ( x1 - cx ) + dy * ( y1 - cy ) );
436 : 0 : double C = ( x1 - cx ) * ( x1 - cx ) + ( y1 - cy ) * ( y1 - cy ) - radius * radius;
437 : :
438 : 0 : double det = B * B - 4 * A * C;
439 : 0 : if ( A <= 0.000000000001 || det < 0 )
440 : : // Should never happen, No real solutions.
441 : 0 : return;
442 : :
443 : 0 : if ( qgsDoubleNear( det, 0.0 ) )
444 : : {
445 : : // Could potentially happen.... One solution.
446 : 0 : double t = -B / ( 2 * A );
447 : 0 : xRes = x1 + t * dx;
448 : 0 : yRes = y1 + t * dy;
449 : 0 : }
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 : 0 : double t = ( -B + std::sqrt( det ) ) / ( 2 * A );
456 : 0 : xRes = x1 + t * dx;
457 : 0 : yRes = y1 + t * dy;
458 : : }
459 : :
460 : 0 : if ( multiplier != 1 )
461 : : {
462 : 0 : xRes /= multiplier;
463 : 0 : yRes /= multiplier;
464 : 0 : }
465 : 0 : }
|