Branch data Line data Source code
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 : 0 : QLineF segment( int index, QRectF rect, double radius )
26 : : {
27 : 0 : const int yMultiplier = rect.height() < 0 ? -1 : 1;
28 : 0 : switch ( index )
29 : : {
30 : : case 0:
31 : 0 : return QLineF( rect.left() + radius,
32 : 0 : rect.top(),
33 : 0 : rect.right() - radius,
34 : 0 : rect.top() );
35 : : case 1:
36 : 0 : return QLineF( rect.right(),
37 : 0 : rect.top() + yMultiplier * radius,
38 : 0 : rect.right(),
39 : 0 : rect.bottom() - yMultiplier * radius );
40 : : case 2:
41 : 0 : return QLineF( rect.right() - radius,
42 : 0 : rect.bottom(),
43 : 0 : rect.left() + radius,
44 : 0 : rect.bottom() );
45 : : case 3:
46 : 0 : return QLineF( rect.left(),
47 : 0 : rect.bottom() - yMultiplier * radius,
48 : 0 : rect.left(),
49 : 0 : rect.top() + yMultiplier * radius );
50 : : default:
51 : 0 : return QLineF();
52 : : }
53 : 0 : }
54 : :
55 : 0 : QPolygonF QgsShapeGenerator::createBalloon( const QgsPointXY &origin, const QRectF &rect, double wedgeWidth )
56 : : {
57 : 0 : return createBalloon( origin, rect, wedgeWidth, 0 ).toFillPolygon();
58 : : }
59 : :
60 : 0 : QPainterPath QgsShapeGenerator::createBalloon( const QgsPointXY &origin, const QRectF &rect, double wedgeWidth, double cornerRadius )
61 : : {
62 : 0 : int balloonSegment = -1;
63 : 0 : QPointF balloonSegmentPoint1;
64 : 0 : QPointF balloonSegmentPoint2;
65 : :
66 : 0 : const bool invertedY = rect.height() < 0;
67 : :
68 : 0 : 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 : 0 : if ( rect.contains( origin.toQPointF() ) )
72 : : {
73 : 0 : balloonSegment = -1;
74 : 0 : }
75 : : else
76 : : {
77 : : //edge list
78 : 0 : QList<QLineF> segmentList;
79 : 0 : segmentList << segment( 0, rect, cornerRadius );
80 : 0 : segmentList << segment( 1, rect, cornerRadius );
81 : 0 : segmentList << segment( 2, rect, cornerRadius );
82 : 0 : segmentList << segment( 3, rect, cornerRadius );
83 : :
84 : : // find closest edge / closest edge point
85 : 0 : double minEdgeDist = std::numeric_limits<double>::max();
86 : 0 : int minEdgeIndex = -1;
87 : 0 : QLineF minEdge;
88 : 0 : QgsPointXY minEdgePoint( 0, 0 );
89 : :
90 : 0 : for ( int i = 0; i < 4; ++i )
91 : : {
92 : 0 : QLineF currentSegment = segmentList.at( i );
93 : 0 : QgsPointXY currentMinDistPoint;
94 : 0 : double currentMinDist = origin.sqrDistToSegment( currentSegment.x1(), currentSegment.y1(), currentSegment.x2(), currentSegment.y2(), currentMinDistPoint );
95 : 0 : bool isPreferredSegment = false;
96 : 0 : if ( qgsDoubleNear( currentMinDist, minEdgeDist ) )
97 : : {
98 : : // two segments are close - work out which looks nicer
99 : 0 : const double angle = fmod( origin.azimuth( currentMinDistPoint ) + 360.0, 360.0 );
100 : 0 : if ( angle < 45 || angle > 315 )
101 : 0 : isPreferredSegment = i == 0;
102 : 0 : else if ( angle < 135 )
103 : 0 : isPreferredSegment = i == 3;
104 : 0 : else if ( angle < 225 )
105 : 0 : isPreferredSegment = i == 2;
106 : : else
107 : 0 : isPreferredSegment = i == 1;
108 : 0 : }
109 : 0 : else if ( currentMinDist < minEdgeDist )
110 : 0 : isPreferredSegment = true;
111 : :
112 : 0 : if ( isPreferredSegment )
113 : : {
114 : 0 : minEdgeIndex = i;
115 : 0 : minEdgePoint = currentMinDistPoint;
116 : 0 : minEdgeDist = currentMinDist;
117 : 0 : minEdge = currentSegment;
118 : 0 : }
119 : 0 : }
120 : :
121 : 0 : if ( minEdgeIndex >= 0 )
122 : : {
123 : 0 : balloonSegment = minEdgeIndex;
124 : 0 : QPointF minEdgeEnd = minEdge.p2();
125 : 0 : balloonSegmentPoint1 = QPointF( minEdgePoint.x(), minEdgePoint.y() );
126 : :
127 : 0 : const double segmentLength = minEdge.length();
128 : 0 : const double clampedWedgeWidth = std::clamp( wedgeWidth, 0.0, segmentLength );
129 : 0 : if ( std::sqrt( minEdgePoint.sqrDist( minEdgeEnd.x(), minEdgeEnd.y() ) ) < clampedWedgeWidth )
130 : : {
131 : 0 : double x = 0;
132 : 0 : double y = 0;
133 : 0 : QgsGeometryUtils::pointOnLineWithDistance( minEdge.p2().x(), minEdge.p2().y(), minEdge.p1().x(), minEdge.p1().y(), clampedWedgeWidth, x, y );
134 : 0 : balloonSegmentPoint1 = QPointF( x, y );
135 : 0 : }
136 : :
137 : : {
138 : 0 : double x = 0;
139 : 0 : double y = 0;
140 : 0 : QgsGeometryUtils::pointOnLineWithDistance( balloonSegmentPoint1.x(), balloonSegmentPoint1.y(), minEdge.p2().x(), minEdge.p2().y(), clampedWedgeWidth, x, y );
141 : 0 : balloonSegmentPoint2 = QPointF( x, y );
142 : : }
143 : 0 : }
144 : 0 : }
145 : :
146 : 0 : QPainterPath path;
147 : 0 : QPointF p0;
148 : 0 : QPointF p1;
149 : 0 : for ( int i = 0; i < 4; ++i )
150 : : {
151 : 0 : QLineF currentSegment = segment( i, rect, cornerRadius );
152 : 0 : if ( i == 0 )
153 : : {
154 : 0 : p0 = currentSegment.p1();
155 : 0 : path.moveTo( currentSegment.p1() );
156 : 0 : }
157 : : else
158 : : {
159 : 0 : if ( invertedY )
160 : 0 : path.arcTo( std::min( p1.x(), currentSegment.p1().x() ),
161 : 0 : std::min( p1.y(), currentSegment.p1().y() ),
162 : 0 : cornerRadius, cornerRadius,
163 : 0 : i == 0 ? -180 : ( i == 1 ? -90 : ( i == 2 ? 0 : 90 ) ),
164 : : 90 );
165 : : else
166 : 0 : path.arcTo( std::min( p1.x(), currentSegment.p1().x() ),
167 : 0 : std::min( p1.y(), currentSegment.p1().y() ),
168 : 0 : cornerRadius, cornerRadius,
169 : 0 : i == 0 ? 180 : ( i == 1 ? 90 : ( i == 2 ? 0 : -90 ) ),
170 : : -90 );
171 : : }
172 : :
173 : 0 : if ( i == balloonSegment )
174 : : {
175 : 0 : path.lineTo( balloonSegmentPoint1 );
176 : 0 : path.lineTo( origin.toQPointF() );
177 : 0 : path.lineTo( balloonSegmentPoint2 );
178 : 0 : }
179 : :
180 : 0 : p1 = currentSegment.p2();
181 : 0 : path.lineTo( p1 );
182 : 0 : }
183 : :
184 : 0 : if ( invertedY )
185 : 0 : path.arcTo( std::min( p1.x(), p0.x() ),
186 : 0 : std::min( p1.y(), p0.y() ),
187 : 0 : cornerRadius, cornerRadius,
188 : : 180, 90 );
189 : : else
190 : 0 : path.arcTo( std::min( p1.x(), p0.x() ),
191 : 0 : std::min( p1.y(), p0.y() ),
192 : 0 : cornerRadius, cornerRadius,
193 : : -180, -90 );
194 : :
195 : 0 : return path;
196 : 0 : }
|