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