QGIS API Documentation 3.28.0-Firenze (ed3ad0430f)
labelposition.cpp
Go to the documentation of this file.
1/*
2 * libpal - Automated Placement of Labels Library
3 *
4 * Copyright (C) 2008 Maxence Laurent, MIS-TIC, HEIG-VD
5 * University of Applied Sciences, Western Switzerland
6 * http://www.hes-so.ch
7 *
8 * Contact:
9 * maxence.laurent <at> heig-vd <dot> ch
10 * or
11 * eric.taillard <at> heig-vd <dot> ch
12 *
13 * This file is part of libpal.
14 *
15 * libpal is free software: you can redistribute it and/or modify
16 * it under the terms of the GNU General Public License as published by
17 * the Free Software Foundation, either version 3 of the License, or
18 * (at your option) any later version.
19 *
20 * libpal is distributed in the hope that it will be useful,
21 * but WITHOUT ANY WARRANTY; without even the implied warranty of
22 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 * GNU General Public License for more details.
24 *
25 * You should have received a copy of the GNU General Public License
26 * along with libpal. If not, see <http://www.gnu.org/licenses/>.
27 *
28 */
29
30#include "layer.h"
31#include "pal.h"
32#include "costcalculator.h"
33#include "feature.h"
34#include "geomfunction.h"
35#include "labelposition.h"
36#include "qgsgeos.h"
37#include "qgsmessagelog.h"
38#include <cmath>
39#include <cfloat>
40
41using namespace pal;
42
43LabelPosition::LabelPosition( int id, double x1, double y1, double w, double h, double alpha, double cost, FeaturePart *feature, bool isReversed, Quadrant quadrant )
44 : id( id )
45 , feature( feature )
46 , probFeat( 0 )
47 , nbOverlap( 0 )
48 , alpha( alpha )
49 , w( w )
50 , h( h )
51 , partId( -1 )
52 , reversed( isReversed )
53 , upsideDown( false )
54 , quadrant( quadrant )
55 , mCost( cost )
56 , mHasObstacleConflict( false )
57 , mUpsideDownCharCount( 0 )
58{
59 type = GEOS_POLYGON;
60 nbPoints = 4;
61 x.resize( nbPoints );
62 y.resize( nbPoints );
63
64 // alpha take his value bw 0 and 2*pi rad
65 while ( this->alpha > 2 * M_PI )
66 this->alpha -= 2 * M_PI;
67
68 while ( this->alpha < 0 )
69 this->alpha += 2 * M_PI;
70
71 const double beta = this->alpha + M_PI_2;
72
73 double dx1, dx2, dy1, dy2;
74
75 dx1 = std::cos( this->alpha ) * w;
76 dy1 = std::sin( this->alpha ) * w;
77
78 dx2 = std::cos( beta ) * h;
79 dy2 = std::sin( beta ) * h;
80
81 x[0] = x1;
82 y[0] = y1;
83
84 x[1] = x1 + dx1;
85 y[1] = y1 + dy1;
86
87 x[2] = x1 + dx1 + dx2;
88 y[2] = y1 + dy1 + dy2;
89
90 x[3] = x1 + dx2;
91 y[3] = y1 + dy2;
92
93 // upside down ? (curved labels are always correct)
94 if ( !feature->layer()->isCurved() &&
95 this->alpha > M_PI_2 && this->alpha <= 3 * M_PI_2 )
96 {
98 {
99 // Turn label upsidedown by inverting boundary points
100 double tx, ty;
101
102 tx = x[0];
103 ty = y[0];
104
105 x[0] = x[2];
106 y[0] = y[2];
107
108 x[2] = tx;
109 y[2] = ty;
110
111 tx = x[1];
112 ty = y[1];
113
114 x[1] = x[3];
115 y[1] = y[3];
116
117 x[3] = tx;
118 y[3] = ty;
119
120 if ( this->alpha < M_PI )
121 this->alpha += M_PI;
122 else
123 this->alpha -= M_PI;
124
125 // labels with text shown upside down are not classified as upsideDown,
126 // only those whose boundary points have been inverted
127 upsideDown = true;
128 }
129 }
130
131 for ( int i = 0; i < nbPoints; ++i )
132 {
133 xmin = std::min( xmin, x[i] );
134 xmax = std::max( xmax, x[i] );
135 ymin = std::min( ymin, y[i] );
136 ymax = std::max( ymax, y[i] );
137 }
138}
139
141 : PointSet( other )
142{
143 id = other.id;
144 mCost = other.mCost;
145 feature = other.feature;
146 probFeat = other.probFeat;
147 nbOverlap = other.nbOverlap;
148
149 alpha = other.alpha;
150 w = other.w;
151 h = other.h;
152
153 if ( other.mNextPart )
154 mNextPart = std::make_unique< LabelPosition >( *other.mNextPart );
155
156 partId = other.partId;
157 upsideDown = other.upsideDown;
158 reversed = other.reversed;
159 quadrant = other.quadrant;
160 mHasObstacleConflict = other.mHasObstacleConflict;
161 mUpsideDownCharCount = other.mUpsideDownCharCount;
162}
163
164bool LabelPosition::isIn( double *bbox )
165{
166 int i;
167
168 for ( i = 0; i < 4; i++ )
169 {
170 if ( x[i] >= bbox[0] && x[i] <= bbox[2] &&
171 y[i] >= bbox[1] && y[i] <= bbox[3] )
172 return true;
173 }
174
175 if ( mNextPart )
176 return mNextPart->isIn( bbox );
177 else
178 return false;
179}
180
181bool LabelPosition::isIntersect( double *bbox )
182{
183 int i;
184
185 for ( i = 0; i < 4; i++ )
186 {
187 if ( x[i] >= bbox[0] && x[i] <= bbox[2] &&
188 y[i] >= bbox[1] && y[i] <= bbox[3] )
189 return true;
190 }
191
192 if ( mNextPart )
193 return mNextPart->isIntersect( bbox );
194 else
195 return false;
196}
197
198bool LabelPosition::intersects( const GEOSPreparedGeometry *geometry )
199{
200 if ( !mGeos )
202
203 try
204 {
205 if ( GEOSPreparedIntersects_r( QgsGeos::getGEOSHandler(), geometry, mGeos ) == 1 )
206 {
207 return true;
208 }
209 else if ( mNextPart )
210 {
211 return mNextPart->intersects( geometry );
212 }
213 }
214 catch ( GEOSException &e )
215 {
216 qWarning( "GEOS exception: %s", e.what() );
217 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
218 return false;
219 }
220
221 return false;
222}
223
224bool LabelPosition::within( const GEOSPreparedGeometry *geometry )
225{
226 if ( !mGeos )
228
229 try
230 {
231 if ( GEOSPreparedContains_r( QgsGeos::getGEOSHandler(), geometry, mGeos ) != 1 )
232 {
233 return false;
234 }
235 else if ( mNextPart )
236 {
237 return mNextPart->within( geometry );
238 }
239 }
240 catch ( GEOSException &e )
241 {
242 qWarning( "GEOS exception: %s", e.what() );
243 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
244 return false;
245 }
246
247 return true;
248}
249
250bool LabelPosition::isInside( double *bbox )
251{
252 for ( int i = 0; i < 4; i++ )
253 {
254 if ( !( x[i] >= bbox[0] && x[i] <= bbox[2] &&
255 y[i] >= bbox[1] && y[i] <= bbox[3] ) )
256 return false;
257 }
258
259 if ( mNextPart )
260 return mNextPart->isInside( bbox );
261 else
262 return true;
263}
264
266{
267 if ( this->probFeat == lp->probFeat ) // bugfix #1
268 return false; // always overlaping itself !
269
270 // if either this label doesn't cause collisions, or the other one doesn't, then we don't conflict!
273 return false;
274
275 if ( !nextPart() && !lp->nextPart() )
276 {
277 if ( qgsDoubleNear( alpha, 0 ) && qgsDoubleNear( lp->alpha, 0 ) )
278 {
279 // simple case -- both candidates are oriented to axis, so shortcut with easy calculation
280 return boundingBoxIntersects( lp );
281 }
282 }
283
284 return isInConflictMultiPart( lp );
285}
286
287bool LabelPosition::isInConflictMultiPart( const LabelPosition *lp ) const
288{
289 if ( !mMultipartGeos )
290 createMultiPartGeosGeom();
291
292 if ( !lp->mMultipartGeos )
293 lp->createMultiPartGeosGeom();
294
295 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
296 try
297 {
298 const bool result = ( GEOSPreparedIntersects_r( geosctxt, preparedMultiPartGeom(), lp->mMultipartGeos ) == 1 );
299 return result;
300 }
301 catch ( GEOSException &e )
302 {
303 qWarning( "GEOS exception: %s", e.what() );
304 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
305 return false;
306 }
307
308 return false;
309}
310
311int LabelPosition::partCount() const
312{
313 if ( mNextPart )
314 return mNextPart->partCount() + 1;
315 else
316 return 1;
317}
318
319void LabelPosition::offsetPosition( double xOffset, double yOffset )
320{
321 for ( int i = 0; i < 4; i++ )
322 {
323 x[i] += xOffset;
324 y[i] += yOffset;
325 }
326
327 if ( mNextPart )
328 mNextPart->offsetPosition( xOffset, yOffset );
329
331}
332
334{
335 return id;
336}
337
338double LabelPosition::getX( int i ) const
339{
340 return ( i >= 0 && i < 4 ? x[i] : -1 );
341}
342
343double LabelPosition::getY( int i ) const
344{
345 return ( i >= 0 && i < 4 ? y[i] : -1 );
346}
347
349{
350 return alpha;
351}
352
354{
355 if ( mCost >= 1 )
356 {
357 mCost -= int ( mCost ); // label cost up to 1
358 }
359}
360
362{
363 return feature;
364}
365
366void LabelPosition::getBoundingBox( double amin[2], double amax[2] ) const
367{
368 if ( mNextPart )
369 {
370 mNextPart->getBoundingBox( amin, amax );
371 }
372 else
373 {
374 amin[0] = std::numeric_limits<double>::max();
375 amax[0] = std::numeric_limits<double>::lowest();
376 amin[1] = std::numeric_limits<double>::max();
377 amax[1] = std::numeric_limits<double>::lowest();
378 }
379 for ( int c = 0; c < 4; c++ )
380 {
381 if ( x[c] < amin[0] )
382 amin[0] = x[c];
383 if ( x[c] > amax[0] )
384 amax[0] = x[c];
385 if ( y[c] < amin[1] )
386 amin[1] = y[c];
387 if ( y[c] > amax[1] )
388 amax[1] = y[c];
389 }
390}
391
393{
394 mHasObstacleConflict = conflicts;
395 if ( mNextPart )
396 mNextPart->setConflictsWithObstacle( conflicts );
397}
398
400{
401 mHasHardConflict = conflicts;
402 if ( mNextPart )
403 mNextPart->setHasHardObstacleConflict( conflicts );
404}
405
407{
408 double amin[2];
409 double amax[2];
410 getBoundingBox( amin, amax );
411 index.remove( this, QgsRectangle( amin[0], amin[1], amax[0], amax[1] ) );
412}
413
415{
416 double amin[2];
417 double amax[2];
418 getBoundingBox( amin, amax );
419 index.insert( this, QgsRectangle( amin[0], amin[1], amax[0], amax[1] ) );
420}
421
422
423void LabelPosition::createMultiPartGeosGeom() const
424{
425 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
426
427 std::vector< const GEOSGeometry * > geometries;
428 const LabelPosition *tmp1 = this;
429 while ( tmp1 )
430 {
431 const GEOSGeometry *partGeos = tmp1->geos();
432 if ( !GEOSisEmpty_r( geosctxt, partGeos ) )
433 geometries.emplace_back( partGeos );
434 tmp1 = tmp1->nextPart();
435 }
436
437 const std::size_t partCount = geometries.size();
438 GEOSGeometry **geomarr = new GEOSGeometry*[ partCount ];
439 for ( std::size_t i = 0; i < partCount; ++i )
440 {
441 geomarr[i ] = GEOSGeom_clone_r( geosctxt, geometries[i] );
442 }
443
444 mMultipartGeos = GEOSGeom_createCollection_r( geosctxt, GEOS_MULTIPOLYGON, geomarr, partCount );
445 delete [] geomarr;
446}
447
448const GEOSPreparedGeometry *LabelPosition::preparedMultiPartGeom() const
449{
450 if ( !mMultipartGeos )
451 createMultiPartGeosGeom();
452
453 if ( !mMultipartPreparedGeos )
454 {
455 mMultipartPreparedGeos = GEOSPrepare_r( QgsGeos::getGEOSHandler(), mMultipartGeos );
456 }
457 return mMultipartPreparedGeos;
458}
459
460double LabelPosition::getDistanceToPoint( double xp, double yp ) const
461{
462 //first check if inside, if so then distance is -1
463 bool contains = false;
464 if ( alpha == 0 )
465 {
466 // easy case -- horizontal label
467 contains = x[0] <= xp && x[1] >= xp && y[0] <= yp && y[2] >= yp;
468 }
469 else
470 {
471 contains = containsPoint( xp, yp );
472 }
473
474 double distance = -1;
475 if ( !contains )
476 {
477 if ( alpha == 0 )
478 {
479 const double dx = std::max( std::max( x[0] - xp, 0.0 ), xp - x[1] );
480 const double dy = std::max( std::max( y[0] - yp, 0.0 ), yp - y[2] );
481 distance = std::sqrt( dx * dx + dy * dy );
482 }
483 else
484 {
485 distance = std::sqrt( minDistanceToPoint( xp, yp ) );
486 }
487 }
488
489 if ( mNextPart && distance > 0 )
490 return std::min( distance, mNextPart->getDistanceToPoint( xp, yp ) );
491
492 return distance;
493}
494
496{
497 if ( !mGeos )
499
500 if ( !line->mGeos )
501 line->createGeosGeom();
502
503 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
504 try
505 {
506 if ( GEOSPreparedIntersects_r( geosctxt, line->preparedGeom(), mGeos ) == 1 )
507 {
508 return true;
509 }
510 else if ( mNextPart )
511 {
512 return mNextPart->crossesLine( line );
513 }
514 }
515 catch ( GEOSException &e )
516 {
517 qWarning( "GEOS exception: %s", e.what() );
518 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
519 return false;
520 }
521
522 return false;
523}
524
526{
527 if ( !mGeos )
529
530 if ( !polygon->mGeos )
531 polygon->createGeosGeom();
532
533 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
534 try
535 {
536 if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mGeos ) == 1
537 && GEOSPreparedContains_r( geosctxt, polygon->preparedGeom(), mGeos ) != 1 )
538 {
539 return true;
540 }
541 else if ( mNextPart )
542 {
543 return mNextPart->crossesBoundary( polygon );
544 }
545 }
546 catch ( GEOSException &e )
547 {
548 qWarning( "GEOS exception: %s", e.what() );
549 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
550 return false;
551 }
552
553 return false;
554}
555
557{
558 //effectively take the average polygon intersection cost for all label parts
559 const double totalCost = polygonIntersectionCostForParts( polygon );
560 const int n = partCount();
561 return std::ceil( totalCost / n );
562}
563
565{
566 if ( !mGeos )
568
569 if ( !polygon->mGeos )
570 polygon->createGeosGeom();
571
572 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
573 try
574 {
575 if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mGeos ) == 1 )
576 {
577 return true;
578 }
579 }
580 catch ( GEOSException &e )
581 {
582 qWarning( "GEOS exception: %s", e.what() );
583 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
584 }
585
586 if ( mNextPart )
587 {
588 return mNextPart->intersectsWithPolygon( polygon );
589 }
590 else
591 {
592 return false;
593 }
594}
595
596double LabelPosition::polygonIntersectionCostForParts( PointSet *polygon ) const
597{
598 if ( !mGeos )
600
601 if ( !polygon->mGeos )
602 polygon->createGeosGeom();
603
604 GEOSContextHandle_t geosctxt = QgsGeos::getGEOSHandler();
605 double cost = 0;
606 try
607 {
608 if ( GEOSPreparedIntersects_r( geosctxt, polygon->preparedGeom(), mGeos ) == 1 )
609 {
610 //at least a partial intersection
611 cost += 1;
612
613 double px, py;
614
615 // check each corner
616 for ( int i = 0; i < 4; ++i )
617 {
618 px = x[i];
619 py = y[i];
620
621 for ( int a = 0; a < 2; ++a ) // and each middle of segment
622 {
623 if ( polygon->containsPoint( px, py ) )
624 cost++;
625 px = ( x[i] + x[( i + 1 ) % 4] ) / 2.0;
626 py = ( y[i] + y[( i + 1 ) % 4] ) / 2.0;
627 }
628 }
629
630 px = ( x[0] + x[2] ) / 2.0;
631 py = ( y[0] + y[2] ) / 2.0;
632
633 //check the label center. if covered by polygon, cost of 4
634 if ( polygon->containsPoint( px, py ) )
635 cost += 4;
636 }
637 }
638 catch ( GEOSException &e )
639 {
640 qWarning( "GEOS exception: %s", e.what() );
641 QgsMessageLog::logMessage( QObject::tr( "Exception: %1" ).arg( e.what() ), QObject::tr( "GEOS" ) );
642 }
643
644 //maintain scaling from 0 -> 12
645 cost = 12.0 * cost / 13.0;
646
647 if ( mNextPart )
648 {
649 cost += mNextPart->polygonIntersectionCostForParts( polygon );
650 }
651
652 return cost;
653}
654
656{
657 double angleDiff = 0.0;
658 double angleLast = 0.0;
659 LabelPosition *tmp = this;
660 while ( tmp )
661 {
662 if ( tmp != this ) // not first?
663 {
664 double diff = std::fabs( tmp->getAlpha() - angleLast );
665 if ( diff > 2 * M_PI )
666 diff -= 2 * M_PI;
667 diff = std::min( diff, 2 * M_PI - diff ); // difference 350 deg is actually just 10 deg...
668 angleDiff += diff;
669 }
670
671 angleLast = tmp->getAlpha();
672 tmp = tmp->nextPart();
673 }
674 return angleDiff;
675}
A rtree spatial index for use in the pal labeling engine.
Definition: palrtree.h:36
void insert(T *data, const QgsRectangle &bounds)
Inserts new data into the spatial index, with the specified bounds.
Definition: palrtree.h:59
void remove(T *data, const QgsRectangle &bounds)
Removes existing data from the spatial index, with the specified bounds.
Definition: palrtree.h:78
@ AllowOverlapAtNoCost
Labels may freely overlap other labels, at no cost.
static GEOSContextHandle_t getGEOSHandler()
Definition: qgsgeos.cpp:3446
Qgis::LabelOverlapHandling overlapHandling() const
Returns the technique to use for handling overlapping labels for the feature.
static void logMessage(const QString &message, const QString &tag=QString(), Qgis::MessageLevel level=Qgis::MessageLevel::Warning, bool notifyUser=true)
Adds a message to the log instance (and creates it if necessary).
A rectangle specified with double values.
Definition: qgsrectangle.h:42
Main class to handle feature.
Definition: feature.h:65
bool onlyShowUprightLabels() const
Returns true if feature's label must be displayed upright.
Definition: feature.cpp:2376
QgsLabelFeature * feature()
Returns the parent feature.
Definition: feature.h:94
Layer * layer()
Returns the layer that feature belongs to.
Definition: feature.cpp:159
LabelPosition is a candidate feature label position.
Definition: labelposition.h:56
bool intersectsWithPolygon(PointSet *polygon) const
Returns true if any intersection between polygon and position exists.
bool isInConflict(const LabelPosition *ls) const
Check whether or not this overlap with another labelPosition.
bool isIntersect(double *bbox)
Is the labelposition intersect the bounding-box ?
double getAlpha() const
Returns the angle to rotate text (in rad).
Quadrant
Position of label candidate relative to feature.
Definition: labelposition.h:66
LabelPosition::Quadrant quadrant
double angleDifferential()
Returns the angle differential of all LabelPosition parts.
void removeFromIndex(PalRtree< LabelPosition > &index)
Removes the label position from the specified index.
bool isInside(double *bbox)
Is the labelposition inside the bounding-box ?
bool crossesLine(PointSet *line) const
Returns true if this label crosses the specified line.
int getId() const
Returns the id.
FeaturePart * feature
void validateCost()
Make sure the cost is less than 1.
bool isIn(double *bbox)
Is the labelposition in the bounding-box ? (intersect or inside????)
double cost() const
Returns the candidate label position's geographical cost.
void setConflictsWithObstacle(bool conflicts)
Sets whether the position is marked as conflicting with an obstacle feature.
bool intersects(const GEOSPreparedGeometry *geometry)
Returns true if the label position intersects a geometry.
void setHasHardObstacleConflict(bool conflicts)
Sets whether the position is marked as having a hard conflict with an obstacle feature.
bool crossesBoundary(PointSet *polygon) const
Returns true if this label crosses the boundary of the specified polygon.
void offsetPosition(double xOffset, double yOffset)
Shift the label by specified offset.
double getDistanceToPoint(double xp, double yp) const
Gets distance from this label to a point. If point lies inside, returns negative number.
FeaturePart * getFeaturePart() const
Returns the feature corresponding to this labelposition.
const GEOSPreparedGeometry * preparedMultiPartGeom() const
Returns a prepared GEOS representation of all label parts as a multipolygon.
double getX(int i=0) const
Returns the down-left x coordinate.
void getBoundingBox(double amin[2], double amax[2]) const
Returns bounding box - amin: xmin,ymin - amax: xmax,ymax.
double getY(int i=0) const
Returns the down-left y coordinate.
bool within(const GEOSPreparedGeometry *geometry)
Returns true if the label position is within a geometry.
void insertIntoIndex(PalRtree< LabelPosition > &index)
Inserts the label position into the specified index.
int polygonIntersectionCost(PointSet *polygon) const
Returns cost of position intersection with polygon (testing area of intersection and center).
LabelPosition * nextPart() const
Returns the next part of this label position (i.e.
bool isCurved() const
Returns true if the layer has curved labels.
Definition: layer.h:173
The underlying raw pal geometry class.
Definition: pointset.h:77
friend class LabelPosition
Definition: pointset.h:79
double ymax
Definition: pointset.h:261
double ymin
Definition: pointset.h:260
void createGeosGeom() const
Definition: pointset.cpp:99
std::vector< double > y
Definition: pointset.h:231
bool boundingBoxIntersects(const PointSet *other) const
Returns true if the bounding box of this pointset intersects the bounding box of another pointset.
Definition: pointset.cpp:962
std::vector< double > x
Definition: pointset.h:230
const GEOSPreparedGeometry * preparedGeom() const
Definition: pointset.cpp:154
GEOSGeometry * mGeos
Definition: pointset.h:234
double xmin
Definition: pointset.h:258
const GEOSGeometry * geos() const
Returns the point set's GEOS geometry.
Definition: pointset.cpp:1050
void invalidateGeos() const
Definition: pointset.cpp:166
double minDistanceToPoint(double px, double py, double *rx=nullptr, double *ry=nullptr) const
Returns the squared minimum distance between the point set geometry and the point (px,...
Definition: pointset.cpp:858
double xmax
Definition: pointset.h:259
bool containsPoint(double x, double y) const
Tests whether point set contains a specified point.
Definition: pointset.cpp:270
As part of the API refactoring and improvements which landed in the Processing API was substantially reworked from the x version This was done in order to allow much of the underlying Processing framework to be ported into c
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2527