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