QGIS API Documentation 3.27.0-Master (75dc696944)
qgsshapegenerator.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsshapegenerator.cpp
3 ----------------
4 begin : March 2021
5 copyright : (C) 2021 Nyall Dawson
6 email : nyall dot dawson at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
18#include "qgsshapegenerator.h"
19#include "qgsgeometryutils.h"
20#include <QLineF>
21#include <QList>
22#include <QPainterPath>
23#include <algorithm>
24
25QLineF segment( int index, QRectF rect, double radius )
26{
27 const int yMultiplier = rect.height() < 0 ? -1 : 1;
28 switch ( index )
29 {
30 case 0:
31 return QLineF( rect.left() + radius,
32 rect.top(),
33 rect.right() - radius,
34 rect.top() );
35 case 1:
36 return QLineF( rect.right(),
37 rect.top() + yMultiplier * radius,
38 rect.right(),
39 rect.bottom() - yMultiplier * radius );
40 case 2:
41 return QLineF( rect.right() - radius,
42 rect.bottom(),
43 rect.left() + radius,
44 rect.bottom() );
45 case 3:
46 return QLineF( rect.left(),
47 rect.bottom() - yMultiplier * radius,
48 rect.left(),
49 rect.top() + yMultiplier * radius );
50 default:
51 return QLineF();
52 }
53}
54
55QPolygonF QgsShapeGenerator::createBalloon( const QgsPointXY &origin, const QRectF &rect, double wedgeWidth )
56{
57 return createBalloon( origin, rect, wedgeWidth, 0 ).toFillPolygon();
58}
59
60QPainterPath QgsShapeGenerator::createBalloon( const QgsPointXY &origin, const QRectF &rect, double wedgeWidth, double cornerRadius )
61{
62 int balloonSegment = -1;
63 QPointF balloonSegmentPoint1;
64 QPointF balloonSegmentPoint2;
65
66 const bool invertedY = rect.height() < 0;
67
68 cornerRadius = std::min( cornerRadius, std::min( std::fabs( rect.height() ), rect.width() ) / 2.0 );
69
70 //first test if the point is in the frame. In that case we don't need a balloon and can just use a rect
71 if ( rect.contains( origin.toQPointF() ) )
72 {
73 balloonSegment = -1;
74 }
75 else
76 {
77 //edge list
78 QList<QLineF> segmentList;
79 segmentList << segment( 0, rect, cornerRadius );
80 segmentList << segment( 1, rect, cornerRadius );
81 segmentList << segment( 2, rect, cornerRadius );
82 segmentList << segment( 3, rect, cornerRadius );
83
84 // find closest edge / closest edge point
85 double minEdgeDist = std::numeric_limits<double>::max();
86 int minEdgeIndex = -1;
87 QLineF minEdge;
88 QgsPointXY minEdgePoint( 0, 0 );
89
90 for ( int i = 0; i < 4; ++i )
91 {
92 QLineF currentSegment = segmentList.at( i );
93 QgsPointXY currentMinDistPoint;
94 double currentMinDist = origin.sqrDistToSegment( currentSegment.x1(), currentSegment.y1(), currentSegment.x2(), currentSegment.y2(), currentMinDistPoint );
95 bool isPreferredSegment = false;
96 if ( qgsDoubleNear( currentMinDist, minEdgeDist ) )
97 {
98 // two segments are close - work out which looks nicer
99 const double angle = fmod( origin.azimuth( currentMinDistPoint ) + 360.0, 360.0 );
100 if ( angle < 45 || angle > 315 )
101 isPreferredSegment = i == 0;
102 else if ( angle < 135 )
103 isPreferredSegment = i == 3;
104 else if ( angle < 225 )
105 isPreferredSegment = i == 2;
106 else
107 isPreferredSegment = i == 1;
108 }
109 else if ( currentMinDist < minEdgeDist )
110 isPreferredSegment = true;
111
112 if ( isPreferredSegment )
113 {
114 minEdgeIndex = i;
115 minEdgePoint = currentMinDistPoint;
116 minEdgeDist = currentMinDist;
117 minEdge = currentSegment;
118 }
119 }
120
121 if ( minEdgeIndex >= 0 )
122 {
123 balloonSegment = minEdgeIndex;
124 QPointF minEdgeEnd = minEdge.p2();
125 balloonSegmentPoint1 = QPointF( minEdgePoint.x(), minEdgePoint.y() );
126
127 const double segmentLength = minEdge.length();
128 const double clampedWedgeWidth = std::clamp( wedgeWidth, 0.0, segmentLength );
129 if ( std::sqrt( minEdgePoint.sqrDist( minEdgeEnd.x(), minEdgeEnd.y() ) ) < clampedWedgeWidth )
130 {
131 double x = 0;
132 double y = 0;
133 QgsGeometryUtils::pointOnLineWithDistance( minEdge.p2().x(), minEdge.p2().y(), minEdge.p1().x(), minEdge.p1().y(), clampedWedgeWidth, x, y );
134 balloonSegmentPoint1 = QPointF( x, y );
135 }
136
137 {
138 double x = 0;
139 double y = 0;
140 QgsGeometryUtils::pointOnLineWithDistance( balloonSegmentPoint1.x(), balloonSegmentPoint1.y(), minEdge.p2().x(), minEdge.p2().y(), clampedWedgeWidth, x, y );
141 balloonSegmentPoint2 = QPointF( x, y );
142 }
143 }
144 }
145
146 QPainterPath path;
147 QPointF p0;
148 QPointF p1;
149 for ( int i = 0; i < 4; ++i )
150 {
151 QLineF currentSegment = segment( i, rect, cornerRadius );
152 if ( i == 0 )
153 {
154 p0 = currentSegment.p1();
155 path.moveTo( currentSegment.p1() );
156 }
157 else
158 {
159 if ( invertedY )
160 path.arcTo( std::min( p1.x(), currentSegment.p1().x() ),
161 std::min( p1.y(), currentSegment.p1().y() ),
162 cornerRadius, cornerRadius,
163 i == 0 ? -180 : ( i == 1 ? -90 : ( i == 2 ? 0 : 90 ) ),
164 90 );
165 else
166 path.arcTo( std::min( p1.x(), currentSegment.p1().x() ),
167 std::min( p1.y(), currentSegment.p1().y() ),
168 cornerRadius, cornerRadius,
169 i == 0 ? 180 : ( i == 1 ? 90 : ( i == 2 ? 0 : -90 ) ),
170 -90 );
171 }
172
173 if ( i == balloonSegment )
174 {
175 path.lineTo( balloonSegmentPoint1 );
176 path.lineTo( origin.toQPointF() );
177 path.lineTo( balloonSegmentPoint2 );
178 }
179
180 p1 = currentSegment.p2();
181 path.lineTo( p1 );
182 }
183
184 if ( invertedY )
185 path.arcTo( std::min( p1.x(), p0.x() ),
186 std::min( p1.y(), p0.y() ),
187 cornerRadius, cornerRadius,
188 180, 90 );
189 else
190 path.arcTo( std::min( p1.x(), p0.x() ),
191 std::min( p1.y(), p0.y() ),
192 cornerRadius, cornerRadius,
193 -180, -90 );
194
195 return path;
196}
static QgsPoint pointOnLineWithDistance(const QgsPoint &startPoint, const QgsPoint &directionPoint, double distance) SIP_HOLDGIL
Returns a point a specified distance toward a second point.
A class to represent a 2D point.
Definition: qgspointxy.h:59
double sqrDist(double x, double y) const SIP_HOLDGIL
Returns the squared distance between this point a specified x, y coordinate.
Definition: qgspointxy.h:190
double azimuth(const QgsPointXY &other) const SIP_HOLDGIL
Calculates azimuth between this point and other one (clockwise in degree, starting from north)
Definition: qgspointxy.cpp:80
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
QPointF toQPointF() const
Converts a point to a QPointF.
Definition: qgspointxy.h:169
double sqrDistToSegment(double x1, double y1, double x2, double y2, QgsPointXY &minDistPoint, double epsilon=DEFAULT_SEGMENT_EPSILON) const SIP_HOLDGIL
Returns the minimum distance between this point and a segment.
Definition: qgspointxy.cpp:95
static QPolygonF createBalloon(const QgsPointXY &origin, const QRectF &rect, double wedgeWidth)
Generates a "balloon"/"talking bubble" style shape (as a QPolygonF).
double ANALYSIS_EXPORT angle(QgsPoint *p1, QgsPoint *p2, QgsPoint *p3, QgsPoint *p4)
Calculates the angle between two segments (in 2 dimension, z-values are ignored)
Definition: MathUtils.cpp:786
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:2404
QLineF segment(int index, QRectF rect, double radius)