QGIS API Documentation 4.3.0-Master (0978c174f8e)
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 = std::max( ( mLF->symbolSize().width() - geom.boundingBox().width() ) / 2.0, 0.0 );
593 symbolHeightOffset = std::max( ( mLF->symbolSize().height() - geom.boundingBox().height() ) / 2.0, 0.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, double angle )
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 Qgis::TextAnchorPoint textPoint = mLF->lineAnchorTextPoint();
944
945 const double cosAngle = std::cos( angle );
946 const double sinAngle = std::sin( angle );
947 const double halfHeightX = ( labelHeight / 2.0 ) * sinAngle;
948 const double halfHeightY = ( labelHeight / 2.0 ) * cosAngle;
949
950 double candidateCenterX, candidateCenterY;
951 int i = 0;
952 while ( currentDistanceAlongLine <= totalLineLength )
953 {
954 if ( pal->isCanceled() )
955 {
956 return lPos.size();
957 }
958
959 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateCenterX, &candidateCenterY );
960
961 // penalize positions which are further from the line's anchor point
962 double cost = totalLineLength > 0 ? std::fabs( lineAnchorPoint - currentDistanceAlongLine ) / totalLineLength : 0; // <0, 0.5>
963 cost /= 1000; // < 0, 0.0005 >
964
965 double labelX = 0;
966 double labelY = 0;
967 switch ( textPoint )
968 {
970 labelX = candidateCenterX + halfHeightX;
971 labelY = candidateCenterY - halfHeightY;
972 break;
974 labelX = candidateCenterX - ( labelWidth / 2.0 ) * cosAngle + halfHeightX;
975 labelY = candidateCenterY - ( labelWidth / 2.0 ) * sinAngle - halfHeightY;
976 break;
978 labelX = candidateCenterX - labelWidth * cosAngle + halfHeightX;
979 labelY = candidateCenterY - labelWidth * sinAngle - halfHeightY;
980 break;
982 // not possible here
983 break;
984 }
985 lPos.emplace_back(
986 std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
987 );
988
989 currentDistanceAlongLine += lineStepDistance;
990
991 i++;
992
993 if ( lineStepDistance < 0 )
994 break;
995 }
996
997 return lPos.size();
998}
999
1000std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
1001{
1002 double labelWidth = getLabelWidth();
1003 double labelHeight = getLabelHeight();
1004 double distanceLineToLabel = getLabelDistance();
1005 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
1006 if ( flags == 0 )
1007 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1008
1009 // 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
1010 QVector< int > extremeAngleNodes;
1011 PointSet *line = mapShape;
1012 int numberNodes = line->nbPoints;
1013 std::vector< double > &x = line->x;
1014 std::vector< double > &y = line->y;
1015
1016 // closed line? if so, we need to handle the final node angle
1017 bool closedLine = qgsDoubleNear( x[0], x[numberNodes - 1] ) && qgsDoubleNear( y[0], y[numberNodes - 1] );
1018 for ( int i = 1; i <= numberNodes - ( closedLine ? 1 : 2 ); ++i )
1019 {
1020 double x1 = x[i - 1];
1021 double x2 = x[i];
1022 double x3 = x[i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
1023 double y1 = y[i - 1];
1024 double y2 = y[i];
1025 double y3 = y[i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
1026 if ( qgsDoubleNear( y2, y3 ) && qgsDoubleNear( x2, x3 ) )
1027 continue;
1028 if ( qgsDoubleNear( y1, y2 ) && qgsDoubleNear( x1, x2 ) )
1029 continue;
1030 double vertexAngle = M_PI - ( std::atan2( y3 - y2, x3 - x2 ) - std::atan2( y2 - y1, x2 - x1 ) );
1031 vertexAngle = QgsGeometryUtilsBase::normalizedAngle( vertexAngle );
1032
1033 // extreme angles form more than 45 degree angle at a node - these are the ones we don't want labels to cross
1034 if ( vertexAngle < M_PI * 135.0 / 180.0 || vertexAngle > M_PI * 225.0 / 180.0 )
1035 extremeAngleNodes << i;
1036 }
1037 extremeAngleNodes << numberNodes - 1;
1038
1039 if ( extremeAngleNodes.isEmpty() )
1040 {
1041 // no extreme angles - createCandidatesAlongLineNearMidpoint will be more appropriate
1042 return 0;
1043 }
1044
1045 // calculate lengths of segments, and work out longest straight-ish segment
1046 std::vector< double > segmentLengths( numberNodes - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
1047 std::vector< double > distanceToSegment( numberNodes ); // absolute distance bw pt[0] and pt[i] along the line
1048 double totalLineLength = 0.0;
1049 QVector< double > straightSegmentLengths;
1050 QVector< double > straightSegmentAngles;
1051 straightSegmentLengths.reserve( extremeAngleNodes.size() + 1 );
1052 straightSegmentAngles.reserve( extremeAngleNodes.size() + 1 );
1053 double currentStraightSegmentLength = 0;
1054 double longestSegmentLength = 0;
1055 double segmentStartX = x[0];
1056 double segmentStartY = y[0];
1057 for ( int i = 0; i < numberNodes - 1; i++ )
1058 {
1059 if ( i == 0 )
1060 distanceToSegment[i] = 0;
1061 else
1062 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
1063
1064 segmentLengths[i] = QgsGeometryUtilsBase::distance2D( x[i], y[i], x[i + 1], y[i + 1] );
1065 totalLineLength += segmentLengths[i];
1066 if ( extremeAngleNodes.contains( i ) )
1067 {
1068 // at an extreme angle node, so reset counters
1069 straightSegmentLengths << currentStraightSegmentLength;
1070 straightSegmentAngles << QgsGeometryUtilsBase::normalizedAngle( std::atan2( y[i] - segmentStartY, x[i] - segmentStartX ) );
1071 longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
1072 currentStraightSegmentLength = 0;
1073 segmentStartX = x[i];
1074 segmentStartY = y[i];
1075 }
1076 currentStraightSegmentLength += segmentLengths[i];
1077 }
1078 distanceToSegment[line->nbPoints - 1] = totalLineLength;
1079 straightSegmentLengths << currentStraightSegmentLength;
1080 straightSegmentAngles << QgsGeometryUtilsBase::normalizedAngle( std::atan2( y[numberNodes - 1] - segmentStartY, x[numberNodes - 1] - segmentStartX ) );
1081 longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
1082 const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
1083
1084 if ( totalLineLength < labelWidth )
1085 {
1086 return 0; //createCandidatesAlongLineNearMidpoint will be more appropriate
1087 }
1088
1089 const Qgis::TextAnchorPoint textPoint = mLF->lineAnchorTextPoint();
1090
1091 const std::size_t candidateTargetCount = maximumLineCandidates();
1092 double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
1093 lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
1094
1095 double distanceToEndOfSegment = 0.0;
1096 int lastNodeInSegment = 0;
1097 // finally, loop through all these straight segments. For each we create candidates along the straight segment.
1098 for ( int i = 0; i < straightSegmentLengths.count(); ++i )
1099 {
1100 currentStraightSegmentLength = straightSegmentLengths.at( i );
1101 double currentSegmentAngle = straightSegmentAngles.at( i );
1102 lastNodeInSegment = extremeAngleNodes.at( i );
1103 double distanceToStartOfSegment = distanceToEndOfSegment;
1104 distanceToEndOfSegment = distanceToSegment[lastNodeInSegment];
1105 double distanceToCenterOfSegment = 0.5 * ( distanceToEndOfSegment + distanceToStartOfSegment );
1106
1107 if ( currentStraightSegmentLength < labelWidth )
1108 // can't fit a label on here
1109 continue;
1110
1111 double currentDistanceAlongLine = distanceToStartOfSegment;
1112 double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
1113 double candidateLength = 0.0;
1114 double cost = 0.0;
1115 double angle = 0.0;
1116 double beta = 0.0;
1117
1118 //calculate some cost penalties
1119 double segmentCost = 1.0 - ( distanceToEndOfSegment - distanceToStartOfSegment ) / longestSegmentLength; // 0 -> 1 (lower for longer segments)
1120 double segmentAngleCost = 1 - std::fabs( std::fmod( currentSegmentAngle, M_PI ) - M_PI_2 ) / M_PI_2; // 0 -> 1, lower for more horizontal segments
1121
1122 while ( currentDistanceAlongLine + labelWidth < distanceToEndOfSegment )
1123 {
1124 if ( pal->isCanceled() )
1125 {
1126 return lPos.size();
1127 }
1128
1129 // calculate positions along linestring corresponding to start and end of current label candidate
1130 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1131 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1132
1133 candidateLength = QgsGeometryUtilsBase::distance2D( candidateEndX, candidateEndY, candidateStartX, candidateStartY );
1134
1135
1136 // LOTS OF DIFFERENT COSTS TO BALANCE HERE - feel free to tweak these, but please add a unit test
1137 // which covers the situation you are adjusting for (e.g., "given equal length lines, choose the more horizontal line")
1138
1139 cost = candidateLength / labelWidth;
1140 if ( cost > 0.98 )
1141 cost = 0.0001;
1142 else
1143 {
1144 // jaggy line has a greater cost
1145 cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1146 }
1147
1148 const double labelCenter = currentDistanceAlongLine + labelWidth / 2.0;
1149 double labelTextAnchor = 0;
1150 switch ( textPoint )
1151 {
1153 labelTextAnchor = currentDistanceAlongLine;
1154 break;
1156 labelTextAnchor = currentDistanceAlongLine + labelWidth / 2.0;
1157 break;
1159 labelTextAnchor = currentDistanceAlongLine + labelWidth;
1160 break;
1162 // not possible here
1163 break;
1164 }
1165
1166 const bool placementIsFlexible = mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1167 // penalize positions which are further from the straight segments's midpoint
1168 if ( placementIsFlexible )
1169 {
1170 // only apply this if labels are being placed toward the center of overall lines -- otherwise it messes with the distance from anchor cost
1171 double costCenter = 2 * std::fabs( labelCenter - distanceToCenterOfSegment ) / ( distanceToEndOfSegment - distanceToStartOfSegment ); // 0 -> 1
1172 cost += costCenter * 0.0005; // < 0, 0.0005 >
1173 }
1174
1175 if ( !closedLine )
1176 {
1177 // penalize positions which are further from line anchor point of whole linestring (by default the middle of the line)
1178 // this only applies to non closed linestrings, since the middle of a closed linestring is effectively arbitrary
1179 // and irrelevant to labeling
1180 double costLineCenter = 2 * std::fabs( labelTextAnchor - lineAnchorPoint ) / totalLineLength; // 0 -> 1
1181 cost += costLineCenter * 0.0005; // < 0, 0.0005 >
1182 }
1183
1184 if ( placementIsFlexible )
1185 {
1186 cost += segmentCost * 0.0005; // prefer labels on longer straight segments
1187 cost += segmentAngleCost * 0.0001; // prefer more horizontal segments, but this is less important than length considerations
1188 }
1189
1190 if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1191 {
1192 angle = 0.0;
1193 }
1194 else
1195 angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1196
1197 labelWidth = getLabelWidth( angle );
1198 labelHeight = getLabelHeight( angle );
1199 beta = angle + M_PI_2;
1200
1201 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Line )
1202 {
1203 // find out whether the line direction for this candidate is from right to left
1204 bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1205 // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1206 bool reversed = ( ( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1207 bool aboveLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) );
1208 bool belowLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) );
1209
1210 if ( belowLine )
1211 {
1212 if ( !mLF->permissibleZonePrepared()
1213 || GeomFunction::
1214 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1215 {
1216 const double candidateCost = cost + ( reversed ? 0 : 0.001 );
1217 lPos.emplace_back(
1218 std::make_unique< LabelPosition >(
1219 i,
1220 candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ),
1221 candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ),
1222 labelWidth,
1223 labelHeight,
1224 angle,
1225 candidateCost,
1226 this,
1229 )
1230 ); // Line
1231 }
1232 }
1233 if ( aboveLine )
1234 {
1235 if ( !mLF->permissibleZonePrepared()
1236 || GeomFunction::
1237 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) * distanceLineToLabel, candidateStartY + std::sin( beta ) * distanceLineToLabel, labelWidth, labelHeight, angle ) )
1238 {
1239 const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1240 lPos.emplace_back(
1241 std::make_unique< LabelPosition >(
1242 i,
1243 candidateStartX + std::cos( beta ) * distanceLineToLabel,
1244 candidateStartY + std::sin( beta ) * distanceLineToLabel,
1245 labelWidth,
1246 labelHeight,
1247 angle,
1248 candidateCost,
1249 this,
1252 )
1253 ); // Line
1254 }
1255 }
1257 {
1258 if ( !mLF->permissibleZonePrepared()
1259 || GeomFunction::
1260 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1261 {
1262 const double candidateCost = cost + 0.002;
1263 lPos.emplace_back(
1264 std::make_unique< LabelPosition >(
1265 i,
1266 candidateStartX - labelHeight * std::cos( beta ) / 2,
1267 candidateStartY - labelHeight * std::sin( beta ) / 2,
1268 labelWidth,
1269 labelHeight,
1270 angle,
1271 candidateCost,
1272 this,
1275 )
1276 ); // Line
1277 }
1278 }
1279 }
1280 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
1281 {
1282 // TODO: this code is likely dead -- it doesn't look possible to reach here with a Horizontal arrangement
1283 lPos.emplace_back(
1284 std::make_unique<
1285 LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
1286 ); // Line
1287 }
1288 else
1289 {
1290 // an invalid arrangement?
1291 }
1292
1293 currentDistanceAlongLine += lineStepDistance;
1294 }
1295 }
1296
1297 return lPos.size();
1298}
1299
1300std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, double initialCost, Pal *pal )
1301{
1302 double distanceLineToLabel = getLabelDistance();
1303
1304 double labelWidth = getLabelWidth();
1305 double labelHeight = getLabelHeight();
1306
1307 double angle;
1308 double cost;
1309
1310 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
1311 if ( flags == 0 )
1312 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1313
1314 PointSet *line = mapShape;
1315 int nbPoints = line->nbPoints;
1316 std::vector< double > &x = line->x;
1317 std::vector< double > &y = line->y;
1318
1319 std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
1320 std::vector< double > distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
1321
1322 double totalLineLength = 0.0; // line length
1323 for ( int i = 0; i < line->nbPoints - 1; i++ )
1324 {
1325 if ( i == 0 )
1326 distanceToSegment[i] = 0;
1327 else
1328 distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
1329
1330 segmentLengths[i] = QgsGeometryUtilsBase::distance2D( x[i], y[i], x[i + 1], y[i + 1] );
1331 totalLineLength += segmentLengths[i];
1332 }
1333 distanceToSegment[line->nbPoints - 1] = totalLineLength;
1334
1335 double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
1336 double currentDistanceAlongLine = 0;
1337
1338 const Qgis::TextAnchorPoint textPoint = mLF->lineAnchorTextPoint();
1339
1340 const std::size_t candidateTargetCount = maximumLineCandidates();
1341
1342 if ( totalLineLength > labelWidth )
1343 {
1344 lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
1345 }
1346 else if ( !line->isClosed() ) // line length < label width => centering label position
1347 {
1348 currentDistanceAlongLine = -( labelWidth - totalLineLength ) / 2.0;
1349 lineStepDistance = -1;
1350 totalLineLength = labelWidth;
1351 }
1352 else
1353 {
1354 // closed line, not long enough for label => no candidates!
1355 currentDistanceAlongLine = std::numeric_limits< double >::max();
1356 }
1357
1358 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!
1359
1360 switch ( mLF->lineAnchorType() )
1361 {
1363 break;
1364
1366 switch ( textPoint )
1367 {
1369 currentDistanceAlongLine = std::min( lineAnchorPoint, totalLineLength * 0.99 - labelWidth );
1370 break;
1372 currentDistanceAlongLine = std::min( lineAnchorPoint - labelWidth / 2, totalLineLength * 0.99 - labelWidth );
1373 break;
1375 currentDistanceAlongLine = std::min( lineAnchorPoint - labelWidth, totalLineLength * 0.99 - labelWidth );
1376 break;
1378 // not possible here
1379 break;
1380 }
1381 lineStepDistance = -1;
1382 break;
1383 }
1384
1385 double candidateLength;
1386 double beta;
1387 double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
1388 int i = 0;
1389 while ( currentDistanceAlongLine <= totalLineLength - labelWidth || mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::Strict )
1390 {
1391 if ( pal->isCanceled() )
1392 {
1393 return lPos.size();
1394 }
1395
1396 // calculate positions along linestring corresponding to start and end of current label candidate
1397 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1398 line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1399
1400 if ( currentDistanceAlongLine < 0 )
1401 {
1402 // label is bigger than line, use whole available line
1403 candidateLength = QgsGeometryUtilsBase::distance2D( x[nbPoints - 1], y[nbPoints - 1], x[0], y[0] );
1404 }
1405 else
1406 {
1407 candidateLength = QgsGeometryUtilsBase::distance2D( candidateEndX, candidateEndY, candidateStartX, candidateStartY );
1408 }
1409
1410 cost = candidateLength / labelWidth;
1411 if ( cost > 0.98 )
1412 cost = 0.0001;
1413 else
1414 {
1415 // jaggy line has a greater cost
1416 cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1417 }
1418
1419 // penalize positions which are further from the line's anchor point
1420 double textAnchorPoint = 0;
1421 switch ( textPoint )
1422 {
1424 textAnchorPoint = currentDistanceAlongLine;
1425 break;
1427 textAnchorPoint = currentDistanceAlongLine + labelWidth / 2;
1428 break;
1430 textAnchorPoint = currentDistanceAlongLine + labelWidth;
1431 break;
1433 // not possible here
1434 break;
1435 }
1436 double costCenter = totalLineLength > 0 ? std::fabs( lineAnchorPoint - textAnchorPoint ) / totalLineLength : 0; // <0, 0.5>
1437 cost += costCenter / 1000; // < 0, 0.0005 >
1438 cost += initialCost;
1439
1440 if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1441 {
1442 angle = 0.0;
1443 }
1444 else
1445 angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1446
1447 labelWidth = getLabelWidth( angle );
1448 labelHeight = getLabelHeight( angle );
1449 beta = angle + M_PI_2;
1450
1451 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Line )
1452 {
1453 // find out whether the line direction for this candidate is from right to left
1454 bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1455 // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1456 bool reversed = ( ( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1457 bool aboveLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) );
1458 bool belowLine = ( !reversed && ( flags & Qgis::LabelLinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & Qgis::LabelLinePlacementFlag::AboveLine ) );
1459
1460 if ( aboveLine )
1461 {
1462 if ( !mLF->permissibleZonePrepared()
1463 || GeomFunction::
1464 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) * distanceLineToLabel, candidateStartY + std::sin( beta ) * distanceLineToLabel, labelWidth, labelHeight, angle ) )
1465 {
1466 const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1467 lPos.emplace_back(
1468 std::make_unique< LabelPosition >(
1469 i,
1470 candidateStartX + std::cos( beta ) * distanceLineToLabel,
1471 candidateStartY + std::sin( beta ) * distanceLineToLabel,
1472 labelWidth,
1473 labelHeight,
1474 angle,
1475 candidateCost,
1476 this,
1479 )
1480 ); // Line
1481 }
1482 }
1483 if ( belowLine )
1484 {
1485 if ( !mLF->permissibleZonePrepared()
1486 || GeomFunction::
1487 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1488 {
1489 const double candidateCost = cost + ( !reversed ? 0.001 : 0 );
1490 lPos.emplace_back(
1491 std::make_unique< LabelPosition >(
1492 i,
1493 candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ),
1494 candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ),
1495 labelWidth,
1496 labelHeight,
1497 angle,
1498 candidateCost,
1499 this,
1502 )
1503 ); // Line
1504 }
1505 }
1507 {
1508 if ( !mLF->permissibleZonePrepared()
1509 || GeomFunction::
1510 containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1511 {
1512 const double candidateCost = cost + 0.002;
1513 lPos.emplace_back(
1514 std::make_unique< LabelPosition >(
1515 i,
1516 candidateStartX - labelHeight * std::cos( beta ) / 2,
1517 candidateStartY - labelHeight * std::sin( beta ) / 2,
1518 labelWidth,
1519 labelHeight,
1520 angle,
1521 candidateCost,
1522 this,
1525 )
1526 ); // Line
1527 }
1528 }
1529 }
1530 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
1531 {
1532 // TODO: this code is likely dead -- it doesn't look possible to reach here with a Horizontal arrangement
1533 lPos.emplace_back(
1534 std::make_unique<
1535 LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
1536 ); // Line
1537 }
1538 else
1539 {
1540 // an invalid arrangement?
1541 }
1542
1543 currentDistanceAlongLine += lineStepDistance;
1544
1545 i++;
1546
1547 if ( lineStepDistance < 0 )
1548 break;
1549 }
1550
1551 return lPos.size();
1552}
1553
1554std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset(
1555 PointSet *mapShape,
1556 const std::vector< double> &pathDistances,
1558 const double offsetAlongLine,
1559 bool &labeledLineSegmentIsRightToLeft,
1560 bool applyAngleConstraints,
1562 double additionalCharacterSpacing,
1563 double additionalWordSpacing
1564)
1565{
1566 const QgsPrecalculatedTextMetrics *metrics = qgis::down_cast< QgsTextLabelFeature * >( mLF )->textMetrics();
1567 Q_ASSERT( metrics );
1568
1569 const double maximumCharacterAngleInside = applyAngleConstraints ? std::fabs( qgis::down_cast< QgsTextLabelFeature *>( mLF )->maximumCharacterAngleInside() ) : -1;
1570 const double maximumCharacterAngleOutside = applyAngleConstraints ? std::fabs( qgis::down_cast< QgsTextLabelFeature *>( mLF )->maximumCharacterAngleOutside() ) : -1;
1571
1572 std::unique_ptr< QgsTextRendererUtils::CurvePlacementProperties > placement(
1574 generateCurvedTextPlacement( *metrics, mapShape->x.data(), mapShape->y.data(), mapShape->nbPoints, pathDistances, offsetAlongLine, direction, maximumCharacterAngleInside, maximumCharacterAngleOutside, flags, additionalCharacterSpacing, additionalWordSpacing )
1575 );
1576
1577 labeledLineSegmentIsRightToLeft = !( flags & Qgis::CurvedTextFlag::UprightCharactersOnly ) ? placement->labeledLineSegmentIsRightToLeft : placement->flippedCharacterPlacementToGetUprightLabels;
1578
1579 if ( placement->graphemePlacement.empty() )
1580 return nullptr;
1581
1582 auto it = placement->graphemePlacement.constBegin();
1583 auto firstPosition
1584 = std::make_unique< LabelPosition >( 0, it->x, it->y, it->width, it->height, it->angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
1585 firstPosition->setUpsideDownCharCount( placement->upsideDownCharCount );
1586 firstPosition->setPartId( it->graphemeIndex );
1587 LabelPosition *previousPosition = firstPosition.get();
1588 it++;
1589
1590 bool skipWhitespace = false;
1591 switch ( mLF->whitespaceCollisionHandling() )
1592 {
1594 break;
1595
1597 skipWhitespace = true;
1598 break;
1599 }
1600
1601 while ( it != placement->graphemePlacement.constEnd() )
1602 {
1603 if ( skipWhitespace && it->isWhitespace )
1604 {
1605 it++;
1606 continue;
1607 }
1608 auto position
1609 = std::make_unique< LabelPosition >( 0, it->x, it->y, it->width, it->height, it->angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
1610 position->setPartId( it->graphemeIndex );
1611
1612 LabelPosition *nextPosition = position.get();
1613 previousPosition->setNextPart( std::move( position ) );
1614 previousPosition = nextPosition;
1615 it++;
1616 }
1617
1618 return firstPosition;
1619}
1620
1621std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1622{
1623 const QgsPrecalculatedTextMetrics *li = qgis::down_cast< QgsTextLabelFeature *>( mLF )->textMetrics();
1624 Q_ASSERT( li );
1625
1626 // label info must be present
1627 if ( !li )
1628 return 0;
1629
1630 const int characterCount = li->count();
1631 if ( characterCount == 0 )
1632 return 0;
1633
1634 switch ( mLF->curvedLabelMode() )
1635 {
1639 return createDefaultCurvedCandidatesAlongLine( lPos, mapShape, allowOverrun, pal );
1641 return createCurvedCandidateWithCharactersAtVertices( lPos, mapShape, pal );
1642 }
1644}
1645
1646std::size_t FeaturePart::createDefaultCurvedCandidatesAlongLine( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1647{
1648 const QgsPrecalculatedTextMetrics *li = qgis::down_cast< QgsTextLabelFeature *>( mLF )->textMetrics();
1649 const int characterCount = li->count();
1650
1651 bool stretchWordSpacingToFit = mLF->curvedLabelMode() == Qgis::CurvedLabelMode::StretchWordSpacingToFitLine;
1652 double totalCharacterWidth = 0;
1653 int spaceCount = 0;
1654 for ( int i = 0; i < characterCount; ++i )
1655 {
1656 totalCharacterWidth += li->characterWidth( i );
1657 if ( stretchWordSpacingToFit && li->grapheme( i ) == ' ' )
1658 {
1659 spaceCount++;
1660 }
1661 }
1662 if ( spaceCount == 0 )
1663 {
1664 // if no spaces in the label, disable stretch word spacing to fit mode and fallback to standard curved placement
1665 stretchWordSpacingToFit = false;
1666 }
1667
1668 const bool stretchCharacterSpacingToFit = mLF->curvedLabelMode() == Qgis::CurvedLabelMode::StretchCharacterSpacingToFitLine;
1669 const bool usingStretchToFitMode = stretchCharacterSpacingToFit || stretchWordSpacingToFit;
1670
1671 // TODO - we may need an explicit penalty for overhanging labels. Currently, they are penalized just because they
1672 // are further from the line center, so non-overhanging placements are picked where possible.
1673
1674 std::unique_ptr< PointSet > expanded;
1675 double shapeLength = mapShape->length();
1676
1677 // in stretch modes we force allowOverrun to false, as we fit the text exactly
1678 // to the actual line length
1679 if ( totalRepeats() > 1 || usingStretchToFitMode )
1680 allowOverrun = false;
1681
1682 geos::unique_ptr originalPoint;
1683 if ( !usingStretchToFitMode )
1684 {
1685 // unless in strict mode, label overrun should NEVER exceed the label length (or labels would sit off in space).
1686 // in fact, let's require that a minimum of 5% of the label text has to sit on the feature,
1687 // as we don't want a label sitting right at the start or end corner of a line
1688 double overrun = 0;
1689 switch ( mLF->lineAnchorType() )
1690 {
1692 overrun = std::min( mLF->overrunDistance(), totalCharacterWidth * 0.95 );
1693 break;
1695 // in strict mode, we force sufficient overrun to ensure label will always "fit", even if it's placed
1696 // 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
1697 overrun = std::max( mLF->overrunDistance(), totalCharacterWidth * 1.05 );
1698 break;
1699 }
1700
1701 if ( totalCharacterWidth > shapeLength )
1702 {
1703 if ( !allowOverrun || shapeLength < totalCharacterWidth - 2 * overrun )
1704 {
1705 // label doesn't fit on this line, don't waste time trying to make candidates
1706 return 0;
1707 }
1708 }
1709
1710 // calculate the anchor point for the original line shape as a GEOS point.
1711 // this must be done BEFORE we account for overrun by extending the shape!
1712 originalPoint = mapShape->interpolatePoint( shapeLength * mLF->lineAnchorPercent() );
1713
1714 if ( allowOverrun && overrun > 0 )
1715 {
1716 // expand out line on either side to fit label
1717 expanded = mapShape->clone();
1718 expanded->extendLineByDistance( overrun, overrun, mLF->overrunSmoothDistance() );
1719 mapShape = expanded.get();
1720 shapeLength += 2 * overrun;
1721 }
1722 }
1723
1724 Qgis::LabelLinePlacementFlags flags = mLF->arrangementFlags();
1725 if ( flags == 0 )
1726 flags = Qgis::LabelLinePlacementFlag::OnLine; // default flag
1727 const bool hasAboveBelowLinePlacement = flags & Qgis::LabelLinePlacementFlag::AboveLine || flags & Qgis::LabelLinePlacementFlag::BelowLine;
1728 const double offsetDistance = mLF->distLabel() + li->characterHeight( 0 ) / 2;
1729 std::unique_ptr< PointSet > mapShapeOffsetPositive;
1730 bool positiveShapeHasNegativeDistance = false;
1731 std::unique_ptr< PointSet > mapShapeOffsetNegative;
1732 bool negativeShapeHasNegativeDistance = false;
1733 if ( hasAboveBelowLinePlacement && !qgsDoubleNear( offsetDistance, 0 ) )
1734 {
1735 // create offsetted map shapes to be used for above and below line placements
1737 mapShapeOffsetPositive = mapShape->clone();
1739 mapShapeOffsetNegative = mapShape->clone();
1740 if ( offsetDistance >= 0.0 || !( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) )
1741 {
1742 if ( mapShapeOffsetPositive )
1743 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance );
1744 positiveShapeHasNegativeDistance = offsetDistance < 0;
1745 if ( mapShapeOffsetNegative )
1746 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance * -1 );
1747 negativeShapeHasNegativeDistance = offsetDistance > 0;
1748 }
1749 else
1750 {
1751 // In case of a negative offset distance, above line placement switch to below line and vice versa
1753 {
1754 flags &= ~static_cast< int >( Qgis::LabelLinePlacementFlag::AboveLine );
1756 }
1758 {
1759 flags &= ~static_cast< int >( Qgis::LabelLinePlacementFlag::BelowLine );
1761 }
1762 if ( mapShapeOffsetPositive )
1763 mapShapeOffsetPositive->offsetCurveByDistance( offsetDistance * -1 );
1764 positiveShapeHasNegativeDistance = offsetDistance > 0;
1765 if ( mapShapeOffsetNegative )
1766 mapShapeOffsetNegative->offsetCurveByDistance( offsetDistance );
1767 negativeShapeHasNegativeDistance = offsetDistance < 0;
1768 }
1769 }
1770
1771 const Qgis::TextAnchorPoint textPoint = mLF->lineAnchorTextPoint();
1772
1773 std::vector< std::unique_ptr< LabelPosition >> positions;
1774 std::unique_ptr< LabelPosition > backupPlacement;
1775 for ( PathOffset offset : { PositiveOffset, NoOffset, NegativeOffset } )
1776 {
1777 PointSet *currentMapShape = nullptr;
1778 if ( offset == PositiveOffset && hasAboveBelowLinePlacement )
1779 {
1780 currentMapShape = mapShapeOffsetPositive.get();
1781 }
1782 if ( offset == NoOffset && flags & Qgis::LabelLinePlacementFlag::OnLine )
1783 {
1784 currentMapShape = mapShape;
1785 }
1786 if ( offset == NegativeOffset && hasAboveBelowLinePlacement )
1787 {
1788 currentMapShape = mapShapeOffsetNegative.get();
1789 }
1790 if ( !currentMapShape )
1791 continue;
1792
1793 // distance calculation
1794 const auto [pathDistances, totalDistance] = currentMapShape->edgeDistances();
1795 if ( qgsDoubleNear( totalDistance, 0.0 ) )
1796 continue;
1797
1798 double lineAnchorPoint = 0;
1799 if ( !usingStretchToFitMode )
1800 {
1801 if ( originalPoint )
1802 {
1803 // the actual anchor point for the offset curves is the closest point on those offset curves
1804 // to the anchor point on the original line. This avoids anchor points which differ greatly
1805 // on the positive/negative offset lines due to line curvature.
1806 lineAnchorPoint = currentMapShape->lineLocatePoint( originalPoint.get() );
1807 }
1808 else
1809 {
1810 lineAnchorPoint = totalDistance * mLF->lineAnchorPercent();
1811 if ( offset == NegativeOffset )
1812 lineAnchorPoint = totalDistance - lineAnchorPoint;
1813 }
1814 }
1815
1816 if ( pal->isCanceled() )
1817 return 0;
1818
1819 const std::size_t candidateTargetCount = maximumLineCandidates();
1820 double delta = std::max( li->characterHeight( 0 ) / 6, totalDistance / candidateTargetCount );
1821
1822 // generate curved labels
1823 double distanceAlongLineToStartCandidate = 0;
1824 bool singleCandidateOnly = false;
1825 double additionalCharacterSpacing = 0.0;
1826 double additionalWordSpacing = 0.0;
1827 if ( usingStretchToFitMode )
1828 {
1829 // calculate required expansion/compression of spacing
1830 double extraSpace = totalDistance - totalCharacterWidth;
1831
1832 // add a little bit of additional tolerance -- if we try to aim EXACTLY
1833 // for the end of the line, then we risk precision issues pushing us PAST
1834 // the end of the line and the string being truncated
1835 if ( extraSpace > 0 )
1836 extraSpace *= 0.995;
1837 else
1838 extraSpace *= 1.005;
1839
1840 if ( stretchWordSpacingToFit )
1841 {
1842 if ( spaceCount > 0 )
1843 additionalWordSpacing = extraSpace / spaceCount;
1844 else
1845 continue; // cannot stretch a single word
1846 }
1847 else
1848 {
1849 if ( characterCount > 1 )
1850 additionalCharacterSpacing = extraSpace / ( characterCount - 1 );
1851 }
1852
1853 // force a single candidate covering the whole line starting at 0
1854 distanceAlongLineToStartCandidate = 0;
1855 delta = totalDistance + 1.0; // (ensure loop runs exactly once)
1856 singleCandidateOnly = true;
1857 }
1858 else
1859 {
1860 switch ( mLF->lineAnchorType() )
1861 {
1863 break;
1864
1866 switch ( textPoint )
1867 {
1869 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint, 0.0, totalDistance * 0.999 );
1870 break;
1872 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth() / 2, 0.0, totalDistance * 0.999 - getLabelWidth() / 2 );
1873 break;
1875 distanceAlongLineToStartCandidate = std::clamp( lineAnchorPoint - getLabelWidth(), 0.0, totalDistance * 0.999 - getLabelWidth() );
1876 break;
1878 // not possible here
1879 break;
1880 }
1881 singleCandidateOnly = true;
1882 break;
1883 }
1884 }
1885
1886 bool hasTestedFirstPlacement = false;
1887 for ( ; distanceAlongLineToStartCandidate <= totalDistance; distanceAlongLineToStartCandidate += delta )
1888 {
1889 if ( singleCandidateOnly && hasTestedFirstPlacement )
1890 break;
1891
1892 if ( pal->isCanceled() )
1893 return 0;
1894
1895 hasTestedFirstPlacement = true;
1896 // placements may need to be reversed if using map orientation and the line has right-to-left direction
1897 bool labeledLineSegmentIsRightToLeft = false;
1900 Qgis::CurvedTextFlags curvedTextFlags;
1901 if ( onlyShowUprightLabels() && ( !singleCandidateOnly || !( flags & Qgis::LabelLinePlacementFlag::MapOrientation ) ) )
1903
1904 std::unique_ptr< LabelPosition > labelPosition
1905 = curvedPlacementAtOffset( currentMapShape, pathDistances, direction, distanceAlongLineToStartCandidate, labeledLineSegmentIsRightToLeft, !singleCandidateOnly, curvedTextFlags, additionalCharacterSpacing, additionalWordSpacing );
1906 if ( !labelPosition )
1907 {
1908 continue;
1909 }
1910
1911
1912 bool isBackupPlacementOnly = false;
1914 {
1915 if ( ( currentMapShape == mapShapeOffsetPositive.get() && positiveShapeHasNegativeDistance ) || ( currentMapShape == mapShapeOffsetNegative.get() && negativeShapeHasNegativeDistance ) )
1916 {
1917 labeledLineSegmentIsRightToLeft = !labeledLineSegmentIsRightToLeft;
1918 }
1919
1920 if ( ( offset != NoOffset ) && !labeledLineSegmentIsRightToLeft && !( flags & Qgis::LabelLinePlacementFlag::AboveLine ) )
1921 {
1922 if ( singleCandidateOnly && offset == PositiveOffset )
1923 isBackupPlacementOnly = true;
1924 else
1925 continue;
1926 }
1927 if ( ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft && !( flags & Qgis::LabelLinePlacementFlag::BelowLine ) )
1928 {
1929 if ( singleCandidateOnly && offset == PositiveOffset )
1930 isBackupPlacementOnly = true;
1931 else
1932 continue;
1933 }
1934 }
1935
1936 backupPlacement.reset();
1937
1938 // evaluate cost
1939 const double angleDiff = labelPosition->angleDifferential();
1940 const double angleDiffAvg = characterCount > 1 ? ( angleDiff / ( characterCount - 1 ) ) : 0; // <0, pi> but pi/8 is much already
1941
1942 // if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the
1943 // anchor weighting is sufficient to push labels towards start/end
1944 const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1945 double cost = angleDiffAvg / 100; // <0, 0.031 > but usually <0, 0.003 >
1946 if ( cost < 0.0001 )
1947 cost = 0.0001;
1948
1949 // for stretch-to-fit modes we ignore anchor distance cost as we always fit the whole line
1950 if ( !usingStretchToFitMode )
1951 {
1952 // penalize positions which are further from the line's anchor point
1953 double labelTextAnchor = 0;
1954 switch ( textPoint )
1955 {
1957 labelTextAnchor = distanceAlongLineToStartCandidate;
1958 break;
1960 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
1961 break;
1963 labelTextAnchor = distanceAlongLineToStartCandidate + getLabelWidth();
1964 break;
1966 // not possible here
1967 break;
1968 }
1969 double costCenter = std::fabs( lineAnchorPoint - labelTextAnchor ) / totalDistance; // <0, 0.5>
1970 cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
1971 }
1972
1973 const bool isBelow = ( offset != NoOffset ) && labeledLineSegmentIsRightToLeft;
1974 if ( isBelow )
1975 {
1976 // add additional cost for on line placement
1977 cost += 0.001;
1978 }
1979 else if ( offset == NoOffset )
1980 {
1981 // add additional cost for below line placement
1982 cost += 0.002;
1983 }
1984
1985 labelPosition->setCost( cost );
1986
1987 auto p = std::make_unique< LabelPosition >( *labelPosition );
1988 if ( p && mLF->permissibleZonePrepared() )
1989 {
1990 bool within = true;
1991 LabelPosition *currentPos = p.get();
1992 while ( within && currentPos )
1993 {
1994 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
1995 currentPos = currentPos->nextPart();
1996 }
1997 if ( !within )
1998 {
1999 p.reset();
2000 }
2001 }
2002
2003 if ( p )
2004 {
2005 if ( isBackupPlacementOnly )
2006 backupPlacement = std::move( p );
2007 else
2008 positions.emplace_back( std::move( p ) );
2009 }
2010 }
2011 }
2012
2013 for ( std::unique_ptr< LabelPosition > &pos : positions )
2014 {
2015 lPos.emplace_back( std::move( pos ) );
2016 }
2017
2018 if ( backupPlacement )
2019 lPos.emplace_back( std::move( backupPlacement ) );
2020
2021 return positions.size();
2022}
2023
2024std::size_t FeaturePart::createCurvedCandidateWithCharactersAtVertices( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal )
2025{
2026 const QgsPrecalculatedTextMetrics *metrics = qgis::down_cast< QgsTextLabelFeature * >( mLF )->textMetrics();
2027
2028 const int characterCount = metrics->count();
2029 const int vertexCount = mapShape->getNumPoints();
2030 if ( characterCount == 0 || vertexCount == 0 )
2031 return 0;
2032
2033 const double distLabel = mLF->distLabel();
2034
2035 std::unique_ptr< LabelPosition > firstPosition;
2036 LabelPosition *previousPosition = nullptr;
2037
2038 int vertexIndex = 0;
2039 int characterIndex = -1;
2040 for ( ; vertexIndex < vertexCount; ++vertexIndex )
2041 {
2042 if ( pal->isCanceled() )
2043 return 0;
2044
2045 bool isWhiteSpace = true;
2046 while ( isWhiteSpace )
2047 {
2048 characterIndex++;
2049 if ( characterIndex >= characterCount )
2050 break;
2051
2052 isWhiteSpace = metrics->grapheme( characterIndex ).trimmed().isEmpty() || metrics->grapheme( characterIndex ) == '\t';
2053 }
2054
2055 if ( characterIndex >= characterCount )
2056 break;
2057
2058 double x = mapShape->x[vertexIndex];
2059 double y = mapShape->y[vertexIndex];
2060
2061 // use the angle of the segment starting at the current vertex
2062 // if it is the last vertex then reuse the angle of the preceding segment
2063 double angle = 0.0;
2064 if ( vertexIndex < vertexCount - 1 )
2065 {
2066 angle = std::atan2( mapShape->y[vertexIndex + 1] - y, mapShape->x[vertexIndex + 1] - x );
2067 }
2068 else if ( vertexIndex > 0 )
2069 {
2070 angle = std::atan2( y - mapShape->y[vertexIndex - 1], x - mapShape->x[vertexIndex - 1] );
2071 }
2072 if ( !qgsDoubleNear( distLabel, 0.0 ) )
2073 {
2074 x -= std::sin( angle ) * distLabel;
2075 y += std::cos( angle ) * distLabel;
2076 }
2077
2078 const double width = metrics->characterWidth( characterIndex );
2079 const double height = metrics->characterHeight( characterIndex );
2080 auto currentPosition = std::make_unique< LabelPosition >( 0, x, y, width, height, angle, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
2081 currentPosition->setPartId( characterIndex );
2082
2083 if ( !firstPosition )
2084 {
2085 firstPosition = std::move( currentPosition );
2086 previousPosition = firstPosition.get();
2087 }
2088 else
2089 {
2090 LabelPosition *rawCurrent = currentPosition.get();
2091 previousPosition->setNextPart( std::move( currentPosition ) );
2092 previousPosition = rawCurrent;
2093 }
2094 }
2095
2096 if ( !firstPosition )
2097 return 0;
2098
2099 if ( mLF->permissibleZonePrepared() )
2100 {
2101 bool within = true;
2102 LabelPosition *currentPos = firstPosition.get();
2103 while ( within && currentPos )
2104 {
2105 within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
2106 currentPos = currentPos->nextPart();
2107 }
2108 if ( !within )
2109 {
2110 return 0;
2111 }
2112 }
2113
2114 lPos.emplace_back( std::move( firstPosition ) );
2115 return 1;
2116}
2117
2118/*
2119 * seg 2
2120 * pt3 ____________pt2
2121 * ¦ ¦
2122 * ¦ ¦
2123 * seg 3 ¦ BBOX ¦ seg 1
2124 * ¦ ¦
2125 * ¦____________¦
2126 * pt0 seg 0 pt1
2127 *
2128 */
2129
2130std::size_t FeaturePart::createCandidatesForPolygon( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
2131{
2132 double labelWidth = getLabelWidth();
2133 double labelHeight = getLabelHeight();
2134
2135 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
2136 const std::size_t targetPolygonCandidates = maxPolygonCandidates > 0
2137 ? std::min( maxPolygonCandidates, static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * area() ) ) )
2138 : 0;
2139
2140 const double totalArea = area();
2141
2142 mapShape->parent = nullptr;
2143
2144 if ( pal->isCanceled() )
2145 return 0;
2146
2147 QVector<PointSet *> shapes_final = splitPolygons( mapShape, labelWidth, labelHeight );
2148#if 0
2149 QgsDebugMsgLevel( u"PAL split polygons resulted in:"_s, 2 );
2150 for ( PointSet *ps : shapes_final )
2151 {
2152 QgsDebugMsgLevel( ps->toWkt(), 2 );
2153 }
2154#endif
2155
2156 std::size_t nbp = 0;
2157
2158 if ( !shapes_final.isEmpty() )
2159 {
2160 int id = 0; // ids for candidates
2161 double dlx, dly; // delta from label center and bottom-left corner
2162 double alpha = 0.0; // rotation for the label
2163 double px, py;
2164
2165 double beta;
2166 double diago = std::sqrt( labelWidth * labelWidth / 4.0 + labelHeight * labelHeight / 4 );
2167 double rx, ry;
2168 std::vector< OrientedConvexHullBoundingBox > boxes;
2169 boxes.reserve( shapes_final.size() );
2170
2171 // Compute bounding box for each finalShape
2172 while ( !shapes_final.isEmpty() )
2173 {
2174 PointSet *shape = shapes_final.takeFirst();
2175 bool ok = false;
2177 if ( ok )
2178 boxes.emplace_back( box );
2179
2180 if ( shape->parent )
2181 delete shape;
2182 }
2183
2184 if ( pal->isCanceled() )
2185 return 0;
2186
2187 double densityX = 1.0 / std::sqrt( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() );
2188 double densityY = densityX;
2189 int numTry = 0;
2190
2191 //fit in polygon only mode slows down calculation a lot, so if it's enabled
2192 //then use a smaller limit for number of iterations
2193 int maxTry = mLF->permissibleZonePrepared() ? 7 : 10;
2194
2195 std::size_t numberCandidatesGenerated = 0;
2196
2197 do
2198 {
2199 for ( OrientedConvexHullBoundingBox &box : boxes )
2200 {
2201 // there is two possibilities here:
2202 // 1. no maximum candidates for polygon setting is in effect (i.e. maxPolygonCandidates == 0). In that case,
2203 // we base our dx/dy on the current maximumPolygonCandidatesPerMapUnitSquared value. That should give us the desired
2204 // density of candidates straight up. Easy!
2205 // 2. a maximum candidate setting IS in effect. In that case, we want to generate a good initial estimate for dx/dy
2206 // which gives us a good spatial coverage of the polygon while roughly matching the desired maximum number of candidates.
2207 // If dx/dy is too small, then too many candidates will be generated, which is both slow AND results in poor coverage of the
2208 // polygon (after culling candidates to the max number, only those clustered around the polygon's pole of inaccessibility
2209 // will remain).
2210 double dx = densityX;
2211 double dy = densityY;
2212 if ( numTry == 0 && maxPolygonCandidates > 0 )
2213 {
2214 // scale maxPolygonCandidates for just this convex hull
2215 const double boxArea = box.width * box.length;
2216 double maxThisBox = targetPolygonCandidates * boxArea / totalArea;
2217 dx = std::max( dx, std::sqrt( boxArea / maxThisBox ) * 0.8 );
2218 dy = dx;
2219 }
2220
2221 if ( pal->isCanceled() )
2222 return numberCandidatesGenerated;
2223
2224 if ( ( box.length * box.width ) > ( xmax - xmin ) * ( ymax - ymin ) * 5 )
2225 {
2226 // Very Large BBOX (should never occur)
2227 continue;
2228 }
2229
2230 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal && mLF->permissibleZonePrepared() )
2231 {
2232 //check width/height of bbox is sufficient for label
2233 if ( mLF->permissibleZone().boundingBox().width() < labelWidth || mLF->permissibleZone().boundingBox().height() < labelHeight )
2234 {
2235 //no way label can fit in this box, skip it
2236 continue;
2237 }
2238 }
2239
2240 bool enoughPlace = false;
2241 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Free )
2242 {
2243 enoughPlace = true;
2244 px = ( box.x[0] + box.x[2] ) / 2 - labelWidth;
2245 py = ( box.y[0] + box.y[2] ) / 2 - labelHeight;
2246 int i, j;
2247
2248 // Virtual label: center on bbox center, label size = 2x original size
2249 // alpha = 0.
2250 // If all corner are in bbox then place candidates horizontaly
2251 for ( rx = px, i = 0; i < 2; rx = rx + 2 * labelWidth, i++ )
2252 {
2253 for ( ry = py, j = 0; j < 2; ry = ry + 2 * labelHeight, j++ )
2254 {
2255 if ( !mapShape->containsPoint( rx, ry ) )
2256 {
2257 enoughPlace = false;
2258 break;
2259 }
2260 }
2261 if ( !enoughPlace )
2262 {
2263 break;
2264 }
2265 }
2266
2267 } // arrangement== FREE ?
2268
2269 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal || enoughPlace )
2270 {
2271 alpha = 0.0; // HORIZ
2272 }
2273 else if ( box.length > 1.5 * labelWidth && box.width > 1.5 * labelWidth )
2274 {
2275 if ( box.alpha <= M_PI_4 )
2276 {
2277 alpha = box.alpha;
2278 }
2279 else
2280 {
2281 alpha = box.alpha - M_PI_2;
2282 }
2283 }
2284 else if ( box.length > box.width )
2285 {
2286 alpha = box.alpha - M_PI_2;
2287 }
2288 else
2289 {
2290 alpha = box.alpha;
2291 }
2292
2293 beta = std::atan2( labelHeight, labelWidth ) + alpha;
2294
2295
2296 //alpha = box->alpha;
2297
2298 // delta from label center and down-left corner
2299 dlx = std::cos( beta ) * diago;
2300 dly = std::sin( beta ) * diago;
2301
2302 double px0 = box.width / 2.0;
2303 double py0 = box.length / 2.0;
2304
2305 px0 -= std::ceil( px0 / dx ) * dx;
2306 py0 -= std::ceil( py0 / dy ) * dy;
2307
2308 for ( px = px0; px <= box.width; px += dx )
2309 {
2310 if ( pal->isCanceled() )
2311 break;
2312
2313 for ( py = py0; py <= box.length; py += dy )
2314 {
2315 rx = std::cos( box.alpha ) * px + std::cos( box.alpha - M_PI_2 ) * py;
2316 ry = std::sin( box.alpha ) * px + std::sin( box.alpha - M_PI_2 ) * py;
2317
2318 rx += box.x[0];
2319 ry += box.y[0];
2320
2321 if ( mLF->permissibleZonePrepared() )
2322 {
2323 if ( GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), rx - dlx, ry - dly, labelWidth, labelHeight, alpha ) )
2324 {
2325 // cost is set to minimal value, evaluated later
2326 lPos.emplace_back(
2327 std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
2328 );
2329 numberCandidatesGenerated++;
2330 }
2331 }
2332 else
2333 {
2334 // TODO - this should be an intersection test, not just a contains test of the candidate centroid
2335 // because in some cases we would want to allow candidates which mostly overlap the polygon even though
2336 // their centroid doesn't overlap (e.g. a "U" shaped polygon)
2337 // but the bugs noted in CostCalculator currently prevent this
2338 if ( mapShape->containsPoint( rx, ry ) )
2339 {
2340 auto potentialCandidate = std::make_unique<
2341 LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over );
2342 // cost is set to minimal value, evaluated later
2343 lPos.emplace_back( std::move( potentialCandidate ) );
2344 numberCandidatesGenerated++;
2345 }
2346 }
2347 }
2348 }
2349 } // forall box
2350
2351 nbp = numberCandidatesGenerated;
2352 if ( maxPolygonCandidates > 0 && nbp < targetPolygonCandidates )
2353 {
2354 densityX /= 2;
2355 densityY /= 2;
2356 numTry++;
2357 }
2358 else
2359 {
2360 break;
2361 }
2362 } while ( numTry < maxTry );
2363
2364 nbp = numberCandidatesGenerated;
2365 }
2366 else
2367 {
2368 nbp = 0;
2369 }
2370
2371 return nbp;
2372}
2373
2374std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal )
2375{
2376 // calculate distance between horizontal lines
2377 const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
2378 std::size_t candidatesCreated = 0;
2379
2380 double labelWidth = getLabelWidth();
2381 double labelHeight = getLabelHeight();
2382 double distanceToLabel = getLabelDistance();
2383 const QgsMargins &visualMargin = mLF->visualMargin();
2384
2385 /*
2386 * From Rylov & Reimer (2016) "A practical algorithm for the external annotation of area features":
2387 *
2388 * The list of rules adapted to the
2389 * needs of externally labelling areal features is as follows:
2390 * R1. Labels should be placed horizontally.
2391 * R2. Label should be placed entirely outside at some
2392 * distance from the area feature.
2393 * R3. Name should not cross the boundary of its area
2394 * feature.
2395 * R4. The name should be placed in way that takes into
2396 * account the shape of the feature by achieving a
2397 * balance between the feature and its name, emphasizing their relationship.
2398 * R5. The lettering to the right and slightly above the
2399 * symbol is prioritized.
2400 *
2401 * In the following subsections we utilize four of the five rules
2402 * for two subtasks of label placement, namely, for candidate
2403 * positions generation (R1, R2, and R3) and for measuring their
2404 * ‘goodness’ (R4). The rule R5 is applicable only in the case when
2405 * the area of a polygonal feature is small and the feature can be
2406 * treated and labelled as a point-feature
2407 */
2408
2409 /*
2410 * QGIS approach (cite Dawson (2020) if you want ;) )
2411 *
2412 * We differ from the horizontal sweep line approach described by Rylov & Reimer and instead
2413 * rely on just generating a set of points at regular intervals along the boundary of the polygon (exterior ring).
2414 *
2415 * In practice, this generates similar results as Rylov & Reimer, but has the additional benefits that:
2416 * 1. It avoids the need to calculate intersections between the sweep line and the polygon
2417 * 2. For horizontal or near horizontal segments, Rylov & Reimer propose generating evenly spaced points along
2418 * these segments-- i.e. the same approach as we do for the whole polygon
2419 * 3. It's easier to determine in advance exactly how many candidate positions we'll be generating, and accordingly
2420 * we can easily pick the distance between points along the exterior ring so that the number of positions generated
2421 * matches our target number (targetPolygonCandidates)
2422 */
2423
2424 // TO consider -- for very small polygons (wrt label size), treat them just like a point feature?
2425
2426 double cx, cy;
2427 getCentroid( cx, cy, false );
2428
2429 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2430
2431 // be a bit sneaky and only buffer out 50% here, and then do the remaining 50% when we make the label candidate itself.
2432 // this avoids candidates being created immediately over the buffered ring and always intersecting with it...
2433 geos::unique_ptr buffer( GEOSBuffer_r( ctxt, geos(), distanceToLabel * 0.5, 1 ) );
2434 std::unique_ptr< QgsAbstractGeometry> gg( QgsGeos::fromGeos( buffer.get() ) );
2435
2436 geos::prepared_unique_ptr preparedBuffer( GEOSPrepare_r( ctxt, buffer.get() ) );
2437
2438 const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( gg.get() );
2439 if ( !poly )
2440 return candidatesCreated;
2441
2443 if ( !ring )
2444 return candidatesCreated;
2445
2446 // 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,
2447 // i.e a loooooong skinny polygon with small area should still generate a large number of candidates)
2448 const double ringLength = ring->length();
2449 const double circleArea = std::pow( ringLength, 2 ) / ( 4 * M_PI );
2450 const std::size_t candidatesForArea = static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * circleArea ) );
2451 const std::size_t targetPolygonCandidates = std::max( static_cast< std::size_t >( 16 ), maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, candidatesForArea ) : candidatesForArea );
2452
2453 // assume each position generates one candidate
2454 const double delta = ringLength / targetPolygonCandidates;
2455 geos::unique_ptr geosPoint;
2456
2457 const double maxDistCentroidToLabelX = std::max( xmax - cx, cx - xmin ) + distanceToLabel;
2458 const double maxDistCentroidToLabelY = std::max( ymax - cy, cy - ymin ) + distanceToLabel;
2459 const double estimateOfMaxPossibleDistanceCentroidToLabel = std::sqrt( maxDistCentroidToLabelX * maxDistCentroidToLabelX + maxDistCentroidToLabelY * maxDistCentroidToLabelY );
2460
2461 // Satisfy R1: Labels should be placed horizontally.
2462 const double labelAngle = 0;
2463
2464 std::size_t i = lPos.size();
2465 auto addCandidate = [&]( double x, double y, Qgis::LabelPredefinedPointPosition position ) {
2466 double labelX = 0;
2467 double labelY = 0;
2469
2470 // Satisfy R2: Label should be placed entirely outside at some distance from the area feature.
2471 createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel * 0.5, visualMargin, 0, 0, labelAngle );
2472
2473 auto candidate = std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, labelAngle, 0, this, LabelPosition::LabelDirectionToLine::SameDirection, quadrant );
2474 if ( candidate->intersects( preparedBuffer.get() ) )
2475 {
2476 // satisfy R3. Name should not cross the boundary of its area feature.
2477
2478 // actually, we use the buffered geometry here, because a label shouldn't be closer to the polygon then the minimum distance value
2479 return;
2480 }
2481
2482 // cost candidates by their distance to the feature's centroid (following Rylov & Reimer)
2483
2484 // Satisfy R4. The name should be placed in way that takes into
2485 // account the shape of the feature by achieving a
2486 // balance between the feature and its name, emphasizing their relationship.
2487
2488
2489 // here we deviate a little from R&R, and instead of just calculating the centroid distance
2490 // to centroid of label, we calculate the distance from the centroid to the nearest point on the label
2491
2492 const double centroidDistance = candidate->getDistanceToPoint( cx, cy, false );
2493 const double centroidCost = centroidDistance / estimateOfMaxPossibleDistanceCentroidToLabel;
2494 candidate->setCost( centroidCost );
2495
2496 lPos.emplace_back( std::move( candidate ) );
2497 candidatesCreated++;
2498 ++i;
2499 };
2500
2501 ring->visitPointsByRegularDistance( delta, [&]( double x, double y, double, double, double startSegmentX, double startSegmentY, double, double, double endSegmentX, double endSegmentY, double, double ) {
2502 // get normal angle for segment
2503 float angle = atan2( static_cast< float >( endSegmentY - startSegmentY ), static_cast< float >( endSegmentX - startSegmentX ) ) * 180 / M_PI;
2504 if ( angle < 0 )
2505 angle += 360;
2506
2507 // adapted fom Rylov & Reimer figure 9
2508 if ( angle >= 0 && angle <= 5 )
2509 {
2512 }
2513 else if ( angle <= 85 )
2514 {
2516 }
2517 else if ( angle <= 90 )
2518 {
2521 }
2522
2523 else if ( angle <= 95 )
2524 {
2527 }
2528 else if ( angle <= 175 )
2529 {
2531 }
2532 else if ( angle <= 180 )
2533 {
2536 }
2537
2538 else if ( angle <= 185 )
2539 {
2542 }
2543 else if ( angle <= 265 )
2544 {
2546 }
2547 else if ( angle <= 270 )
2548 {
2551 }
2552 else if ( angle <= 275 )
2553 {
2556 }
2557 else if ( angle <= 355 )
2558 {
2560 }
2561 else
2562 {
2565 }
2566
2567 return !pal->isCanceled();
2568 } );
2569
2570 return candidatesCreated;
2571}
2572
2573std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( Pal *pal )
2574{
2575 std::vector< std::unique_ptr< LabelPosition > > lPos;
2576 double angleInRadians = mLF->hasFixedAngle() ? mLF->fixedAngle() : 0.0;
2577
2578 if ( mLF->hasFixedPosition() )
2579 {
2580 lPos.emplace_back(
2581 std::make_unique<
2582 LabelPosition>( 0, mLF->fixedPosition().x(), mLF->fixedPosition().y(), getLabelWidth( angleInRadians ), getLabelHeight( angleInRadians ), angleInRadians, 0.0, this, LabelPosition::LabelDirectionToLine::SameDirection, Qgis::LabelQuadrantPosition::Over )
2583 );
2584 }
2585 else
2586 {
2587 switch ( type )
2588 {
2589 case GEOS_POINT:
2590 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OrderedPositionsAroundPoint )
2591 createCandidatesAtOrderedPositionsOverPoint( x[0], y[0], lPos, angleInRadians );
2592 else if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OverPoint || mLF->hasFixedQuadrant() )
2593 createCandidatesOverPoint( x[0], y[0], lPos, angleInRadians );
2594 else
2595 createCandidatesAroundPoint( x[0], y[0], lPos, angleInRadians );
2596 break;
2597
2598 case GEOS_LINESTRING:
2599 if ( mLF->layer()->arrangement() == Qgis::LabelPlacement::Horizontal )
2600 createHorizontalCandidatesAlongLine( lPos, this, pal, angleInRadians );
2601 else if ( mLF->layer()->isCurved() )
2602 createCurvedCandidatesAlongLine( lPos, this, true, pal );
2603 else
2604 createCandidatesAlongLine( lPos, this, true, pal );
2605 break;
2606
2607 case GEOS_POLYGON:
2608 {
2609 const double labelWidth = getLabelWidth();
2610 const double labelHeight = getLabelHeight();
2611
2612 const bool allowOutside = mLF->polygonPlacementFlags() & Qgis::LabelPolygonPlacementFlag::AllowPlacementOutsideOfPolygon;
2613 const bool allowInside = mLF->polygonPlacementFlags() & Qgis::LabelPolygonPlacementFlag::AllowPlacementInsideOfPolygon;
2614 //check width/height of bbox is sufficient for label
2615
2616 if ( ( allowOutside && !allowInside ) || ( mLF->layer()->arrangement() == Qgis::LabelPlacement::OutsidePolygons ) )
2617 {
2618 // only allowed to place outside of polygon
2620 }
2621 else if ( allowOutside && ( std::fabs( xmax - xmin ) < labelWidth || std::fabs( ymax - ymin ) < labelHeight ) )
2622 {
2623 //no way label can fit in this polygon -- shortcut and only place label outside
2625 }
2626 else
2627 {
2628 std::size_t created = 0;
2629 if ( allowInside )
2630 {
2631 switch ( mLF->layer()->arrangement() )
2632 {
2634 {
2635 double cx, cy;
2636 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2637 if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
2638 created += createCandidateCenteredOverPoint( cx, cy, lPos, angleInRadians );
2639 created += createCandidatesAroundPoint( cx, cy, lPos, angleInRadians );
2640 break;
2641 }
2643 {
2644 double cx, cy;
2645 getCentroid( cx, cy, mLF->layer()->centroidInside() );
2646 created += createCandidatesOverPoint( cx, cy, lPos, angleInRadians );
2647 break;
2648 }
2650 created += createCandidatesAlongLine( lPos, this, false, pal );
2651 break;
2653 created += createCurvedCandidatesAlongLine( lPos, this, false, pal );
2654 break;
2655 default:
2656 created += createCandidatesForPolygon( lPos, this, pal );
2657 break;
2658 }
2659 }
2660
2661 if ( allowOutside )
2662 {
2663 // add fallback for labels outside the polygon
2665
2666 if ( created > 0 )
2667 {
2668 // TODO (maybe) increase cost for outside placements (i.e. positions at indices >= created)?
2669 // From my initial testing this doesn't seem necessary
2670 }
2671 }
2672 }
2673 }
2674 }
2675 }
2676
2677 return lPos;
2678}
2679
2680void FeaturePart::addSizePenalty( std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4] ) const
2681{
2682 if ( !mGeos )
2684
2685 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2686 int geomType = GEOSGeomTypeId_r( ctxt, mGeos );
2687
2688 double sizeCost = 0;
2689 if ( geomType == GEOS_LINESTRING )
2690 {
2691 const double l = length();
2692 if ( l <= 0 )
2693 return; // failed to calculate length
2694 double bbox_length = std::max( bbx[2] - bbx[0], bby[2] - bby[0] );
2695 if ( l >= bbox_length / 4 )
2696 return; // the line is longer than quarter of height or width - don't penalize it
2697
2698 sizeCost = 1 - ( l / ( bbox_length / 4 ) ); // < 0,1 >
2699 }
2700 else if ( geomType == GEOS_POLYGON )
2701 {
2702 const double a = area();
2703 if ( a <= 0 )
2704 return;
2705 double bbox_area = ( bbx[2] - bbx[0] ) * ( bby[2] - bby[0] );
2706 if ( a >= bbox_area / 16 )
2707 return; // covers more than 1/16 of our view - don't penalize it
2708
2709 sizeCost = 1 - ( a / ( bbox_area / 16 ) ); // < 0, 1 >
2710 }
2711 else
2712 return; // no size penalty for points
2713
2714 // apply the penalty
2715 for ( std::unique_ptr< LabelPosition > &pos : lPos )
2716 {
2717 pos->setCost( pos->cost() + sizeCost / 100 );
2718 }
2719}
2720
2722{
2723 if ( !nbPoints || !p2->nbPoints )
2724 return false;
2725
2726 // here we only care if the lines start or end at the other line -- we don't want to test
2727 // touches as that is true for "T" type joins!
2728 const double x1first = x.front();
2729 const double x1last = x.back();
2730 const double x2first = p2->x.front();
2731 const double x2last = p2->x.back();
2732 const double y1first = y.front();
2733 const double y1last = y.back();
2734 const double y2first = p2->y.front();
2735 const double y2last = p2->y.back();
2736
2737 const bool p2startTouches = ( qgsDoubleNear( x1first, x2first ) && qgsDoubleNear( y1first, y2first ) ) || ( qgsDoubleNear( x1last, x2first ) && qgsDoubleNear( y1last, y2first ) );
2738
2739 const bool p2endTouches = ( qgsDoubleNear( x1first, x2last ) && qgsDoubleNear( y1first, y2last ) ) || ( qgsDoubleNear( x1last, x2last ) && qgsDoubleNear( y1last, y2last ) );
2740 // only one endpoint can touch, not both
2741 if ( ( !p2startTouches && !p2endTouches ) || ( p2startTouches && p2endTouches ) )
2742 return false;
2743
2744 // now we know that we have one line endpoint touching only, but there's still a chance
2745 // that the other side of p2 may touch the original line NOT at the other endpoint
2746 // so we need to check that this point doesn't intersect
2747 const double p2otherX = p2startTouches ? x2last : x2first;
2748 const double p2otherY = p2startTouches ? y2last : y2first;
2749
2750 GEOSContextHandle_t geosctxt = QgsGeosContext::get();
2751
2752 try
2753 {
2754#if GEOS_VERSION_MAJOR > 3 || ( GEOS_VERSION_MAJOR == 3 && GEOS_VERSION_MINOR >= 12 )
2755 return ( GEOSPreparedIntersectsXY_r( geosctxt, preparedGeom(), p2otherX, p2otherY ) != 1 );
2756#else
2757 GEOSCoordSequence *coord = GEOSCoordSeq_create_r( geosctxt, 1, 2 );
2758 GEOSCoordSeq_setXY_r( geosctxt, coord, 0, p2otherX, p2otherY );
2759 geos::unique_ptr p2OtherEnd( GEOSGeom_createPoint_r( geosctxt, coord ) );
2760 return ( GEOSPreparedIntersects_r( geosctxt, preparedGeom(), p2OtherEnd.get() ) != 1 );
2761#endif
2762 }
2763 catch ( QgsGeosException &e )
2764 {
2765 qWarning( "GEOS exception: %s", e.what() );
2766 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2767 return false;
2768 }
2769}
2770
2772{
2773 if ( !mGeos )
2775 if ( !other->mGeos )
2776 other->createGeosGeom();
2777
2778 GEOSContextHandle_t ctxt = QgsGeosContext::get();
2779 try
2780 {
2781 GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos );
2782 GEOSGeometry *g2 = GEOSGeom_clone_r( ctxt, other->mGeos );
2783 GEOSGeometry *geoms[2] = { g1, g2 };
2784 geos::unique_ptr g( GEOSGeom_createCollection_r( ctxt, GEOS_MULTILINESTRING, geoms, 2 ) );
2785 geos::unique_ptr gTmp( GEOSLineMerge_r( ctxt, g.get() ) );
2786
2787 if ( GEOSGeomTypeId_r( ctxt, gTmp.get() ) != GEOS_LINESTRING )
2788 {
2789 // sometimes it's not possible to merge lines (e.g. they don't touch at endpoints)
2790 return false;
2791 }
2793
2794 // set up new geometry
2795 mGeos = gTmp.release();
2796 mOwnsGeom = true;
2797
2798 deleteCoords();
2799 qDeleteAll( mHoles );
2800 mHoles.clear();
2802 return true;
2803 }
2804 catch ( QgsGeosException &e )
2805 {
2806 qWarning( "GEOS exception: %s", e.what() );
2807 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2808 return false;
2809 }
2810}
2811
2813{
2814 if ( mLF->alwaysShow() )
2815 {
2816 //if feature is set to always show, bump the priority up by orders of magnitude
2817 //so that other feature's labels are unlikely to be placed over the label for this feature
2818 //(negative numbers due to how pal::extract calculates inactive cost)
2819 return -0.2;
2820 }
2821
2822 return mLF->priority() >= 0 ? mLF->priority() : mLF->layer()->priority();
2823}
2824
2826{
2827 bool result = false;
2828
2829 switch ( mLF->layer()->upsidedownLabels() )
2830 {
2832 result = true;
2833 break;
2835 // upright only dynamic labels
2836 if ( !hasFixedRotation() || ( !hasFixedPosition() && fixedAngle() == 0.0 ) )
2837 {
2838 result = true;
2839 }
2840 break;
2842 break;
2843 }
2844 return result;
2845}
@ StretchCharacterSpacingToFitLine
Increases (or decreases) the character spacing used for each label in order to fit the entire text ov...
Definition qgis.h:1309
@ Default
Default curved placement, characters are placed in an optimal position along the line....
Definition qgis.h:1307
@ StretchWordSpacingToFitLine
Increases (or decreases) the word spacing used for each label in order to fit the entire text over th...
Definition qgis.h:1310
@ PlaceCharactersAtVertices
Each individual character from the label text is placed such that their left-baseline position is loc...
Definition qgis.h:1308
@ BelowLine
Labels can be placed below a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1398
@ MapOrientation
Signifies that the AboveLine and BelowLine flags should respect the map's orientation rather than the...
Definition qgis.h:1399
@ OnLine
Labels can be placed directly over a line feature.
Definition qgis.h:1396
@ AboveLine
Labels can be placed above a line feature. Unless MapOrientation is also specified this mode respects...
Definition qgis.h:1397
@ FromSymbolBounds
Offset distance applies from rendered symbol bounds.
Definition qgis.h:1363
LabelPrioritization
Label prioritization.
Definition qgis.h:1273
@ PreferCloser
Prefer closer labels, falling back to alternate positions before larger distances.
Definition qgis.h:1274
@ PreferPositionOrdering
Prefer labels follow position ordering, falling back to more distance labels before alternate positio...
Definition qgis.h:1275
@ OverPoint
Arranges candidates over a point (or centroid of a polygon), or at a preset offset from the point....
Definition qgis.h:1289
@ AroundPoint
Arranges candidates in a circle around a point (or centroid of a polygon). Applies to point or polygo...
Definition qgis.h:1288
@ Line
Arranges candidates parallel to a generalised line representing the feature or parallel to a polygon'...
Definition qgis.h:1290
@ Free
Arranges candidates scattered throughout a polygon feature. Candidates are rotated to respect the pol...
Definition qgis.h:1293
@ OrderedPositionsAroundPoint
Candidates are placed in predefined positions around a point. Preference is given to positions with g...
Definition qgis.h:1294
@ Horizontal
Arranges horizontal candidates scattered throughout a polygon feature or along a line feature....
Definition qgis.h:1292
@ PerimeterCurved
Arranges candidates following the curvature of a polygon's boundary. Applies to polygon layers only.
Definition qgis.h:1295
@ OutsidePolygons
Candidates are placed outside of polygon boundaries. Applies to polygon layers only.
Definition qgis.h:1296
@ AllowPlacementInsideOfPolygon
Labels can be placed inside a polygon feature.
Definition qgis.h:1422
@ AllowPlacementOutsideOfPolygon
Labels can be placed outside of a polygon feature.
Definition qgis.h:1421
TextAnchorPoint
Anchor point of label text.
Definition qgis.h:1475
@ EndOfText
Anchor using end of text.
Definition qgis.h:1478
@ StartOfText
Anchor using start of text.
Definition qgis.h:1476
@ CenterOfText
Anchor using center of text.
Definition qgis.h:1477
@ FollowPlacement
Automatically set the anchor point based on the line anchor point value. Values <25% of line length w...
Definition qgis.h:1479
QFlags< LabelLinePlacementFlag > LabelLinePlacementFlags
Line placement flags, which control how candidates are generated for a linear feature.
Definition qgis.h:1410
LabelQuadrantPosition
Label quadrant positions.
Definition qgis.h:1375
@ AboveRight
Above right.
Definition qgis.h:1378
@ BelowLeft
Below left.
Definition qgis.h:1382
@ Above
Above center.
Definition qgis.h:1377
@ BelowRight
Below right.
Definition qgis.h:1384
@ Right
Right middle.
Definition qgis.h:1381
@ AboveLeft
Above left.
Definition qgis.h:1376
@ Below
Below center.
Definition qgis.h:1383
@ Over
Center middle.
Definition qgis.h:1380
@ TreatWhitespaceAsCollision
Treat overlapping whitespace text in labels and whitespace overlapping obstacles as collisions.
Definition qgis.h:1262
@ IgnoreWhitespaceCollisions
Ignore overlapping whitespace text in labels and whitespace overlapping obstacles.
Definition qgis.h:1263
@ UprightCharactersOnly
Permit upright characters only. If not present then upside down text placement is permitted.
Definition qgis.h:3191
QFlags< CurvedTextFlag > CurvedTextFlags
Flags controlling behavior of curved text generation.
Definition qgis.h:3201
LabelPredefinedPointPosition
Positions for labels when using the Qgis::LabelPlacement::OrderedPositionsAroundPoint placement mode.
Definition qgis.h:1322
@ OverPoint
Label directly centered over point.
Definition qgis.h:1335
@ MiddleLeft
Label on left of point.
Definition qgis.h:1328
@ TopRight
Label on top-right of point.
Definition qgis.h:1327
@ MiddleRight
Label on right of point.
Definition qgis.h:1329
@ TopSlightlyRight
Label on top of point, slightly right of center.
Definition qgis.h:1326
@ TopMiddle
Label directly above point.
Definition qgis.h:1325
@ BottomSlightlyLeft
Label below point, slightly left of center.
Definition qgis.h:1331
@ BottomRight
Label on bottom right of point.
Definition qgis.h:1334
@ BottomLeft
Label on bottom-left of point.
Definition qgis.h:1330
@ BottomSlightlyRight
Label below point, slightly right of center.
Definition qgis.h:1333
@ TopLeft
Label on top-left of point.
Definition qgis.h:1323
@ BottomMiddle
Label directly below point.
Definition qgis.h:1332
@ TopSlightlyLeft
Label on top of point, slightly left of center.
Definition qgis.h:1324
@ FlipUpsideDownLabels
Upside-down labels (90 <= angle < 270) are shown upright.
Definition qgis.h:1444
@ AlwaysAllowUpsideDown
Show upside down for all labels, including dynamic ones.
Definition qgis.h:1446
@ AllowUpsideDownWhenRotationIsDefined
Show upside down when rotation is layer- or data-defined.
Definition qgis.h:1445
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:1585
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...
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:2374
bool hasFixedRotation() const
Returns true if the feature's label has a fixed rotation.
Definition feature.h:311
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:1554
double getLabelHeight(double angle=0.0) const
Returns the height of the label, optionally taking an angle (in radians) into account.
Definition feature.h:302
QList< FeaturePart * > mHoles
Definition feature.h:377
double getLabelDistance() const
Returns the distance from the anchor point to the label.
Definition feature.h:308
~FeaturePart() override
Deletes the feature.
Definition feature.cpp:85
std::size_t createHorizontalCandidatesAlongLine(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal, double angle)
Generate horizontal candidates for line feature.
Definition feature.cpp:900
bool hasFixedPosition() const
Returns true if the feature's label has a fixed position.
Definition feature.h:317
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:2024
std::size_t createCandidatesForPolygon(std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal)
Generate candidates for polygon features.
Definition feature.cpp:2130
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:1646
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:1000
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:314
std::size_t maximumLineCandidates() const
Returns the maximum number of line candidates to generate for this feature.
Definition feature.cpp:183
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:2771
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:1621
bool onlyShowUprightLabels() const
Returns true if feature's label must be displayed upright.
Definition feature.cpp:2825
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:376
double getLabelWidth(double angle=0.0) const
Returns the width of the label, optionally taking an angle (in radians) into account.
Definition feature.h:297
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:2573
bool isConnected(FeaturePart *p2)
Check whether this part is connected with some other part.
Definition feature.cpp:2721
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:1300
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:2680
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:2812
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:148
std::unique_ptr< const GEOSPreparedGeometry, GeosDeleter > prepared_unique_ptr
Scoped GEOS prepared geometry pointer.
Definition qgsgeos.h:153
#define BUILTIN_UNREACHABLE
Definition qgis.h:8051
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:7417
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:80
Represents the minimum area, oriented bounding box surrounding a convex hull.
Definition pointset.h:59
struct GEOSGeom_t GEOSGeometry
Definition util.h:41