QGIS API Documentation  3.21.0-Master (5b68dc587e)
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 
25 QLineF 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 
55 QPolygonF QgsShapeGenerator::createBalloon( const QgsPointXY &origin, const QRectF &rect, double wedgeWidth )
56 {
57  return createBalloon( origin, rect, wedgeWidth, 0 ).toFillPolygon();
58 }
59 
60 QPainterPath 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:1234
QLineF segment(int index, QRectF rect, double radius)