Branch data Line data Source code
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 "qgsgeometry.h"
31 : : #include "pal.h"
32 : : #include "layer.h"
33 : : #include "feature.h"
34 : : #include "geomfunction.h"
35 : : #include "labelposition.h"
36 : : #include "pointset.h"
37 : : #include "util.h"
38 : : #include "qgis.h"
39 : : #include "qgsgeos.h"
40 : : #include "qgsmessagelog.h"
41 : : #include "costcalculator.h"
42 : : #include "qgsgeometryutils.h"
43 : : #include "qgslabeling.h"
44 : : #include "qgspolygon.h"
45 : : #include <QLinkedList>
46 : : #include <cmath>
47 : : #include <cfloat>
48 : :
49 : : using namespace pal;
50 : :
51 : 0 : FeaturePart::FeaturePart( QgsLabelFeature *feat, const GEOSGeometry *geom )
52 : 0 : : mLF( feat )
53 : 0 : {
54 : : // we'll remove const, but we won't modify that geometry
55 : 0 : mGeos = const_cast<GEOSGeometry *>( geom );
56 : 0 : mOwnsGeom = false; // geometry is owned by Feature class
57 : :
58 : 0 : extractCoords( geom );
59 : :
60 : 0 : holeOf = nullptr;
61 : 0 : for ( int i = 0; i < mHoles.count(); i++ )
62 : : {
63 : 0 : mHoles.at( i )->holeOf = this;
64 : 0 : }
65 : :
66 : 0 : }
67 : :
68 : 0 : FeaturePart::FeaturePart( const FeaturePart &other )
69 : 0 : : PointSet( other )
70 : 0 : , mLF( other.mLF )
71 : 0 : {
72 : 0 : for ( const FeaturePart *hole : std::as_const( other.mHoles ) )
73 : : {
74 : 0 : mHoles << new FeaturePart( *hole );
75 : 0 : mHoles.last()->holeOf = this;
76 : : }
77 : 0 : }
78 : :
79 : 0 : FeaturePart::~FeaturePart()
80 : 0 : {
81 : : // X and Y are deleted in PointSet
82 : :
83 : 0 : qDeleteAll( mHoles );
84 : 0 : mHoles.clear();
85 : 0 : }
86 : :
87 : 0 : void FeaturePart::extractCoords( const GEOSGeometry *geom )
88 : : {
89 : 0 : const GEOSCoordSequence *coordSeq = nullptr;
90 : 0 : GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
91 : :
92 : 0 : type = GEOSGeomTypeId_r( geosctxt, geom );
93 : :
94 : 0 : if ( type == GEOS_POLYGON )
95 : : {
96 : 0 : if ( GEOSGetNumInteriorRings_r( geosctxt, geom ) > 0 )
97 : : {
98 : 0 : int numHoles = GEOSGetNumInteriorRings_r( geosctxt, geom );
99 : :
100 : 0 : for ( int i = 0; i < numHoles; ++i )
101 : : {
102 : 0 : const GEOSGeometry *interior = GEOSGetInteriorRingN_r( geosctxt, geom, i );
103 : 0 : FeaturePart *hole = new FeaturePart( mLF, interior );
104 : 0 : hole->holeOf = nullptr;
105 : : // possibly not needed. it's not done for the exterior ring, so I'm not sure
106 : : // why it's just done here...
107 : 0 : GeomFunction::reorderPolygon( hole->x, hole->y );
108 : :
109 : 0 : mHoles << hole;
110 : 0 : }
111 : 0 : }
112 : :
113 : : // use exterior ring for the extraction of coordinates that follows
114 : 0 : geom = GEOSGetExteriorRing_r( geosctxt, geom );
115 : 0 : }
116 : : else
117 : : {
118 : 0 : qDeleteAll( mHoles );
119 : 0 : mHoles.clear();
120 : : }
121 : :
122 : : // find out number of points
123 : 0 : nbPoints = GEOSGetNumCoordinates_r( geosctxt, geom );
124 : 0 : coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, geom );
125 : :
126 : : // initialize bounding box
127 : 0 : xmin = ymin = std::numeric_limits<double>::max();
128 : 0 : xmax = ymax = std::numeric_limits<double>::lowest();
129 : :
130 : : // initialize coordinate arrays
131 : 0 : deleteCoords();
132 : 0 : x.resize( nbPoints );
133 : 0 : y.resize( nbPoints );
134 : :
135 : 0 : for ( int i = 0; i < nbPoints; ++i )
136 : : {
137 : : #if GEOS_VERSION_MAJOR>3 || GEOS_VERSION_MINOR>=8
138 : 0 : GEOSCoordSeq_getXY_r( geosctxt, coordSeq, i, &x[i], &y[i] );
139 : : #else
140 : : GEOSCoordSeq_getX_r( geosctxt, coordSeq, i, &x[i] );
141 : : GEOSCoordSeq_getY_r( geosctxt, coordSeq, i, &y[i] );
142 : : #endif
143 : :
144 : 0 : xmax = x[i] > xmax ? x[i] : xmax;
145 : 0 : xmin = x[i] < xmin ? x[i] : xmin;
146 : :
147 : 0 : ymax = y[i] > ymax ? y[i] : ymax;
148 : 0 : ymin = y[i] < ymin ? y[i] : ymin;
149 : 0 : }
150 : 0 : }
151 : :
152 : 0 : Layer *FeaturePart::layer()
153 : : {
154 : 0 : return mLF->layer();
155 : : }
156 : :
157 : 0 : QgsFeatureId FeaturePart::featureId() const
158 : : {
159 : 0 : return mLF->id();
160 : : }
161 : :
162 : 0 : std::size_t FeaturePart::maximumPointCandidates() const
163 : : {
164 : 0 : return mLF->layer()->maximumPointLabelCandidates();
165 : : }
166 : :
167 : 0 : std::size_t FeaturePart::maximumLineCandidates() const
168 : : {
169 : 0 : if ( mCachedMaxLineCandidates > 0 )
170 : 0 : return mCachedMaxLineCandidates;
171 : :
172 : 0 : const double l = length();
173 : 0 : if ( l > 0 )
174 : : {
175 : 0 : const std::size_t candidatesForLineLength = static_cast< std::size_t >( std::ceil( mLF->layer()->mPal->maximumLineCandidatesPerMapUnit() * l ) );
176 : 0 : const std::size_t maxForLayer = mLF->layer()->maximumLineLabelCandidates();
177 : 0 : if ( maxForLayer == 0 )
178 : 0 : mCachedMaxLineCandidates = candidatesForLineLength;
179 : : else
180 : 0 : mCachedMaxLineCandidates = std::min( candidatesForLineLength, maxForLayer );
181 : 0 : }
182 : : else
183 : : {
184 : 0 : mCachedMaxLineCandidates = 1;
185 : : }
186 : 0 : return mCachedMaxLineCandidates;
187 : 0 : }
188 : :
189 : 0 : std::size_t FeaturePart::maximumPolygonCandidates() const
190 : : {
191 : 0 : if ( mCachedMaxPolygonCandidates > 0 )
192 : 0 : return mCachedMaxPolygonCandidates;
193 : :
194 : 0 : const double a = area();
195 : 0 : if ( a > 0 )
196 : : {
197 : 0 : const std::size_t candidatesForArea = static_cast< std::size_t >( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * a ) );
198 : 0 : const std::size_t maxForLayer = mLF->layer()->maximumPolygonLabelCandidates();
199 : 0 : if ( maxForLayer == 0 )
200 : 0 : mCachedMaxPolygonCandidates = candidatesForArea;
201 : : else
202 : 0 : mCachedMaxPolygonCandidates = std::min( candidatesForArea, maxForLayer );
203 : 0 : }
204 : : else
205 : : {
206 : 0 : mCachedMaxPolygonCandidates = 1;
207 : : }
208 : 0 : return mCachedMaxPolygonCandidates;
209 : 0 : }
210 : :
211 : 0 : bool FeaturePart::hasSameLabelFeatureAs( FeaturePart *part ) const
212 : : {
213 : 0 : if ( !part )
214 : 0 : return false;
215 : :
216 : 0 : if ( mLF->layer()->name() != part->layer()->name() )
217 : 0 : return false;
218 : :
219 : 0 : if ( mLF->id() == part->featureId() )
220 : 0 : return true;
221 : :
222 : : // any part of joined features are also treated as having the same label feature
223 : 0 : int connectedFeatureId = mLF->layer()->connectedFeatureId( mLF->id() );
224 : 0 : return connectedFeatureId >= 0 && connectedFeatureId == mLF->layer()->connectedFeatureId( part->featureId() );
225 : 0 : }
226 : :
227 : 0 : LabelPosition::Quadrant FeaturePart::quadrantFromOffset() const
228 : : {
229 : 0 : QPointF quadOffset = mLF->quadOffset();
230 : 0 : qreal quadOffsetX = quadOffset.x(), quadOffsetY = quadOffset.y();
231 : :
232 : 0 : if ( quadOffsetX < 0 )
233 : : {
234 : 0 : if ( quadOffsetY < 0 )
235 : : {
236 : 0 : return LabelPosition::QuadrantAboveLeft;
237 : : }
238 : 0 : else if ( quadOffsetY > 0 )
239 : : {
240 : 0 : return LabelPosition::QuadrantBelowLeft;
241 : : }
242 : : else
243 : : {
244 : 0 : return LabelPosition::QuadrantLeft;
245 : : }
246 : : }
247 : 0 : else if ( quadOffsetX > 0 )
248 : : {
249 : 0 : if ( quadOffsetY < 0 )
250 : : {
251 : 0 : return LabelPosition::QuadrantAboveRight;
252 : : }
253 : 0 : else if ( quadOffsetY > 0 )
254 : : {
255 : 0 : return LabelPosition::QuadrantBelowRight;
256 : : }
257 : : else
258 : : {
259 : 0 : return LabelPosition::QuadrantRight;
260 : : }
261 : : }
262 : : else
263 : : {
264 : 0 : if ( quadOffsetY < 0 )
265 : : {
266 : 0 : return LabelPosition::QuadrantAbove;
267 : : }
268 : 0 : else if ( quadOffsetY > 0 )
269 : : {
270 : 0 : return LabelPosition::QuadrantBelow;
271 : : }
272 : : else
273 : : {
274 : 0 : return LabelPosition::QuadrantOver;
275 : : }
276 : : }
277 : 0 : }
278 : :
279 : 0 : int FeaturePart::totalRepeats() const
280 : : {
281 : 0 : return mTotalRepeats;
282 : : }
283 : :
284 : 0 : void FeaturePart::setTotalRepeats( int totalRepeats )
285 : : {
286 : 0 : mTotalRepeats = totalRepeats;
287 : 0 : }
288 : :
289 : 0 : std::size_t FeaturePart::createCandidateCenteredOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
290 : : {
291 : : // get from feature
292 : 0 : double labelW = getLabelWidth( angle );
293 : 0 : double labelH = getLabelHeight( angle );
294 : :
295 : 0 : double cost = 0.00005;
296 : 0 : int id = lPos.size();
297 : :
298 : 0 : double xdiff = -labelW / 2.0;
299 : 0 : double ydiff = -labelH / 2.0;
300 : :
301 : 0 : feature()->setAnchorPosition( QgsPointXY( x, y ) );
302 : :
303 : 0 : double lx = x + xdiff;
304 : 0 : double ly = y + ydiff;
305 : :
306 : 0 : if ( mLF->permissibleZonePrepared() )
307 : : {
308 : 0 : if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), lx, ly, labelW, labelH, angle ) )
309 : : {
310 : 0 : return 0;
311 : : }
312 : 0 : }
313 : :
314 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( id, lx, ly, labelW, labelH, angle, cost, this, false, LabelPosition::QuadrantOver ) );
315 : 0 : return 1;
316 : 0 : }
317 : :
318 : 0 : std::size_t FeaturePart::createCandidatesOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
319 : : {
320 : : // get from feature
321 : 0 : double labelW = getLabelWidth( angle );
322 : 0 : double labelH = getLabelHeight( angle );
323 : :
324 : 0 : double cost = 0.0001;
325 : 0 : int id = lPos.size();
326 : :
327 : 0 : double xdiff = -labelW / 2.0;
328 : 0 : double ydiff = -labelH / 2.0;
329 : :
330 : 0 : feature()->setAnchorPosition( QgsPointXY( x, y ) );
331 : :
332 : 0 : if ( !qgsDoubleNear( mLF->quadOffset().x(), 0.0 ) )
333 : : {
334 : 0 : xdiff += labelW / 2.0 * mLF->quadOffset().x();
335 : 0 : }
336 : 0 : if ( !qgsDoubleNear( mLF->quadOffset().y(), 0.0 ) )
337 : : {
338 : 0 : ydiff += labelH / 2.0 * mLF->quadOffset().y();
339 : 0 : }
340 : :
341 : 0 : if ( ! mLF->hasFixedPosition() )
342 : : {
343 : 0 : if ( !qgsDoubleNear( angle, 0.0 ) )
344 : : {
345 : 0 : double xd = xdiff * std::cos( angle ) - ydiff * std::sin( angle );
346 : 0 : double yd = xdiff * std::sin( angle ) + ydiff * std::cos( angle );
347 : 0 : xdiff = xd;
348 : 0 : ydiff = yd;
349 : 0 : }
350 : 0 : }
351 : :
352 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::AroundPoint )
353 : : {
354 : : //if in "around point" placement mode, then we use the label distance to determine
355 : : //the label's offset
356 : 0 : if ( qgsDoubleNear( mLF->quadOffset().x(), 0.0 ) )
357 : : {
358 : 0 : ydiff += mLF->quadOffset().y() * mLF->distLabel();
359 : 0 : }
360 : 0 : else if ( qgsDoubleNear( mLF->quadOffset().y(), 0.0 ) )
361 : : {
362 : 0 : xdiff += mLF->quadOffset().x() * mLF->distLabel();
363 : 0 : }
364 : : else
365 : : {
366 : 0 : xdiff += mLF->quadOffset().x() * M_SQRT1_2 * mLF->distLabel();
367 : 0 : ydiff += mLF->quadOffset().y() * M_SQRT1_2 * mLF->distLabel();
368 : : }
369 : 0 : }
370 : : else
371 : : {
372 : 0 : if ( !qgsDoubleNear( mLF->positionOffset().x(), 0.0 ) )
373 : : {
374 : 0 : xdiff += mLF->positionOffset().x();
375 : 0 : }
376 : 0 : if ( !qgsDoubleNear( mLF->positionOffset().y(), 0.0 ) )
377 : : {
378 : 0 : ydiff += mLF->positionOffset().y();
379 : 0 : }
380 : : }
381 : :
382 : 0 : double lx = x + xdiff;
383 : 0 : double ly = y + ydiff;
384 : :
385 : 0 : if ( mLF->permissibleZonePrepared() )
386 : : {
387 : 0 : if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), lx, ly, labelW, labelH, angle ) )
388 : : {
389 : 0 : return 0;
390 : 0 : }
391 : 0 : }
392 : :
393 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( id, lx, ly, labelW, labelH, angle, cost, this, false, quadrantFromOffset() ) );
394 : 0 : return 1;
395 : 0 : }
396 : :
397 : 0 : std::unique_ptr<LabelPosition> FeaturePart::createCandidatePointOnSurface( PointSet *mapShape )
398 : : {
399 : : double px, py;
400 : : try
401 : : {
402 : 0 : GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
403 : 0 : geos::unique_ptr pointGeom( GEOSPointOnSurface_r( geosctxt, mapShape->geos() ) );
404 : 0 : if ( pointGeom )
405 : : {
406 : 0 : const GEOSCoordSequence *coordSeq = GEOSGeom_getCoordSeq_r( geosctxt, pointGeom.get() );
407 : : #if GEOS_VERSION_MAJOR>3 || GEOS_VERSION_MINOR>=8
408 : 0 : unsigned int nPoints = 0;
409 : 0 : GEOSCoordSeq_getSize_r( geosctxt, coordSeq, &nPoints );
410 : 0 : if ( nPoints == 0 )
411 : 0 : return nullptr;
412 : 0 : GEOSCoordSeq_getXY_r( geosctxt, coordSeq, 0, &px, &py );
413 : : #else
414 : : GEOSCoordSeq_getX_r( geosctxt, coordSeq, 0, &px );
415 : : GEOSCoordSeq_getY_r( geosctxt, coordSeq, 0, &py );
416 : : #endif
417 : 0 : }
418 : 0 : }
419 : : catch ( GEOSException &e )
420 : : {
421 : 0 : qWarning( "GEOS exception: %s", e.what() );
422 : 0 : QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
423 : 0 : return nullptr;
424 : 0 : }
425 : :
426 : 0 : return std::make_unique< LabelPosition >( 0, px, py, getLabelWidth(), getLabelHeight(), 0.0, 0.0, this, false, LabelPosition::QuadrantOver );
427 : 0 : }
428 : :
429 : 0 : void createCandidateAtOrderedPositionOverPoint( double &labelX, double &labelY, LabelPosition::Quadrant &quadrant, double x, double y, double labelWidth, double labelHeight, QgsPalLayerSettings::PredefinedPointPosition position, double distanceToLabel, const QgsMargins &visualMargin, double symbolWidthOffset, double symbolHeightOffset )
430 : : {
431 : 0 : double alpha = 0.0;
432 : 0 : double deltaX = 0;
433 : 0 : double deltaY = 0;
434 : 0 : switch ( position )
435 : : {
436 : : case QgsPalLayerSettings::TopLeft:
437 : 0 : quadrant = LabelPosition::QuadrantAboveLeft;
438 : 0 : alpha = 3 * M_PI_4;
439 : 0 : deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
440 : 0 : deltaY = -visualMargin.bottom() + symbolHeightOffset;
441 : 0 : break;
442 : :
443 : : case QgsPalLayerSettings::TopSlightlyLeft:
444 : 0 : quadrant = LabelPosition::QuadrantAboveRight; //right quadrant, so labels are left-aligned
445 : 0 : alpha = M_PI_2;
446 : 0 : deltaX = -labelWidth / 4.0 - visualMargin.left();
447 : 0 : deltaY = -visualMargin.bottom() + symbolHeightOffset;
448 : 0 : break;
449 : :
450 : : case QgsPalLayerSettings::TopMiddle:
451 : 0 : quadrant = LabelPosition::QuadrantAbove;
452 : 0 : alpha = M_PI_2;
453 : 0 : deltaX = -labelWidth / 2.0;
454 : 0 : deltaY = -visualMargin.bottom() + symbolHeightOffset;
455 : 0 : break;
456 : :
457 : : case QgsPalLayerSettings::TopSlightlyRight:
458 : 0 : quadrant = LabelPosition::QuadrantAboveLeft; //left quadrant, so labels are right-aligned
459 : 0 : alpha = M_PI_2;
460 : 0 : deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right();
461 : 0 : deltaY = -visualMargin.bottom() + symbolHeightOffset;
462 : 0 : break;
463 : :
464 : : case QgsPalLayerSettings::TopRight:
465 : 0 : quadrant = LabelPosition::QuadrantAboveRight;
466 : 0 : alpha = M_PI_4;
467 : 0 : deltaX = - visualMargin.left() + symbolWidthOffset;
468 : 0 : deltaY = -visualMargin.bottom() + symbolHeightOffset;
469 : 0 : break;
470 : :
471 : : case QgsPalLayerSettings::MiddleLeft:
472 : 0 : quadrant = LabelPosition::QuadrantLeft;
473 : 0 : alpha = M_PI;
474 : 0 : deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
475 : 0 : deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin??
476 : 0 : break;
477 : :
478 : : case QgsPalLayerSettings::MiddleRight:
479 : 0 : quadrant = LabelPosition::QuadrantRight;
480 : 0 : alpha = 0.0;
481 : 0 : deltaX = -visualMargin.left() + symbolWidthOffset;
482 : 0 : deltaY = -labelHeight / 2.0;// TODO - should this be adjusted by visual margin??
483 : 0 : break;
484 : :
485 : : case QgsPalLayerSettings::BottomLeft:
486 : 0 : quadrant = LabelPosition::QuadrantBelowLeft;
487 : 0 : alpha = 5 * M_PI_4;
488 : 0 : deltaX = -labelWidth + visualMargin.right() - symbolWidthOffset;
489 : 0 : deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
490 : 0 : break;
491 : :
492 : : case QgsPalLayerSettings::BottomSlightlyLeft:
493 : 0 : quadrant = LabelPosition::QuadrantBelowRight; //right quadrant, so labels are left-aligned
494 : 0 : alpha = 3 * M_PI_2;
495 : 0 : deltaX = -labelWidth / 4.0 - visualMargin.left();
496 : 0 : deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
497 : 0 : break;
498 : :
499 : : case QgsPalLayerSettings::BottomMiddle:
500 : 0 : quadrant = LabelPosition::QuadrantBelow;
501 : 0 : alpha = 3 * M_PI_2;
502 : 0 : deltaX = -labelWidth / 2.0;
503 : 0 : deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
504 : 0 : break;
505 : :
506 : : case QgsPalLayerSettings::BottomSlightlyRight:
507 : 0 : quadrant = LabelPosition::QuadrantBelowLeft; //left quadrant, so labels are right-aligned
508 : 0 : alpha = 3 * M_PI_2;
509 : 0 : deltaX = -labelWidth * 3.0 / 4.0 + visualMargin.right();
510 : 0 : deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
511 : 0 : break;
512 : :
513 : : case QgsPalLayerSettings::BottomRight:
514 : 0 : quadrant = LabelPosition::QuadrantBelowRight;
515 : 0 : alpha = 7 * M_PI_4;
516 : 0 : deltaX = -visualMargin.left() + symbolWidthOffset;
517 : 0 : deltaY = -labelHeight + visualMargin.top() - symbolHeightOffset;
518 : 0 : break;
519 : : }
520 : :
521 : : //have bearing, distance - calculate reference point
522 : 0 : double referenceX = std::cos( alpha ) * distanceToLabel + x;
523 : 0 : double referenceY = std::sin( alpha ) * distanceToLabel + y;
524 : :
525 : 0 : labelX = referenceX + deltaX;
526 : 0 : labelY = referenceY + deltaY;
527 : 0 : }
528 : :
529 : 0 : std::size_t FeaturePart::createCandidatesAtOrderedPositionsOverPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
530 : : {
531 : 0 : const QVector< QgsPalLayerSettings::PredefinedPointPosition > positions = mLF->predefinedPositionOrder();
532 : 0 : double labelWidth = getLabelWidth( angle );
533 : 0 : double labelHeight = getLabelHeight( angle );
534 : 0 : double distanceToLabel = getLabelDistance();
535 : 0 : const QgsMargins &visualMargin = mLF->visualMargin();
536 : :
537 : 0 : double symbolWidthOffset = ( mLF->offsetType() == QgsPalLayerSettings::FromSymbolBounds ? mLF->symbolSize().width() / 2.0 : 0.0 );
538 : 0 : double symbolHeightOffset = ( mLF->offsetType() == QgsPalLayerSettings::FromSymbolBounds ? mLF->symbolSize().height() / 2.0 : 0.0 );
539 : :
540 : 0 : double cost = 0.0001;
541 : 0 : std::size_t i = lPos.size();
542 : :
543 : 0 : const std::size_t maxNumberCandidates = mLF->layer()->maximumPointLabelCandidates();
544 : 0 : std::size_t created = 0;
545 : 0 : for ( QgsPalLayerSettings::PredefinedPointPosition position : positions )
546 : : {
547 : 0 : LabelPosition::Quadrant quadrant = LabelPosition::QuadrantAboveLeft;
548 : :
549 : 0 : double labelX = 0;
550 : 0 : double labelY = 0;
551 : 0 : createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel, visualMargin, symbolWidthOffset, symbolHeightOffset );
552 : :
553 : 0 : if ( ! mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), labelX, labelY, labelWidth, labelHeight, angle ) )
554 : : {
555 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, false, quadrant ) );
556 : 0 : created++;
557 : : //TODO - tweak
558 : 0 : cost += 0.001;
559 : 0 : if ( maxNumberCandidates > 0 && created >= maxNumberCandidates )
560 : 0 : break;
561 : 0 : }
562 : 0 : ++i;
563 : : }
564 : :
565 : 0 : return created;
566 : 0 : }
567 : :
568 : 0 : std::size_t FeaturePart::createCandidatesAroundPoint( double x, double y, std::vector< std::unique_ptr< LabelPosition > > &lPos, double angle )
569 : : {
570 : 0 : double labelWidth = getLabelWidth( angle );
571 : 0 : double labelHeight = getLabelHeight( angle );
572 : 0 : double distanceToLabel = getLabelDistance();
573 : :
574 : 0 : std::size_t maxNumberCandidates = mLF->layer()->maximumPointLabelCandidates();
575 : 0 : if ( maxNumberCandidates == 0 )
576 : 0 : maxNumberCandidates = 16;
577 : :
578 : 0 : int icost = 0;
579 : 0 : int inc = 2;
580 : 0 : int id = lPos.size();
581 : :
582 : 0 : double candidateAngleIncrement = 2 * M_PI / maxNumberCandidates; /* angle bw 2 pos */
583 : :
584 : : /* various angles */
585 : 0 : double a90 = M_PI_2;
586 : 0 : double a180 = M_PI;
587 : 0 : double a270 = a180 + a90;
588 : 0 : double a360 = 2 * M_PI;
589 : :
590 : : double gamma1, gamma2;
591 : :
592 : 0 : if ( distanceToLabel > 0 )
593 : : {
594 : 0 : gamma1 = std::atan2( labelHeight / 2, distanceToLabel + labelWidth / 2 );
595 : 0 : gamma2 = std::atan2( labelWidth / 2, distanceToLabel + labelHeight / 2 );
596 : 0 : }
597 : : else
598 : : {
599 : 0 : gamma1 = gamma2 = a90 / 3.0;
600 : : }
601 : :
602 : 0 : if ( gamma1 > a90 / 3.0 )
603 : 0 : gamma1 = a90 / 3.0;
604 : :
605 : 0 : if ( gamma2 > a90 / 3.0 )
606 : 0 : gamma2 = a90 / 3.0;
607 : :
608 : 0 : std::size_t numberCandidatesGenerated = 0;
609 : :
610 : : std::size_t i;
611 : : double angleToCandidate;
612 : 0 : for ( i = 0, angleToCandidate = M_PI_4; i < maxNumberCandidates; i++, angleToCandidate += candidateAngleIncrement )
613 : : {
614 : 0 : double labelX = x;
615 : 0 : double labelY = y;
616 : :
617 : 0 : if ( angleToCandidate > a360 )
618 : 0 : angleToCandidate -= a360;
619 : :
620 : 0 : LabelPosition::Quadrant quadrant = LabelPosition::QuadrantOver;
621 : :
622 : 0 : if ( angleToCandidate < gamma1 || angleToCandidate > a360 - gamma1 ) // on the right
623 : : {
624 : 0 : labelX += distanceToLabel;
625 : 0 : double iota = ( angleToCandidate + gamma1 );
626 : 0 : if ( iota > a360 - gamma1 )
627 : 0 : iota -= a360;
628 : :
629 : : //ly += -yrm/2.0 + tan(alpha)*(distlabel + xrm/2);
630 : 0 : labelY += -labelHeight + labelHeight * iota / ( 2 * gamma1 );
631 : :
632 : 0 : quadrant = LabelPosition::QuadrantRight;
633 : 0 : }
634 : 0 : else if ( angleToCandidate < a90 - gamma2 ) // top-right
635 : : {
636 : 0 : labelX += distanceToLabel * std::cos( angleToCandidate );
637 : 0 : labelY += distanceToLabel * std::sin( angleToCandidate );
638 : 0 : quadrant = LabelPosition::QuadrantAboveRight;
639 : 0 : }
640 : 0 : else if ( angleToCandidate < a90 + gamma2 ) // top
641 : : {
642 : : //lx += -xrm/2.0 - tan(alpha+a90)*(distlabel + yrm/2);
643 : 0 : labelX += -labelWidth * ( angleToCandidate - a90 + gamma2 ) / ( 2 * gamma2 );
644 : 0 : labelY += distanceToLabel;
645 : 0 : quadrant = LabelPosition::QuadrantAbove;
646 : 0 : }
647 : 0 : else if ( angleToCandidate < a180 - gamma1 ) // top left
648 : : {
649 : 0 : labelX += distanceToLabel * std::cos( angleToCandidate ) - labelWidth;
650 : 0 : labelY += distanceToLabel * std::sin( angleToCandidate );
651 : 0 : quadrant = LabelPosition::QuadrantAboveLeft;
652 : 0 : }
653 : 0 : else if ( angleToCandidate < a180 + gamma1 ) // left
654 : : {
655 : 0 : labelX += -distanceToLabel - labelWidth;
656 : : //ly += -yrm/2.0 - tan(alpha)*(distlabel + xrm/2);
657 : 0 : labelY += - ( angleToCandidate - a180 + gamma1 ) * labelHeight / ( 2 * gamma1 );
658 : 0 : quadrant = LabelPosition::QuadrantLeft;
659 : 0 : }
660 : 0 : else if ( angleToCandidate < a270 - gamma2 ) // down - left
661 : : {
662 : 0 : labelX += distanceToLabel * std::cos( angleToCandidate ) - labelWidth;
663 : 0 : labelY += distanceToLabel * std::sin( angleToCandidate ) - labelHeight;
664 : 0 : quadrant = LabelPosition::QuadrantBelowLeft;
665 : 0 : }
666 : 0 : else if ( angleToCandidate < a270 + gamma2 ) // down
667 : : {
668 : 0 : labelY += -distanceToLabel - labelHeight;
669 : : //lx += -xrm/2.0 + tan(alpha+a90)*(distlabel + yrm/2);
670 : 0 : labelX += -labelWidth + ( angleToCandidate - a270 + gamma2 ) * labelWidth / ( 2 * gamma2 );
671 : 0 : quadrant = LabelPosition::QuadrantBelow;
672 : 0 : }
673 : 0 : else if ( angleToCandidate < a360 ) // down - right
674 : : {
675 : 0 : labelX += distanceToLabel * std::cos( angleToCandidate );
676 : 0 : labelY += distanceToLabel * std::sin( angleToCandidate ) - labelHeight;
677 : 0 : quadrant = LabelPosition::QuadrantBelowRight;
678 : 0 : }
679 : :
680 : : double cost;
681 : :
682 : 0 : if ( maxNumberCandidates == 1 )
683 : 0 : cost = 0.0001;
684 : : else
685 : 0 : cost = 0.0001 + 0.0020 * double( icost ) / double( maxNumberCandidates - 1 );
686 : :
687 : :
688 : 0 : if ( mLF->permissibleZonePrepared() )
689 : : {
690 : 0 : if ( !GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), labelX, labelY, labelWidth, labelHeight, angle ) )
691 : : {
692 : 0 : continue;
693 : : }
694 : 0 : }
695 : :
696 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( id + i, labelX, labelY, labelWidth, labelHeight, angle, cost, this, false, quadrant ) );
697 : 0 : numberCandidatesGenerated++;
698 : :
699 : 0 : icost += inc;
700 : :
701 : 0 : if ( icost == static_cast< int >( maxNumberCandidates ) )
702 : : {
703 : 0 : icost = static_cast< int >( maxNumberCandidates ) - 1;
704 : 0 : inc = -2;
705 : 0 : }
706 : 0 : else if ( icost > static_cast< int >( maxNumberCandidates ) )
707 : : {
708 : 0 : icost = static_cast< int >( maxNumberCandidates ) - 2;
709 : 0 : inc = -2;
710 : 0 : }
711 : :
712 : 0 : }
713 : :
714 : 0 : return numberCandidatesGenerated;
715 : 0 : }
716 : :
717 : 0 : std::size_t FeaturePart::createCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
718 : : {
719 : 0 : if ( allowOverrun )
720 : : {
721 : 0 : double shapeLength = mapShape->length();
722 : 0 : if ( totalRepeats() > 1 && shapeLength < getLabelWidth() )
723 : 0 : return 0;
724 : 0 : else if ( shapeLength < getLabelWidth() - 2 * std::min( getLabelWidth(), mLF->overrunDistance() ) )
725 : : {
726 : : // label doesn't fit on this line, don't waste time trying to make candidates
727 : 0 : return 0;
728 : : }
729 : 0 : }
730 : :
731 : : //prefer to label along straightish segments:
732 : 0 : std::size_t candidates = 0;
733 : :
734 : 0 : if ( mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::HintOnly )
735 : 0 : candidates = createCandidatesAlongLineNearStraightSegments( lPos, mapShape, pal );
736 : :
737 : 0 : const std::size_t candidateTargetCount = maximumLineCandidates();
738 : 0 : if ( candidates < candidateTargetCount )
739 : : {
740 : : // but not enough candidates yet, so fallback to labeling near whole line's midpoint
741 : 0 : candidates = createCandidatesAlongLineNearMidpoint( lPos, mapShape, candidates > 0 ? 0.01 : 0.0, pal );
742 : 0 : }
743 : 0 : return candidates;
744 : 0 : }
745 : :
746 : 0 : std::size_t FeaturePart::createHorizontalCandidatesAlongLine( std::vector<std::unique_ptr<LabelPosition> > &lPos, PointSet *mapShape, Pal *pal )
747 : : {
748 : 0 : const double labelWidth = getLabelWidth();
749 : 0 : const double labelHeight = getLabelHeight();
750 : :
751 : 0 : PointSet *line = mapShape;
752 : 0 : int nbPoints = line->nbPoints;
753 : 0 : std::vector< double > &x = line->x;
754 : 0 : std::vector< double > &y = line->y;
755 : :
756 : 0 : std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
757 : 0 : std::vector< double >distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
758 : :
759 : 0 : double totalLineLength = 0.0; // line length
760 : 0 : for ( int i = 0; i < line->nbPoints - 1; i++ )
761 : : {
762 : 0 : if ( i == 0 )
763 : 0 : distanceToSegment[i] = 0;
764 : : else
765 : 0 : distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
766 : :
767 : 0 : segmentLengths[i] = GeomFunction::dist_euc2d( x[i], y[i], x[i + 1], y[i + 1] );
768 : 0 : totalLineLength += segmentLengths[i];
769 : 0 : }
770 : 0 : distanceToSegment[line->nbPoints - 1] = totalLineLength;
771 : :
772 : 0 : const std::size_t candidateTargetCount = maximumLineCandidates();
773 : 0 : double lineStepDistance = 0;
774 : :
775 : 0 : const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
776 : 0 : double currentDistanceAlongLine = lineStepDistance;
777 : 0 : switch ( mLF->lineAnchorType() )
778 : : {
779 : : case QgsLabelLineSettings::AnchorType::HintOnly:
780 : 0 : lineStepDistance = totalLineLength / ( candidateTargetCount + 1 ); // distance to move along line with each candidate
781 : 0 : break;
782 : :
783 : : case QgsLabelLineSettings::AnchorType::Strict:
784 : 0 : currentDistanceAlongLine = lineAnchorPoint;
785 : 0 : lineStepDistance = -1;
786 : 0 : break;
787 : : }
788 : :
789 : : double candidateCenterX, candidateCenterY;
790 : 0 : int i = 0;
791 : 0 : while ( currentDistanceAlongLine <= totalLineLength )
792 : : {
793 : 0 : if ( pal->isCanceled() )
794 : : {
795 : 0 : return lPos.size();
796 : : }
797 : :
798 : 0 : line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateCenterX, &candidateCenterY );
799 : :
800 : : // penalize positions which are further from the line's anchor point
801 : 0 : double cost = std::fabs( lineAnchorPoint - currentDistanceAlongLine ) / totalLineLength; // <0, 0.5>
802 : 0 : cost /= 1000; // < 0, 0.0005 >
803 : :
804 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateCenterX - labelWidth / 2, candidateCenterY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) );
805 : :
806 : 0 : currentDistanceAlongLine += lineStepDistance;
807 : :
808 : 0 : i++;
809 : :
810 : 0 : if ( lineStepDistance < 0 )
811 : 0 : break;
812 : : }
813 : :
814 : 0 : return lPos.size();
815 : 0 : }
816 : :
817 : 0 : std::size_t FeaturePart::createCandidatesAlongLineNearStraightSegments( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
818 : : {
819 : 0 : double labelWidth = getLabelWidth();
820 : 0 : double labelHeight = getLabelHeight();
821 : 0 : double distanceLineToLabel = getLabelDistance();
822 : 0 : QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
823 : 0 : if ( flags == 0 )
824 : 0 : flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
825 : :
826 : : // 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
827 : 0 : QVector< int > extremeAngleNodes;
828 : 0 : PointSet *line = mapShape;
829 : 0 : int numberNodes = line->nbPoints;
830 : 0 : std::vector< double > &x = line->x;
831 : 0 : std::vector< double > &y = line->y;
832 : :
833 : : // closed line? if so, we need to handle the final node angle
834 : 0 : bool closedLine = qgsDoubleNear( x[0], x[ numberNodes - 1] ) && qgsDoubleNear( y[0], y[numberNodes - 1 ] );
835 : 0 : for ( int i = 1; i <= numberNodes - ( closedLine ? 1 : 2 ); ++i )
836 : : {
837 : 0 : double x1 = x[i - 1];
838 : 0 : double x2 = x[i];
839 : 0 : double x3 = x[ i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
840 : 0 : double y1 = y[i - 1];
841 : 0 : double y2 = y[i];
842 : 0 : double y3 = y[ i == numberNodes - 1 ? 1 : i + 1]; // wraparound for closed linestrings
843 : 0 : if ( qgsDoubleNear( y2, y3 ) && qgsDoubleNear( x2, x3 ) )
844 : 0 : continue;
845 : 0 : if ( qgsDoubleNear( y1, y2 ) && qgsDoubleNear( x1, x2 ) )
846 : 0 : continue;
847 : 0 : double vertexAngle = M_PI - ( std::atan2( y3 - y2, x3 - x2 ) - std::atan2( y2 - y1, x2 - x1 ) );
848 : 0 : vertexAngle = QgsGeometryUtils::normalizedAngle( vertexAngle );
849 : :
850 : : // extreme angles form more than 45 degree angle at a node - these are the ones we don't want labels to cross
851 : 0 : if ( vertexAngle < M_PI * 135.0 / 180.0 || vertexAngle > M_PI * 225.0 / 180.0 )
852 : 0 : extremeAngleNodes << i;
853 : 0 : }
854 : 0 : extremeAngleNodes << numberNodes - 1;
855 : :
856 : 0 : if ( extremeAngleNodes.isEmpty() )
857 : : {
858 : : // no extreme angles - createCandidatesAlongLineNearMidpoint will be more appropriate
859 : 0 : return 0;
860 : : }
861 : :
862 : : // calculate lengths of segments, and work out longest straight-ish segment
863 : 0 : std::vector< double > segmentLengths( numberNodes - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
864 : 0 : std::vector< double > distanceToSegment( numberNodes ); // absolute distance bw pt[0] and pt[i] along the line
865 : 0 : double totalLineLength = 0.0;
866 : 0 : QVector< double > straightSegmentLengths;
867 : 0 : QVector< double > straightSegmentAngles;
868 : 0 : straightSegmentLengths.reserve( extremeAngleNodes.size() + 1 );
869 : 0 : straightSegmentAngles.reserve( extremeAngleNodes.size() + 1 );
870 : 0 : double currentStraightSegmentLength = 0;
871 : 0 : double longestSegmentLength = 0;
872 : 0 : int segmentIndex = 0;
873 : 0 : double segmentStartX = x[0];
874 : 0 : double segmentStartY = y[0];
875 : 0 : for ( int i = 0; i < numberNodes - 1; i++ )
876 : : {
877 : 0 : if ( i == 0 )
878 : 0 : distanceToSegment[i] = 0;
879 : : else
880 : 0 : distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
881 : :
882 : 0 : segmentLengths[i] = GeomFunction::dist_euc2d( x[i], y[i], x[i + 1], y[i + 1] );
883 : 0 : totalLineLength += segmentLengths[i];
884 : 0 : if ( extremeAngleNodes.contains( i ) )
885 : : {
886 : : // at an extreme angle node, so reset counters
887 : 0 : straightSegmentLengths << currentStraightSegmentLength;
888 : 0 : straightSegmentAngles << QgsGeometryUtils::normalizedAngle( std::atan2( y[i] - segmentStartY, x[i] - segmentStartX ) );
889 : 0 : longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
890 : 0 : segmentIndex++;
891 : 0 : currentStraightSegmentLength = 0;
892 : 0 : segmentStartX = x[i];
893 : 0 : segmentStartY = y[i];
894 : 0 : }
895 : 0 : currentStraightSegmentLength += segmentLengths[i];
896 : 0 : }
897 : 0 : distanceToSegment[line->nbPoints - 1] = totalLineLength;
898 : 0 : straightSegmentLengths << currentStraightSegmentLength;
899 : 0 : straightSegmentAngles << QgsGeometryUtils::normalizedAngle( std::atan2( y[numberNodes - 1] - segmentStartY, x[numberNodes - 1] - segmentStartX ) );
900 : 0 : longestSegmentLength = std::max( longestSegmentLength, currentStraightSegmentLength );
901 : 0 : const double lineAnchorPoint = totalLineLength * mLF->lineAnchorPercent();
902 : :
903 : 0 : if ( totalLineLength < labelWidth )
904 : : {
905 : 0 : return 0; //createCandidatesAlongLineNearMidpoint will be more appropriate
906 : : }
907 : :
908 : 0 : const std::size_t candidateTargetCount = maximumLineCandidates();
909 : 0 : double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
910 : 0 : lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
911 : :
912 : 0 : double distanceToEndOfSegment = 0.0;
913 : 0 : int lastNodeInSegment = 0;
914 : : // finally, loop through all these straight segments. For each we create candidates along the straight segment.
915 : 0 : for ( int i = 0; i < straightSegmentLengths.count(); ++i )
916 : : {
917 : 0 : currentStraightSegmentLength = straightSegmentLengths.at( i );
918 : 0 : double currentSegmentAngle = straightSegmentAngles.at( i );
919 : 0 : lastNodeInSegment = extremeAngleNodes.at( i );
920 : 0 : double distanceToStartOfSegment = distanceToEndOfSegment;
921 : 0 : distanceToEndOfSegment = distanceToSegment[ lastNodeInSegment ];
922 : 0 : double distanceToCenterOfSegment = 0.5 * ( distanceToEndOfSegment + distanceToStartOfSegment );
923 : :
924 : 0 : if ( currentStraightSegmentLength < labelWidth )
925 : : // can't fit a label on here
926 : 0 : continue;
927 : :
928 : 0 : double currentDistanceAlongLine = distanceToStartOfSegment;
929 : : double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
930 : 0 : double candidateLength = 0.0;
931 : 0 : double cost = 0.0;
932 : 0 : double angle = 0.0;
933 : 0 : double beta = 0.0;
934 : :
935 : : //calculate some cost penalties
936 : 0 : double segmentCost = 1.0 - ( distanceToEndOfSegment - distanceToStartOfSegment ) / longestSegmentLength; // 0 -> 1 (lower for longer segments)
937 : 0 : double segmentAngleCost = 1 - std::fabs( std::fmod( currentSegmentAngle, M_PI ) - M_PI_2 ) / M_PI_2; // 0 -> 1, lower for more horizontal segments
938 : :
939 : 0 : while ( currentDistanceAlongLine + labelWidth < distanceToEndOfSegment )
940 : : {
941 : 0 : if ( pal->isCanceled() )
942 : : {
943 : 0 : return lPos.size();
944 : : }
945 : :
946 : : // calculate positions along linestring corresponding to start and end of current label candidate
947 : 0 : line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
948 : 0 : line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
949 : :
950 : 0 : candidateLength = std::sqrt( ( candidateEndX - candidateStartX ) * ( candidateEndX - candidateStartX ) + ( candidateEndY - candidateStartY ) * ( candidateEndY - candidateStartY ) );
951 : :
952 : :
953 : : // LOTS OF DIFFERENT COSTS TO BALANCE HERE - feel free to tweak these, but please add a unit test
954 : : // which covers the situation you are adjusting for (e.g., "given equal length lines, choose the more horizontal line")
955 : :
956 : 0 : cost = candidateLength / labelWidth;
957 : 0 : if ( cost > 0.98 )
958 : 0 : cost = 0.0001;
959 : : else
960 : : {
961 : : // jaggy line has a greater cost
962 : 0 : cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
963 : : }
964 : :
965 : : // penalize positions which are further from the straight segments's midpoint
966 : 0 : double labelCenter = currentDistanceAlongLine + labelWidth / 2.0;
967 : 0 : const bool placementIsFlexible = mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
968 : 0 : if ( placementIsFlexible )
969 : : {
970 : : // only apply this if labels are being placed toward the center of overall lines -- otherwise it messes with the distance from anchor cost
971 : 0 : double costCenter = 2 * std::fabs( labelCenter - distanceToCenterOfSegment ) / ( distanceToEndOfSegment - distanceToStartOfSegment ); // 0 -> 1
972 : 0 : cost += costCenter * 0.0005; // < 0, 0.0005 >
973 : 0 : }
974 : :
975 : 0 : if ( !closedLine )
976 : : {
977 : : // penalize positions which are further from absolute center of whole linestring
978 : : // this only applies to non closed linestrings, since the middle of a closed linestring is effectively arbitrary
979 : : // and irrelevant to labeling
980 : 0 : double costLineCenter = 2 * std::fabs( labelCenter - lineAnchorPoint ) / totalLineLength; // 0 -> 1
981 : 0 : cost += costLineCenter * 0.0005; // < 0, 0.0005 >
982 : 0 : }
983 : :
984 : 0 : if ( placementIsFlexible )
985 : : {
986 : 0 : cost += segmentCost * 0.0005; // prefer labels on longer straight segments
987 : 0 : cost += segmentAngleCost * 0.0001; // prefer more horizontal segments, but this is less important than length considerations
988 : 0 : }
989 : :
990 : 0 : if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
991 : : {
992 : 0 : angle = 0.0;
993 : 0 : }
994 : : else
995 : 0 : angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
996 : :
997 : 0 : labelWidth = getLabelWidth( angle );
998 : 0 : labelHeight = getLabelHeight( angle );
999 : 0 : beta = angle + M_PI_2;
1000 : :
1001 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Line )
1002 : : {
1003 : : // find out whether the line direction for this candidate is from right to left
1004 : 0 : bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1005 : : // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1006 : 0 : bool reversed = ( ( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1007 : 0 : bool aboveLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) );
1008 : 0 : bool belowLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) );
1009 : :
1010 : 0 : if ( belowLine )
1011 : : {
1012 : 0 : if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1013 : : {
1014 : 0 : const double candidateCost = cost + ( reversed ? 0 : 0.001 );
1015 : 0 : 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::QuadrantOver ) ); // Line
1016 : 0 : }
1017 : 0 : }
1018 : 0 : if ( aboveLine )
1019 : : {
1020 : 0 : if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) *distanceLineToLabel, candidateStartY + std::sin( beta ) *distanceLineToLabel, labelWidth, labelHeight, angle ) )
1021 : : {
1022 : 0 : const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1023 : 0 : 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::QuadrantOver ) ); // Line
1024 : 0 : }
1025 : 0 : }
1026 : 0 : if ( flags & QgsLabeling::LinePlacementFlag::OnLine )
1027 : : {
1028 : 0 : if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1029 : : {
1030 : 0 : const double candidateCost = cost + 0.002;
1031 : 0 : 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::QuadrantOver ) ); // Line
1032 : 0 : }
1033 : 0 : }
1034 : 0 : }
1035 : 0 : else if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Horizontal )
1036 : : {
1037 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) ); // Line
1038 : 0 : }
1039 : : else
1040 : : {
1041 : : // an invalid arrangement?
1042 : : }
1043 : :
1044 : 0 : currentDistanceAlongLine += lineStepDistance;
1045 : : }
1046 : 0 : }
1047 : :
1048 : 0 : return lPos.size();
1049 : 0 : }
1050 : :
1051 : 0 : std::size_t FeaturePart::createCandidatesAlongLineNearMidpoint( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, double initialCost, Pal *pal )
1052 : : {
1053 : 0 : double distanceLineToLabel = getLabelDistance();
1054 : :
1055 : 0 : double labelWidth = getLabelWidth();
1056 : 0 : double labelHeight = getLabelHeight();
1057 : :
1058 : : double angle;
1059 : : double cost;
1060 : :
1061 : 0 : QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
1062 : 0 : if ( flags == 0 )
1063 : 0 : flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
1064 : :
1065 : 0 : PointSet *line = mapShape;
1066 : 0 : int nbPoints = line->nbPoints;
1067 : 0 : std::vector< double > &x = line->x;
1068 : 0 : std::vector< double > &y = line->y;
1069 : :
1070 : 0 : std::vector< double > segmentLengths( nbPoints - 1 ); // segments lengths distance bw pt[i] && pt[i+1]
1071 : 0 : std::vector< double >distanceToSegment( nbPoints ); // absolute distance bw pt[0] and pt[i] along the line
1072 : :
1073 : 0 : double totalLineLength = 0.0; // line length
1074 : 0 : for ( int i = 0; i < line->nbPoints - 1; i++ )
1075 : : {
1076 : 0 : if ( i == 0 )
1077 : 0 : distanceToSegment[i] = 0;
1078 : : else
1079 : 0 : distanceToSegment[i] = distanceToSegment[i - 1] + segmentLengths[i - 1];
1080 : :
1081 : 0 : segmentLengths[i] = GeomFunction::dist_euc2d( x[i], y[i], x[i + 1], y[i + 1] );
1082 : 0 : totalLineLength += segmentLengths[i];
1083 : 0 : }
1084 : 0 : distanceToSegment[line->nbPoints - 1] = totalLineLength;
1085 : :
1086 : 0 : double lineStepDistance = ( totalLineLength - labelWidth ); // distance to move along line with each candidate
1087 : 0 : double currentDistanceAlongLine = 0;
1088 : :
1089 : 0 : const std::size_t candidateTargetCount = maximumLineCandidates();
1090 : :
1091 : 0 : if ( totalLineLength > labelWidth )
1092 : : {
1093 : 0 : lineStepDistance = std::min( std::min( labelHeight, labelWidth ), lineStepDistance / candidateTargetCount );
1094 : 0 : }
1095 : 0 : else if ( !line->isClosed() ) // line length < label width => centering label position
1096 : : {
1097 : 0 : currentDistanceAlongLine = - ( labelWidth - totalLineLength ) / 2.0;
1098 : 0 : lineStepDistance = -1;
1099 : 0 : totalLineLength = labelWidth;
1100 : 0 : }
1101 : : else
1102 : : {
1103 : : // closed line, not long enough for label => no candidates!
1104 : 0 : currentDistanceAlongLine = std::numeric_limits< double >::max();
1105 : : }
1106 : :
1107 : 0 : 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!
1108 : :
1109 : 0 : switch ( mLF->lineAnchorType() )
1110 : : {
1111 : : case QgsLabelLineSettings::AnchorType::HintOnly:
1112 : 0 : break;
1113 : :
1114 : : case QgsLabelLineSettings::AnchorType::Strict:
1115 : 0 : currentDistanceAlongLine = std::min( lineAnchorPoint, totalLineLength * 0.99 - labelWidth );
1116 : 0 : lineStepDistance = -1;
1117 : 0 : break;
1118 : : }
1119 : :
1120 : : double candidateLength;
1121 : : double beta;
1122 : : double candidateStartX, candidateStartY, candidateEndX, candidateEndY;
1123 : 0 : int i = 0;
1124 : 0 : while ( currentDistanceAlongLine <= totalLineLength - labelWidth || mLF->lineAnchorType() == QgsLabelLineSettings::AnchorType::Strict )
1125 : : {
1126 : 0 : if ( pal->isCanceled() )
1127 : : {
1128 : 0 : return lPos.size();
1129 : : }
1130 : :
1131 : : // calculate positions along linestring corresponding to start and end of current label candidate
1132 : 0 : line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine, &candidateStartX, &candidateStartY );
1133 : 0 : line->getPointByDistance( segmentLengths.data(), distanceToSegment.data(), currentDistanceAlongLine + labelWidth, &candidateEndX, &candidateEndY );
1134 : :
1135 : 0 : if ( currentDistanceAlongLine < 0 )
1136 : : {
1137 : : // label is bigger than line, use whole available line
1138 : 0 : candidateLength = std::sqrt( ( x[nbPoints - 1] - x[0] ) * ( x[nbPoints - 1] - x[0] )
1139 : 0 : + ( y[nbPoints - 1] - y[0] ) * ( y[nbPoints - 1] - y[0] ) );
1140 : 0 : }
1141 : : else
1142 : : {
1143 : 0 : candidateLength = std::sqrt( ( candidateEndX - candidateStartX ) * ( candidateEndX - candidateStartX ) + ( candidateEndY - candidateStartY ) * ( candidateEndY - candidateStartY ) );
1144 : : }
1145 : :
1146 : 0 : cost = candidateLength / labelWidth;
1147 : 0 : if ( cost > 0.98 )
1148 : 0 : cost = 0.0001;
1149 : : else
1150 : : {
1151 : : // jaggy line has a greater cost
1152 : 0 : cost = ( 1 - cost ) / 100; // ranges from 0.0001 to 0.01 (however a cost 0.005 is already a lot!)
1153 : : }
1154 : :
1155 : : // penalize positions which are further from the line's anchor point
1156 : 0 : double costCenter = std::fabs( lineAnchorPoint - ( currentDistanceAlongLine + labelWidth / 2 ) ) / totalLineLength; // <0, 0.5>
1157 : 0 : cost += costCenter / 1000; // < 0, 0.0005 >
1158 : 0 : cost += initialCost;
1159 : :
1160 : 0 : if ( qgsDoubleNear( candidateEndY, candidateStartY ) && qgsDoubleNear( candidateEndX, candidateStartX ) )
1161 : : {
1162 : 0 : angle = 0.0;
1163 : 0 : }
1164 : : else
1165 : 0 : angle = std::atan2( candidateEndY - candidateStartY, candidateEndX - candidateStartX );
1166 : :
1167 : 0 : labelWidth = getLabelWidth( angle );
1168 : 0 : labelHeight = getLabelHeight( angle );
1169 : 0 : beta = angle + M_PI_2;
1170 : :
1171 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Line )
1172 : : {
1173 : : // find out whether the line direction for this candidate is from right to left
1174 : 0 : bool isRightToLeft = ( angle > M_PI_2 || angle <= -M_PI_2 );
1175 : : // meaning of above/below may be reversed if using map orientation and the line has right-to-left direction
1176 : 0 : bool reversed = ( ( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) ? isRightToLeft : false );
1177 : 0 : bool aboveLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) );
1178 : 0 : bool belowLine = ( !reversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) || ( reversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) );
1179 : :
1180 : 0 : if ( aboveLine )
1181 : : {
1182 : 0 : if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX + std::cos( beta ) *distanceLineToLabel, candidateStartY + std::sin( beta ) *distanceLineToLabel, labelWidth, labelHeight, angle ) )
1183 : : {
1184 : 0 : const double candidateCost = cost + ( !reversed ? 0 : 0.001 ); // no extra cost for above line placements
1185 : 0 : 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::QuadrantOver ) ); // Line
1186 : 0 : }
1187 : 0 : }
1188 : 0 : if ( belowLine )
1189 : : {
1190 : 0 : if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - std::cos( beta ) * ( distanceLineToLabel + labelHeight ), candidateStartY - std::sin( beta ) * ( distanceLineToLabel + labelHeight ), labelWidth, labelHeight, angle ) )
1191 : : {
1192 : 0 : const double candidateCost = cost + ( !reversed ? 0.001 : 0 );
1193 : 0 : 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::QuadrantOver ) ); // Line
1194 : 0 : }
1195 : 0 : }
1196 : 0 : if ( flags & QgsLabeling::LinePlacementFlag::OnLine )
1197 : : {
1198 : 0 : if ( !mLF->permissibleZonePrepared() || GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), candidateStartX - labelHeight * std::cos( beta ) / 2, candidateStartY - labelHeight * std::sin( beta ) / 2, labelWidth, labelHeight, angle ) )
1199 : : {
1200 : 0 : const double candidateCost = cost + 0.002;
1201 : 0 : 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::QuadrantOver ) ); // Line
1202 : 0 : }
1203 : 0 : }
1204 : 0 : }
1205 : 0 : else if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Horizontal )
1206 : : {
1207 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( i, candidateStartX - labelWidth / 2, candidateStartY - labelHeight / 2, labelWidth, labelHeight, 0, cost, this, false, LabelPosition::QuadrantOver ) ); // Line
1208 : 0 : }
1209 : : else
1210 : : {
1211 : : // an invalid arrangement?
1212 : : }
1213 : :
1214 : 0 : currentDistanceAlongLine += lineStepDistance;
1215 : :
1216 : 0 : i++;
1217 : :
1218 : 0 : if ( lineStepDistance < 0 )
1219 : 0 : break;
1220 : : }
1221 : :
1222 : 0 : return lPos.size();
1223 : 0 : }
1224 : :
1225 : :
1226 : 0 : std::unique_ptr< LabelPosition > FeaturePart::curvedPlacementAtOffset( PointSet *path_positions, double *path_distances, int &orientation, const double offsetAlongLine, bool &reversed, bool &flip, bool applyAngleConstraints )
1227 : : {
1228 : 0 : double offsetAlongSegment = offsetAlongLine;
1229 : 0 : int index = 1;
1230 : : // Find index of segment corresponding to starting offset
1231 : 0 : while ( index < path_positions->nbPoints && offsetAlongSegment > path_distances[index] )
1232 : : {
1233 : 0 : offsetAlongSegment -= path_distances[index];
1234 : 0 : index += 1;
1235 : : }
1236 : 0 : if ( index >= path_positions->nbPoints )
1237 : : {
1238 : 0 : return nullptr;
1239 : : }
1240 : :
1241 : 0 : LabelInfo *li = mLF->curvedLabelInfo();
1242 : :
1243 : 0 : double string_height = li->label_height;
1244 : :
1245 : 0 : const double segment_length = path_distances[index];
1246 : 0 : if ( qgsDoubleNear( segment_length, 0.0 ) )
1247 : : {
1248 : : // Not allowed to place across on 0 length segments or discontinuities
1249 : 0 : return nullptr;
1250 : : }
1251 : :
1252 : 0 : if ( orientation == 0 ) // Must be map orientation
1253 : : {
1254 : : // Calculate the orientation based on the angle of the path segment under consideration
1255 : :
1256 : 0 : double _distance = offsetAlongSegment;
1257 : 0 : int endindex = index;
1258 : :
1259 : 0 : double startLabelX = 0;
1260 : 0 : double startLabelY = 0;
1261 : 0 : double endLabelX = 0;
1262 : 0 : double endLabelY = 0;
1263 : 0 : for ( int i = 0; i < li->char_num; i++ )
1264 : : {
1265 : 0 : LabelInfo::CharacterInfo &ci = li->char_info[i];
1266 : : double characterStartX, characterStartY;
1267 : 0 : if ( !nextCharPosition( ci.width, path_distances[endindex], path_positions, endindex, _distance, characterStartX, characterStartY, endLabelX, endLabelY ) )
1268 : : {
1269 : 0 : return nullptr;
1270 : : }
1271 : 0 : if ( i == 0 )
1272 : : {
1273 : 0 : startLabelX = characterStartX;
1274 : 0 : startLabelY = characterStartY;
1275 : 0 : }
1276 : 0 : }
1277 : :
1278 : : // Determine the angle of the path segment under consideration
1279 : 0 : double dx = endLabelX - startLabelX;
1280 : 0 : double dy = endLabelY - startLabelY;
1281 : 0 : const double lineAngle = std::atan2( -dy, dx ) * 180 / M_PI;
1282 : :
1283 : 0 : bool isRightToLeft = ( lineAngle > 90 || lineAngle < -90 );
1284 : 0 : reversed = isRightToLeft;
1285 : 0 : orientation = isRightToLeft ? -1 : 1;
1286 : 0 : }
1287 : :
1288 : 0 : if ( !showUprightLabels() )
1289 : : {
1290 : 0 : if ( orientation < 0 )
1291 : : {
1292 : 0 : flip = true; // Report to the caller, that the orientation is flipped
1293 : 0 : reversed = !reversed;
1294 : 0 : orientation = 1;
1295 : 0 : }
1296 : 0 : }
1297 : :
1298 : 0 : std::unique_ptr< LabelPosition > slp;
1299 : 0 : LabelPosition *slp_tmp = nullptr;
1300 : :
1301 : 0 : double old_x = path_positions->x[index - 1];
1302 : 0 : double old_y = path_positions->y[index - 1];
1303 : :
1304 : 0 : double new_x = path_positions->x[index];
1305 : 0 : double new_y = path_positions->y[index];
1306 : :
1307 : 0 : double dx = new_x - old_x;
1308 : 0 : double dy = new_y - old_y;
1309 : :
1310 : 0 : double angle = std::atan2( -dy, dx );
1311 : :
1312 : 0 : for ( int i = 0; i < li->char_num; i++ )
1313 : : {
1314 : 0 : double last_character_angle = angle;
1315 : :
1316 : : // grab the next character according to the orientation
1317 : 0 : LabelInfo::CharacterInfo &ci = ( orientation > 0 ? li->char_info[i] : li->char_info[li->char_num - i - 1] );
1318 : 0 : if ( qgsDoubleNear( ci.width, 0.0 ) )
1319 : : // Certain scripts rely on zero-width character, skip those to prevent failure (see #15801)
1320 : 0 : continue;
1321 : :
1322 : : double start_x, start_y, end_x, end_y;
1323 : 0 : if ( !nextCharPosition( ci.width, path_distances[index], path_positions, index, offsetAlongSegment, start_x, start_y, end_x, end_y ) )
1324 : : {
1325 : 0 : return nullptr;
1326 : : }
1327 : :
1328 : : // Calculate angle from the start of the character to the end based on start_/end_ position
1329 : 0 : angle = std::atan2( start_y - end_y, end_x - start_x );
1330 : :
1331 : : // Test last_character_angle vs angle
1332 : : // since our rendering angle has changed then check against our
1333 : : // max allowable angle change.
1334 : 0 : double angle_delta = last_character_angle - angle;
1335 : : // normalise between -180 and 180
1336 : 0 : while ( angle_delta > M_PI ) angle_delta -= 2 * M_PI;
1337 : 0 : while ( angle_delta < -M_PI ) angle_delta += 2 * M_PI;
1338 : 0 : if ( applyAngleConstraints && ( ( li->max_char_angle_inside > 0 && angle_delta > 0
1339 : 0 : && angle_delta > li->max_char_angle_inside * ( M_PI / 180 ) )
1340 : 0 : || ( li->max_char_angle_outside < 0 && angle_delta < 0
1341 : 0 : && angle_delta < li->max_char_angle_outside * ( M_PI / 180 ) ) ) )
1342 : : {
1343 : 0 : return nullptr;
1344 : : }
1345 : :
1346 : : // Shift the character downwards since the draw position is specified at the baseline
1347 : : // and we're calculating the mean line here
1348 : 0 : double dist = 0.9 * li->label_height / 2;
1349 : 0 : if ( orientation < 0 )
1350 : : {
1351 : 0 : dist = -dist;
1352 : 0 : flip = true;
1353 : 0 : }
1354 : 0 : start_x += dist * std::cos( angle + M_PI_2 );
1355 : 0 : start_y -= dist * std::sin( angle + M_PI_2 );
1356 : :
1357 : 0 : double render_angle = angle;
1358 : :
1359 : 0 : double render_x = start_x;
1360 : 0 : double render_y = start_y;
1361 : :
1362 : : // Center the text on the line
1363 : : //render_x -= ((string_height/2.0) - 1.0)*math.cos(render_angle+math.pi/2)
1364 : : //render_y += ((string_height/2.0) - 1.0)*math.sin(render_angle+math.pi/2)
1365 : :
1366 : 0 : if ( orientation < 0 )
1367 : : {
1368 : : // rotate in place
1369 : 0 : render_x += ci.width * std::cos( render_angle ); //- (string_height-2)*sin(render_angle);
1370 : 0 : render_y -= ci.width * std::sin( render_angle ); //+ (string_height-2)*cos(render_angle);
1371 : 0 : render_angle += M_PI;
1372 : 0 : }
1373 : :
1374 : 0 : std::unique_ptr< LabelPosition > tmp = std::make_unique< LabelPosition >( 0, render_x /*- xBase*/, render_y /*- yBase*/, ci.width, string_height, -render_angle, 0.0001, this, false, LabelPosition::QuadrantOver );
1375 : 0 : tmp->setPartId( orientation > 0 ? i : li->char_num - i - 1 );
1376 : 0 : LabelPosition *next = tmp.get();
1377 : 0 : if ( !slp )
1378 : 0 : slp = std::move( tmp );
1379 : : else
1380 : 0 : slp_tmp->setNextPart( std::move( tmp ) );
1381 : 0 : slp_tmp = next;
1382 : :
1383 : : // Normalise to 0 <= angle < 2PI
1384 : 0 : while ( render_angle >= 2 * M_PI ) render_angle -= 2 * M_PI;
1385 : 0 : while ( render_angle < 0 ) render_angle += 2 * M_PI;
1386 : :
1387 : 0 : if ( render_angle > M_PI_2 && render_angle < 1.5 * M_PI )
1388 : 0 : slp->incrementUpsideDownCharCount();
1389 : 0 : }
1390 : :
1391 : 0 : return slp;
1392 : 0 : }
1393 : :
1394 : 0 : static std::unique_ptr< LabelPosition > _createCurvedCandidate( LabelPosition *lp, double angle, double dist )
1395 : : {
1396 : 0 : std::unique_ptr< LabelPosition > newLp = std::make_unique< LabelPosition >( *lp );
1397 : 0 : newLp->offsetPosition( dist * std::cos( angle + M_PI_2 ), dist * std::sin( angle + M_PI_2 ) );
1398 : 0 : return newLp;
1399 : 0 : }
1400 : :
1401 : 0 : std::size_t FeaturePart::createCurvedCandidatesAlongLine( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, bool allowOverrun, Pal *pal )
1402 : : {
1403 : 0 : LabelInfo *li = mLF->curvedLabelInfo();
1404 : :
1405 : : // label info must be present
1406 : 0 : if ( !li || li->char_num == 0 )
1407 : 0 : return 0;
1408 : :
1409 : : // TODO - we may need an explicit penalty for overhanging labels. Currently, they are penalized just because they
1410 : : // are further from the line center, so non-overhanding placements are picked where possible.
1411 : :
1412 : 0 : double totalCharacterWidth = 0;
1413 : 0 : for ( int i = 0; i < li->char_num; ++i )
1414 : 0 : totalCharacterWidth += li->char_info[ i ].width;
1415 : :
1416 : 0 : std::unique_ptr< PointSet > expanded;
1417 : 0 : double shapeLength = mapShape->length();
1418 : :
1419 : 0 : if ( totalRepeats() > 1 )
1420 : 0 : allowOverrun = false;
1421 : :
1422 : : // label overrun should NEVER exceed the label length (or labels would sit off in space).
1423 : : // in fact, let's require that a minimum of 5% of the label text has to sit on the feature,
1424 : : // as we don't want a label sitting right at the start or end corner of a line
1425 : 0 : double overrun = std::min( mLF->overrunDistance(), totalCharacterWidth * 0.95 );
1426 : 0 : if ( totalCharacterWidth > shapeLength )
1427 : : {
1428 : 0 : if ( !allowOverrun || shapeLength < totalCharacterWidth - 2 * overrun )
1429 : : {
1430 : : // label doesn't fit on this line, don't waste time trying to make candidates
1431 : 0 : return 0;
1432 : : }
1433 : 0 : }
1434 : :
1435 : 0 : if ( allowOverrun && overrun > 0 )
1436 : : {
1437 : : // expand out line on either side to fit label
1438 : 0 : expanded = mapShape->clone();
1439 : 0 : expanded->extendLineByDistance( overrun, overrun, mLF->overrunSmoothDistance() );
1440 : 0 : mapShape = expanded.get();
1441 : 0 : shapeLength = mapShape->length();
1442 : 0 : }
1443 : :
1444 : : // distance calculation
1445 : 0 : std::unique_ptr< double [] > path_distances = std::make_unique<double[]>( mapShape->nbPoints );
1446 : 0 : double total_distance = 0;
1447 : 0 : double old_x = -1.0, old_y = -1.0;
1448 : 0 : for ( int i = 0; i < mapShape->nbPoints; i++ )
1449 : : {
1450 : 0 : if ( i == 0 )
1451 : 0 : path_distances[i] = 0;
1452 : : else
1453 : 0 : path_distances[i] = std::sqrt( std::pow( old_x - mapShape->x[i], 2 ) + std::pow( old_y - mapShape->y[i], 2 ) );
1454 : 0 : old_x = mapShape->x[i];
1455 : 0 : old_y = mapShape->y[i];
1456 : :
1457 : 0 : total_distance += path_distances[i];
1458 : 0 : }
1459 : :
1460 : 0 : if ( qgsDoubleNear( total_distance, 0.0 ) )
1461 : : {
1462 : 0 : return 0;
1463 : : }
1464 : :
1465 : 0 : const double lineAnchorPoint = total_distance * mLF->lineAnchorPercent();
1466 : :
1467 : 0 : if ( pal->isCanceled() )
1468 : 0 : return 0;
1469 : :
1470 : 0 : std::vector< std::unique_ptr< LabelPosition >> positions;
1471 : 0 : const std::size_t candidateTargetCount = maximumLineCandidates();
1472 : 0 : double delta = std::max( li->label_height / 6, total_distance / candidateTargetCount );
1473 : :
1474 : 0 : QgsLabeling::LinePlacementFlags flags = mLF->arrangementFlags();
1475 : 0 : if ( flags == 0 )
1476 : 0 : flags = QgsLabeling::LinePlacementFlag::OnLine; // default flag
1477 : :
1478 : : // generate curved labels
1479 : 0 : double distanceAlongLineToStartCandidate = 0;
1480 : 0 : bool singleCandidateOnly = false;
1481 : 0 : switch ( mLF->lineAnchorType() )
1482 : : {
1483 : : case QgsLabelLineSettings::AnchorType::HintOnly:
1484 : 0 : break;
1485 : :
1486 : : case QgsLabelLineSettings::AnchorType::Strict:
1487 : 0 : distanceAlongLineToStartCandidate = std::min( lineAnchorPoint, total_distance * 0.99 - getLabelWidth() );
1488 : 0 : singleCandidateOnly = true;
1489 : 0 : break;
1490 : : }
1491 : :
1492 : 0 : for ( ; distanceAlongLineToStartCandidate <= total_distance; distanceAlongLineToStartCandidate += delta )
1493 : : {
1494 : 0 : bool flip = false;
1495 : : // placements may need to be reversed if using map orientation and the line has right-to-left direction
1496 : 0 : bool reversed = false;
1497 : :
1498 : 0 : if ( pal->isCanceled() )
1499 : 0 : return 0;
1500 : :
1501 : : // an orientation of 0 means try both orientations and choose the best
1502 : 0 : int orientation = 0;
1503 : 0 : if ( !( flags & QgsLabeling::LinePlacementFlag::MapOrientation ) )
1504 : : {
1505 : : //... but if we are using line orientation flags, then we can only accept a single orientation,
1506 : : // as we need to ensure that the labels fall inside or outside the polyline or polygon (and not mixed)
1507 : 0 : orientation = 1;
1508 : 0 : }
1509 : :
1510 : 0 : std::unique_ptr< LabelPosition > slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly );
1511 : 0 : if ( !slp )
1512 : 0 : continue;
1513 : :
1514 : : // If we placed too many characters upside down
1515 : 0 : if ( slp->upsideDownCharCount() >= li->char_num / 2.0 )
1516 : : {
1517 : : // if labels should be shown upright then retry with the opposite orientation
1518 : 0 : if ( ( showUprightLabels() && !flip ) )
1519 : : {
1520 : 0 : orientation = -orientation;
1521 : 0 : slp = curvedPlacementAtOffset( mapShape, path_distances.get(), orientation, distanceAlongLineToStartCandidate, reversed, flip, !singleCandidateOnly );
1522 : 0 : }
1523 : 0 : }
1524 : 0 : if ( !slp )
1525 : 0 : continue;
1526 : :
1527 : : // evaluate cost
1528 : 0 : double angle_diff = 0.0, angle_last = 0.0, diff;
1529 : 0 : LabelPosition *tmp = slp.get();
1530 : 0 : double sin_avg = 0, cos_avg = 0;
1531 : 0 : while ( tmp )
1532 : : {
1533 : 0 : if ( tmp != slp.get() ) // not first?
1534 : : {
1535 : 0 : diff = std::fabs( tmp->getAlpha() - angle_last );
1536 : 0 : if ( diff > 2 * M_PI ) diff -= 2 * M_PI;
1537 : 0 : diff = std::min( diff, 2 * M_PI - diff ); // difference 350 deg is actually just 10 deg...
1538 : 0 : angle_diff += diff;
1539 : 0 : }
1540 : :
1541 : 0 : sin_avg += std::sin( tmp->getAlpha() );
1542 : 0 : cos_avg += std::cos( tmp->getAlpha() );
1543 : 0 : angle_last = tmp->getAlpha();
1544 : 0 : tmp = tmp->nextPart();
1545 : : }
1546 : :
1547 : : // if anchor placement is towards start or end of line, we need to slightly tweak the costs to ensure that the
1548 : : // anchor weighting is sufficient to push labels towards start/end
1549 : 0 : const bool anchorIsFlexiblePlacement = !singleCandidateOnly && mLF->lineAnchorPercent() > 0.1 && mLF->lineAnchorPercent() < 0.9;
1550 : 0 : double angle_diff_avg = li->char_num > 1 ? ( angle_diff / ( li->char_num - 1 ) ) : 0; // <0, pi> but pi/8 is much already
1551 : 0 : double cost = angle_diff_avg / 100; // <0, 0.031 > but usually <0, 0.003 >
1552 : 0 : if ( cost < 0.0001 )
1553 : 0 : cost = 0.0001;
1554 : :
1555 : : // penalize positions which are further from the line's anchor point
1556 : 0 : double labelCenter = distanceAlongLineToStartCandidate + getLabelWidth() / 2;
1557 : 0 : double costCenter = std::fabs( lineAnchorPoint - labelCenter ) / total_distance; // <0, 0.5>
1558 : 0 : cost += costCenter / ( anchorIsFlexiblePlacement ? 100 : 10 ); // < 0, 0.005 >, or <0, 0.05> if preferring placement close to start/end of line
1559 : 0 : slp->setCost( cost );
1560 : :
1561 : : // average angle is calculated with respect to periodicity of angles
1562 : 0 : double angle_avg = std::atan2( sin_avg / li->char_num, cos_avg / li->char_num );
1563 : 0 : bool localreversed = flip ? !reversed : reversed;
1564 : : // displacement - we loop through 3 times, generating above, online then below line placements successively
1565 : 0 : for ( int i = 0; i <= 2; ++i )
1566 : : {
1567 : 0 : std::unique_ptr< LabelPosition > p;
1568 : 0 : if ( i == 0 && ( ( !localreversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) || ( localreversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) ) )
1569 : 0 : p = _createCurvedCandidate( slp.get(), angle_avg, mLF->distLabel() + li->label_height / 2 );
1570 : 0 : if ( i == 1 && flags & QgsLabeling::LinePlacementFlag::OnLine )
1571 : : {
1572 : 0 : p = _createCurvedCandidate( slp.get(), angle_avg, 0 );
1573 : 0 : p->setCost( p->cost() + 0.002 );
1574 : 0 : }
1575 : 0 : if ( i == 2 && ( ( !localreversed && ( flags & QgsLabeling::LinePlacementFlag::BelowLine ) ) || ( localreversed && ( flags & QgsLabeling::LinePlacementFlag::AboveLine ) ) ) )
1576 : : {
1577 : 0 : p = _createCurvedCandidate( slp.get(), angle_avg, -li->label_height / 2 - mLF->distLabel() );
1578 : 0 : p->setCost( p->cost() + 0.001 );
1579 : 0 : }
1580 : :
1581 : 0 : if ( p && mLF->permissibleZonePrepared() )
1582 : : {
1583 : 0 : bool within = true;
1584 : 0 : LabelPosition *currentPos = p.get();
1585 : 0 : while ( within && currentPos )
1586 : : {
1587 : 0 : within = GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), currentPos->getX(), currentPos->getY(), currentPos->getWidth(), currentPos->getHeight(), currentPos->getAlpha() );
1588 : 0 : currentPos = currentPos->nextPart();
1589 : : }
1590 : 0 : if ( !within )
1591 : : {
1592 : 0 : p.reset();
1593 : 0 : }
1594 : 0 : }
1595 : :
1596 : 0 : if ( p )
1597 : 0 : positions.emplace_back( std::move( p ) );
1598 : 0 : }
1599 : 0 : if ( singleCandidateOnly )
1600 : 0 : break;
1601 : 0 : }
1602 : :
1603 : 0 : for ( std::unique_ptr< LabelPosition > &pos : positions )
1604 : : {
1605 : 0 : lPos.emplace_back( std::move( pos ) );
1606 : : }
1607 : :
1608 : 0 : return positions.size();
1609 : 0 : }
1610 : :
1611 : : /*
1612 : : * seg 2
1613 : : * pt3 ____________pt2
1614 : : * ¦ ¦
1615 : : * ¦ ¦
1616 : : * seg 3 ¦ BBOX ¦ seg 1
1617 : : * ¦ ¦
1618 : : * ¦____________¦
1619 : : * pt0 seg 0 pt1
1620 : : *
1621 : : */
1622 : :
1623 : 0 : std::size_t FeaturePart::createCandidatesForPolygon( std::vector< std::unique_ptr< LabelPosition > > &lPos, PointSet *mapShape, Pal *pal )
1624 : : {
1625 : 0 : double labelWidth = getLabelWidth();
1626 : 0 : double labelHeight = getLabelHeight();
1627 : :
1628 : 0 : const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
1629 : 0 : const std::size_t targetPolygonCandidates = maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * area() ) ) )
1630 : : : 0;
1631 : :
1632 : 0 : const double totalArea = area();
1633 : :
1634 : 0 : mapShape->parent = nullptr;
1635 : :
1636 : 0 : if ( pal->isCanceled() )
1637 : 0 : return 0;
1638 : :
1639 : 0 : QLinkedList<PointSet *> shapes_final = splitPolygons( mapShape, labelWidth, labelHeight );
1640 : : #if 0
1641 : : QgsDebugMsg( QStringLiteral( "PAL split polygons resulted in:" ) );
1642 : : for ( PointSet *ps : shapes_final )
1643 : : {
1644 : : QgsDebugMsg( ps->toWkt() );
1645 : : }
1646 : : #endif
1647 : :
1648 : 0 : std::size_t nbp = 0;
1649 : :
1650 : 0 : if ( !shapes_final.isEmpty() )
1651 : : {
1652 : 0 : int id = 0; // ids for candidates
1653 : : double dlx, dly; // delta from label center and bottom-left corner
1654 : 0 : double alpha = 0.0; // rotation for the label
1655 : : double px, py;
1656 : :
1657 : : double beta;
1658 : 0 : double diago = std::sqrt( labelWidth * labelWidth / 4.0 + labelHeight * labelHeight / 4 );
1659 : : double rx, ry;
1660 : 0 : std::vector< OrientedConvexHullBoundingBox > boxes;
1661 : 0 : boxes.reserve( shapes_final.size() );
1662 : :
1663 : : // Compute bounding box for each finalShape
1664 : 0 : while ( !shapes_final.isEmpty() )
1665 : : {
1666 : 0 : PointSet *shape = shapes_final.takeFirst();
1667 : 0 : bool ok = false;
1668 : 0 : OrientedConvexHullBoundingBox box = shape->computeConvexHullOrientedBoundingBox( ok );
1669 : 0 : if ( ok )
1670 : 0 : boxes.emplace_back( box );
1671 : :
1672 : 0 : if ( shape->parent )
1673 : 0 : delete shape;
1674 : : }
1675 : :
1676 : 0 : if ( pal->isCanceled() )
1677 : 0 : return 0;
1678 : :
1679 : 0 : double densityX = 1.0 / std::sqrt( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() );
1680 : 0 : double densityY = densityX;
1681 : 0 : int numTry = 0;
1682 : :
1683 : : //fit in polygon only mode slows down calculation a lot, so if it's enabled
1684 : : //then use a smaller limit for number of iterations
1685 : 0 : int maxTry = mLF->permissibleZonePrepared() ? 7 : 10;
1686 : :
1687 : 0 : std::size_t numberCandidatesGenerated = 0;
1688 : :
1689 : 0 : do
1690 : : {
1691 : 0 : for ( OrientedConvexHullBoundingBox &box : boxes )
1692 : : {
1693 : : // there is two possibilities here:
1694 : : // 1. no maximum candidates for polygon setting is in effect (i.e. maxPolygonCandidates == 0). In that case,
1695 : : // we base our dx/dy on the current maximumPolygonCandidatesPerMapUnitSquared value. That should give us the desired
1696 : : // density of candidates straight up. Easy!
1697 : : // 2. a maximum candidate setting IS in effect. In that case, we want to generate a good initial estimate for dx/dy
1698 : : // which gives us a good spatial coverage of the polygon while roughly matching the desired maximum number of candidates.
1699 : : // If dx/dy is too small, then too many candidates will be generated, which is both slow AND results in poor coverage of the
1700 : : // polygon (after culling candidates to the max number, only those clustered around the polygon's pole of inaccessibility
1701 : : // will remain).
1702 : 0 : double dx = densityX;
1703 : 0 : double dy = densityY;
1704 : 0 : if ( numTry == 0 && maxPolygonCandidates > 0 )
1705 : : {
1706 : : // scale maxPolygonCandidates for just this convex hull
1707 : 0 : const double boxArea = box.width * box.length;
1708 : 0 : double maxThisBox = targetPolygonCandidates * boxArea / totalArea;
1709 : 0 : dx = std::max( dx, std::sqrt( boxArea / maxThisBox ) * 0.8 );
1710 : 0 : dy = dx;
1711 : 0 : }
1712 : :
1713 : 0 : if ( pal->isCanceled() )
1714 : 0 : return numberCandidatesGenerated;
1715 : :
1716 : 0 : if ( ( box.length * box.width ) > ( xmax - xmin ) * ( ymax - ymin ) * 5 )
1717 : : {
1718 : : // Very Large BBOX (should never occur)
1719 : 0 : continue;
1720 : : }
1721 : :
1722 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Horizontal && mLF->permissibleZonePrepared() )
1723 : : {
1724 : : //check width/height of bbox is sufficient for label
1725 : 0 : if ( mLF->permissibleZone().boundingBox().width() < labelWidth ||
1726 : 0 : mLF->permissibleZone().boundingBox().height() < labelHeight )
1727 : : {
1728 : : //no way label can fit in this box, skip it
1729 : 0 : continue;
1730 : : }
1731 : 0 : }
1732 : :
1733 : 0 : bool enoughPlace = false;
1734 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Free )
1735 : : {
1736 : 0 : enoughPlace = true;
1737 : 0 : px = ( box.x[0] + box.x[2] ) / 2 - labelWidth;
1738 : 0 : py = ( box.y[0] + box.y[2] ) / 2 - labelHeight;
1739 : : int i, j;
1740 : :
1741 : : // Virtual label: center on bbox center, label size = 2x original size
1742 : : // alpha = 0.
1743 : : // If all corner are in bbox then place candidates horizontaly
1744 : 0 : for ( rx = px, i = 0; i < 2; rx = rx + 2 * labelWidth, i++ )
1745 : : {
1746 : 0 : for ( ry = py, j = 0; j < 2; ry = ry + 2 * labelHeight, j++ )
1747 : : {
1748 : 0 : if ( !mapShape->containsPoint( rx, ry ) )
1749 : : {
1750 : 0 : enoughPlace = false;
1751 : 0 : break;
1752 : : }
1753 : 0 : }
1754 : 0 : if ( !enoughPlace )
1755 : : {
1756 : 0 : break;
1757 : : }
1758 : 0 : }
1759 : :
1760 : 0 : } // arrangement== FREE ?
1761 : :
1762 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Horizontal || enoughPlace )
1763 : : {
1764 : 0 : alpha = 0.0; // HORIZ
1765 : 0 : }
1766 : 0 : else if ( box.length > 1.5 * labelWidth && box.width > 1.5 * labelWidth )
1767 : : {
1768 : 0 : if ( box.alpha <= M_PI_4 )
1769 : : {
1770 : 0 : alpha = box.alpha;
1771 : 0 : }
1772 : : else
1773 : : {
1774 : 0 : alpha = box.alpha - M_PI_2;
1775 : : }
1776 : 0 : }
1777 : 0 : else if ( box.length > box.width )
1778 : : {
1779 : 0 : alpha = box.alpha - M_PI_2;
1780 : 0 : }
1781 : : else
1782 : : {
1783 : 0 : alpha = box.alpha;
1784 : : }
1785 : :
1786 : 0 : beta = std::atan2( labelHeight, labelWidth ) + alpha;
1787 : :
1788 : :
1789 : : //alpha = box->alpha;
1790 : :
1791 : : // delta from label center and down-left corner
1792 : 0 : dlx = std::cos( beta ) * diago;
1793 : 0 : dly = std::sin( beta ) * diago;
1794 : :
1795 : 0 : double px0 = box.width / 2.0;
1796 : 0 : double py0 = box.length / 2.0;
1797 : :
1798 : 0 : px0 -= std::ceil( px0 / dx ) * dx;
1799 : 0 : py0 -= std::ceil( py0 / dy ) * dy;
1800 : :
1801 : 0 : for ( px = px0; px <= box.width; px += dx )
1802 : : {
1803 : 0 : if ( pal->isCanceled() )
1804 : 0 : break;
1805 : :
1806 : 0 : for ( py = py0; py <= box.length; py += dy )
1807 : : {
1808 : :
1809 : 0 : rx = std::cos( box.alpha ) * px + std::cos( box.alpha - M_PI_2 ) * py;
1810 : 0 : ry = std::sin( box.alpha ) * px + std::sin( box.alpha - M_PI_2 ) * py;
1811 : :
1812 : 0 : rx += box.x[0];
1813 : 0 : ry += box.y[0];
1814 : :
1815 : 0 : if ( mLF->permissibleZonePrepared() )
1816 : : {
1817 : 0 : if ( GeomFunction::containsCandidate( mLF->permissibleZonePrepared(), rx - dlx, ry - dly, labelWidth, labelHeight, alpha ) )
1818 : : {
1819 : : // cost is set to minimal value, evaluated later
1820 : 0 : lPos.emplace_back( std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, false, LabelPosition::QuadrantOver ) );
1821 : 0 : numberCandidatesGenerated++;
1822 : 0 : }
1823 : 0 : }
1824 : : else
1825 : : {
1826 : : // TODO - this should be an intersection test, not just a contains test of the candidate centroid
1827 : : // because in some cases we would want to allow candidates which mostly overlap the polygon even though
1828 : : // their centroid doesn't overlap (e.g. a "U" shaped polygon)
1829 : : // but the bugs noted in CostCalculator currently prevent this
1830 : 0 : if ( mapShape->containsPoint( rx, ry ) )
1831 : : {
1832 : 0 : std::unique_ptr< LabelPosition > potentialCandidate = std::make_unique< LabelPosition >( id++, rx - dlx, ry - dly, labelWidth, labelHeight, alpha, 0.0001, this, false, LabelPosition::QuadrantOver );
1833 : : // cost is set to minimal value, evaluated later
1834 : 0 : lPos.emplace_back( std::move( potentialCandidate ) );
1835 : 0 : numberCandidatesGenerated++;
1836 : 0 : }
1837 : : }
1838 : 0 : }
1839 : 0 : }
1840 : : } // forall box
1841 : :
1842 : 0 : nbp = numberCandidatesGenerated;
1843 : 0 : if ( maxPolygonCandidates > 0 && nbp < targetPolygonCandidates )
1844 : : {
1845 : 0 : densityX /= 2;
1846 : 0 : densityY /= 2;
1847 : 0 : numTry++;
1848 : 0 : }
1849 : : else
1850 : : {
1851 : 0 : break;
1852 : : }
1853 : 0 : }
1854 : 0 : while ( numTry < maxTry );
1855 : :
1856 : 0 : nbp = numberCandidatesGenerated;
1857 : 0 : }
1858 : : else
1859 : : {
1860 : 0 : nbp = 0;
1861 : : }
1862 : :
1863 : 0 : return nbp;
1864 : 0 : }
1865 : :
1866 : 0 : std::size_t FeaturePart::createCandidatesOutsidePolygon( std::vector<std::unique_ptr<LabelPosition> > &lPos, Pal *pal )
1867 : : {
1868 : : // calculate distance between horizontal lines
1869 : 0 : const std::size_t maxPolygonCandidates = mLF->layer()->maximumPolygonLabelCandidates();
1870 : 0 : std::size_t candidatesCreated = 0;
1871 : :
1872 : 0 : double labelWidth = getLabelWidth();
1873 : 0 : double labelHeight = getLabelHeight();
1874 : 0 : double distanceToLabel = getLabelDistance();
1875 : 0 : const QgsMargins &visualMargin = mLF->visualMargin();
1876 : :
1877 : : /*
1878 : : * From Rylov & Reimer (2016) "A practical algorithm for the external annotation of area features":
1879 : : *
1880 : : * The list of rules adapted to the
1881 : : * needs of externally labelling areal features is as follows:
1882 : : * R1. Labels should be placed horizontally.
1883 : : * R2. Label should be placed entirely outside at some
1884 : : * distance from the area feature.
1885 : : * R3. Name should not cross the boundary of its area
1886 : : * feature.
1887 : : * R4. The name should be placed in way that takes into
1888 : : * account the shape of the feature by achieving a
1889 : : * balance between the feature and its name, emphasizing their relationship.
1890 : : * R5. The lettering to the right and slightly above the
1891 : : * symbol is prioritized.
1892 : : *
1893 : : * In the following subsections we utilize four of the five rules
1894 : : * for two subtasks of label placement, namely, for candidate
1895 : : * positions generation (R1, R2, and R3) and for measuring their
1896 : : * ‘goodness’ (R4). The rule R5 is applicable only in the case when
1897 : : * the area of a polygonal feature is small and the feature can be
1898 : : * treated and labelled as a point-feature
1899 : : */
1900 : :
1901 : : /*
1902 : : * QGIS approach (cite Dawson (2020) if you want ;) )
1903 : : *
1904 : : * We differ from the horizontal sweep line approach described by Rylov & Reimer and instead
1905 : : * rely on just generating a set of points at regular intervals along the boundary of the polygon (exterior ring).
1906 : : *
1907 : : * In practice, this generates similar results as Rylov & Reimer, but has the additional benefits that:
1908 : : * 1. It avoids the need to calculate intersections between the sweep line and the polygon
1909 : : * 2. For horizontal or near horizontal segments, Rylov & Reimer propose generating evenly spaced points along
1910 : : * these segments-- i.e. the same approach as we do for the whole polygon
1911 : : * 3. It's easier to determine in advance exactly how many candidate positions we'll be generating, and accordingly
1912 : : * we can easily pick the distance between points along the exterior ring so that the number of positions generated
1913 : : * matches our target number (targetPolygonCandidates)
1914 : : */
1915 : :
1916 : : // TO consider -- for very small polygons (wrt label size), treat them just like a point feature?
1917 : :
1918 : : double cx, cy;
1919 : 0 : getCentroid( cx, cy, false );
1920 : :
1921 : 0 : GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
1922 : :
1923 : : // be a bit sneaky and only buffer out 50% here, and then do the remaining 50% when we make the label candidate itself.
1924 : : // this avoids candidates being created immediately over the buffered ring and always intersecting with it...
1925 : 0 : geos::unique_ptr buffer( GEOSBuffer_r( ctxt, geos(), distanceToLabel * 0.5, 1 ) );
1926 : 0 : std::unique_ptr< QgsAbstractGeometry> gg( QgsGeos::fromGeos( buffer.get() ) );
1927 : :
1928 : 0 : geos::prepared_unique_ptr preparedBuffer( GEOSPrepare_r( ctxt, buffer.get() ) );
1929 : :
1930 : 0 : const QgsPolygon *poly = qgsgeometry_cast< const QgsPolygon * >( gg.get() );
1931 : 0 : if ( !poly )
1932 : 0 : return candidatesCreated;
1933 : :
1934 : 0 : const QgsLineString *ring = qgsgeometry_cast< const QgsLineString *>( poly->exteriorRing() );
1935 : 0 : if ( !ring )
1936 : 0 : return candidatesCreated;
1937 : :
1938 : : // 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,
1939 : : // i.e a loooooong skinny polygon with small area should still generate a large number of candidates)
1940 : 0 : const double ringLength = ring->length();
1941 : 0 : const double circleArea = std::pow( ringLength, 2 ) / ( 4 * M_PI );
1942 : 0 : const std::size_t candidatesForArea = static_cast< std::size_t>( std::ceil( mLF->layer()->mPal->maximumPolygonCandidatesPerMapUnitSquared() * circleArea ) );
1943 : 0 : const std::size_t targetPolygonCandidates = std::max( static_cast< std::size_t >( 16 ), maxPolygonCandidates > 0 ? std::min( maxPolygonCandidates, candidatesForArea ) : candidatesForArea );
1944 : :
1945 : : // assume each position generates one candidate
1946 : 0 : const double delta = ringLength / targetPolygonCandidates;
1947 : 0 : geos::unique_ptr geosPoint;
1948 : :
1949 : 0 : const double maxDistCentroidToLabelX = std::max( xmax - cx, cx - xmin ) + distanceToLabel;
1950 : 0 : const double maxDistCentroidToLabelY = std::max( ymax - cy, cy - ymin ) + distanceToLabel;
1951 : 0 : const double estimateOfMaxPossibleDistanceCentroidToLabel = std::sqrt( maxDistCentroidToLabelX * maxDistCentroidToLabelX + maxDistCentroidToLabelY * maxDistCentroidToLabelY );
1952 : :
1953 : : // Satisfy R1: Labels should be placed horizontally.
1954 : 0 : const double labelAngle = 0;
1955 : :
1956 : 0 : std::size_t i = lPos.size();
1957 : 0 : auto addCandidate = [&]( double x, double y, QgsPalLayerSettings::PredefinedPointPosition position )
1958 : : {
1959 : 0 : double labelX = 0;
1960 : 0 : double labelY = 0;
1961 : 0 : LabelPosition::Quadrant quadrant = LabelPosition::QuadrantAboveLeft;
1962 : :
1963 : : // Satisfy R2: Label should be placed entirely outside at some distance from the area feature.
1964 : 0 : createCandidateAtOrderedPositionOverPoint( labelX, labelY, quadrant, x, y, labelWidth, labelHeight, position, distanceToLabel * 0.5, visualMargin, 0, 0 );
1965 : :
1966 : 0 : std::unique_ptr< LabelPosition > candidate = std::make_unique< LabelPosition >( i, labelX, labelY, labelWidth, labelHeight, labelAngle, 0, this, false, quadrant );
1967 : 0 : if ( candidate->intersects( preparedBuffer.get() ) )
1968 : : {
1969 : : // satisfy R3. Name should not cross the boundary of its area feature.
1970 : :
1971 : : // actually, we use the buffered geometry here, because a label shouldn't be closer to the polygon then the minimum distance value
1972 : 0 : return;
1973 : : }
1974 : :
1975 : : // cost candidates by their distance to the feature's centroid (following Rylov & Reimer)
1976 : :
1977 : : // Satisfy R4. The name should be placed in way that takes into
1978 : : // account the shape of the feature by achieving a
1979 : : // balance between the feature and its name, emphasizing their relationship.
1980 : :
1981 : :
1982 : : // here we deviate a little from R&R, and instead of just calculating the centroid distance
1983 : : // to centroid of label, we calculate the distance from the centroid to the nearest point on the label
1984 : :
1985 : 0 : const double centroidDistance = candidate->getDistanceToPoint( cx, cy );
1986 : 0 : const double centroidCost = centroidDistance / estimateOfMaxPossibleDistanceCentroidToLabel;
1987 : 0 : candidate->setCost( centroidCost );
1988 : :
1989 : 0 : lPos.emplace_back( std::move( candidate ) );
1990 : 0 : candidatesCreated++;
1991 : 0 : ++i;
1992 : 0 : };
1993 : :
1994 : 0 : ring->visitPointsByRegularDistance( delta, [&]( double x, double y, double, double,
1995 : : double startSegmentX, double startSegmentY, double, double,
1996 : : double endSegmentX, double endSegmentY, double, double )
1997 : : {
1998 : : // get normal angle for segment
1999 : 0 : float angle = atan2( static_cast< float >( endSegmentY - startSegmentY ), static_cast< float >( endSegmentX - startSegmentX ) ) * 180 / M_PI;
2000 : 0 : if ( angle < 0 )
2001 : 0 : angle += 360;
2002 : :
2003 : : // adapted fom Rylov & Reimer figure 9
2004 : 0 : if ( angle >= 0 && angle <= 5 )
2005 : : {
2006 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopMiddle );
2007 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopLeft );
2008 : 0 : }
2009 : 0 : else if ( angle <= 85 )
2010 : : {
2011 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopLeft );
2012 : 0 : }
2013 : 0 : else if ( angle <= 90 )
2014 : : {
2015 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopLeft );
2016 : 0 : addCandidate( x, y, QgsPalLayerSettings::MiddleLeft );
2017 : 0 : }
2018 : :
2019 : 0 : else if ( angle <= 95 )
2020 : : {
2021 : 0 : addCandidate( x, y, QgsPalLayerSettings::MiddleLeft );
2022 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomLeft );
2023 : 0 : }
2024 : 0 : else if ( angle <= 175 )
2025 : : {
2026 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomLeft );
2027 : 0 : }
2028 : 0 : else if ( angle <= 180 )
2029 : : {
2030 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomLeft );
2031 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomMiddle );
2032 : 0 : }
2033 : :
2034 : 0 : else if ( angle <= 185 )
2035 : : {
2036 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomMiddle );
2037 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomRight );
2038 : 0 : }
2039 : 0 : else if ( angle <= 265 )
2040 : : {
2041 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomRight );
2042 : 0 : }
2043 : 0 : else if ( angle <= 270 )
2044 : : {
2045 : 0 : addCandidate( x, y, QgsPalLayerSettings::BottomRight );
2046 : 0 : addCandidate( x, y, QgsPalLayerSettings::MiddleRight );
2047 : 0 : }
2048 : 0 : else if ( angle <= 275 )
2049 : : {
2050 : 0 : addCandidate( x, y, QgsPalLayerSettings::MiddleRight );
2051 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopRight );
2052 : 0 : }
2053 : 0 : else if ( angle <= 355 )
2054 : : {
2055 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopRight );
2056 : 0 : }
2057 : : else
2058 : : {
2059 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopRight );
2060 : 0 : addCandidate( x, y, QgsPalLayerSettings::TopMiddle );
2061 : : }
2062 : :
2063 : 0 : return !pal->isCanceled();
2064 : : } );
2065 : :
2066 : 0 : return candidatesCreated;
2067 : 0 : }
2068 : :
2069 : 0 : std::vector< std::unique_ptr< LabelPosition > > FeaturePart::createCandidates( Pal *pal )
2070 : : {
2071 : 0 : std::vector< std::unique_ptr< LabelPosition > > lPos;
2072 : 0 : double angle = mLF->hasFixedAngle() ? mLF->fixedAngle() : 0.0;
2073 : :
2074 : 0 : if ( mLF->hasFixedPosition() )
2075 : : {
2076 : 0 : lPos.emplace_back( std::make_unique< LabelPosition> ( 0, mLF->fixedPosition().x(), mLF->fixedPosition().y(), getLabelWidth( angle ), getLabelHeight( angle ), angle, 0.0, this, false, LabelPosition::Quadrant::QuadrantOver ) );
2077 : 0 : }
2078 : : else
2079 : : {
2080 : 0 : switch ( type )
2081 : : {
2082 : : case GEOS_POINT:
2083 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::OrderedPositionsAroundPoint )
2084 : 0 : createCandidatesAtOrderedPositionsOverPoint( x[0], y[0], lPos, angle );
2085 : 0 : else if ( mLF->layer()->arrangement() == QgsPalLayerSettings::OverPoint || mLF->hasFixedQuadrant() )
2086 : 0 : createCandidatesOverPoint( x[0], y[0], lPos, angle );
2087 : : else
2088 : 0 : createCandidatesAroundPoint( x[0], y[0], lPos, angle );
2089 : 0 : break;
2090 : :
2091 : : case GEOS_LINESTRING:
2092 : 0 : if ( mLF->layer()->arrangement() == QgsPalLayerSettings::Horizontal )
2093 : 0 : createHorizontalCandidatesAlongLine( lPos, this, pal );
2094 : 0 : else if ( mLF->layer()->isCurved() )
2095 : 0 : createCurvedCandidatesAlongLine( lPos, this, true, pal );
2096 : : else
2097 : 0 : createCandidatesAlongLine( lPos, this, true, pal );
2098 : 0 : break;
2099 : :
2100 : : case GEOS_POLYGON:
2101 : : {
2102 : 0 : const double labelWidth = getLabelWidth();
2103 : 0 : const double labelHeight = getLabelHeight();
2104 : :
2105 : 0 : const bool allowOutside = mLF->polygonPlacementFlags() & QgsLabeling::PolygonPlacementFlag::AllowPlacementOutsideOfPolygon;
2106 : 0 : const bool allowInside = mLF->polygonPlacementFlags() & QgsLabeling::PolygonPlacementFlag::AllowPlacementInsideOfPolygon;
2107 : : //check width/height of bbox is sufficient for label
2108 : :
2109 : 0 : if ( ( allowOutside && !allowInside ) || ( mLF->layer()->arrangement() == QgsPalLayerSettings::OutsidePolygons ) )
2110 : : {
2111 : : // only allowed to place outside of polygon
2112 : 0 : createCandidatesOutsidePolygon( lPos, pal );
2113 : 0 : }
2114 : 0 : else if ( allowOutside && ( std::fabs( xmax - xmin ) < labelWidth ||
2115 : 0 : std::fabs( ymax - ymin ) < labelHeight ) )
2116 : : {
2117 : : //no way label can fit in this polygon -- shortcut and only place label outside
2118 : 0 : createCandidatesOutsidePolygon( lPos, pal );
2119 : 0 : }
2120 : : else
2121 : : {
2122 : 0 : std::size_t created = 0;
2123 : 0 : if ( allowInside )
2124 : : {
2125 : 0 : switch ( mLF->layer()->arrangement() )
2126 : : {
2127 : : case QgsPalLayerSettings::AroundPoint:
2128 : : {
2129 : : double cx, cy;
2130 : 0 : getCentroid( cx, cy, mLF->layer()->centroidInside() );
2131 : 0 : if ( qgsDoubleNear( mLF->distLabel(), 0.0 ) )
2132 : 0 : created += createCandidateCenteredOverPoint( cx, cy, lPos, angle );
2133 : 0 : created += createCandidatesAroundPoint( cx, cy, lPos, angle );
2134 : 0 : break;
2135 : : }
2136 : : case QgsPalLayerSettings::OverPoint:
2137 : : {
2138 : : double cx, cy;
2139 : 0 : getCentroid( cx, cy, mLF->layer()->centroidInside() );
2140 : 0 : created += createCandidatesOverPoint( cx, cy, lPos, angle );
2141 : 0 : break;
2142 : : }
2143 : : case QgsPalLayerSettings::Line:
2144 : 0 : created += createCandidatesAlongLine( lPos, this, false, pal );
2145 : 0 : break;
2146 : : case QgsPalLayerSettings::PerimeterCurved:
2147 : 0 : created += createCurvedCandidatesAlongLine( lPos, this, false, pal );
2148 : 0 : break;
2149 : : default:
2150 : 0 : created += createCandidatesForPolygon( lPos, this, pal );
2151 : 0 : break;
2152 : : }
2153 : 0 : }
2154 : :
2155 : 0 : if ( allowOutside )
2156 : : {
2157 : : // add fallback for labels outside the polygon
2158 : 0 : createCandidatesOutsidePolygon( lPos, pal );
2159 : :
2160 : 0 : if ( created > 0 )
2161 : : {
2162 : : // TODO (maybe) increase cost for outside placements (i.e. positions at indices >= created)?
2163 : : // From my initial testing this doesn't seem necessary
2164 : 0 : }
2165 : 0 : }
2166 : : }
2167 : : }
2168 : 0 : }
2169 : : }
2170 : :
2171 : 0 : return lPos;
2172 : 0 : }
2173 : :
2174 : 0 : void FeaturePart::addSizePenalty( std::vector< std::unique_ptr< LabelPosition > > &lPos, double bbx[4], double bby[4] )
2175 : : {
2176 : 0 : if ( !mGeos )
2177 : 0 : createGeosGeom();
2178 : :
2179 : 0 : GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
2180 : 0 : int geomType = GEOSGeomTypeId_r( ctxt, mGeos );
2181 : :
2182 : 0 : double sizeCost = 0;
2183 : 0 : if ( geomType == GEOS_LINESTRING )
2184 : : {
2185 : 0 : const double l = length();
2186 : 0 : if ( l <= 0 )
2187 : 0 : return; // failed to calculate length
2188 : 0 : double bbox_length = std::max( bbx[2] - bbx[0], bby[2] - bby[0] );
2189 : 0 : if ( l >= bbox_length / 4 )
2190 : 0 : return; // the line is longer than quarter of height or width - don't penalize it
2191 : :
2192 : 0 : sizeCost = 1 - ( l / ( bbox_length / 4 ) ); // < 0,1 >
2193 : 0 : }
2194 : 0 : else if ( geomType == GEOS_POLYGON )
2195 : : {
2196 : 0 : const double a = area();
2197 : 0 : if ( a <= 0 )
2198 : 0 : return;
2199 : 0 : double bbox_area = ( bbx[2] - bbx[0] ) * ( bby[2] - bby[0] );
2200 : 0 : if ( a >= bbox_area / 16 )
2201 : 0 : return; // covers more than 1/16 of our view - don't penalize it
2202 : :
2203 : 0 : sizeCost = 1 - ( a / ( bbox_area / 16 ) ); // < 0, 1 >
2204 : 0 : }
2205 : : else
2206 : 0 : return; // no size penalty for points
2207 : :
2208 : : // apply the penalty
2209 : 0 : for ( std::unique_ptr< LabelPosition > &pos : lPos )
2210 : : {
2211 : 0 : pos->setCost( pos->cost() + sizeCost / 100 );
2212 : : }
2213 : 0 : }
2214 : :
2215 : 0 : bool FeaturePart::isConnected( FeaturePart *p2 )
2216 : : {
2217 : 0 : if ( !p2->mGeos )
2218 : 0 : p2->createGeosGeom();
2219 : :
2220 : : try
2221 : : {
2222 : 0 : return ( GEOSPreparedTouches_r( QgsGeos::getGEOSHandler(), preparedGeom(), p2->mGeos ) == 1 );
2223 : 0 : }
2224 : : catch ( GEOSException &e )
2225 : : {
2226 : 0 : qWarning( "GEOS exception: %s", e.what() );
2227 : 0 : QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2228 : 0 : return false;
2229 : 0 : }
2230 : 0 : }
2231 : :
2232 : 0 : bool FeaturePart::mergeWithFeaturePart( FeaturePart *other )
2233 : : {
2234 : 0 : if ( !mGeos )
2235 : 0 : createGeosGeom();
2236 : 0 : if ( !other->mGeos )
2237 : 0 : other->createGeosGeom();
2238 : :
2239 : 0 : GEOSContextHandle_t ctxt = QgsGeos::getGEOSHandler();
2240 : : try
2241 : : {
2242 : 0 : GEOSGeometry *g1 = GEOSGeom_clone_r( ctxt, mGeos );
2243 : 0 : GEOSGeometry *g2 = GEOSGeom_clone_r( ctxt, other->mGeos );
2244 : 0 : GEOSGeometry *geoms[2] = { g1, g2 };
2245 : 0 : geos::unique_ptr g( GEOSGeom_createCollection_r( ctxt, GEOS_MULTILINESTRING, geoms, 2 ) );
2246 : 0 : geos::unique_ptr gTmp( GEOSLineMerge_r( ctxt, g.get() ) );
2247 : :
2248 : 0 : if ( GEOSGeomTypeId_r( ctxt, gTmp.get() ) != GEOS_LINESTRING )
2249 : : {
2250 : : // sometimes it's not possible to merge lines (e.g. they don't touch at endpoints)
2251 : 0 : return false;
2252 : : }
2253 : 0 : invalidateGeos();
2254 : :
2255 : : // set up new geometry
2256 : 0 : mGeos = gTmp.release();
2257 : 0 : mOwnsGeom = true;
2258 : :
2259 : 0 : deleteCoords();
2260 : 0 : qDeleteAll( mHoles );
2261 : 0 : mHoles.clear();
2262 : 0 : extractCoords( mGeos );
2263 : 0 : return true;
2264 : 0 : }
2265 : : catch ( GEOSException &e )
2266 : : {
2267 : 0 : qWarning( "GEOS exception: %s", e.what() );
2268 : 0 : QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
2269 : 0 : return false;
2270 : 0 : }
2271 : 0 : }
2272 : :
2273 : 0 : double FeaturePart::calculatePriority() const
2274 : : {
2275 : 0 : if ( mLF->alwaysShow() )
2276 : : {
2277 : : //if feature is set to always show, bump the priority up by orders of magnitude
2278 : : //so that other feature's labels are unlikely to be placed over the label for this feature
2279 : : //(negative numbers due to how pal::extract calculates inactive cost)
2280 : 0 : return -0.2;
2281 : : }
2282 : :
2283 : 0 : return mLF->priority() >= 0 ? mLF->priority() : mLF->layer()->priority();
2284 : 0 : }
2285 : :
2286 : 0 : bool FeaturePart::showUprightLabels() const
2287 : : {
2288 : 0 : bool uprightLabel = false;
2289 : :
2290 : 0 : switch ( mLF->layer()->upsidedownLabels() )
2291 : : {
2292 : : case Layer::Upright:
2293 : 0 : uprightLabel = true;
2294 : 0 : break;
2295 : : case Layer::ShowDefined:
2296 : : // upright only dynamic labels
2297 : 0 : if ( !hasFixedRotation() || ( !hasFixedPosition() && fixedAngle() == 0.0 ) )
2298 : : {
2299 : 0 : uprightLabel = true;
2300 : 0 : }
2301 : 0 : break;
2302 : : case Layer::ShowAll:
2303 : 0 : break;
2304 : : default:
2305 : 0 : uprightLabel = true;
2306 : 0 : }
2307 : 0 : return uprightLabel;
2308 : : }
2309 : :
2310 : 0 : bool FeaturePart::nextCharPosition( double charWidth, double segmentLength, PointSet *path_positions, int &index, double ¤tDistanceAlongSegment,
2311 : : double &characterStartX, double &characterStartY, double &characterEndX, double &characterEndY ) const
2312 : : {
2313 : : // Coordinates this character will start at
2314 : 0 : if ( qgsDoubleNear( segmentLength, 0.0 ) )
2315 : : {
2316 : : // Not allowed to place across on 0 length segments or discontinuities
2317 : 0 : return false;
2318 : : }
2319 : :
2320 : 0 : double segmentStartX = path_positions->x[index - 1];
2321 : 0 : double segmentStartY = path_positions->y[index - 1];
2322 : :
2323 : 0 : double segmentEndX = path_positions->x[index];
2324 : 0 : double segmentEndY = path_positions->y[index];
2325 : :
2326 : 0 : double segmentDx = segmentEndX - segmentStartX;
2327 : 0 : double segmentDy = segmentEndY - segmentStartY;
2328 : :
2329 : 0 : characterStartX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
2330 : 0 : characterStartY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
2331 : :
2332 : : // Coordinates this character ends at, calculated below
2333 : 0 : characterEndX = 0;
2334 : 0 : characterEndY = 0;
2335 : :
2336 : 0 : if ( segmentLength - currentDistanceAlongSegment >= charWidth )
2337 : : {
2338 : : // if the distance remaining in this segment is enough, we just go further along the segment
2339 : 0 : currentDistanceAlongSegment += charWidth;
2340 : 0 : characterEndX = segmentStartX + segmentDx * currentDistanceAlongSegment / segmentLength;
2341 : 0 : characterEndY = segmentStartY + segmentDy * currentDistanceAlongSegment / segmentLength;
2342 : 0 : }
2343 : : else
2344 : : {
2345 : : // If there isn't enough distance left on this segment
2346 : : // then we need to search until we find the line segment that ends further than ci.width away
2347 : 0 : do
2348 : : {
2349 : 0 : segmentStartX = segmentEndX;
2350 : 0 : segmentStartY = segmentEndY;
2351 : 0 : index++;
2352 : 0 : if ( index >= path_positions->nbPoints ) // Bail out if we run off the end of the shape
2353 : : {
2354 : 0 : return false;
2355 : : }
2356 : 0 : segmentEndX = path_positions->x[index];
2357 : 0 : segmentEndY = path_positions->y[index];
2358 : 0 : }
2359 : 0 : while ( std::sqrt( std::pow( characterStartX - segmentEndX, 2 ) + std::pow( characterStartY - segmentEndY, 2 ) ) < charWidth ); // Distance from start_ to new_
2360 : :
2361 : : // Calculate the position to place the end of the character on
2362 : 0 : GeomFunction::findLineCircleIntersection( characterStartX, characterStartY, charWidth, segmentStartX, segmentStartY, segmentEndX, segmentEndY, characterEndX, characterEndY );
2363 : :
2364 : : // Need to calculate distance on the new segment
2365 : 0 : currentDistanceAlongSegment = std::sqrt( std::pow( segmentStartX - characterEndX, 2 ) + std::pow( segmentStartY - characterEndY, 2 ) );
2366 : : }
2367 : 0 : return true;
2368 : 0 : }
|