QGIS API Documentation  3.23.0-Master (dd0cd13a00)
qgscadutils.cpp
Go to the documentation of this file.
1 /***************************************************************************
2  qgscadutils.cpp
3  -------------------
4  begin : September 2017
5  copyright : (C) 2017 by Martin Dobias
6  email : wonder dot sk at gmail dot com
7  ***************************************************************************/
8 /***************************************************************************
9  * *
10  * This program is free software; you can redistribute it and/or modify *
11  * it under the terms of the GNU General Public License as published by *
12  * the Free Software Foundation; either version 2 of the License, or *
13  * (at your option) any later version. *
14  * *
15  ***************************************************************************/
16 
17 #include "qgscadutils.h"
18 
19 #include "qgslogger.h"
20 #include "qgssnappingutils.h"
21 #include "qgsgeometryutils.h"
22 
23 // tolerances for soft constraints (last values, and common angles)
24 // for angles, both tolerance in pixels and degrees are used for better performance
25 static const double SOFT_CONSTRAINT_TOLERANCE_PIXEL = 15;
26 static const double SOFT_CONSTRAINT_TOLERANCE_DEGREES = 10;
27 
28 
30 struct EdgesOnlyFilter : public QgsPointLocator::MatchFilter
31 {
32  bool acceptMatch( const QgsPointLocator::Match &m ) override { return m.hasEdge(); }
33 };
35 
36 
38 {
40  res.valid = true;
41  res.softLockCommonAngle = -1;
42 
43  // try to snap to anything
44  const QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint, nullptr, true );
45  res.snapMatch = snapMatch;
46  QgsPointXY point = snapMatch.isValid() ? snapMatch.point() : originalMapPoint;
47  QgsPointXY edgePt0, edgePt1;
48  if ( snapMatch.hasEdge() )
49  {
50  snapMatch.edgePoints( edgePt0, edgePt1 );
51  // note : res.edgeMatch should be removed, as we can just check snapMatch.hasEdge()
52  res.edgeMatch = snapMatch;
53  }
54  else
55  {
57  }
58 
59  QgsPointXY previousPt, penultimatePt;
60  if ( ctx.cadPoints().count() >= 2 )
61  previousPt = ctx.cadPoint( 1 );
62  if ( ctx.cadPoints().count() >= 3 )
63  penultimatePt = ctx.cadPoint( 2 );
64 
65  // *****************************
66  // ---- X constraint
67  if ( ctx.xConstraint.locked )
68  {
69  if ( !ctx.xConstraint.relative )
70  {
71  point.setX( ctx.xConstraint.value );
72  }
73  else if ( ctx.cadPoints().count() >= 2 )
74  {
75  point.setX( previousPt.x() + ctx.xConstraint.value );
76  }
77  if ( snapMatch.hasEdge() && !ctx.yConstraint.locked )
78  {
79  // intersect with snapped segment line at X coordinate
80  const double dx = edgePt1.x() - edgePt0.x();
81  if ( dx == 0 )
82  {
83  point.setY( edgePt0.y() );
84  }
85  else
86  {
87  const double dy = edgePt1.y() - edgePt0.y();
88  point.setY( edgePt0.y() + ( dy * ( point.x() - edgePt0.x() ) ) / dx );
89  }
90  }
91  }
92 
93  // *****************************
94  // ---- Y constraint
95  if ( ctx.yConstraint.locked )
96  {
97  if ( !ctx.yConstraint.relative )
98  {
99  point.setY( ctx.yConstraint.value );
100  }
101  else if ( ctx.cadPoints().count() >= 2 )
102  {
103  point.setY( previousPt.y() + ctx.yConstraint.value );
104  }
105  if ( snapMatch.hasEdge() && !ctx.xConstraint.locked )
106  {
107  // intersect with snapped segment line at Y coordinate
108  const double dy = edgePt1.y() - edgePt0.y();
109  if ( dy == 0 )
110  {
111  point.setX( edgePt0.x() );
112  }
113  else
114  {
115  const double dx = edgePt1.x() - edgePt0.x();
116  point.setX( edgePt0.x() + ( dx * ( point.y() - edgePt0.y() ) ) / dy );
117  }
118  }
119  }
120 
121  // *****************************
122  // ---- Common Angle constraint
123  if ( !ctx.angleConstraint.locked && ctx.cadPoints().count() >= 2 && ctx.commonAngleConstraint.locked && ctx.commonAngleConstraint.value != 0 )
124  {
125  const double commonAngle = ctx.commonAngleConstraint.value * M_PI / 180;
126  // see if soft common angle constraint should be performed
127  // only if not in HardLock mode
128  double softAngle = std::atan2( point.y() - previousPt.y(),
129  point.x() - previousPt.x() );
130  double deltaAngle = 0;
131  if ( ctx.commonAngleConstraint.relative && ctx.cadPoints().count() >= 3 )
132  {
133  // compute the angle relative to the last segment (0° is aligned with last segment)
134  deltaAngle = std::atan2( previousPt.y() - penultimatePt.y(),
135  previousPt.x() - penultimatePt.x() );
136  softAngle -= deltaAngle;
137  }
138  const int quo = std::round( softAngle / commonAngle );
139  if ( std::fabs( softAngle - quo * commonAngle ) * 180.0 * M_1_PI <= SOFT_CONSTRAINT_TOLERANCE_DEGREES )
140  {
141  // also check the distance in pixel to the line, otherwise it's too sticky at long ranges
142  softAngle = quo * commonAngle;
143  // http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html
144  // use the direction vector (cos(a),sin(a)) from previous point. |x2-x1|=1 since sin2+cos2=1
145  const double dist = std::fabs( std::cos( softAngle + deltaAngle ) * ( previousPt.y() - point.y() )
146  - std::sin( softAngle + deltaAngle ) * ( previousPt.x() - point.x() ) );
147  if ( dist / ctx.mapUnitsPerPixel < SOFT_CONSTRAINT_TOLERANCE_PIXEL )
148  {
149  res.softLockCommonAngle = 180.0 / M_PI * softAngle;
150  }
151  }
152  }
153 
154  // angle can be locked in one of the two ways:
155  // 1. "hard" lock defined by the user
156  // 2. "soft" lock from common angle (e.g. 45 degrees)
157  bool angleLocked = false, angleRelative = false;
158  double angleValueDeg = 0;
159  if ( ctx.angleConstraint.locked )
160  {
161  angleLocked = true;
162  angleRelative = ctx.angleConstraint.relative;
163  angleValueDeg = ctx.angleConstraint.value;
164  }
165  else if ( res.softLockCommonAngle != -1 )
166  {
167  angleLocked = true;
168  angleRelative = ctx.commonAngleConstraint.relative;
169  angleValueDeg = res.softLockCommonAngle;
170  }
171 
172  // *****************************
173  // ---- Angle constraint
174  // input angles are in degrees
175  if ( angleLocked )
176  {
177  double angleValue = angleValueDeg * M_PI / 180;
178  if ( angleRelative && ctx.cadPoints().count() >= 3 )
179  {
180  // compute the angle relative to the last segment (0° is aligned with last segment)
181  angleValue += std::atan2( previousPt.y() - penultimatePt.y(),
182  previousPt.x() - penultimatePt.x() );
183  }
184 
185  const double cosa = std::cos( angleValue );
186  const double sina = std::sin( angleValue );
187  const double v = ( point.x() - previousPt.x() ) * cosa + ( point.y() - previousPt.y() ) * sina;
188  if ( ctx.xConstraint.locked && ctx.yConstraint.locked )
189  {
190  // do nothing if both X,Y are already locked
191  }
192  else if ( ctx.xConstraint.locked )
193  {
194  if ( qgsDoubleNear( cosa, 0.0 ) )
195  {
196  res.valid = false;
197  }
198  else
199  {
200  double x = ctx.xConstraint.value;
201  if ( !ctx.xConstraint.relative )
202  {
203  x -= previousPt.x();
204  }
205  point.setY( previousPt.y() + x * sina / cosa );
206  }
207  }
208  else if ( ctx.yConstraint.locked )
209  {
210  if ( qgsDoubleNear( sina, 0.0 ) )
211  {
212  res.valid = false;
213  }
214  else
215  {
216  double y = ctx.yConstraint.value;
217  if ( !ctx.yConstraint.relative )
218  {
219  y -= previousPt.y();
220  }
221  point.setX( previousPt.x() + y * cosa / sina );
222  }
223  }
224  else
225  {
226  point.setX( previousPt.x() + cosa * v );
227  point.setY( previousPt.y() + sina * v );
228  }
229 
230  if ( snapMatch.hasEdge() && !ctx.distanceConstraint.locked )
231  {
232  // magnetize to the intersection of the snapped segment and the lockedAngle
233 
234  // line of previous point + locked angle
235  const double x1 = previousPt.x();
236  const double y1 = previousPt.y();
237  const double x2 = previousPt.x() + cosa;
238  const double y2 = previousPt.y() + sina;
239  // line of snapped segment
240  const double x3 = edgePt0.x();
241  const double y3 = edgePt0.y();
242  const double x4 = edgePt1.x();
243  const double y4 = edgePt1.y();
244 
245  const double d = ( x1 - x2 ) * ( y3 - y4 ) - ( y1 - y2 ) * ( x3 - x4 );
246 
247  // do not compute intersection if lines are almost parallel
248  // this threshold might be adapted
249  if ( std::fabs( d ) > 0.01 )
250  {
251  point.setX( ( ( x3 - x4 ) * ( x1 * y2 - y1 * x2 ) - ( x1 - x2 ) * ( x3 * y4 - y3 * x4 ) ) / d );
252  point.setY( ( ( y3 - y4 ) * ( x1 * y2 - y1 * x2 ) - ( y1 - y2 ) * ( x3 * y4 - y3 * x4 ) ) / d );
253  }
254  }
255  }
256 
257  // *****************************
258  // ---- Distance constraint
259  if ( ctx.distanceConstraint.locked && ctx.cadPoints().count() >= 2 )
260  {
261  if ( ctx.xConstraint.locked || ctx.yConstraint.locked )
262  {
263  // perform both to detect errors in constraints
264  if ( ctx.xConstraint.locked )
265  {
266  const QgsPointXY verticalPt0( ctx.xConstraint.value, point.y() );
267  const QgsPointXY verticalPt1( ctx.xConstraint.value, point.y() + 1 );
268  res.valid &= QgsGeometryUtils::lineCircleIntersection( previousPt, ctx.distanceConstraint.value, verticalPt0, verticalPt1, point );
269  }
270  if ( ctx.yConstraint.locked )
271  {
272  const QgsPointXY horizontalPt0( point.x(), ctx.yConstraint.value );
273  const QgsPointXY horizontalPt1( point.x() + 1, ctx.yConstraint.value );
274  res.valid &= QgsGeometryUtils::lineCircleIntersection( previousPt, ctx.distanceConstraint.value, horizontalPt0, horizontalPt1, point );
275  }
276  }
277  else
278  {
279  const double dist = std::sqrt( point.sqrDist( previousPt ) );
280  if ( dist == 0 )
281  {
282  // handle case where mouse is over origin and distance constraint is enabled
283  // take arbitrary horizontal line
284  point.set( previousPt.x() + ctx.distanceConstraint.value, previousPt.y() );
285  }
286  else
287  {
288  const double vP = ctx.distanceConstraint.value / dist;
289  point.set( previousPt.x() + ( point.x() - previousPt.x() ) * vP,
290  previousPt.y() + ( point.y() - previousPt.y() ) * vP );
291  }
292 
293  if ( snapMatch.hasEdge() && !ctx.angleConstraint.locked )
294  {
295  // we will magnietize to the intersection of that segment and the lockedDistance !
296  res.valid &= QgsGeometryUtils::lineCircleIntersection( previousPt, ctx.distanceConstraint.value, edgePt0, edgePt1, point );
297  }
298  }
299  }
300 
301  // *****************************
302  // ---- calculate CAD values
303  QgsDebugMsgLevel( QStringLiteral( "point: %1 %2" ).arg( point.x() ).arg( point.y() ), 4 );
304  QgsDebugMsgLevel( QStringLiteral( "previous point: %1 %2" ).arg( previousPt.x() ).arg( previousPt.y() ), 4 );
305  QgsDebugMsgLevel( QStringLiteral( "penultimate point: %1 %2" ).arg( penultimatePt.x() ).arg( penultimatePt.y() ), 4 );
306  //QgsDebugMsg( QStringLiteral( "dx: %1 dy: %2" ).arg( point.x() - previousPt.x() ).arg( point.y() - previousPt.y() ) );
307  //QgsDebugMsg( QStringLiteral( "ddx: %1 ddy: %2" ).arg( previousPt.x() - penultimatePt.x() ).arg( previousPt.y() - penultimatePt.y() ) );
308 
309  res.finalMapPoint = point;
310 
311  return res;
312 }
313 
315 {
316  QgsDebugMsg( QStringLiteral( "Constraints (locked / relative / value" ) );
317  QgsDebugMsg( QStringLiteral( "Angle: %1 %2 %3" ).arg( angleConstraint.locked ).arg( angleConstraint.relative ).arg( angleConstraint.value ) );
318  QgsDebugMsg( QStringLiteral( "Distance: %1 %2 %3" ).arg( distanceConstraint.locked ).arg( distanceConstraint.relative ).arg( distanceConstraint.value ) );
319  QgsDebugMsg( QStringLiteral( "X: %1 %2 %3" ).arg( xConstraint.locked ).arg( xConstraint.relative ).arg( xConstraint.value ) );
320  QgsDebugMsg( QStringLiteral( "Y: %1 %2 %3" ).arg( yConstraint.locked ).arg( yConstraint.relative ).arg( yConstraint.value ) );
321 }
bool locked
Whether the constraint is active, i.e. should be considered.
Definition: qgscadutils.h:55
double value
Numeric value of the constraint (coordinate/distance in map units or angle in degrees)
Definition: qgscadutils.h:59
bool relative
Whether the value is relative to previous value.
Definition: qgscadutils.h:57
Defines constraints for the QgsCadUtils::alignMapPoint() method.
Definition: qgscadutils.h:99
QgsCadUtils::AlignMapPointConstraint yConstraint
Constraint for Y coordinate.
Definition: qgscadutils.h:109
QgsCadUtils::AlignMapPointConstraint xConstraint
Constraint for X coordinate.
Definition: qgscadutils.h:107
double mapUnitsPerPixel
Map units/pixel ratio from map canvas.
Definition: qgscadutils.h:104
QgsPoint cadPoint(int index) const
Returns the recent CAD point at the specified index (in map coordinates).
Definition: qgscadutils.h:168
QgsCadUtils::AlignMapPointConstraint distanceConstraint
Constraint for distance.
Definition: qgscadutils.h:123
QgsSnappingUtils * snappingUtils
Snapping utils that will be used to snap point to map. Must not be nullptr.
Definition: qgscadutils.h:102
QgsCadUtils::AlignMapPointConstraint commonAngleConstraint
Constraint for soft lock to a common angle.
Definition: qgscadutils.h:127
QList< QgsPoint > cadPoints() const
Returns the list of recent CAD points in map coordinates.
Definition: qgscadutils.h:144
void dump() const
Dumps the context's properties, for debugging.
QgsCadUtils::AlignMapPointConstraint angleConstraint
Constraint for angle.
Definition: qgscadutils.h:125
Structure returned from alignMapPoint() method.
Definition: qgscadutils.h:68
QgsPointXY finalMapPoint
map point aligned according to the constraints
Definition: qgscadutils.h:75
bool valid
Whether the combination of constraints is actually valid.
Definition: qgscadutils.h:72
QgsPointLocator::Match snapMatch
Snapped point - only valid if actually used for something.
Definition: qgscadutils.h:81
QgsPointLocator::Match edgeMatch
Snapped segment - only valid if actually used for something.
Definition: qgscadutils.h:87
double softLockCommonAngle
Angle (in degrees) to which we have soft-locked ourselves (if not set it is -1)
Definition: qgscadutils.h:90
static QgsCadUtils::AlignMapPointOutput alignMapPoint(const QgsPointXY &originalMapPoint, const QgsCadUtils::AlignMapPointContext &ctx)
Applies X/Y/angle/distance constraints from the given context to a map point.
Definition: qgscadutils.cpp:37
static bool lineCircleIntersection(const QgsPointXY &center, double radius, const QgsPointXY &linePoint1, const QgsPointXY &linePoint2, QgsPointXY &intersection) SIP_HOLDGIL
Compute the intersection of a line and a circle.
A class to represent a 2D point.
Definition: qgspointxy.h:59
void set(double x, double y) SIP_HOLDGIL
Sets the x and y value of the point.
Definition: qgspointxy.h:139
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
void setX(double x) SIP_HOLDGIL
Sets the x value of the point.
Definition: qgspointxy.h:122
double y
Definition: qgspointxy.h:63
Q_GADGET double x
Definition: qgspointxy.h:62
void setY(double y) SIP_HOLDGIL
Sets the y value of the point.
Definition: qgspointxy.h:132
QgsPointLocator::Match snapToMap(QPoint point, QgsPointLocator::MatchFilter *filter=nullptr, bool relaxed=false)
Snap to map according to the current configuration.
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference)
Definition: qgis.h:1504
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
Interface that allows rejection of some matches in intersection queries (e.g.
virtual bool acceptMatch(const QgsPointLocator::Match &match)=0
QgsPointXY point() const
for vertex / edge match coords depending on what class returns it (geom.cache: layer coords,...
bool hasEdge() const
Returns true if the Match is an edge.
void edgePoints(QgsPointXY &pt1, QgsPointXY &pt2) const
Only for a valid edge match - obtain endpoints of the edge.