QGIS API Documentation 4.1.0-Master (5bf3c20f3c9)
Loading...
Searching...
No Matches
feature.cpp
Go to the documentation of this file.
1/*
2 * libpal - Automated Placement of Labels Library
3 *
4 * Copyright (C) 2008 Maxence Laurent, MIS-TIC, HEIG-VD
5 * University of Applied Sciences, Western Switzerland
6 * http://www.hes-so.ch
7 *
8 * Contact:
9 * maxence.laurent <at> heig-vd <dot> ch
10 * or
11 * eric.taillard <at> heig-vd <dot> ch
12 *
13 * This file is part of libpal.
14 *
15 * libpal is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License as published by
17 * the Free Software Foundation, either version 3 of the License, or
18 * (at your option) any later version.
19 *
20 * libpal is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * You should have received a copy of the GNU General Public License
26 * along with libpal. If not, see <http://www.gnu.org/licenses/>.
27 *
28 */
29
30#include "feature.h"
31
32#include <cmath>
33
34#include "geomfunction.h"
35#include "labelposition.h"
36#include "layer.h"
37#include "pal.h"
38#include "pointset.h"
39#include "qgis.h"
40#include "qgsgeometry.h"
41#include "qgsgeometryutils.h"
43#include "qgsgeos.h"
44#include "qgsmessagelog.h"
45#include "qgspolygon.h"
46#include "qgstextlabelfeature.h"
48
49#include <QString>
50
51using namespace Qt::StringLiterals;
52
53using namespace pal;
54
56 : mLF( feat )
57{
58 // we'll remove const, but we won't modify that geometry
59 mGeos = const_cast<GEOSGeometry *>( geom );
60 mOwnsGeom = false; // geometry is owned by Feature class
61
62 extractCoords( geom );
63
64 holeOf = nullptr;
65 for ( int i = 0; i < mHoles.count(); i++ )
66 {
67 mHoles.at( i )->holeOf = this;
68 }
69}
70
72 : PointSet( other )
73 , mLF( other.mLF )
74 , mTotalRepeats( other.mTotalRepeats )
75 , mCachedMaxLineCandidates( other.mCachedMaxLineCandidates )
76 , mCachedMaxPolygonCandidates( other.mCachedMaxPolygonCandidates )
77{
78 for ( const FeaturePart *hole : std::as_const( other.mHoles ) )
79 {
80 mHoles << new FeaturePart( *hole );
81 mHoles.last()->holeOf = this;
82 }
83}
84
86{
87 // X and Y are deleted in PointSet
88
89 qDeleteAll( mHoles );
90 mHoles.clear();
91}
92
94{
95 const GEOSCoordSequence *coordSeq = nullptr;
96 GEOSContextHandle_t geosctxt = QgsGeosContext::get();
97
98 type = GEOSGeomTypeId_r( geosctxt, geom );
99
100 if ( type == GEOS_POLYGON )
101 {
102 if ( GEOSGetNumInteriorRings_r( geosctxt, geom ) > 0 )
103 {
104 int numHoles = GEOSGetNumInteriorRings_r( geosctxt, geom );
105
106 for ( int i = 0; i < numHoles; ++i )
107 {
108 const GEOSGeometry *interior = GEOSGetInteriorRingN_r( geosctxt, geom, i );
109 FeaturePart *hole = new FeaturePart( mLF, interior );
110 hole->holeOf = nullptr;
111 // possibly not needed. it's not done for the exterior ring, so I'm not sure
112 // why it's just done here...
113 GeomFunction::reorderPolygon( hole->x, hole->y );
114
115 mHoles << hole;
116 }
117 }
118
119 // use exterior ring for the extraction of coordinates that follows
120 geom = GEOSGetExteriorRing_r( geosctxt, geom );
121 }
122 else
123 {
124 qDeleteAll( mHoles );
125 mHoles.clear();
126 }
127
128 // find out number of points
129 nbPoints = GEOSGetNumCoordinates_r( geosctxt, geom );
130 coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, geom );
131
132 // initialize bounding box
133 xmin = ymin = std::numeric_limits<double>::max();
134 xmax = ymax = std::numeric_limits<double>::lowest();
135
136 // initialize coordinate arrays
137 deleteCoords();
138 x.resize( nbPoints );
139 y.resize( nbPoints );
140
141#if GEOS_VERSION_MAJOR > 3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 10 )
142 GEOSCoordSeq_copyToArrays_r( geosctxt, coordSeq, x.data(), y.data(), nullptr, nullptr );
143 auto xminmax = std::minmax_element( x.begin(), x.end() );
144 xmin = *xminmax.first;
145 xmax = *xminmax.second;
146 auto yminmax = std::minmax_element( y.begin(), y.end() );
147 ymin = *yminmax.first;
148 ymax = *yminmax.second;
149#else
150 for ( int i = 0; i < nbPoints; ++i )
151 {
152 GEOSCoordSeq_getXY_r( geosctxt, coordSeq, i, &x[i], &y[i] );
153
154 xmax = x[i] > xmax ? x[i] : xmax;
155 xmin = x[i] < xmin ? x[i] : xmin;
156
157 ymax = y[i] > ymax ? y[i] : ymax;
158 ymin = y[i] < ymin ? y[i] : ymin;
159 }
160#endif
161}
162
164{
165 return mLF->layer();
166}
167
169{
170 return mLF->id();
171}
172
174{
175 return mLF->subPartId();
176}
177
179{
180 return mLF->layer()->maximumPointLabelCandidates();
181}
182
184{
185 if ( mCachedMaxLineCandidates > 0 )
186 return mCachedMaxLineCandidates;
187
188 const double l = length();
189 if ( l > 0 )
190 {
191 const std::size_t candidatesForLineLength = static_cast< std::size_t >( std::ceil( mLF->layer()->mPal->maximumLineCandidatesPerMapUnit() * l ) );
192 const std::size_t maxForLayer = mLF->layer()->maximumLineLabelCandidates();
193 if ( maxForLayer == 0 )
194 mCachedMaxLineCandidates = candidatesForLineLength;
195 else
196 mCachedMaxLineCandidates = std::min( candidatesForLineLength, maxForLayer );
197 }
198 else
199 {
200 mCachedMaxLineCandidates = 1;
201 }
202 return mCachedMaxLineCandidates;
203}
204
206{
207 if ( mCachedMaxPolygonCandidates > 0 )
208 return mCachedMaxPolygonCandidates;
209
210 const double a = area();
211 if ( a > 0 )
212 {
213 const std::size_t candidatesForArea = static_cast< std::size_t >( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * a ) );
214 const std::size_t maxForLayer = mLF->layer()->maximumPolygonLabelCandidates();
215 if ( maxForLayer == 0 )
216 mCachedMaxPolygonCandidates = candidatesForArea;
217 else
218 mCachedMaxPolygonCandidates = std::min( candidatesForArea, maxForLayer );
219 }
220 else
221 {
222 mCachedMaxPolygonCandidates = 1;
223 }
224 return mCachedMaxPolygonCandidates;
225}
226
228{
229 if ( !part )
230 return false;
231
232 if ( mLF->layer()->name() != part->layer()->name() )
233 return false;
234
235 if ( mLF->id() == part->featureId() && mLF->subPartId() == part->subPartId() )
236 return true;
237
238 // any part of joined features are also treated as having the same label feature
239 int connectedFeatureId = mLF->layer()->connectedFeatureId( mLF->id() );
240 return connectedFeatureId >= 0 && connectedFeatureId == mLF->layer()->connectedFeatureId( part->featureId() );
241}
242
243Qgis::LabelQuadrantPosition FeaturePart::quadrantFromOffset() const
244{
245 QPointF quadOffset = mLF->quadOffset();
246 qreal quadOffsetX = quadOffset.x(), quadOffsetY = quadOffset.y();
247
248 if ( quadOffsetX < 0 )
249 {
250 if ( quadOffsetY < 0 )
251 {
253 }
254 else if ( quadOffsetY > 0 )
255 {
257 }
258 else
259 {
261 }
262 }
263 else if ( quadOffsetX > 0 )
264 {
265 if ( quadOffsetY < 0 )
266 {
268 }
269 else if ( quadOffsetY > 0 )
270 {
272 }
273 else
274 {
276 }
277 }
278 else
279 {
280 if ( quadOffsetY < 0 )
281 {
283 }
284 else if ( quadOffsetY > 0 )
285 {
287 }
288 else
289 {
291 }
292 }
293}
294
296{
297 return mTotalRepeats;
298}
299
301{
302 mTotalRepeats = totalRepeats;
303}
304
305std::size_t FeaturePart::createCandidateCenteredOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
306{
307 // get from feature
308 double labelW = getLabelWidth( angle );
309 double labelH = getLabelHeight( angle );
310
311 double cost = 0.00005;
312 int id = lPos.size();
313
314 double xdiff = -labelW / 2.0;
315 double ydiff = -labelH / 2.0;
316
318
319 double lx = x + xdiff;
320 double ly = y + ydiff;
321
322 if ( mLF->permissibleZonePrepared() )
323 {
324 if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), lx, ly, labelW, labelH, angle ) )
325 {
326 return 0;
327 }
328 }
329
330 lPos.emplace_back( std::make_unique< LabelPosition >( id, lx, ly, labelW, labelH, angle, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over ) );
331 return 1;
332}
333
334std::size_t FeaturePart::createCandidatesOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
335{
336 // get from feature
337 double labelW = getLabelWidth( angle );
338 double labelH = getLabelHeight( angle );
339
340 double cost = 0.0001;
341 int id = lPos.size();
342
343 double xdiff = -labelW / 2.0;
344 double ydiff = -labelH / 2.0;
345
347
348 if ( !qgsDoubleNear( mLF->quadOffset().x(), 0.0 ) )
349 {
350 xdiff += labelW / 2.0 * mLF->quadOffset().x();
351 }
352 if ( !qgsDoubleNear( mLF->quadOffset().y(), 0.0 ) )
353 {
354 ydiff += labelH / 2.0 * mLF->quadOffset().y();
355 }
356
357 if ( !mLF->hasFixedPosition() )
358 {
359 if ( !qgsDoubleNear( angle, 0.0 ) )
360 {
361 double xd = xdiff * std::cos( angle ) - ydiff * std::sin( angle );
362 double yd = xdiff * std::sin( angle ) + ydiff * std::cos( angle );
363 xdiff = xd;
364 ydiff = yd;
365 }
366 }
367
368 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::AroundPoint )
369 {
370 //if in "around point" placement mode, then we use the label distance to determine
371 //the label's offset
372 if ( qgsDoubleNear( mLF->quadOffset().x(), 0.0 ) )
373 {
374 ydiff += mLF->quadOffset().y() * mLF->distLabel();
375 }
376 else if ( qgsDoubleNear( mLF->quadOffset().y(), 0.0 ) )
377 {
378 xdiff += mLF->quadOffset().x() * mLF->distLabel();
379 }
380 else
381 {
382 xdiff += mLF->quadOffset().x() * M_SQRT1_2 * mLF->distLabel();
383 ydiff += mLF->quadOffset().y() * M_SQRT1_2 * mLF->distLabel();
384 }
385 }
386 else
387 {
388 if ( !qgsDoubleNear( mLF->positionOffset().x(), 0.0 ) )
389 {
390 xdiff += mLF->positionOffset().x();
391 }
392 if ( !qgsDoubleNear( mLF->positionOffset().y(), 0.0 ) )
393 {
394 ydiff += mLF->positionOffset().y();
395 }
396 }
397
398 double lx = x + xdiff;
399 double ly = y + ydiff;
400
401 if ( mLF->permissibleZonePrepared() )
402 {
403 if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), lx, ly, labelW, labelH, angle ) )
404 {
405 return 0;
406 }
407 }
408
409 lPos.emplace_back( std::make_unique< LabelPosition >( id, lx, ly, labelW, labelH, angle, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, quadrantFromOffset() ) );
410 return 1;
411}
412
413std::unique_ptr<LabelPosition> FeaturePart::createCandidatePointOnSurface( PointSet *mapShape )
414{
415 double px, py;
416 try
417 {
418 GEOSContextHandle_t geosctxt = QgsGeosContext::get();
419 geos::unique_ptr pointGeom( GEOSPointOnSurface_r( geosctxt, mapShape->geos() ) );
420 if ( pointGeom )
421 {
422 const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, pointGeom.get() );
423 unsigned int nPoints = 0;
424 GEOSCoordSeq_getSize_r( geosctxt, coordSeq, &nPoints );
425 if ( nPoints == 0 )
426 return nullptr;
427 GEOSCoordSeq_getXY_r( geosctxt, coordSeq, 0, &px, &py );
428 }
429 else
430 {
431 return nullptr;
432 }
433 }
434 catch ( QgsGeosException &e )
435 {
436 qWarning( "GEOS exception: %s", e.what() );
437 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
438 return nullptr;
439 }
440
441 return std::make_unique< LabelPosition >( 0, px, py, getLabelWidth(), getLabelHeight(), 0.0, 0.0, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
442}
443
445 double &labelX,
446 double &labelY,
448 double x,
449 double y,
450 double labelWidth,
451 double labelHeight,
453 double distanceToLabel,
454 const QgsMargins &visualMargin,
455 double symbolWidthOffset,
456 double symbolHeightOffset,
457 double angle
458)
459{
460 double alpha = 0.0;
461 double deltaX = 0;
462 double deltaY = 0;
463
464 switch ( position )
465 {
468 alpha = 3 * M_PI_4;
469 deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
470 deltaY = -visualMargin.bottom() + symbolHeightOffset;
471 break;
472
474 quadrant = Qgis::LabelQuadrantPosition::AboveRight; //right quadrant, so labels are left-aligned
475 alpha = M_PI_2;
476 deltaX = -labelWidth / 4.0 - visualMargin.left();
477 deltaY = -visualMargin.bottom() + symbolHeightOffset;
478 break;
479
482 alpha = M_PI_2;
483 deltaX = -labelWidth / 2.0;
484 deltaY = -visualMargin.bottom() + symbolHeightOffset;
485 break;
486
488 quadrant = Qgis::LabelQuadrantPosition::AboveLeft; //left quadrant, so labels are right-aligned
489 alpha = M_PI_2;
490 deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right();
491 deltaY = -visualMargin.bottom() + symbolHeightOffset;
492 break;
493
496 alpha = M_PI_4;
497 deltaX = -visualMargin.left() + symbolWidthOffset;
498 deltaY = -visualMargin.bottom() + symbolHeightOffset;
499 break;
500
503 alpha = M_PI;
504 deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
505 deltaY = -labelHeight / 2.0; // TODO - should this be adjusted by visual margin??
506 break;
507
510 alpha = 0.0;
511 deltaX = -visualMargin.left() + symbolWidthOffset;
512 deltaY = -labelHeight / 2.0; // TODO - should this be adjusted by visual margin??
513 break;
514
517 alpha = 5 * M_PI_4;
518 deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
519 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
520 break;
521
523 quadrant = Qgis::LabelQuadrantPosition::BelowRight; //right quadrant, so labels are left-aligned
524 alpha = 3 * M_PI_2;
525 deltaX = -labelWidth / 4.0 - visualMargin.left();
526 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
527 break;
528
531 alpha = 3 * M_PI_2;
532 deltaX = -labelWidth / 2.0;
533 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
534 break;
535
537 quadrant = Qgis::LabelQuadrantPosition::BelowLeft; //left quadrant, so labels are right-aligned
538 alpha = 3 * M_PI_2;
539 deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right();
540 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
541 break;
542
545 alpha = 7 * M_PI_4;
546 deltaX = -visualMargin.left() + symbolWidthOffset;
547 deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
548 break;
549
552 alpha = 0;
553 distanceToLabel = 0;
554 deltaX = -labelWidth / 2.0;
555 deltaY = -labelHeight / 2.0; // TODO - should this be adjusted by visual margin??
556 break;
557 }
558
559 // Take care of the label angle when creating candidates. See pr comments #44944 for details
560 // https://github.com/qgis/QGIS/pull/44944#issuecomment-914670088
561 QTransform transformRotation;
562 transformRotation.rotate( angle * 180 / M_PI );
563 transformRotation.map( deltaX, deltaY, &deltaX, &deltaY );
564
565 //have bearing, distance - calculate reference point
566 double referenceX = std::cos( alpha ) * distanceToLabel + x;
567 double referenceY = std::sin( alpha ) * distanceToLabel + y;
568
569 labelX = referenceX + deltaX;
570 labelY = referenceY + deltaY;
571}
572
573std::size_t FeaturePart::createCandidatesAtOrderedPositionsOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
574{
575 const QVector< Qgis::LabelPredefinedPointPosition > positions = mLF->predefinedPositionOrder();
576 const double labelWidth = getLabelWidth( angle );
577 const double labelHeight = getLabelHeight( angle );
578 double distanceToLabel = getLabelDistance();
579 const double maximumDistanceToLabel = mLF->maximumDistance();
580
581 const QgsMargins &visualMargin = mLF->visualMargin();
582
583 double symbolWidthOffset { 0 };
584 double symbolHeightOffset { 0 };
585
586 if ( mLF->offsetType() == Qgis::LabelOffsetType::FromSymbolBounds )
587 {
588 // Multi?
589 if ( mLF->feature().geometry().constParts().hasNext() )
590 {
591 const QgsGeometry geom { QgsGeos::fromGeos( mLF->geometry() ) };
592 symbolWidthOffset = ( mLF->symbolSize().width() - geom.boundingBox().width() ) / 2.0;
593 symbolHeightOffset = ( mLF->symbolSize().height() - geom.boundingBox().height() ) / 2.0;
594 }
595 else
596 {
597 symbolWidthOffset = mLF->symbolSize().width() / 2.0;
598 symbolHeightOffset = mLF->symbolSize().height() / 2.0;
599 }
600 }
601
602 int candidatesPerPosition = 1;
603 double distanceStep = 0;
604 if ( maximumDistanceToLabel > distanceToLabel && !qgsDoubleNear( maximumDistanceToLabel, 0 ) )
605 {
606 // if we are placing labels over a distance range, we calculate the number of candidates
607 // based on the calculated valid area for labels
608 const double rayLength = maximumDistanceToLabel - distanceToLabel;
609
610 // we want at least two candidates per "ray", one at the min distance and one at the max
611 candidatesPerPosition = std::max( 2, static_cast< int >( std::ceil( mLF->layer()->mPal->maximumLineCandidatesPerMapUnit() * 1.5 * rayLength ) ) );
612 distanceStep = rayLength / ( candidatesPerPosition - 1 );
613 }
614
615 double cost = 0.0001;
616 std::size_t i = lPos.size();
617
618 const Qgis::LabelPrioritization prioritization = mLF->prioritization();
619 const std::size_t maxNumberCandidates = mLF->layer()->maximumPointLabelCandidates() * candidatesPerPosition;
620 std::size_t created = 0;
621
622 auto addCandidate =
623 [this, x, y, labelWidth, labelHeight, angle, visualMargin, symbolWidthOffset, symbolHeightOffset, &created, &cost, &lPos, &i, maxNumberCandidates]( Qgis::LabelPredefinedPointPosition position, double distance )
624 -> bool {
626
627 double labelX = 0;
628 double labelY = 0;
629 createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distance, visualMargin, symbolWidthOffset, symbolHeightOffset, angle );
630
631 if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), labelX, labelY, labelWidth, labelHeight, angle ) )
632 {
633 lPos.emplace_back( std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, quadrant ) );
634 ++created;
635 ++i;
636 //TODO - tweak
637 cost += 0.001;
638 if ( maxNumberCandidates > 0 && created >= maxNumberCandidates )
639 return false;
640 }
641 return true;
642 };
643
644 switch ( prioritization )
645 {
646 // the two cases below are identical, we just change which loop is the outer and inner loop
647 // remember to keep these in sync!!
649 {
650 for ( Qgis::LabelPredefinedPointPosition position : positions )
651 {
652 double currentDistance = distanceToLabel;
653 for ( int distanceIndex = 0; distanceIndex < candidatesPerPosition; ++distanceIndex, currentDistance += distanceStep )
654 {
655 if ( !addCandidate( position, currentDistance ) )
656 return created;
657 }
658 }
659 break;
660 }
661
663 {
664 double currentDistance = distanceToLabel;
665 for ( int distanceIndex = 0; distanceIndex < candidatesPerPosition; ++distanceIndex, currentDistance += distanceStep )
666 {
667 for ( Qgis::LabelPredefinedPointPosition position : positions )
668 {
669 if ( !addCandidate( position, currentDistance ) )
670 return created;
671 }
672 }
673 break;
674 }
675 }
676 return created;
677}
678
679std::size_t FeaturePart::createCandidatesAroundPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
680{
681 const double labelWidth = getLabelWidth( angle );
682 const double labelHeight = getLabelHeight( angle );
683 const double distanceToLabel = getLabelDistance();
684 const double maximumDistanceToLabel = mLF->maximumDistance();
685
686 // Take care of the label angle when creating candidates. See pr comments #44944 for details
687 // https://github.com/qgis/QGIS/pull/44944#issuecomment-914670088
688 QTransform transformRotation;
689 transformRotation.rotate( angle * 180 / M_PI );
690
691 int rayCount = static_cast< int >( mLF->layer()->maximumPointLabelCandidates() );
692 if ( rayCount == 0 )
693 rayCount = 16;
694
695 int candidatesPerRay = 0;
696 double rayStepDelta = 0;
697 if ( maximumDistanceToLabel > distanceToLabel && !qgsDoubleNear( maximumDistanceToLabel, 0 ) )
698 {
699 // if we are placing labels over a distance range, we calculate the number of candidates
700 // based on the calculated valid area for labels
701 const double rayLength = maximumDistanceToLabel - distanceToLabel;
702
703 // we want at least two candidates per "ray", one at the min distance and one at the max
704 candidatesPerRay = std::max( 2, static_cast< int >( std::ceil( mLF->layer()->mPal->maximumLineCandidatesPerMapUnit() * 1.5 * rayLength ) ) );
705 rayStepDelta = rayLength / ( candidatesPerRay - 1 );
706 }
707 else
708 {
709 candidatesPerRay = 1;
710 }
711
712 int id = static_cast< int >( lPos.size() );
713
714 const double candidateAngleIncrement = 2 * M_PI / static_cast< double >( rayCount ); /* angle bw 2 pos */
715
716 /* various angles */
717 constexpr double a90 = M_PI_2;
718 constexpr double a180 = M_PI;
719 constexpr double a270 = a180 + a90;
720 constexpr double a360 = 2 * M_PI;
721
722 double gamma1, gamma2;
723
724 if ( distanceToLabel > 0 )
725 {
726 gamma1 = std::atan2( labelHeight / 2, distanceToLabel + labelWidth / 2 );
727 gamma2 = std::atan2( labelWidth / 2, distanceToLabel + labelHeight / 2 );
728 }
729 else
730 {
731 gamma1 = gamma2 = a90 / 3.0;
732 }
733
734 if ( gamma1 > a90 / 3.0 )
735 gamma1 = a90 / 3.0;
736
737 if ( gamma2 > a90 / 3.0 )
738 gamma2 = a90 / 3.0;
739
740 std::size_t numberCandidatesGenerated = 0;
741
742 double angleToCandidate = M_PI_4;
743
744 int integerRayCost = 0;
745 int integerRayCostIncrement = 2;
746
747 for ( int rayIndex = 0; rayIndex < rayCount; ++rayIndex, angleToCandidate += candidateAngleIncrement )
748 {
749 double deltaX = 0.0;
750 double deltaY = 0.0;
751
752 if ( angleToCandidate > a360 )
753 angleToCandidate -= a360;
754
755 double rayDistance = distanceToLabel;
756
757 constexpr double RAY_ANGLE_COST_FACTOR = 0.0020;
758 // ray angle cost increases from 0 at 45 degrees up to 1 at 45 + 180, and then decreases
759 // back to 0 at angles greater than 45 + 180
760 // scale ray angle cost to range 0 to 1, and then adjust by a magic constant factor
761 const double scaledRayAngleCost = RAY_ANGLE_COST_FACTOR * static_cast< double >( integerRayCost ) / static_cast< double >( rayCount - 1 );
762
763 for ( int j = 0; j < candidatesPerRay; ++j, rayDistance += rayStepDelta )
764 {
766
767 if ( angleToCandidate < gamma1 || angleToCandidate > a360 - gamma1 ) // on the right
768 {
769 deltaX = rayDistance;
770 double iota = ( angleToCandidate + gamma1 );
771 if ( iota > a360 - gamma1 )
772 iota -= a360;
773
774 deltaY = -labelHeight + labelHeight * iota / ( 2 * gamma1 );
775
777 }
778 else if ( angleToCandidate < a90 - gamma2 ) // top-right
779 {
780 deltaX = rayDistance * std::cos( angleToCandidate );
781 deltaY = rayDistance * std::sin( angleToCandidate );
783 }
784 else if ( angleToCandidate < a90 + gamma2 ) // top
785 {
786 deltaX = -labelWidth * ( angleToCandidate - a90 + gamma2 ) / ( 2 * gamma2 );
787 deltaY = rayDistance;
789 }
790 else if ( angleToCandidate < a180 - gamma1 ) // top left
791 {
792 deltaX = rayDistance * std::cos( angleToCandidate ) - labelWidth;
793 deltaY = rayDistance * std::sin( angleToCandidate );
795 }
796 else if ( angleToCandidate < a180 + gamma1 ) // left
797 {
798 deltaX = -rayDistance - labelWidth;
799 deltaY = -( angleToCandidate - a180 + gamma1 ) * labelHeight / ( 2 * gamma1 );
801 }
802 else if ( angleToCandidate < a270 - gamma2 ) // down - left
803 {
804 deltaX = rayDistance * std::cos( angleToCandidate ) - labelWidth;
805 deltaY = rayDistance * std::sin( angleToCandidate ) - labelHeight;
807 }
808 else if ( angleToCandidate < a270 + gamma2 ) // down
809 {
810 deltaY = -rayDistance - labelHeight;
811 deltaX = -labelWidth + ( angleToCandidate - a270 + gamma2 ) * labelWidth / ( 2 * gamma2 );
813 }
814 else if ( angleToCandidate < a360 ) // down - right
815 {
816 deltaX = rayDistance * std::cos( angleToCandidate );
817 deltaY = rayDistance * std::sin( angleToCandidate ) - labelHeight;
819 }
820
821 transformRotation.map( deltaX, deltaY, &deltaX, &deltaY );
822
823 double labelX = x + deltaX;
824 double labelY = y + deltaY;
825
826 double cost;
827
828 if ( rayCount == 1 )
829 cost = 0.0001;
830 else
831 cost = 0.0001 + scaledRayAngleCost;
832
833 if ( j > 0 )
834 {
835 // cost increases with distance, such that the cost for placing the label at the optimal angle (45)
836 // but at a greater distance is more then the cost for placing the label at the worst angle (45+180)
837 // but at the minimum distance
838 cost += j * RAY_ANGLE_COST_FACTOR + RAY_ANGLE_COST_FACTOR / rayCount;
839 }
840
841 if ( mLF->permissibleZonePrepared() )
842 {
843 if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), labelX, labelY, labelWidth, labelHeight, angle ) )
844 {
845 continue;
846 }
847 }
848
849 lPos.emplace_back( std::make_unique< LabelPosition >( id, labelX, labelY, labelWidth, labelHeight, angle, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, quadrant ) );
850 id++;
851 numberCandidatesGenerated++;
852 }
853
854 integerRayCost += integerRayCostIncrement;
855
856 if ( integerRayCost == static_cast< int >( rayCount ) )
857 {
858 integerRayCost = static_cast< int >( rayCount ) - 1;
859 integerRayCostIncrement = -2;
860 }
861 else if ( integerRayCost > static_cast< int >( rayCount ) )
862 {
863 integerRayCost = static_cast< int >( rayCount ) - 2;
864 integerRayCostIncrement = -2;
865 }
866 }
867
868 return numberCandidatesGenerated;
869}
870
871std::size_t FeaturePart::createCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
872{
873 if ( allowOverrun )
874 {
875 double shapeLength = mapShape->length();
876 if ( totalRepeats() > 1 && shapeLength < getLabelWidth() )
877 return 0;
878 else if ( shapeLength < getLabelWidth() - 2 * std::min( getLabelWidth(), mLF->overrunDistance() ) )
879 {
880 // label doesn't fit on this line, don't waste time trying to make candidates
881 return 0;
882 }
883 }
884
885 //prefer to label along straightish segments:
886 std::size_t candidates = 0;
887
888 if ( mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::HintOnly )
889 candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );
890
891 const std::size_t candidateTargetCount = maximumLineCandidates();
892 if ( candidates < candidateTargetCount )
893 {
894 // but not enough candidates yet, so fallback to labeling near whole line's midpoint
895 candidates = createCandidatesAlongLineNearMidpoint( lPos, mapShape, candidates > 0 ? 0.01 : 0.0, pal );
896 }
897 return candidates;
898}
899
900std::size_t FeaturePart::createHorizontalCandidatesAlongLine( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal )
901{
902 const double labelWidth = getLabelWidth();
903 const double labelHeight = getLabelHeight();
904
905 PointSet *line = mapShape;
906 int nbPoints = line->nbPoints;
907 std::vector< double > &x = line->x;
908 std::vector< double > &y = line->y;
909
910 std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
911 std::vector< double > distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
912
913 double totalLineLength = 0.0; // line length
914 for ( int i = 0; i < line->nbPoints - 1; i++ )
915 {
916 if ( i == 0 )
917 distanceToSegment[i] = 0;
918 else
919 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
920
921 segmentLengths[i] = QgsGeometryUtilsBase::distance2D( x[i], y[i], x[i + 1], y[i + 1] );
922 totalLineLength += segmentLengths[i];
923 }
924 distanceToSegment[line->nbPoints - 1] = totalLineLength;
925
926 const std::size_t candidateTargetCount = maximumLineCandidates();
927 double lineStepDistance = 0;
928
929 const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
930 double currentDistanceAlongLine = lineStepDistance;
931 switch ( mLF->lineAnchorType() )
932 {
934 lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
935 break;
936
938 currentDistanceAlongLine = lineAnchorPoint;
939 lineStepDistance = -1;
940 break;
941 }
942
943 const QgsLabelLineSettings::AnchorTextPoint textPoint = mLF->lineAnchorTextPoint();
944
945 double candidateCenterX, candidateCenterY;
946 int i = 0;
947 while ( currentDistanceAlongLine <= totalLineLength )
948 {
949 if ( pal->isCanceled() )
950 {
951 return lPos.size();
952 }
953
954 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateCenterX, &candidateCenterY );
955
956 // penalize positions which are further from the line's anchor point
957 double cost = totalLineLength > 0 ? std::fabs( lineAnchorPoint - currentDistanceAlongLine ) / totalLineLength : 0; // <0, 0.5>
958 cost /= 1000; // < 0, 0.0005 >
959
960 double labelX = 0;
961 switch ( textPoint )
962 {
964 labelX = candidateCenterX;
965 break;
967 labelX = candidateCenterX - labelWidth / 2;
968 break;
970 labelX = candidateCenterX - labelWidth;
971 break;
973 // not possible here
974 break;
975 }
976 lPos.emplace_back(
977 std::make_unique<
978 LabelPosition >( i, labelX, candidateCenterY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
979 );
980
981 currentDistanceAlongLine += lineStepDistance;
982
983 i++;
984
985 if ( lineStepDistance < 0 )
986 break;
987 }
988
989 return lPos.size();
990}
991
992std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
993{
994 double labelWidth = getLabelWidth();
995 double labelHeight = getLabelHeight();
996 double distanceLineToLabel = getLabelDistance();
997 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
998 if ( flags == 0 )
999 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1000
1001 // first scan through the whole line and look for segments where the angle at a node is greater than 45 degrees - these form a "hard break" which labels shouldn't cross over
1002 QVector< int > extremeAngleNodes;
1003 PointSet *line = mapShape;
1004 int numberNodes = line->nbPoints;
1005 std::vector< double > &x = line->x;
1006 std::vector< double > &y = line->y;
1007
1008 // closed line? if so, we need to handle the final node angle
1009 bool closedLine = qgsDoubleNear( x[0], x[numberNodes - 1] ) && qgsDoubleNear( y[0], y[numberNodes - 1] );
1010 for ( int i = 1; i <= numberNodes - ( closedLine ? 1 : 2 ); ++i )
1011 {
1012 double x1 = x[i - 1];
1013 double x2 = x[i];
1014 double x3 = x[i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
1015 double y1 = y[i - 1];
1016 double y2 = y[i];
1017 double y3 = y[i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
1018 if ( qgsDoubleNear( y2, y3 ) && qgsDoubleNear( x2, x3 ) )
1019 continue;
1020 if ( qgsDoubleNear( y1, y2 ) && qgsDoubleNear( x1, x2 ) )
1021 continue;
1022 double vertexAngle = M_PI - ( std::atan2( y3 - y2, x3 - x2 ) - std::atan2( y2 - y1, x2 - x1 ) );
1023 vertexAngle = QgsGeometryUtilsBase::normalizedAngle( vertexAngle );
1024
1025 // extreme angles form more than 45 degree angle at a node - these are the ones we don't want labels to cross
1026 if ( vertexAngle < M_PI * 135.0 / 180.0 || vertexAngle > M_PI * 225.0 / 180.0 )
1027 extremeAngleNodes << i;
1028 }
1029 extremeAngleNodes << numberNodes - 1;
1030
1031 if ( extremeAngleNodes.isEmpty() )
1032 {
1033 // no extreme angles - createCandidatesAlongLineNearMidpoint will be more appropriate
1034 return 0;
1035 }
1036
1037 // calculate lengths of segments, and work out longest straight-ish segment
1038 std::vector< double > segmentLengths( numberNodes - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
1039 std::vector< double > distanceToSegment( numberNodes ); // absolute distance bw pt[0] and pt[i] along the line
1040 double totalLineLength = 0.0;
1041 QVector< double > straightSegmentLengths;
1042 QVector< double > straightSegmentAngles;
1043 straightSegmentLengths.reserve( extremeAngleNodes.size() + 1 );
1044 straightSegmentAngles.reserve( extremeAngleNodes.size() + 1 );
1045 double currentStraightSegmentLength = 0;
1046 double longestSegmentLength = 0;
1047 double segmentStartX = x[0];
1048 double segmentStartY = y[0];
1049 for ( int i = 0; i < numberNodes - 1; i++ )
1050 {
1051 if ( i == 0 )
1052 distanceToSegment[i] = 0;
1053 else
1054 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
1055
1056 segmentLengths[i] = QgsGeometryUtilsBase::distance2D( x[i], y[i], x[i + 1], y[i + 1] );
1057 totalLineLength += segmentLengths[i];
1058 if ( extremeAngleNodes.contains( i ) )
1059 {
1060 // at an extreme angle node, so reset counters
1061 straightSegmentLengths << currentStraightSegmentLength;
1062 straightSegmentAngles << QgsGeometryUtilsBase::normalizedAngle( std::atan2( y[i] - segmentStartY, x[i] - segmentStartX ) );
1063 longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
1064 currentStraightSegmentLength = 0;
1065 segmentStartX = x[i];
1066 segmentStartY = y[i];
1067 }
1068 currentStraightSegmentLength += segmentLengths[i];
1069 }
1070 distanceToSegment[line->nbPoints - 1] = totalLineLength;
1071 straightSegmentLengths << currentStraightSegmentLength;
1072 straightSegmentAngles << QgsGeometryUtilsBase::normalizedAngle( std::atan2( y[numberNodes - 1] - segmentStartY, x[numberNodes - 1] - segmentStartX ) );
1073 longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
1074 const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
1075
1076 if ( totalLineLength < labelWidth )
1077 {
1078 return 0; //createCandidatesAlongLineNearMidpoint will be more appropriate
1079 }
1080
1081 const QgsLabelLineSettings::AnchorTextPoint textPoint = mLF->lineAnchorTextPoint();
1082
1083 const std::size_t candidateTargetCount = maximumLineCandidates();
1084 double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
1085 lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
1086
1087 double distanceToEndOfSegment = 0.0;
1088 int lastNodeInSegment = 0;
1089 // finally, loop through all these straight segments. For each we create candidates along the straight segment.
1090 for ( int i = 0; i < straightSegmentLengths.count(); ++i )
1091 {
1092 currentStraightSegmentLength = straightSegmentLengths.at( i );
1093 double currentSegmentAngle = straightSegmentAngles.at( i );
1094 lastNodeInSegment = extremeAngleNodes.at( i );
1095 double distanceToStartOfSegment = distanceToEndOfSegment;
1096 distanceToEndOfSegment = distanceToSegment[lastNodeInSegment];
1097 double distanceToCenterOfSegment = 0.5 * ( distanceToEndOfSegment + distanceToStartOfSegment );
1098
1099 if ( currentStraightSegmentLength < labelWidth )
1100 // can't fit a label on here
1101 continue;
1102
1103 double currentDistanceAlongLine = distanceToStartOfSegment;
1104 double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
1105 double candidateLength = 0.0;
1106 double cost = 0.0;
1107 double angle = 0.0;
1108 double beta = 0.0;
1109
1110 //calculate some cost penalties
1111 double segmentCost = 1.0 - ( distanceToEndOfSegment - distanceToStartOfSegment ) / longestSegmentLength; // 0 -> 1 (lower for longer segments)
1112 double segmentAngleCost = 1 - std::fabs( std::fmod( currentSegmentAngle, M_PI ) - M_PI_2 ) / M_PI_2; // 0 -> 1, lower for more horizontal segments
1113
1114 while ( currentDistanceAlongLine + labelWidth < distanceToEndOfSegment )
1115 {
1116 if ( pal->isCanceled() )
1117 {
1118 return lPos.size();
1119 }
1120
1121 // calculate positions along linestring corresponding to start and end of current label candidate
1122 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1123 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1124
1125 candidateLength = QgsGeometryUtilsBase::distance2D( candidateEndX, candidateEndY, candidateStartX, candidateStartY );
1126
1127
1128 // LOTS OF DIFFERENT COSTS TO BALANCE HERE - feel free to tweak these, but please add a unit test
1129 // which covers the situation you are adjusting for (e.g., "given equal length lines, choose the more horizontal line")
1130
1131 cost = candidateLength / labelWidth;
1132 if ( cost > 0.98 )
1133 cost = 0.0001;
1134 else
1135 {
1136 // jaggy line has a greater cost
1137 cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1138 }
1139
1140 const double labelCenter = currentDistanceAlongLine + labelWidth / 2.0;
1141 double labelTextAnchor = 0;
1142 switch ( textPoint )
1143 {
1145 labelTextAnchor = currentDistanceAlongLine;
1146 break;
1148 labelTextAnchor = currentDistanceAlongLine + labelWidth / 2.0;
1149 break;
1151 labelTextAnchor = currentDistanceAlongLine + labelWidth;
1152 break;
1154 // not possible here
1155 break;
1156 }
1157
1158 const bool placementIsFlexible = mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1159 // penalize positions which are further from the straight segments's midpoint
1160 if ( placementIsFlexible )
1161 {
1162 // only apply this if labels are being placed toward the center of overall lines -- otherwise it messes with the distance from anchor cost
1163 double costCenter = 2 * std::fabs( labelCenter - distanceToCenterOfSegment ) / ( distanceToEndOfSegment - distanceToStartOfSegment ); // 0 -> 1
1164 cost += costCenter * 0.0005; // < 0, 0.0005 >
1165 }
1166
1167 if ( !closedLine )
1168 {
1169 // penalize positions which are further from line anchor point of whole linestring (by default the middle of the line)
1170 // this only applies to non closed linestrings, since the middle of a closed linestring is effectively arbitrary
1171 // and irrelevant to labeling
1172 double costLineCenter = 2 * std::fabs( labelTextAnchor - lineAnchorPoint ) / totalLineLength; // 0 -> 1
1173 cost += costLineCenter * 0.0005; // < 0, 0.0005 >
1174 }
1175
1176 if ( placementIsFlexible )
1177 {
1178 cost += segmentCost * 0.0005; // prefer labels on longer straight segments
1179 cost += segmentAngleCost * 0.0001; // prefer more horizontal segments, but this is less important than length considerations
1180 }
1181
1182 if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1183 {
1184 angle = 0.0;
1185 }
1186 else
1187 angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1188
1189 labelWidth = getLabelWidth( angle );
1190 labelHeight = getLabelHeight( angle );
1191 beta = angle + M_PI_2;
1192
1193 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Line )
1194 {
1195 // find out whether the line direction for this candidate is from right to left
1196 bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1197 // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1198 bool reversed = ( ( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1199 bool aboveLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) );
1200 bool belowLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) );
1201
1202 if ( belowLine )
1203 {
1204 if ( !mLF->permissibleZonePrepared()
1205 || GeomFunction::
1206 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1207 {
1208 const double candidateCost = cost + ( reversed ? 0 : 0.001 );
1209 lPos.emplace_back(
1210 std::make_unique< LabelPosition >(
1211 i,
1212 candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ),
1213 candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ),
1214 labelWidth,
1215 labelHeight,
1216 angle,
1217 candidateCost,
1218 this,
1221 )
1222 ); // Line
1223 }
1224 }
1225 if ( aboveLine )
1226 {
1227 if ( !mLF->permissibleZonePrepared()
1228 || GeomFunction::
1229 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) * distanceLineToLabel, candidateStartY + std::sin( beta ) * distanceLineToLabel, labelWidth, labelHeight, angle ) )
1230 {
1231 const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1232 lPos.emplace_back(
1233 std::make_unique< LabelPosition >(
1234 i,
1235 candidateStartX + std::cos( beta ) * distanceLineToLabel,
1236 candidateStartY + std::sin( beta ) * distanceLineToLabel,
1237 labelWidth,
1238 labelHeight,
1239 angle,
1240 candidateCost,
1241 this,
1244 )
1245 ); // Line
1246 }
1247 }
1249 {
1250 if ( !mLF->permissibleZonePrepared()
1251 || GeomFunction::
1252 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1253 {
1254 const double candidateCost = cost + 0.002;
1255 lPos.emplace_back(
1256 std::make_unique< LabelPosition >(
1257 i,
1258 candidateStartX - labelHeight * std::cos( beta ) / 2,
1259 candidateStartY - labelHeight * std::sin( beta ) / 2,
1260 labelWidth,
1261 labelHeight,
1262 angle,
1263 candidateCost,
1264 this,
1267 )
1268 ); // Line
1269 }
1270 }
1271 }
1272 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
1273 {
1274 lPos.emplace_back(
1275 std::make_unique<
1276 LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
1277 ); // Line
1278 }
1279 else
1280 {
1281 // an invalid arrangement?
1282 }
1283
1284 currentDistanceAlongLine += lineStepDistance;
1285 }
1286 }
1287
1288 return lPos.size();
1289}
1290
1291std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, double initialCost, Pal *pal )
1292{
1293 double distanceLineToLabel = getLabelDistance();
1294
1295 double labelWidth = getLabelWidth();
1296 double labelHeight = getLabelHeight();
1297
1298 double angle;
1299 double cost;
1300
1301 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
1302 if ( flags == 0 )
1303 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1304
1305 PointSet *line = mapShape;
1306 int nbPoints = line->nbPoints;
1307 std::vector< double > &x = line->x;
1308 std::vector< double > &y = line->y;
1309
1310 std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
1311 std::vector< double > distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
1312
1313 double totalLineLength = 0.0; // line length
1314 for ( int i = 0; i < line->nbPoints - 1; i++ )
1315 {
1316 if ( i == 0 )
1317 distanceToSegment[i] = 0;
1318 else
1319 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
1320
1321 segmentLengths[i] = QgsGeometryUtilsBase::distance2D( x[i], y[i], x[i + 1], y[i + 1] );
1322 totalLineLength += segmentLengths[i];
1323 }
1324 distanceToSegment[line->nbPoints - 1] = totalLineLength;
1325
1326 double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
1327 double currentDistanceAlongLine = 0;
1328
1329 const QgsLabelLineSettings::AnchorTextPoint textPoint = mLF->lineAnchorTextPoint();
1330
1331 const std::size_t candidateTargetCount = maximumLineCandidates();
1332
1333 if ( totalLineLength > labelWidth )
1334 {
1335 lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
1336 }
1337 else if ( !line->isClosed() ) // line length < label width => centering label position
1338 {
1339 currentDistanceAlongLine = -( labelWidth - totalLineLength ) / 2.0;
1340 lineStepDistance = -1;
1341 totalLineLength = labelWidth;
1342 }
1343 else
1344 {
1345 // closed line, not long enough for label => no candidates!
1346 currentDistanceAlongLine = std::numeric_limits< double >::max();
1347 }
1348
1349 const double lineAnchorPoint = totalLineLength * std::min( 0.99, mLF->lineAnchorPercent() ); // don't actually go **all** the way to end of line, just very close to!
1350
1351 switch ( mLF->lineAnchorType() )
1352 {
1354 break;
1355
1357 switch ( textPoint )
1358 {
1360 currentDistanceAlongLine = std::min( lineAnchorPoint, totalLineLength * 0.99 - labelWidth );
1361 break;
1363 currentDistanceAlongLine = std::min( lineAnchorPoint - labelWidth / 2, totalLineLength * 0.99 - labelWidth );
1364 break;
1366 currentDistanceAlongLine = std::min( lineAnchorPoint - labelWidth, totalLineLength * 0.99 - labelWidth );
1367 break;
1369 // not possible here
1370 break;
1371 }
1372 lineStepDistance = -1;
1373 break;
1374 }
1375
1376 double candidateLength;
1377 double beta;
1378 double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
1379 int i = 0;
1380 while ( currentDistanceAlongLine <= totalLineLength - labelWidth || mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::Strict )
1381 {
1382 if ( pal->isCanceled() )
1383 {
1384 return lPos.size();
1385 }
1386
1387 // calculate positions along linestring corresponding to start and end of current label candidate
1388 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1389 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1390
1391 if ( currentDistanceAlongLine < 0 )
1392 {
1393 // label is bigger than line, use whole available line
1394 candidateLength = QgsGeometryUtilsBase::distance2D( x[nbPoints - 1], y[nbPoints - 1], x[0], y[0] );
1395 }
1396 else
1397 {
1398 candidateLength = QgsGeometryUtilsBase::distance2D( candidateEndX, candidateEndY, candidateStartX, candidateStartY );
1399 }
1400
1401 cost = candidateLength / labelWidth;
1402 if ( cost > 0.98 )
1403 cost = 0.0001;
1404 else
1405 {
1406 // jaggy line has a greater cost
1407 cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1408 }
1409
1410 // penalize positions which are further from the line's anchor point
1411 double textAnchorPoint = 0;
1412 switch ( textPoint )
1413 {
1415 textAnchorPoint = currentDistanceAlongLine;
1416 break;
1418 textAnchorPoint = currentDistanceAlongLine + labelWidth / 2;
1419 break;
1421 textAnchorPoint = currentDistanceAlongLine + labelWidth;
1422 break;
1424 // not possible here
1425 break;
1426 }
1427 double costCenter = totalLineLength > 0 ? std::fabs( lineAnchorPoint - textAnchorPoint ) / totalLineLength : 0; // <0, 0.5>
1428 cost += costCenter / 1000; // < 0, 0.0005 >
1429 cost += initialCost;
1430
1431 if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1432 {
1433 angle = 0.0;
1434 }
1435 else
1436 angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1437
1438 labelWidth = getLabelWidth( angle );
1439 labelHeight = getLabelHeight( angle );
1440 beta = angle + M_PI_2;
1441
1442 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Line )
1443 {
1444 // find out whether the line direction for this candidate is from right to left
1445 bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1446 // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1447 bool reversed = ( ( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1448 bool aboveLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) );
1449 bool belowLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) );
1450
1451 if ( aboveLine )
1452 {
1453 if ( !mLF->permissibleZonePrepared()
1454 || GeomFunction::
1455 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) * distanceLineToLabel, candidateStartY + std::sin( beta ) * distanceLineToLabel, labelWidth, labelHeight, angle ) )
1456 {
1457 const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1458 lPos.emplace_back(
1459 std::make_unique< LabelPosition >(
1460 i,
1461 candidateStartX + std::cos( beta ) * distanceLineToLabel,
1462 candidateStartY + std::sin( beta ) * distanceLineToLabel,
1463 labelWidth,
1464 labelHeight,
1465 angle,
1466 candidateCost,
1467 this,
1470 )
1471 ); // Line
1472 }
1473 }
1474 if ( belowLine )
1475 {
1476 if ( !mLF->permissibleZonePrepared()
1477 || GeomFunction::
1478 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1479 {
1480 const double candidateCost = cost + ( !reversed ? 0.001 : 0 );
1481 lPos.emplace_back(
1482 std::make_unique< LabelPosition >(
1483 i,
1484 candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ),
1485 candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ),
1486 labelWidth,
1487 labelHeight,
1488 angle,
1489 candidateCost,
1490 this,
1493 )
1494 ); // Line
1495 }
1496 }
1498 {
1499 if ( !mLF->permissibleZonePrepared()
1500 || GeomFunction::
1501 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1502 {
1503 const double candidateCost = cost + 0.002;
1504 lPos.emplace_back(
1505 std::make_unique< LabelPosition >(
1506 i,
1507 candidateStartX - labelHeight * std::cos( beta ) / 2,
1508 candidateStartY - labelHeight * std::sin( beta ) / 2,
1509 labelWidth,
1510 labelHeight,
1511 angle,
1512 candidateCost,
1513 this,
1516 )
1517 ); // Line
1518 }
1519 }
1520 }
1521 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
1522 {
1523 lPos.emplace_back(
1524 std::make_unique<
1525 LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
1526 ); // Line
1527 }
1528 else
1529 {
1530 // an invalid arrangement?
1531 }
1532
1533 currentDistanceAlongLine += lineStepDistance;
1534
1535 i++;
1536
1537 if ( lineStepDistance < 0 )
1538 break;
1539 }
1540
1541 return lPos.size();
1542}
1543
1544std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset(
1545 PointSet *mapShape,
1546 const std::vector< double> &pathDistances,
1548 const double offsetAlongLine,
1549 bool &labeledLineSegmentIsRightToLeft,
1550 bool applyAngleConstraints,
1552 double additionalCharacterSpacing,
1553 double additionalWordSpacing
1554)
1555{
1556 const QgsPrecalculatedTextMetrics *metrics = qgis::down_cast< QgsTextLabelFeature * >( mLF )->textMetrics();
1557 Q_ASSERT( metrics );
1558
1559 const double maximumCharacterAngleInside = applyAngleConstraints ? std::fabs( qgis::down_cast< QgsTextLabelFeature *>( mLF )->maximumCharacterAngleInside() ) : -1;
1560 const double maximumCharacterAngleOutside = applyAngleConstraints ? std::fabs( qgis::down_cast< QgsTextLabelFeature *>( mLF )->maximumCharacterAngleOutside() ) : -1;
1561
1562 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement(
1564 generateCurvedTextPlacement( *metrics, mapShape->x.data(), mapShape->y.data(), mapShape->nbPoints, pathDistances, offsetAlongLine, direction, maximumCharacterAngleInside, maximumCharacterAngleOutside, flags, additionalCharacterSpacing, additionalWordSpacing )
1565 );
1566
1567 labeledLineSegmentIsRightToLeft = !( flags & Qgis::CurvedTextFlag::UprightCharactersOnly ) ? placement->labeledLineSegmentIsRightToLeft : placement->flippedCharacterPlacementToGetUprightLabels;
1568
1569 if ( placement->graphemePlacement.empty() )
1570 return nullptr;
1571
1572 auto it = placement->graphemePlacement.constBegin();
1573 auto firstPosition
1574 = std::make_unique< LabelPosition >( 0, it->x, it->y, it->width, it->height, it->angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
1575 firstPosition->setUpsideDownCharCount( placement->upsideDownCharCount );
1576 firstPosition->setPartId( it->graphemeIndex );
1577 LabelPosition *previousPosition = firstPosition.get();
1578 it++;
1579
1580 bool skipWhitespace = false;
1581 switch ( mLF->whitespaceCollisionHandling() )
1582 {
1584 break;
1585
1587 skipWhitespace = true;
1588 break;
1589 }
1590
1591 while ( it != placement->graphemePlacement.constEnd() )
1592 {
1593 if ( skipWhitespace && it->isWhitespace )
1594 {
1595 it++;
1596 continue;
1597 }
1598 auto position
1599 = std::make_unique< LabelPosition >( 0, it->x, it->y, it->width, it->height, it->angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
1600 position->setPartId( it->graphemeIndex );
1601
1602 LabelPosition *nextPosition = position.get();
1603 previousPosition->setNextPart( std::move( position ) );
1604 previousPosition = nextPosition;
1605 it++;
1606 }
1607
1608 return firstPosition;
1609}
1610
1611std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1612{
1613 const QgsPrecalculatedTextMetrics *li = qgis::down_cast< QgsTextLabelFeature *>( mLF )->textMetrics();
1614 Q_ASSERT( li );
1615
1616 // label info must be present
1617 if ( !li )
1618 return 0;
1619
1620 const int characterCount = li->count();
1621 if ( characterCount == 0 )
1622 return 0;
1623
1624 switch ( mLF->curvedLabelMode() )
1625 {
1629 return createDefaultCurvedCandidatesAlongLine( lPos, mapShape, allowOverrun, pal );
1631 return createCurvedCandidateWithCharactersAtVertices( lPos, mapShape, pal );
1632 }
1634}
1635
1636std::size_t FeaturePart::createDefaultCurvedCandidatesAlongLine( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1637{
1638 const QgsPrecalculatedTextMetrics *li = qgis::down_cast< QgsTextLabelFeature *>( mLF )->textMetrics();
1639 const int characterCount = li->count();
1640
1641 bool stretchWordSpacingToFit = mLF->curvedLabelMode() == Qgis::CurvedLabelMode::StretchWordSpacingToFitLine;
1642 double totalCharacterWidth = 0;
1643 int spaceCount = 0;
1644 for ( int i = 0; i < characterCount; ++i )
1645 {
1646 totalCharacterWidth += li->characterWidth( i );
1647 if ( stretchWordSpacingToFit && li->grapheme( i ) == ' ' )
1648 {
1649 spaceCount++;
1650 }
1651 }
1652 if ( spaceCount == 0 )
1653 {
1654 // if no spaces in the label, disable stretch word spacing to fit mode and fallback to standard curved placement
1655 stretchWordSpacingToFit = false;
1656 }
1657
1658 const bool stretchCharacterSpacingToFit = mLF->curvedLabelMode() == Qgis::CurvedLabelMode::StretchCharacterSpacingToFitLine;
1659 const bool usingStretchToFitMode = stretchCharacterSpacingToFit || stretchWordSpacingToFit;
1660
1661 // TODO - we may need an explicit penalty for overhanging labels. Currently, they are penalized just because they
1662 // are further from the line center, so non-overhanging placements are picked where possible.
1663
1664 std::unique_ptr< PointSet > expanded;
1665 double shapeLength = mapShape->length();
1666
1667 // in stretch modes we force allowOverrun to false, as we fit the text exactly
1668 // to the actual line length
1669 if ( totalRepeats() > 1 || usingStretchToFitMode )
1670 allowOverrun = false;
1671
1672 geos::unique_ptr originalPoint;
1673 if ( !usingStretchToFitMode )
1674 {
1675 // unless in strict mode, label overrun should NEVER exceed the label length (or labels would sit off in space).
1676 // in fact, let's require that a minimum of 5% of the label text has to sit on the feature,
1677 // as we don't want a label sitting right at the start or end corner of a line
1678 double overrun = 0;
1679 switch ( mLF->lineAnchorType() )
1680 {
1682 overrun = std::min( mLF->overrunDistance(), totalCharacterWidth * 0.95 );
1683 break;
1685 // in strict mode, we force sufficient overrun to ensure label will always "fit", even if it's placed
1686 // so that the label start sits right on the end of the line OR the label end sits right on the start of the line
1687 overrun = std::max( mLF->overrunDistance(), totalCharacterWidth * 1.05 );
1688 break;
1689 }
1690
1691 if ( totalCharacterWidth > shapeLength )
1692 {
1693 if ( !allowOverrun || shapeLength < totalCharacterWidth - 2 * overrun )
1694 {
1695 // label doesn't fit on this line, don't waste time trying to make candidates
1696 return 0;
1697 }
1698 }
1699
1700 // calculate the anchor point for the original line shape as a GEOS point.
1701 // this must be done BEFORE we account for overrun by extending the shape!
1702 originalPoint = mapShape->interpolatePoint( shapeLength * mLF->lineAnchorPercent() );
1703
1704 if ( allowOverrun && overrun > 0 )
1705 {
1706 // expand out line on either side to fit label
1707 expanded = mapShape->clone();
1708 expanded->extendLineByDistance( overrun, overrun, mLF->overrunSmoothDistance() );
1709 mapShape = expanded.get();
1710 shapeLength += 2 * overrun;
1711 }
1712 }
1713
1714 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
1715 if ( flags == 0 )
1716 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1717 const bool hasAboveBelowLinePlacement = flags & Qgis::LabelLinePlacementFlag::AboveLine || flags & Qgis::LabelLinePlacementFlag::BelowLine;
1718 const double offsetDistance = mLF->distLabel() + li->characterHeight( 0 ) / 2;
1719 std::unique_ptr< PointSet > mapShapeOffsetPositive;
1720 bool positiveShapeHasNegativeDistance = false;
1721 std::unique_ptr< PointSet > mapShapeOffsetNegative;
1722 bool negativeShapeHasNegativeDistance = false;
1723 if ( hasAboveBelowLinePlacement && !qgsDoubleNear( offsetDistance, 0 ) )
1724 {
1725 // create offsetted map shapes to be used for above and below line placements
1727 mapShapeOffsetPositive = mapShape->clone();
1729 mapShapeOffsetNegative = mapShape->clone();
1730 if ( offsetDistance >= 0.0 || !( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) )
1731 {
1732 if ( mapShapeOffsetPositive )
1733 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance );
1734 positiveShapeHasNegativeDistance = offsetDistance < 0;
1735 if ( mapShapeOffsetNegative )
1736 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance * -1 );
1737 negativeShapeHasNegativeDistance = offsetDistance > 0;
1738 }
1739 else
1740 {
1741 // In case of a negative offset distance, above line placement switch to below line and vice versa
1743 {
1744 flags &= ~static_cast< int >( Qgis::LabelLinePlacementFlag::AboveLine );
1746 }
1748 {
1749 flags &= ~static_cast< int >( Qgis::LabelLinePlacementFlag::BelowLine );
1751 }
1752 if ( mapShapeOffsetPositive )
1753 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance * -1 );
1754 positiveShapeHasNegativeDistance = offsetDistance > 0;
1755 if ( mapShapeOffsetNegative )
1756 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance );
1757 negativeShapeHasNegativeDistance = offsetDistance < 0;
1758 }
1759 }
1760
1761 const QgsLabelLineSettings::AnchorTextPoint textPoint = mLF->lineAnchorTextPoint();
1762
1763 std::vector< std::unique_ptr< LabelPosition >> positions;
1764 std::unique_ptr< LabelPosition > backupPlacement;
1765 for ( PathOffset offset : { PositiveOffset, NoOffset, NegativeOffset } )
1766 {
1767 PointSet *currentMapShape = nullptr;
1768 if ( offset == PositiveOffset && hasAboveBelowLinePlacement )
1769 {
1770 currentMapShape = mapShapeOffsetPositive.get();
1771 }
1772 if ( offset == NoOffset && flags & Qgis::LabelLinePlacementFlag::OnLine )
1773 {
1774 currentMapShape = mapShape;
1775 }
1776 if ( offset == NegativeOffset && hasAboveBelowLinePlacement )
1777 {
1778 currentMapShape = mapShapeOffsetNegative.get();
1779 }
1780 if ( !currentMapShape )
1781 continue;
1782
1783 // distance calculation
1784 const auto [pathDistances, totalDistance] = currentMapShape->edgeDistances();
1785 if ( qgsDoubleNear( totalDistance, 0.0 ) )
1786 continue;
1787
1788 double lineAnchorPoint = 0;
1789 if ( !usingStretchToFitMode )
1790 {
1791 if ( originalPoint )
1792 {
1793 // the actual anchor point for the offset curves is the closest point on those offset curves
1794 // to the anchor point on the original line. This avoids anchor points which differ greatly
1795 // on the positive/negative offset lines due to line curvature.
1796 lineAnchorPoint = currentMapShape->lineLocatePoint( originalPoint.get() );
1797 }
1798 else
1799 {
1800 lineAnchorPoint = totalDistance * mLF->lineAnchorPercent();
1801 if ( offset == NegativeOffset )
1802 lineAnchorPoint = totalDistance - lineAnchorPoint;
1803 }
1804 }
1805
1806 if ( pal->isCanceled() )
1807 return 0;
1808
1809 const std::size_t candidateTargetCount = maximumLineCandidates();
1810 double delta = std::max( li->characterHeight( 0 ) / 6, totalDistance / candidateTargetCount );
1811
1812 // generate curved labels
1813 double distanceAlongLineToStartCandidate = 0;
1814 bool singleCandidateOnly = false;
1815 double additionalCharacterSpacing = 0.0;
1816 double additionalWordSpacing = 0.0;
1817 if ( usingStretchToFitMode )
1818 {
1819 // calculate required expansion/compression of spacing
1820 double extraSpace = totalDistance - totalCharacterWidth;
1821
1822 // add a little bit of additional tolerance -- if we try to aim EXACTLY
1823 // for the end of the line, then we risk precision issues pushing us PAST
1824 // the end of the line and the string being truncated
1825 if ( extraSpace > 0 )
1826 extraSpace *= 0.995;
1827 else
1828 extraSpace *= 1.005;
1829
1830 if ( stretchWordSpacingToFit )
1831 {
1832 if ( spaceCount > 0 )
1833 additionalWordSpacing = extraSpace / spaceCount;
1834 else
1835 continue; // cannot stretch a single word
1836 }
1837 else
1838 {
1839 if ( characterCount > 1 )
1840 additionalCharacterSpacing = extraSpace / ( characterCount - 1 );
1841 }
1842
1843 // force a single candidate covering the whole line starting at 0
1844 distanceAlongLineToStartCandidate = 0;
1845 delta = totalDistance + 1.0; // (ensure loop runs exactly once)
1846 singleCandidateOnly = true;
1847 }
1848 else
1849 {
1850 switch ( mLF->lineAnchorType() )
1851 {
1853 break;
1854
1856 switch ( textPoint )
1857 {
1859 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint, 0.0, totalDistance * 0.999 );
1860 break;
1862 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth() / 2, 0.0, totalDistance * 0.999 - getLabelWidth() / 2 );
1863 break;
1865 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth(), 0.0, totalDistance * 0.999 - getLabelWidth() );
1866 break;
1868 // not possible here
1869 break;
1870 }
1871 singleCandidateOnly = true;
1872 break;
1873 }
1874 }
1875
1876 bool hasTestedFirstPlacement = false;
1877 for ( ; distanceAlongLineToStartCandidate <= totalDistance; distanceAlongLineToStartCandidate += delta )
1878 {
1879 if ( singleCandidateOnly && hasTestedFirstPlacement )
1880 break;
1881
1882 if ( pal->isCanceled() )
1883 return 0;
1884
1885 hasTestedFirstPlacement = true;
1886 // placements may need to be reversed if using map orientation and the line has right-to-left direction
1887 bool labeledLineSegmentIsRightToLeft = false;
1890 Qgis::CurvedTextFlags curvedTextFlags;
1891 if ( onlyShowUprightLabels() && ( !singleCandidateOnly || !( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ) )
1893
1894 std::unique_ptr< LabelPosition > labelPosition
1895 = curvedPlacementAtOffset( currentMapShape, pathDistances, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeft, !singleCandidateOnly, curvedTextFlags, additionalCharacterSpacing, additionalWordSpacing );
1896 if ( !labelPosition )
1897 {
1898 continue;
1899 }
1900
1901
1902 bool isBackupPlacementOnly = false;
1904 {
1905 if ( ( currentMapShape == mapShapeOffsetPositive.get() && positiveShapeHasNegativeDistance ) || ( currentMapShape == mapShapeOffsetNegative.get() && negativeShapeHasNegativeDistance ) )
1906 {
1907 labeledLineSegmentIsRightToLeft = !labeledLineSegmentIsRightToLeft;
1908 }
1909
1910 if ( ( offset != NoOffset ) && !labeledLineSegmentIsRightToLeft && !( flags & Qgis::LabelLinePlacementFlag::AboveLine ) )
1911 {
1912 if ( singleCandidateOnly && offset == PositiveOffset )
1913 isBackupPlacementOnly = true;
1914 else
1915 continue;
1916 }
1917 if ( ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft && !( flags & Qgis::LabelLinePlacementFlag::BelowLine ) )
1918 {
1919 if ( singleCandidateOnly && offset == PositiveOffset )
1920 isBackupPlacementOnly = true;
1921 else
1922 continue;
1923 }
1924 }
1925
1926 backupPlacement.reset();
1927
1928 // evaluate cost
1929 const double angleDiff = labelPosition->angleDifferential();
1930 const double angleDiffAvg = characterCount > 1 ? ( angleDiff / ( characterCount - 1 ) ) : 0; // <0, pi> but pi/8 is much already
1931
1932 // if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the
1933 // anchor weighting is sufficient to push labels towards start/end
1934 const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1935 double cost = angleDiffAvg / 100; // <0, 0.031 > but usually <0, 0.003 >
1936 if ( cost < 0.0001 )
1937 cost = 0.0001;
1938
1939 // for stretch-to-fit modes we ignore anchor distance cost as we always fit the whole line
1940 if ( !usingStretchToFitMode )
1941 {
1942 // penalize positions which are further from the line's anchor point
1943 double labelTextAnchor = 0;
1944 switch ( textPoint )
1945 {
1947 labelTextAnchor = distanceAlongLineToStartCandidate;
1948 break;
1950 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
1951 break;
1953 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth();
1954 break;
1956 // not possible here
1957 break;
1958 }
1959 double costCenter = std::fabs( lineAnchorPoint - labelTextAnchor ) / totalDistance; // <0, 0.5>
1960 cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
1961 }
1962
1963 const bool isBelow = ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft;
1964 if ( isBelow )
1965 {
1966 // add additional cost for on line placement
1967 cost += 0.001;
1968 }
1969 else if ( offset == NoOffset )
1970 {
1971 // add additional cost for below line placement
1972 cost += 0.002;
1973 }
1974
1975 labelPosition->setCost( cost );
1976
1977 auto p = std::make_unique< LabelPosition >( *labelPosition );
1978 if ( p && mLF->permissibleZonePrepared() )
1979 {
1980 bool within = true;
1981 LabelPosition *currentPos = p.get();
1982 while ( within && currentPos )
1983 {
1984 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
1985 currentPos = currentPos->nextPart();
1986 }
1987 if ( !within )
1988 {
1989 p.reset();
1990 }
1991 }
1992
1993 if ( p )
1994 {
1995 if ( isBackupPlacementOnly )
1996 backupPlacement = std::move( p );
1997 else
1998 positions.emplace_back( std::move( p ) );
1999 }
2000 }
2001 }
2002
2003 for ( std::unique_ptr< LabelPosition > &pos : positions )
2004 {
2005 lPos.emplace_back( std::move( pos ) );
2006 }
2007
2008 if ( backupPlacement )
2009 lPos.emplace_back( std::move( backupPlacement ) );
2010
2011 return positions.size();
2012}
2013
2014std::size_t FeaturePart::createCurvedCandidateWithCharactersAtVertices( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal )
2015{
2016 const QgsPrecalculatedTextMetrics *metrics = qgis::down_cast< QgsTextLabelFeature * >( mLF )->textMetrics();
2017
2018 const int characterCount = metrics->count();
2019 const int vertexCount = mapShape->getNumPoints();
2020 if ( characterCount == 0 || vertexCount == 0 )
2021 return 0;
2022
2023 const double distLabel = mLF->distLabel();
2024
2025 std::unique_ptr< LabelPosition > firstPosition;
2026 LabelPosition *previousPosition = nullptr;
2027
2028 int vertexIndex = 0;
2029 int characterIndex = -1;
2030 for ( ; vertexIndex < vertexCount; ++vertexIndex )
2031 {
2032 if ( pal->isCanceled() )
2033 return 0;
2034
2035 bool isWhiteSpace = true;
2036 while ( isWhiteSpace )
2037 {
2038 characterIndex++;
2039 if ( characterIndex >= characterCount )
2040 break;
2041
2042 isWhiteSpace = metrics->grapheme( characterIndex ).trimmed().isEmpty() || metrics->grapheme( characterIndex ) == '\t';
2043 }
2044
2045 if ( characterIndex >= characterCount )
2046 break;
2047
2048 double x = mapShape->x[vertexIndex];
2049 double y = mapShape->y[vertexIndex];
2050
2051 // use the angle of the segment starting at the current vertex
2052 // if it is the last vertex then reuse the angle of the preceding segment
2053 double angle = 0.0;
2054 if ( vertexIndex < vertexCount - 1 )
2055 {
2056 angle = std::atan2( mapShape->y[vertexIndex + 1] - y, mapShape->x[vertexIndex + 1] - x );
2057 }
2058 else if ( vertexIndex > 0 )
2059 {
2060 angle = std::atan2( y - mapShape->y[vertexIndex - 1], x - mapShape->x[vertexIndex - 1] );
2061 }
2062 if ( !qgsDoubleNear( distLabel, 0.0 ) )
2063 {
2064 x -= std::sin( angle ) * distLabel;
2065 y += std::cos( angle ) * distLabel;
2066 }
2067
2068 const double width = metrics->characterWidth( characterIndex );
2069 const double height = metrics->characterHeight( characterIndex );
2070 auto currentPosition = std::make_unique< LabelPosition >( 0, x, y, width, height, angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
2071 currentPosition->setPartId( characterIndex );
2072
2073 if ( !firstPosition )
2074 {
2075 firstPosition = std::move( currentPosition );
2076 previousPosition = firstPosition.get();
2077 }
2078 else
2079 {
2080 LabelPosition *rawCurrent = currentPosition.get();
2081 previousPosition->setNextPart( std::move( currentPosition ) );
2082 previousPosition = rawCurrent;
2083 }
2084 }
2085
2086 if ( !firstPosition )
2087 return 0;
2088
2089 if ( mLF->permissibleZonePrepared() )
2090 {
2091 bool within = true;
2092 LabelPosition *currentPos = firstPosition.get();
2093 while ( within && currentPos )
2094 {
2095 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
2096 currentPos = currentPos->nextPart();
2097 }
2098 if ( !within )
2099 {
2100 return 0;
2101 }
2102 }
2103
2104 lPos.emplace_back( std::move( firstPosition ) );
2105 return 1;
2106}
2107
2108/*
2109 * seg 2
2110 * pt3 ____________pt2
2111 * ¦ ¦
2112 * ¦ ¦
2113 * seg 3 ¦ BBOX ¦ seg 1
2114 * ¦ ¦
2115 * ¦____________¦
2116 * pt0 seg 0 pt1
2117 *
2118 */
2119
2120std::size_t FeaturePart::createCandidatesForPolygon( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
2121{
2122 double labelWidth = getLabelWidth();
2123 double labelHeight = getLabelHeight();
2124
2125 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
2126 const std::size_t targetPolygonCandidates = maxPolygonCandidates > 0
2127 ? std::min( maxPolygonCandidates, static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * area() ) ) )
2128 : 0;
2129
2130 const double totalArea = area();
2131
2132 mapShape->parent = nullptr;
2133
2134 if ( pal->isCanceled() )
2135 return 0;
2136
2137 QVector<PointSet *> shapes_final = splitPolygons( mapShape, labelWidth, labelHeight );
2138#if 0
2139 QgsDebugMsgLevel( u"PAL split polygons resulted in:"_s, 2 );
2140 for ( PointSet *ps : shapes_final )
2141 {
2142 QgsDebugMsgLevel( ps->toWkt(), 2 );
2143 }
2144#endif
2145
2146 std::size_t nbp = 0;
2147
2148 if ( !shapes_final.isEmpty() )
2149 {
2150 int id = 0; // ids for candidates
2151 double dlx, dly; // delta from label center and bottom-left corner
2152 double alpha = 0.0; // rotation for the label
2153 double px, py;
2154
2155 double beta;
2156 double diago = std::sqrt( labelWidth * labelWidth / 4.0 + labelHeight * labelHeight / 4 );
2157 double rx, ry;
2158 std::vector< OrientedConvexHullBoundingBox > boxes;
2159 boxes.reserve( shapes_final.size() );
2160
2161 // Compute bounding box for each finalShape
2162 while ( !shapes_final.isEmpty() )
2163 {
2164 PointSet *shape = shapes_final.takeFirst();
2165 bool ok = false;
2167 if ( ok )
2168 boxes.emplace_back( box );
2169
2170 if ( shape->parent )
2171 delete shape;
2172 }
2173
2174 if ( pal->isCanceled() )
2175 return 0;
2176
2177 double densityX = 1.0 / std::sqrt( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() );
2178 double densityY = densityX;
2179 int numTry = 0;
2180
2181 //fit in polygon only mode slows down calculation a lot, so if it's enabled
2182 //then use a smaller limit for number of iterations
2183 int maxTry = mLF->permissibleZonePrepared() ? 7 : 10;
2184
2185 std::size_t numberCandidatesGenerated = 0;
2186
2187 do
2188 {
2189 for ( OrientedConvexHullBoundingBox &box : boxes )
2190 {
2191 // there is two possibilities here:
2192 // 1. no maximum candidates for polygon setting is in effect (i.e. maxPolygonCandidates == 0). In that case,
2193 // we base our dx/dy on the current maximumPolygonCandidatesPerMapUnitSquared value. That should give us the desired
2194 // density of candidates straight up. Easy!
2195 // 2. a maximum candidate setting IS in effect. In that case, we want to generate a good initial estimate for dx/dy
2196 // which gives us a good spatial coverage of the polygon while roughly matching the desired maximum number of candidates.
2197 // If dx/dy is too small, then too many candidates will be generated, which is both slow AND results in poor coverage of the
2198 // polygon (after culling candidates to the max number, only those clustered around the polygon's pole of inaccessibility
2199 // will remain).
2200 double dx = densityX;
2201 double dy = densityY;
2202 if ( numTry == 0 && maxPolygonCandidates > 0 )
2203 {
2204 // scale maxPolygonCandidates for just this convex hull
2205 const double boxArea = box.width * box.length;
2206 double maxThisBox = targetPolygonCandidates * boxArea / totalArea;
2207 dx = std::max( dx, std::sqrt( boxArea / maxThisBox ) * 0.8 );
2208 dy = dx;
2209 }
2210
2211 if ( pal->isCanceled() )
2212 return numberCandidatesGenerated;
2213
2214 if ( ( box.length * box.width ) > ( xmax - xmin ) * ( ymax - ymin ) * 5 )
2215 {
2216 // Very Large BBOX (should never occur)
2217 continue;
2218 }
2219
2220 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal && mLF->permissibleZonePrepared() )
2221 {
2222 //check width/height of bbox is sufficient for label
2223 if ( mLF->permissibleZone().boundingBox().width() < labelWidth || mLF->permissibleZone().boundingBox().height() < labelHeight )
2224 {
2225 //no way label can fit in this box, skip it
2226 continue;
2227 }
2228 }
2229
2230 bool enoughPlace = false;
2231 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Free )
2232 {
2233 enoughPlace = true;
2234 px = ( box.x[0] + box.x[2] ) / 2 - labelWidth;
2235 py = ( box.y[0] + box.y[2] ) / 2 - labelHeight;
2236 int i, j;
2237
2238 // Virtual label: center on bbox center, label size = 2x original size
2239 // alpha = 0.
2240 // If all corner are in bbox then place candidates horizontaly
2241 for ( rx = px, i = 0; i < 2; rx = rx + 2 * labelWidth, i++ )
2242 {
2243 for ( ry = py, j = 0; j < 2; ry = ry + 2 * labelHeight, j++ )
2244 {
2245 if ( !mapShape->containsPoint( rx, ry ) )
2246 {
2247 enoughPlace = false;
2248 break;
2249 }
2250 }
2251 if ( !enoughPlace )
2252 {
2253 break;
2254 }
2255 }
2256
2257 } // arrangement== FREE ?
2258
2259 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal || enoughPlace )
2260 {
2261 alpha = 0.0; // HORIZ
2262 }
2263 else if ( box.length > 1.5 * labelWidth && box.width > 1.5 * labelWidth )
2264 {
2265 if ( box.alpha <= M_PI_4 )
2266 {
2267 alpha = box.alpha;
2268 }
2269 else
2270 {
2271 alpha = box.alpha - M_PI_2;
2272 }
2273 }
2274 else if ( box.length > box.width )
2275 {
2276 alpha = box.alpha - M_PI_2;
2277 }
2278 else
2279 {
2280 alpha = box.alpha;
2281 }
2282
2283 beta = std::atan2( labelHeight, labelWidth ) + alpha;
2284
2285
2286 //alpha = box->alpha;
2287
2288 // delta from label center and down-left corner
2289 dlx = std::cos( beta ) * diago;
2290 dly = std::sin( beta ) * diago;
2291
2292 double px0 = box.width / 2.0;
2293 double py0 = box.length / 2.0;
2294
2295 px0 -= std::ceil( px0 / dx ) * dx;
2296 py0 -= std::ceil( py0 / dy ) * dy;
2297
2298 for ( px = px0; px <= box.width; px += dx )
2299 {
2300 if ( pal->isCanceled() )
2301 break;
2302
2303 for ( py = py0; py <= box.length; py += dy )
2304 {
2305 rx = std::cos( box.alpha ) * px + std::cos( box.alpha - M_PI_2 ) * py;
2306 ry = std::sin( box.alpha ) * px + std::sin( box.alpha - M_PI_2 ) * py;
2307
2308 rx += box.x[0];
2309 ry += box.y[0];
2310
2311 if ( mLF->permissibleZonePrepared() )
2312 {
2313 if ( GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), rx - dlx, ry - dly, labelWidth, labelHeight, alpha ) )
2314 {
2315 // cost is set to minimal value, evaluated later
2316 lPos.emplace_back(
2317 std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
2318 );
2319 numberCandidatesGenerated++;
2320 }
2321 }
2322 else
2323 {
2324 // TODO - this should be an intersection test, not just a contains test of the candidate centroid
2325 // because in some cases we would want to allow candidates which mostly overlap the polygon even though
2326 // their centroid doesn't overlap (e.g. a "U" shaped polygon)
2327 // but the bugs noted in CostCalculator currently prevent this
2328 if ( mapShape->containsPoint( rx, ry ) )
2329 {
2330 auto potentialCandidate = std::make_unique<
2331 LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
2332 // cost is set to minimal value, evaluated later
2333 lPos.emplace_back( std::move( potentialCandidate ) );
2334 numberCandidatesGenerated++;
2335 }
2336 }
2337 }
2338 }
2339 } // forall box
2340
2341 nbp = numberCandidatesGenerated;
2342 if ( maxPolygonCandidates > 0 && nbp < targetPolygonCandidates )
2343 {
2344 densityX /= 2;
2345 densityY /= 2;
2346 numTry++;
2347 }
2348 else
2349 {
2350 break;
2351 }
2352 } while ( numTry < maxTry );
2353
2354 nbp = numberCandidatesGenerated;
2355 }
2356 else
2357 {
2358 nbp = 0;
2359 }
2360
2361 return nbp;
2362}
2363
2364std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal )
2365{
2366 // calculate distance between horizontal lines
2367 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
2368 std::size_t candidatesCreated = 0;
2369
2370 double labelWidth = getLabelWidth();
2371 double labelHeight = getLabelHeight();
2372 double distanceToLabel = getLabelDistance();
2373 const QgsMargins &visualMargin = mLF->visualMargin();
2374
2375 /*
2376 * From Rylov & Reimer (2016) "A practical algorithm for the external annotation of area features":
2377 *
2378 * The list of rules adapted to the
2379 * needs of externally labelling areal features is as follows:
2380 * R1. Labels should be placed horizontally.
2381 * R2. Label should be placed entirely outside at some
2382 * distance from the area feature.
2383 * R3. Name should not cross the boundary of its area
2384 * feature.
2385 * R4. The name should be placed in way that takes into
2386 * account the shape of the feature by achieving a
2387 * balance between the feature and its name, emphasizing their relationship.
2388 * R5. The lettering to the right and slightly above the
2389 * symbol is prioritized.
2390 *
2391 * In the following subsections we utilize four of the five rules
2392 * for two subtasks of label placement, namely, for candidate
2393 * positions generation (R1, R2, and R3) and for measuring their
2394 * ‘goodness’ (R4). The rule R5 is applicable only in the case when
2395 * the area of a polygonal feature is small and the feature can be
2396 * treated and labelled as a point-feature
2397 */
2398
2399 /*
2400 * QGIS approach (cite Dawson (2020) if you want ;) )
2401 *
2402 * We differ from the horizontal sweep line approach described by Rylov & Reimer and instead
2403 * rely on just generating a set of points at regular intervals along the boundary of the polygon (exterior ring).
2404 *
2405 * In practice, this generates similar results as Rylov & Reimer, but has the additional benefits that:
2406 * 1. It avoids the need to calculate intersections between the sweep line and the polygon
2407 * 2. For horizontal or near horizontal segments, Rylov & Reimer propose generating evenly spaced points along
2408 * these segments-- i.e. the same approach as we do for the whole polygon
2409 * 3. It's easier to determine in advance exactly how many candidate positions we'll be generating, and accordingly
2410 * we can easily pick the distance between points along the exterior ring so that the number of positions generated
2411 * matches our target number (targetPolygonCandidates)
2412 */
2413
2414 // TO consider -- for very small polygons (wrt label size), treat them just like a point feature?
2415
2416 double cx, cy;
2417 getCentroid( cx, cy, false );
2418
2419 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2420
2421 // be a bit sneaky and only buffer out 50% here, and then do the remaining 50% when we make the label candidate itself.
2422 // this avoids candidates being created immediately over the buffered ring and always intersecting with it...
2423 geos::unique_ptr buffer( GEOSBuffer_r( ctxt, geos(), distanceToLabel * 0.5, 1 ) );
2424 std::unique_ptr< QgsAbstractGeometry> gg( QgsGeos::fromGeos( buffer.get() ) );
2425
2426 geos::prepared_unique_ptr preparedBuffer( GEOSPrepare_r( ctxt, buffer.get() ) );
2427
2428 const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( gg.get() );
2429 if ( !poly )
2430 return candidatesCreated;
2431
2433 if ( !ring )
2434 return candidatesCreated;
2435
2436 // we cheat here -- we don't use the polygon area when calculating the number of candidates, and rather use the perimeter (because that's more relevant,
2437 // i.e a loooooong skinny polygon with small area should still generate a large number of candidates)
2438 const double ringLength = ring->length();
2439 const double circleArea = std::pow( ringLength, 2 ) / ( 4 * M_PI );
2440 const std::size_t candidatesForArea = static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * circleArea ) );
2441 const std::size_t targetPolygonCandidates = std::max( static_cast< std::size_t >( 16 ), maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, candidatesForArea ) : candidatesForArea );
2442
2443 // assume each position generates one candidate
2444 const double delta = ringLength / targetPolygonCandidates;
2445 geos::unique_ptr geosPoint;
2446
2447 const double maxDistCentroidToLabelX = std::max( xmax - cx, cx - xmin ) + distanceToLabel;
2448 const double maxDistCentroidToLabelY = std::max( ymax - cy, cy - ymin ) + distanceToLabel;
2449 const double estimateOfMaxPossibleDistanceCentroidToLabel = std::sqrt( maxDistCentroidToLabelX * maxDistCentroidToLabelX + maxDistCentroidToLabelY * maxDistCentroidToLabelY );
2450
2451 // Satisfy R1: Labels should be placed horizontally.
2452 const double labelAngle = 0;
2453
2454 std::size_t i = lPos.size();
2455 auto addCandidate = [&]( double x, double y, Qgis::LabelPredefinedPointPosition position ) {
2456 double labelX = 0;
2457 double labelY = 0;
2459
2460 // Satisfy R2: Label should be placed entirely outside at some distance from the area feature.
2461 createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel * 0.5, visualMargin, 0, 0, labelAngle );
2462
2463 auto candidate = std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, labelAngle, 0, this, LabelPosition::LabelDirectionToLine::SameDirection, quadrant );
2464 if ( candidate->intersects( preparedBuffer.get() ) )
2465 {
2466 // satisfy R3. Name should not cross the boundary of its area feature.
2467
2468 // actually, we use the buffered geometry here, because a label shouldn't be closer to the polygon then the minimum distance value
2469 return;
2470 }
2471
2472 // cost candidates by their distance to the feature's centroid (following Rylov & Reimer)
2473
2474 // Satisfy R4. The name should be placed in way that takes into
2475 // account the shape of the feature by achieving a
2476 // balance between the feature and its name, emphasizing their relationship.
2477
2478
2479 // here we deviate a little from R&R, and instead of just calculating the centroid distance
2480 // to centroid of label, we calculate the distance from the centroid to the nearest point on the label
2481
2482 const double centroidDistance = candidate->getDistanceToPoint( cx, cy, false );
2483 const double centroidCost = centroidDistance / estimateOfMaxPossibleDistanceCentroidToLabel;
2484 candidate->setCost( centroidCost );
2485
2486 lPos.emplace_back( std::move( candidate ) );
2487 candidatesCreated++;
2488 ++i;
2489 };
2490
2491 ring->visitPointsByRegularDistance( delta, [&]( double x, double y, double, double, double startSegmentX, double startSegmentY, double, double, double endSegmentX, double endSegmentY, double, double ) {
2492 // get normal angle for segment
2493 float angle = atan2( static_cast< float >( endSegmentY - startSegmentY ), static_cast< float >( endSegmentX - startSegmentX ) ) * 180 / M_PI;
2494 if ( angle < 0 )
2495 angle += 360;
2496
2497 // adapted fom Rylov & Reimer figure 9
2498 if ( angle >= 0 && angle <= 5 )
2499 {
2502 }
2503 else if ( angle <= 85 )
2504 {
2506 }
2507 else if ( angle <= 90 )
2508 {
2511 }
2512
2513 else if ( angle <= 95 )
2514 {
2517 }
2518 else if ( angle <= 175 )
2519 {
2521 }
2522 else if ( angle <= 180 )
2523 {
2526 }
2527
2528 else if ( angle <= 185 )
2529 {
2532 }
2533 else if ( angle <= 265 )
2534 {
2536 }
2537 else if ( angle <= 270 )
2538 {
2541 }
2542 else if ( angle <= 275 )
2543 {
2546 }
2547 else if ( angle <= 355 )
2548 {
2550 }
2551 else
2552 {
2555 }
2556
2557 return !pal->isCanceled();
2558 } );
2559
2560 return candidatesCreated;
2561}
2562
2563std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( Pal *pal )
2564{
2565 std::vector< std::unique_ptr< LabelPosition > > lPos;
2566 double angleInRadians = mLF->hasFixedAngle() ? mLF->fixedAngle() : 0.0;
2567
2568 if ( mLF->hasFixedPosition() )
2569 {
2570 lPos.emplace_back(
2571 std::make_unique<
2572 LabelPosition>( 0, mLF->fixedPosition().x(), mLF->fixedPosition().y(), getLabelWidth( angleInRadians ), getLabelHeight( angleInRadians ), angleInRadians, 0.0, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
2573 );
2574 }
2575 else
2576 {
2577 switch ( type )
2578 {
2579 case GEOS_POINT:
2580 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OrderedPositionsAroundPoint )
2581 createCandidatesAtOrderedPositionsOverPoint( x[0], y[0], lPos, angleInRadians );
2582 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OverPoint || mLF->hasFixedQuadrant() )
2583 createCandidatesOverPoint( x[0], y[0], lPos, angleInRadians );
2584 else
2585 createCandidatesAroundPoint( x[0], y[0], lPos, angleInRadians );
2586 break;
2587
2588 case GEOS_LINESTRING:
2589 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
2591 else if ( mLF->layer()->isCurved() )
2592 createCurvedCandidatesAlongLine( lPos, this, true, pal );
2593 else
2594 createCandidatesAlongLine( lPos, this, true, pal );
2595 break;
2596
2597 case GEOS_POLYGON:
2598 {
2599 const double labelWidth = getLabelWidth();
2600 const double labelHeight = getLabelHeight();
2601
2602 const bool allowOutside = mLF->polygonPlacementFlags() & Qgis::LabelPolygonPlacementFlag::AllowPlacementOutsideOfPolygon;
2603 const bool allowInside = mLF->polygonPlacementFlags() & Qgis::LabelPolygonPlacementFlag::AllowPlacementInsideOfPolygon;
2604 //check width/height of bbox is sufficient for label
2605
2606 if ( ( allowOutside && !allowInside ) || ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OutsidePolygons ) )
2607 {
2608 // only allowed to place outside of polygon
2610 }
2611 else if ( allowOutside && ( std::fabs( xmax - xmin ) < labelWidth || std::fabs( ymax - ymin ) < labelHeight ) )
2612 {
2613 //no way label can fit in this polygon -- shortcut and only place label outside
2615 }
2616 else
2617 {
2618 std::size_t created = 0;
2619 if ( allowInside )
2620 {
2621 switch ( mLF->layer()->arrangement() )
2622 {
2624 {
2625 double cx, cy;
2626 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2627 if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
2628 created += createCandidateCenteredOverPoint( cx, cy, lPos, angleInRadians );
2629 created += createCandidatesAroundPoint( cx, cy, lPos, angleInRadians );
2630 break;
2631 }
2633 {
2634 double cx, cy;
2635 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2636 created += createCandidatesOverPoint( cx, cy, lPos, angleInRadians );
2637 break;
2638 }
2640 created += createCandidatesAlongLine( lPos, this, false, pal );
2641 break;
2643 created += createCurvedCandidatesAlongLine( lPos, this, false, pal );
2644 break;
2645 default:
2646 created += createCandidatesForPolygon( lPos, this, pal );
2647 break;
2648 }
2649 }
2650
2651 if ( allowOutside )
2652 {
2653 // add fallback for labels outside the polygon
2655
2656 if ( created > 0 )
2657 {
2658 // TODO (maybe) increase cost for outside placements (i.e. positions at indices >= created)?
2659 // From my initial testing this doesn't seem necessary
2660 }
2661 }
2662 }
2663 }
2664 }
2665 }
2666
2667 return lPos;
2668}
2669
2670void FeaturePart::addSizePenalty( std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4] ) const
2671{
2672 if ( !mGeos )
2674
2675 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2676 int geomType = GEOSGeomTypeId_r( ctxt, mGeos );
2677
2678 double sizeCost = 0;
2679 if ( geomType == GEOS_LINESTRING )
2680 {
2681 const double l = length();
2682 if ( l <= 0 )
2683 return; // failed to calculate length
2684 double bbox_length = std::max( bbx[2] - bbx[0], bby[2] - bby[0] );
2685 if ( l >= bbox_length / 4 )
2686 return; // the line is longer than quarter of height or width - don't penalize it
2687
2688 sizeCost = 1 - ( l / ( bbox_length / 4 ) ); // < 0,1 >
2689 }
2690 else if ( geomType == GEOS_POLYGON )
2691 {
2692 const double a = area();
2693 if ( a <= 0 )
2694 return;
2695 double bbox_area = ( bbx[2] - bbx[0] ) * ( bby[2] - bby[0] );
2696 if ( a >= bbox_area / 16 )
2697 return; // covers more than 1/16 of our view - don't penalize it
2698
2699 sizeCost = 1 - ( a / ( bbox_area / 16 ) ); // < 0, 1 >
2700 }
2701 else
2702 return; // no size penalty for points
2703
2704 // apply the penalty
2705 for ( std::unique_ptr< LabelPosition > &pos : lPos )
2706 {
2707 pos->setCost( pos->cost() + sizeCost / 100 );
2708 }
2709}
2710
2712{
2713 if ( !nbPoints || !p2->nbPoints )
2714 return false;
2715
2716 // here we only care if the lines start or end at the other line -- we don't want to test
2717 // touches as that is true for "T" type joins!
2718 const double x1first = x.front();
2719 const double x1last = x.back();
2720 const double x2first = p2->x.front();
2721 const double x2last = p2->x.back();
2722 const double y1first = y.front();
2723 const double y1last = y.back();
2724 const double y2first = p2->y.front();
2725 const double y2last = p2->y.back();
2726
2727 const bool p2startTouches = ( qgsDoubleNear( x1first, x2first ) && qgsDoubleNear( y1first, y2first ) ) || ( qgsDoubleNear( x1last, x2first ) && qgsDoubleNear( y1last, y2first ) );
2728
2729 const bool p2endTouches = ( qgsDoubleNear( x1first, x2last ) && qgsDoubleNear( y1first, y2last ) ) || ( qgsDoubleNear( x1last, x2last ) && qgsDoubleNear( y1last, y2last ) );
2730 // only one endpoint can touch, not both
2731 if ( ( !p2startTouches && !p2endTouches ) || ( p2startTouches && p2endTouches ) )
2732 return false;
2733
2734 // now we know that we have one line endpoint touching only, but there's still a chance
2735 // that the other side of p2 may touch the original line NOT at the other endpoint
2736 // so we need to check that this point doesn't intersect
2737 const double p2otherX = p2startTouches ? x2last : x2first;
2738 const double p2otherY = p2startTouches ? y2last : y2first;
2739
2740 GEOSContextHandle_t geosctxt = QgsGeosContext::get();
2741
2742 try
2743 {
2744#if GEOS_VERSION_MAJOR > 3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12 )
2745 return ( GEOSPreparedIntersectsXY_r( geosctxt, preparedGeom(), p2otherX, p2otherY ) != 1 );
2746#else
2747 GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 1, 2 );
2748 GEOSCoordSeq_setXY_r( geosctxt, coord, 0, p2otherX, p2otherY );
2749 geos::unique_ptr p2OtherEnd( GEOSGeom_createPoint_r( geosctxt, coord ) );
2750 return ( GEOSPreparedIntersects_r( geosctxt, preparedGeom(), p2OtherEnd.get() ) != 1 );
2751#endif
2752 }
2753 catch ( QgsGeosException &e )
2754 {
2755 qWarning( "GEOS exception: %s", e.what() );
2756 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2757 return false;
2758 }
2759}
2760
2762{
2763 if ( !mGeos )
2765 if ( !other->mGeos )
2766 other->createGeosGeom();
2767
2768 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2769 try
2770 {
2771 GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos );
2772 GEOSGeometry *g2 = GEOSGeom_clone_r( ctxt, other->mGeos );
2773 GEOSGeometry *geoms[2] = { g1, g2 };
2774 geos::unique_ptr g( GEOSGeom_createCollection_r( ctxt, GEOS_MULTILINESTRING, geoms, 2 ) );
2775 geos::unique_ptr gTmp( GEOSLineMerge_r( ctxt, g.get() ) );
2776
2777 if ( GEOSGeomTypeId_r( ctxt, gTmp.get() ) != GEOS_LINESTRING )
2778 {
2779 // sometimes it's not possible to merge lines (e.g. they don't touch at endpoints)
2780 return false;
2781 }
2783
2784 // set up new geometry
2785 mGeos = gTmp.release();
2786 mOwnsGeom = true;
2787
2788 deleteCoords();
2789 qDeleteAll( mHoles );
2790 mHoles.clear();
2792 return true;
2793 }
2794 catch ( QgsGeosException &e )
2795 {
2796 qWarning( "GEOS exception: %s", e.what() );
2797 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2798 return false;
2799 }
2800}
2801
2803{
2804 if ( mLF->alwaysShow() )
2805 {
2806 //if feature is set to always show, bump the priority up by orders of magnitude
2807 //so that other feature's labels are unlikely to be placed over the label for this feature
2808 //(negative numbers due to how pal::extract calculates inactive cost)
2809 return -0.2;
2810 }
2811
2812 return mLF->priority() >= 0 ? mLF->priority() : mLF->layer()->priority();
2813}
2814
2816{
2817 bool result = false;
2818
2819 switch ( mLF->layer()->upsidedownLabels() )
2820 {
2822 result = true;
2823 break;
2825 // upright only dynamic labels
2826 if ( !hasFixedRotation() || ( !hasFixedPosition() && fixedAngle() == 0.0 ) )
2827 {
2828 result = true;
2829 }
2830 break;
2832 break;
2833 }
2834 return result;
2835}
@ StretchCharacterSpacingToFitLine
Increases (or decreases) the character spacing used for each label in order to fit the entire text ov...
Definition qgis.h:1254
@ Default
Default curved placement, characters are placed in an optimal position along the line....
Definition qgis.h:1252
@ StretchWordSpacingToFitLine
Increases (or decreases) the word spacing used for each label in order to fit the entire text over th...
Definition qgis.h:1255
@ PlaceCharactersAtVertices
Each individual character from the label text is placed such that their left-baseline position is loc...
Definition qgis.h:1253
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1343
@ MapOrientation
Signifies that the AboveLine and BelowLine flags should respect the map's orientation rather than the...
Definition qgis.h:1344
@ OnLine
Labels can be placed directly over a line feature.
Definition qgis.h:1341
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1342
@ FromSymbolBounds
Offset distance applies from rendered symbol bounds.
Definition qgis.h:1308
LabelPrioritization
Label prioritization.
Definition qgis.h:1218
@ PreferCloser
Prefer closer labels, falling back to alternate positions before larger distances.
Definition qgis.h:1219
@ PreferPositionOrdering
Prefer labels follow position ordering, falling back to more distance labels before alternate positio...
Definition qgis.h:1220
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
Definition qgis.h:1234
@ AroundPoint
Arranges candidates in a circle around a point (or centroid of a polygon). Applies to point or polygo...
Definition qgis.h:1233
@ Line
Arranges candidates parallel to a generalised line representing the feature or parallel to a polygon'...
Definition qgis.h:1235
@ Free
Arranges candidates scattered throughout a polygon feature. Candidates are rotated to respect the pol...
Definition qgis.h:1238
@ OrderedPositionsAroundPoint
Candidates are placed in predefined positions around a point. Preference is given to positions with g...
Definition qgis.h:1239
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature. Applies to polygon layers only...
Definition qgis.h:1237
@ PerimeterCurved
Arranges candidates following the curvature of a polygon's boundary. Applies to polygon layers only.
Definition qgis.h:1240
@ OutsidePolygons
Candidates are placed outside of polygon boundaries. Applies to polygon layers only.
Definition qgis.h:1241
@ AllowPlacementInsideOfPolygon
Labels can be placed inside a polygon feature.
Definition qgis.h:1367
@ AllowPlacementOutsideOfPolygon
Labels can be placed outside of a polygon feature.
Definition qgis.h:1366
QFlags< LabelLinePlacementFlag > LabelLinePlacementFlags
Line placement flags, which control how candidates are generated for a linear feature.
Definition qgis.h:1355
LabelQuadrantPosition
Label quadrant positions.
Definition qgis.h:1320
@ AboveRight
Above right.
Definition qgis.h:1323
@ BelowLeft
Below left.
Definition qgis.h:1327
@ Above
Above center.
Definition qgis.h:1322
@ BelowRight
Below right.
Definition qgis.h:1329
@ Right
Right middle.
Definition qgis.h:1326
@ AboveLeft
Above left.
Definition qgis.h:1321
@ Below
Below center.
Definition qgis.h:1328
@ Over
Center middle.
Definition qgis.h:1325
@ TreatWhitespaceAsCollision
Treat overlapping whitespace text in labels and whitespace overlapping obstacles as collisions.
Definition qgis.h:1207
@ IgnoreWhitespaceCollisions
Ignore overlapping whitespace text in labels and whitespace overlapping obstacles.
Definition qgis.h:1208
@ UprightCharactersOnly
Permit upright characters only. If not present then upside down text placement is permitted.
Definition qgis.h:3094
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
Definition qgis.h:3104
LabelPredefinedPointPosition
Positions for labels when using the Qgis::LabelPlacement::OrderedPositionsAroundPoint placement mode.
Definition qgis.h:1267
@ OverPoint
Label directly centered over point.
Definition qgis.h:1280
@ MiddleLeft
Label on left of point.
Definition qgis.h:1273
@ TopRight
Label on top-right of point.
Definition qgis.h:1272
@ MiddleRight
Label on right of point.
Definition qgis.h:1274
@ TopSlightlyRight
Label on top of point, slightly right of center.
Definition qgis.h:1271
@ TopMiddle
Label directly above point.
Definition qgis.h:1270
@ BottomSlightlyLeft
Label below point, slightly left of center.
Definition qgis.h:1276
@ BottomRight
Label on bottom right of point.
Definition qgis.h:1279
@ BottomLeft
Label on bottom-left of point.
Definition qgis.h:1275
@ BottomSlightlyRight
Label below point, slightly right of center.
Definition qgis.h:1278
@ TopLeft
Label on top-left of point.
Definition qgis.h:1268
@ BottomMiddle
Label directly below point.
Definition qgis.h:1277
@ TopSlightlyLeft
Label on top of point, slightly left of center.
Definition qgis.h:1269
@ FlipUpsideDownLabels
Upside-down labels (90 <= angle < 270) are shown upright.
Definition qgis.h:1389
@ AlwaysAllowUpsideDown
Show upside down for all labels, including dynamic ones.
Definition qgis.h:1391
@ AllowUpsideDownWhenRotationIsDefined
Show upside down when rotation is layer- or data-defined.
Definition qgis.h:1390
const QgsCurve * exteriorRing() const
Returns the curve polygon's exterior ring.
static double distance2D(double x1, double y1, double x2, double y2)
Returns the 2D distance between (x1, y1) and (x2, y2).
static double normalizedAngle(double angle)
Ensures that an angle is in the range 0 <= angle < 2 pi.
A geometry is the spatial representation of a feature.
QgsRectangle boundingBox() const
Returns the bounding box of the geometry.
static GEOSContextHandle_t get()
Returns a thread local instance of a GEOS context, safe for use in the current thread.
static std::unique_ptr< QgsAbstractGeometry > fromGeos(const GEOSGeometry *geos)
Create a geometry from a GEOSGeometry.
Definition qgsgeos.cpp:1562
Describes a feature that should be used within the labeling engine.
QPointF quadOffset() const
Applies to "offset from point" placement strategy and "around point" (in case hasFixedQuadrant() retu...
void setAnchorPosition(const QgsPointXY &anchorPosition)
In case of quadrand or aligned positioning, this is set to the anchor point.
@ Strict
Line anchor is a strict placement, and other placements are not permitted.
@ HintOnly
Line anchor is a hint for preferred placement only, but other placements close to the hint are permit...
AnchorTextPoint
Anchor point of label text.
@ StartOfText
Anchor using start of text.
@ CenterOfText
Anchor using center of text.
@ FollowPlacement
Automatically set the anchor point based on the lineAnchorPercent() value. Values <25% will use the s...
Line string geometry type, with support for z-dimension and m-values.
double length() const override
Returns the planar, 2-dimensional length of the geometry.
void visitPointsByRegularDistance(double distance, const std::function< bool(double x, double y, double z, double m, double startSegmentX, double startSegmentY, double startSegmentZ, double startSegmentM, double endSegmentX, double endSegmentY, double endSegmentZ, double endSegmentM) > &visitPoint) const
Visits regular points along the linestring, spaced by distance.
Defines the four margins of a rectangle.
Definition qgsmargins.h:40
double top() const
Returns the top margin.
Definition qgsmargins.h:76
double right() const
Returns the right margin.
Definition qgsmargins.h:82
double bottom() const
Returns the bottom margin.
Definition qgsmargins.h:88
double left() const
Returns the left margin.
Definition qgsmargins.h:70
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true, const char *file=__builtin_FILE(), const char *function=__builtin_FUNCTION(), int line=__builtin_LINE(), Qgis::StringFormat format=Qgis::StringFormat::PlainText)
Adds a message to the log instance (and creates it if necessary).
Represents a 2D point.
Definition qgspointxy.h:62
Polygon geometry type.
Definition qgspolygon.h:37
Contains precalculated properties regarding text metrics for text to be rendered at a later stage.
int count() const
Returns the total number of characters.
double characterWidth(int position) const
Returns the width of the character at the specified position.
QString grapheme(int index) const
Returns the grapheme at the specified index.
double characterHeight(int position) const
Returns the character height of the character at the specified position (actually font metrics height...
Utility functions for text rendering.
LabelLineDirection
Controls behavior of curved text with respect to line directions.
@ FollowLineDirection
Curved text placement will respect the line direction and ignore painter orientation.
@ RespectPainterOrientation
Curved text will be placed respecting the painter orientation, and the actual line direction will be ...
FeaturePart(QgsLabelFeature *lf, const GEOSGeometry *geom)
Creates a new generic feature.
Definition feature.cpp:55
std::size_t createCandidatesAroundPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generate candidates for point feature, located around a specified point.
Definition feature.cpp:679
std::size_t createCandidatesOutsidePolygon(std::vector< std::unique_ptr< LabelPosition > > &lPos, Pal *pal)
Generate candidates outside of polygon features.
Definition feature.cpp:2364
bool hasFixedRotation() const
Returns true if the feature's label has a fixed rotation.
Definition feature.h:310
std::unique_ptr< LabelPosition > curvedPlacementAtOffset(PointSet *mapShape, const std::vector< double > &pathDistances, QgsTextRendererUtils::LabelLineDirection direction, double distance, bool &labeledLineSegmentIsRightToLeft, bool applyAngleConstraints, Qgis::CurvedTextFlags flags, double additionalCharacterSpacing, double additionalWordSpacing)
Returns the label position for a curved label at a specific offset along a path.
Definition feature.cpp:1544
double getLabelHeight(double angle=0.0) const
Returns the height of the label, optionally taking an angle (in radians) into account.
Definition feature.h:301
QList< FeaturePart * > mHoles
Definition feature.h:376
double getLabelDistance() const
Returns the distance from the anchor point to the label.
Definition feature.h:307
~FeaturePart() override
Deletes the feature.
Definition feature.cpp:85
bool hasFixedPosition() const
Returns true if the feature's label has a fixed position.
Definition feature.h:316
std::size_t createCurvedCandidateWithCharactersAtVertices(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generates a curved candidates for line features, placing individual characters on the line vertices.
Definition feature.cpp:2014
std::size_t createCandidatesForPolygon(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate candidates for polygon features.
Definition feature.cpp:2120
void setTotalRepeats(int repeats)
Returns the total number of repeating labels associated with this label.
Definition feature.cpp:300
std::size_t maximumPolygonCandidates() const
Returns the maximum number of polygon candidates to generate for this feature.
Definition feature.cpp:205
std::size_t createDefaultCurvedCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal)
Generate curved candidates for line features, using default placement.
Definition feature.cpp:1636
QgsFeatureId featureId() const
Returns the unique ID of the feature.
Definition feature.cpp:168
std::size_t createCandidatesAlongLineNearStraightSegments(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate candidates for line feature, by trying to place candidates towards the middle of the longest...
Definition feature.cpp:992
bool hasSameLabelFeatureAs(FeaturePart *part) const
Tests whether this feature part belongs to the same QgsLabelFeature as another feature part.
Definition feature.cpp:227
double fixedAngle() const
Returns the fixed angle for the feature's label.
Definition feature.h:313
std::size_t maximumLineCandidates() const
Returns the maximum number of line candidates to generate for this feature.
Definition feature.cpp:183
std::size_t createHorizontalCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate horizontal candidates for line feature.
Definition feature.cpp:900
int subPartId() const
Returns the unique sub part ID for the feature, for features which register multiple labels.
Definition feature.cpp:173
std::size_t createCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal)
Generate candidates for line feature.
Definition feature.cpp:871
bool mergeWithFeaturePart(FeaturePart *other)
Merge other (connected) part with this one and save the result in this part (other is unchanged).
Definition feature.cpp:2761
std::size_t createCurvedCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal)
Generate curved candidates for line features.
Definition feature.cpp:1611
bool onlyShowUprightLabels() const
Returns true if feature's label must be displayed upright.
Definition feature.cpp:2815
std::size_t createCandidatesOverPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generate one candidate over or offset the specified point.
Definition feature.cpp:334
std::unique_ptr< LabelPosition > createCandidatePointOnSurface(PointSet *mapShape)
Creates a single candidate using the "point on sruface" algorithm.
Definition feature.cpp:413
QgsLabelFeature * mLF
Definition feature.h:375
double getLabelWidth(double angle=0.0) const
Returns the width of the label, optionally taking an angle (in radians) into account.
Definition feature.h:296
QgsLabelFeature * feature()
Returns the parent feature.
Definition feature.h:87
std::vector< std::unique_ptr< LabelPosition > > createCandidates(Pal *pal)
Generates a list of candidate positions for labels for this feature.
Definition feature.cpp:2563
bool isConnected(FeaturePart *p2)
Check whether this part is connected with some other part.
Definition feature.cpp:2711
Layer * layer()
Returns the layer that feature belongs to.
Definition feature.cpp:163
PathOffset
Path offset variances used in curved placement.
Definition feature.h:64
int totalRepeats() const
Returns the total number of repeating labels associated with this label.
Definition feature.cpp:295
std::size_t createCandidatesAlongLineNearMidpoint(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, double initialCost=0.0, Pal *pal=nullptr)
Generate candidates for line feature, by trying to place candidates as close as possible to the line'...
Definition feature.cpp:1291
void addSizePenalty(std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4]) const
Increases the cost of the label candidates for this feature, based on the size of the feature.
Definition feature.cpp:2670
void extractCoords(const GEOSGeometry *geom)
read coordinates from a GEOS geom
Definition feature.cpp:93
double calculatePriority() const
Calculates the priority for the feature.
Definition feature.cpp:2802
std::size_t createCandidatesAtOrderedPositionsOverPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generates candidates following a prioritized list of predefined positions around a point.
Definition feature.cpp:573
std::size_t createCandidateCenteredOverPoint(double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle)
Generate one candidate centered over the specified point.
Definition feature.cpp:305
std::size_t maximumPointCandidates() const
Returns the maximum number of point candidates to generate for this feature.
Definition feature.cpp:178
Pal labeling engine geometry functions.
static bool reorderPolygon(std::vector< double > &x, std::vector< double > &y)
Reorder points to have cross prod ((x,y)[i], (x,y)[i+1), point) > 0 when point is outside.
static bool containsCandidate(const GEOSPreparedGeometry *geom, double x, double y, double width, double height, double alpha)
Returns true if a GEOS prepared geometry totally contains a label candidate.
double getAlpha() const
Returns the angle to rotate text (in radians).
double getHeight() const
void setNextPart(std::unique_ptr< LabelPosition > next)
Sets the next part of this label position (i.e.
double getWidth() const
double getX(int i=0) const
Returns the down-left x coordinate.
double getY(int i=0) const
Returns the down-left y coordinate.
LabelPosition * nextPart() const
Returns the next part of this label position (i.e.
QString name() const
Returns the layer's name.
Definition layer.h:161
Main Pal labeling class.
Definition pal.h:87
geos::unique_ptr interpolatePoint(double distance) const
Returns a GEOS geometry representing the point interpolated on the shape by distance.
std::unique_ptr< PointSet > clone() const
Returns a copy of the point set.
Definition pointset.cpp:267
friend class LabelPosition
Definition pointset.h:78
double lineLocatePoint(const GEOSGeometry *point) const
Returns the distance along the geometry closest to the specified GEOS point.
double length() const
Returns length of line geometry.
void deleteCoords()
Definition pointset.cpp:234
double ymax
Definition pointset.h:257
double ymin
Definition pointset.h:256
double area() const
Returns area of polygon geometry.
bool isClosed() const
Returns true if pointset is closed.
PointSet * holeOf
Definition pointset.h:237
static QVector< PointSet * > splitPolygons(PointSet *inputShape, double labelWidth, double labelHeight)
Split a polygon using some random logic into some other polygons.
Definition pointset.cpp:295
void createGeosGeom() const
Definition pointset.cpp:101
void getPointByDistance(double *d, double *ad, double dl, double *px, double *py) const
Gets a point a set distance along a line geometry.
Definition pointset.cpp:979
std::vector< double > y
Definition pointset.h:227
void getCentroid(double &px, double &py, bool forceInside=false) const
Definition pointset.cpp:921
OrientedConvexHullBoundingBox computeConvexHullOrientedBoundingBox(bool &ok) const
Computes an oriented bounding box for the shape's convex hull.
Definition pointset.cpp:714
friend class Layer
Definition pointset.h:81
std::vector< double > x
Definition pointset.h:226
const GEOSPreparedGeometry * preparedGeom() const
Definition pointset.cpp:156
GEOSGeometry * mGeos
Definition pointset.h:230
double xmin
Definition pointset.h:254
const GEOSGeometry * geos() const
Returns the point set's GEOS geometry.
void invalidateGeos() const
Definition pointset.cpp:168
friend class FeaturePart
Definition pointset.h:77
double xmax
Definition pointset.h:255
bool containsPoint(double x, double y) const
Tests whether point set contains a specified point.
Definition pointset.cpp:272
std::tuple< std::vector< double >, double > edgeDistances() const
Returns a vector of edge distances as well as its total length.
PointSet * parent
Definition pointset.h:238
int getNumPoints() const
Definition pointset.h:173
void createCandidateAtOrderedPositionOverPoint(double &labelX, double &labelY, Qgis::LabelQuadrantPosition &quadrant, double x, double y, double labelWidth, double labelHeight, Qgis::LabelPredefinedPointPosition position, double distanceToLabel, const QgsMargins &visualMargin, double symbolWidthOffset, double symbolHeightOffset, double angle)
Definition feature.cpp:444
std::unique_ptr< GEOSGeometry, GeosDeleter > unique_ptr
Scoped GEOS pointer.
Definition qgsgeos.h:112
std::unique_ptr< const GEOSPreparedGeometry, GeosDeleter > prepared_unique_ptr
Scoped GEOS prepared geometry pointer.
Definition qgsgeos.h:117
#define BUILTIN_UNREACHABLE
Definition qgis.h:7540
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6975
T qgsgeometry_cast(QgsAbstractGeometry *geom)
qint64 QgsFeatureId
64 bit feature ids negative numbers are used for uncommitted/newly added features
#define QgsDebugMsgLevel(str, level)
Definition qgslogger.h:63
Represents the minimum area, oriented bounding box surrounding a convex hull.
Definition pointset.h:59
struct GEOSGeom_t GEOSGeometry
Definition util.h:41