Skip to content

Commit 878dfdd

Browse files
committedOct 3, 2017
Refactor CAD editing alignment logic to new QgsCadUtils class
1 parent 68bb68d commit 878dfdd

File tree

9 files changed

+808
-348
lines changed

9 files changed

+808
-348
lines changed
 

‎doc/api_break.dox

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ QgsAdvancedDigitizingDockWidget {#qgis_api_break_3_0_QgsAdvancedDigitizin
476476

477477
- canvasPressEvent(), canvasReleaseEvent(), canvasMoveEvent() were removed. Handling of events is done in QgsMapToolAdvancedDigitizing.
478478
- snappingMode() was removed. Advanced digitizing now always uses project's snapping configuration.
479+
- lineCircleIntersection() was removed
479480

480481

481482
QgsApplication {#qgis_api_break_3_0_QgsApplication}

‎python/gui/qgsadvanceddigitizingdockwidget.sip

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212

1313

14-
1514
class QgsAdvancedDigitizingDockWidget : QgsDockWidget
1615
{
1716
%Docstring
@@ -151,14 +150,6 @@ class QgsAdvancedDigitizingDockWidget : QgsDockWidget
151150

152151
};
153152

154-
static bool lineCircleIntersection( const QgsPointXY &center, const double radius, const QList<QgsPointXY> &segment, QgsPointXY &intersection );
155-
%Docstring
156-
.. note::
157-
158-
from the two solutions, the intersection will be set to the closest point
159-
:rtype: bool
160-
%End
161-
162153
explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = 0 );
163154
%Docstring
164155
Create an advanced digitizing dock widget

‎src/core/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ SET(QGIS_CORE_SRCS
139139
qgscachedfeatureiterator.cpp
140140
qgscacheindex.cpp
141141
qgscacheindexfeatureid.cpp
142+
qgscadutils.cpp
142143
qgsclipper.cpp
143144
qgscolorramp.cpp
144145
qgscolorscheme.cpp
@@ -792,6 +793,7 @@ SET(QGIS_CORE_HDRS
792793
qgscachedfeatureiterator.h
793794
qgscacheindex.h
794795
qgscacheindexfeatureid.h
796+
qgscadutils.h
795797
qgsclipper.h
796798
qgscolorramp.h
797799
qgscolorscheme.h

‎src/core/qgscadutils.cpp

Lines changed: 368 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,368 @@
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+
22+
// tolerances for soft constraints (last values, and common angles)
23+
// for angles, both tolerance in pixels and degrees are used for better performance
24+
static const double SOFT_CONSTRAINT_TOLERANCE_PIXEL = 15;
25+
static const double SOFT_CONSTRAINT_TOLERANCE_DEGREES = 10;
26+
27+
28+
/// @cond PRIVATE
29+
struct EdgesOnlyFilter : public QgsPointLocator::MatchFilter
30+
{
31+
bool acceptMatch( const QgsPointLocator::Match &m ) override { return m.hasEdge(); }
32+
};
33+
/// @endcond
34+
35+
36+
// TODO: move to geometry utils (if not already there)
37+
static bool lineCircleIntersection( const QgsPointXY &center, const double radius, const QgsPointXY &edgePt0, const QgsPointXY &edgePt1, QgsPointXY &intersection )
38+
{
39+
// formula taken from http://mathworld.wolfram.com/Circle-LineIntersection.html
40+
41+
const double x1 = edgePt0.x() - center.x();
42+
const double y1 = edgePt0.y() - center.y();
43+
const double x2 = edgePt1.x() - center.x();
44+
const double y2 = edgePt1.y() - center.y();
45+
const double dx = x2 - x1;
46+
const double dy = y2 - y1;
47+
48+
const double dr = std::sqrt( std::pow( dx, 2 ) + std::pow( dy, 2 ) );
49+
const double d = x1 * y2 - x2 * y1;
50+
51+
const double disc = std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 );
52+
53+
if ( disc < 0 )
54+
{
55+
//no intersection or tangent
56+
return false;
57+
}
58+
else
59+
{
60+
// two solutions
61+
const int sgnDy = dy < 0 ? -1 : 1;
62+
63+
const double ax = center.x() + ( d * dy + sgnDy * dx * std::sqrt( std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 ) ) ) / ( std::pow( dr, 2 ) );
64+
const double ay = center.y() + ( -d * dx + std::fabs( dy ) * std::sqrt( std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 ) ) ) / ( std::pow( dr, 2 ) );
65+
const QgsPointXY p1( ax, ay );
66+
67+
const double bx = center.x() + ( d * dy - sgnDy * dx * std::sqrt( std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 ) ) ) / ( std::pow( dr, 2 ) );
68+
const double by = center.y() + ( -d * dx - std::fabs( dy ) * std::sqrt( std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 ) ) ) / ( std::pow( dr, 2 ) );
69+
const QgsPointXY p2( bx, by );
70+
71+
// snap to nearest intersection
72+
73+
if ( intersection.sqrDist( p1 ) < intersection.sqrDist( p2 ) )
74+
{
75+
intersection.set( p1.x(), p1.y() );
76+
}
77+
else
78+
{
79+
intersection.set( p2.x(), p2.y() );
80+
}
81+
return true;
82+
}
83+
}
84+
85+
86+
87+
QgsCadUtils::AlignMapPointOutput QgsCadUtils::alignMapPoint( const QgsPointXY &originalMapPoint, const QgsCadUtils::AlignMapPointContext &ctx )
88+
{
89+
QgsCadUtils::AlignMapPointOutput res;
90+
res.valid = true;
91+
res.softLockCommonAngle = -1;
92+
93+
// try to snap to anything
94+
QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint );
95+
QgsPointXY point = snapMatch.isValid() ? snapMatch.point() : originalMapPoint;
96+
97+
// try to snap explicitly to a segment - useful for some constraints
98+
QgsPointXY edgePt0, edgePt1;
99+
EdgesOnlyFilter edgesOnlyFilter;
100+
QgsPointLocator::Match edgeMatch = ctx.snappingUtils->snapToMap( originalMapPoint, &edgesOnlyFilter );
101+
if ( edgeMatch.hasEdge() )
102+
edgeMatch.edgePoints( edgePt0, edgePt1 );
103+
104+
res.edgeMatch = edgeMatch;
105+
106+
QgsPointXY previousPt, penultimatePt;
107+
if ( ctx.cadPointList.count() >= 2 )
108+
previousPt = ctx.cadPointList.at( 1 );
109+
if ( ctx.cadPointList.count() >= 3 )
110+
penultimatePt = ctx.cadPointList.at( 2 );
111+
112+
// *****************************
113+
// ---- X constraint
114+
if ( ctx.xConstraint.locked )
115+
{
116+
if ( !ctx.xConstraint.relative )
117+
{
118+
point.setX( ctx.xConstraint.value );
119+
}
120+
else if ( ctx.cadPointList.count() >= 2 )
121+
{
122+
point.setX( previousPt.x() + ctx.xConstraint.value );
123+
}
124+
if ( edgeMatch.hasEdge() && !ctx.yConstraint.locked )
125+
{
126+
// intersect with snapped segment line at X ccordinate
127+
const double dx = edgePt1.x() - edgePt0.x();
128+
if ( dx == 0 )
129+
{
130+
point.setY( edgePt0.y() );
131+
}
132+
else
133+
{
134+
const double dy = edgePt1.y() - edgePt0.y();
135+
point.setY( edgePt0.y() + ( dy * ( point.x() - edgePt0.x() ) ) / dx );
136+
}
137+
}
138+
}
139+
140+
// *****************************
141+
// ---- Y constraint
142+
if ( ctx.yConstraint.locked )
143+
{
144+
if ( !ctx.yConstraint.relative )
145+
{
146+
point.setY( ctx.yConstraint.value );
147+
}
148+
else if ( ctx.cadPointList.count() >= 2 )
149+
{
150+
point.setY( previousPt.y() + ctx.yConstraint.value );
151+
}
152+
if ( edgeMatch.hasEdge() && !ctx.xConstraint.locked )
153+
{
154+
// intersect with snapped segment line at Y ccordinate
155+
const double dy = edgePt1.y() - edgePt0.y();
156+
if ( dy == 0 )
157+
{
158+
point.setX( edgePt0.x() );
159+
}
160+
else
161+
{
162+
const double dx = edgePt1.x() - edgePt0.x();
163+
point.setX( edgePt0.x() + ( dx * ( point.y() - edgePt0.y() ) ) / dy );
164+
}
165+
}
166+
}
167+
168+
// *****************************
169+
// ---- Common Angle constraint
170+
if ( !ctx.angleConstraint.locked && ctx.cadPointList.count() >= 2 && ctx.commonAngleConstraint.locked && ctx.commonAngleConstraint.value != 0 )
171+
{
172+
double commonAngle = ctx.commonAngleConstraint.value * M_PI / 180;
173+
// see if soft common angle constraint should be performed
174+
// only if not in HardLock mode
175+
double softAngle = std::atan2( point.y() - previousPt.y(),
176+
point.x() - previousPt.x() );
177+
double deltaAngle = 0;
178+
if ( ctx.commonAngleConstraint.relative && ctx.cadPointList.count() >= 3 )
179+
{
180+
// compute the angle relative to the last segment (0° is aligned with last segment)
181+
deltaAngle = std::atan2( previousPt.y() - penultimatePt.y(),
182+
previousPt.x() - penultimatePt.x() );
183+
softAngle -= deltaAngle;
184+
}
185+
int quo = std::round( softAngle / commonAngle );
186+
if ( std::fabs( softAngle - quo * commonAngle ) * 180.0 * M_1_PI <= SOFT_CONSTRAINT_TOLERANCE_DEGREES )
187+
{
188+
// also check the distance in pixel to the line, otherwise it's too sticky at long ranges
189+
softAngle = quo * commonAngle;
190+
// http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html
191+
// use the direction vector (cos(a),sin(a)) from previous point. |x2-x1|=1 since sin2+cos2=1
192+
const double dist = std::fabs( std::cos( softAngle + deltaAngle ) * ( previousPt.y() - point.y() )
193+
- std::sin( softAngle + deltaAngle ) * ( previousPt.x() - point.x() ) );
194+
if ( dist / ctx.mapUnitsPerPixel < SOFT_CONSTRAINT_TOLERANCE_PIXEL )
195+
{
196+
res.softLockCommonAngle = 180.0 / M_PI * softAngle;
197+
}
198+
}
199+
}
200+
201+
// angle can be locked in one of the two ways:
202+
// 1. "hard" lock defined by the user
203+
// 2. "soft" lock from common angle (e.g. 45 degrees)
204+
bool angleLocked = false, angleRelative = false;
205+
int angleValueDeg = 0;
206+
if ( ctx.angleConstraint.locked )
207+
{
208+
angleLocked = true;
209+
angleRelative = ctx.angleConstraint.relative;
210+
angleValueDeg = ctx.angleConstraint.value;
211+
}
212+
else if ( res.softLockCommonAngle != -1 )
213+
{
214+
angleLocked = true;
215+
angleRelative = ctx.commonAngleConstraint.relative;
216+
angleValueDeg = res.softLockCommonAngle;
217+
}
218+
219+
// *****************************
220+
// ---- Angle constraint
221+
// input angles are in degrees
222+
if ( angleLocked )
223+
{
224+
double angleValue = angleValueDeg * M_PI / 180;
225+
if ( angleRelative && ctx.cadPointList.count() >= 3 )
226+
{
227+
// compute the angle relative to the last segment (0° is aligned with last segment)
228+
angleValue += std::atan2( previousPt.y() - penultimatePt.y(),
229+
previousPt.x() - penultimatePt.x() );
230+
}
231+
232+
double cosa = std::cos( angleValue );
233+
double sina = std::sin( angleValue );
234+
double v = ( point.x() - previousPt.x() ) * cosa + ( point.y() - previousPt.y() ) * sina;
235+
if ( ctx.xConstraint.locked && ctx.yConstraint.locked )
236+
{
237+
// do nothing if both X,Y are already locked
238+
}
239+
else if ( ctx.xConstraint.locked )
240+
{
241+
if ( qgsDoubleNear( cosa, 0.0 ) )
242+
{
243+
res.valid = false;
244+
}
245+
else
246+
{
247+
double x = ctx.xConstraint.value;
248+
if ( !ctx.xConstraint.relative )
249+
{
250+
x -= previousPt.x();
251+
}
252+
point.setY( previousPt.y() + x * sina / cosa );
253+
}
254+
}
255+
else if ( ctx.yConstraint.locked )
256+
{
257+
if ( qgsDoubleNear( sina, 0.0 ) )
258+
{
259+
res.valid = false;
260+
}
261+
else
262+
{
263+
double y = ctx.yConstraint.value;
264+
if ( !ctx.yConstraint.relative )
265+
{
266+
y -= previousPt.y();
267+
}
268+
point.setX( previousPt.x() + y * cosa / sina );
269+
}
270+
}
271+
else
272+
{
273+
point.setX( previousPt.x() + cosa * v );
274+
point.setY( previousPt.y() + sina * v );
275+
}
276+
277+
if ( edgeMatch.hasEdge() && !ctx.distanceConstraint.locked )
278+
{
279+
// magnetize to the intersection of the snapped segment and the lockedAngle
280+
281+
// line of previous point + locked angle
282+
const double x1 = previousPt.x();
283+
const double y1 = previousPt.y();
284+
const double x2 = previousPt.x() + cosa;
285+
const double y2 = previousPt.y() + sina;
286+
// line of snapped segment
287+
const double x3 = edgePt0.x();
288+
const double y3 = edgePt0.y();
289+
const double x4 = edgePt1.x();
290+
const double y4 = edgePt1.y();
291+
292+
const double d = ( x1 - x2 ) * ( y3 - y4 ) - ( y1 - y2 ) * ( x3 - x4 );
293+
294+
// do not compute intersection if lines are almost parallel
295+
// this threshold might be adapted
296+
if ( std::fabs( d ) > 0.01 )
297+
{
298+
point.setX( ( ( x3 - x4 ) * ( x1 * y2 - y1 * x2 ) - ( x1 - x2 ) * ( x3 * y4 - y3 * x4 ) ) / d );
299+
point.setY( ( ( y3 - y4 ) * ( x1 * y2 - y1 * x2 ) - ( y1 - y2 ) * ( x3 * y4 - y3 * x4 ) ) / d );
300+
}
301+
}
302+
}
303+
304+
// *****************************
305+
// ---- Distance constraint
306+
if ( ctx.distanceConstraint.locked && ctx.cadPointList.count() >= 2 )
307+
{
308+
if ( ctx.xConstraint.locked || ctx.yConstraint.locked )
309+
{
310+
// perform both to detect errors in constraints
311+
if ( ctx.xConstraint.locked )
312+
{
313+
QgsPointXY verticalPt0( ctx.xConstraint.value, point.y() );
314+
QgsPointXY verticalPt1( ctx.xConstraint.value, point.y() + 1 );
315+
res.valid &= lineCircleIntersection( previousPt, ctx.distanceConstraint.value, verticalPt0, verticalPt1, point );
316+
}
317+
if ( ctx.yConstraint.locked )
318+
{
319+
QgsPointXY horizontalPt0( point.x(), ctx.yConstraint.value );
320+
QgsPointXY horizontalPt1( point.x() + 1, ctx.yConstraint.value );
321+
res.valid &= lineCircleIntersection( previousPt, ctx.distanceConstraint.value, horizontalPt0, horizontalPt1, point );
322+
}
323+
}
324+
else
325+
{
326+
const double dist = std::sqrt( point.sqrDist( previousPt ) );
327+
if ( dist == 0 )
328+
{
329+
// handle case where mouse is over origin and distance constraint is enabled
330+
// take arbitrary horizontal line
331+
point.set( previousPt.x() + ctx.distanceConstraint.value, previousPt.y() );
332+
}
333+
else
334+
{
335+
const double vP = ctx.distanceConstraint.value / dist;
336+
point.set( previousPt.x() + ( point.x() - previousPt.x() ) * vP,
337+
previousPt.y() + ( point.y() - previousPt.y() ) * vP );
338+
}
339+
340+
if ( edgeMatch.hasEdge() && !ctx.angleConstraint.locked )
341+
{
342+
// we will magnietize to the intersection of that segment and the lockedDistance !
343+
res.valid &= lineCircleIntersection( previousPt, ctx.distanceConstraint.value, edgePt0, edgePt1, point );
344+
}
345+
}
346+
}
347+
348+
// *****************************
349+
// ---- calculate CAD values
350+
QgsDebugMsgLevel( QString( "point: %1 %2" ).arg( point.x() ).arg( point.y() ), 4 );
351+
QgsDebugMsgLevel( QString( "previous point: %1 %2" ).arg( previousPt.x() ).arg( previousPt.y() ), 4 );
352+
QgsDebugMsgLevel( QString( "penultimate point: %1 %2" ).arg( penultimatePt.x() ).arg( penultimatePt.y() ), 4 );
353+
//QgsDebugMsg( QString( "dx: %1 dy: %2" ).arg( point.x() - previousPt.x() ).arg( point.y() - previousPt.y() ) );
354+
//QgsDebugMsg( QString( "ddx: %1 ddy: %2" ).arg( previousPt.x() - penultimatePt.x() ).arg( previousPt.y() - penultimatePt.y() ) );
355+
356+
res.finalMapPoint = point;
357+
358+
return res;
359+
}
360+
361+
void QgsCadUtils::AlignMapPointContext::dump() const
362+
{
363+
QgsDebugMsg( "Constraints (locked / relative / value" );
364+
QgsDebugMsg( QString( "Angle: %1 %2 %3" ).arg( angleConstraint.locked ).arg( angleConstraint.relative ).arg( angleConstraint.value ) );
365+
QgsDebugMsg( QString( "Distance: %1 %2 %3" ).arg( distanceConstraint.locked ).arg( distanceConstraint.relative ).arg( distanceConstraint.value ) );
366+
QgsDebugMsg( QString( "X: %1 %2 %3" ).arg( xConstraint.locked ).arg( xConstraint.relative ).arg( xConstraint.value ) );
367+
QgsDebugMsg( QString( "Y: %1 %2 %3" ).arg( yConstraint.locked ).arg( yConstraint.relative ).arg( yConstraint.value ) );
368+
}

‎src/core/qgscadutils.h

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/***************************************************************************
2+
qgscadutils.h
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+
#ifndef QGSCADUTILS_H
18+
#define QGSCADUTILS_H
19+
20+
#include "qgis_core.h"
21+
22+
#include "qgspointlocator.h"
23+
24+
class QgsSnappingUtils;
25+
26+
/**
27+
* The QgsCadUtils class provides routines for CAD editing.
28+
*
29+
* \since QGIS 3.0
30+
*/
31+
class CORE_EXPORT QgsCadUtils
32+
{
33+
public:
34+
35+
//! Structure with details of one constraint
36+
struct AlignMapPointConstraint
37+
{
38+
AlignMapPointConstraint( bool locked = false, bool relative = false, double value = 0 )
39+
: locked( locked )
40+
, relative( relative )
41+
, value( value )
42+
{}
43+
44+
//! Whether the constraint is active, i.e. should be considered
45+
bool locked;
46+
//! Whether the value is relative to previous value
47+
bool relative;
48+
//! Numeric value of the constraint (coordinate/distance in map units or angle in degrees)
49+
double value;
50+
};
51+
52+
//! Structure defining all constraints for alignMapPoint() method
53+
struct AlignMapPointContext
54+
{
55+
//! Snapping utils that will be used to snap point to map. Must not be null
56+
QgsSnappingUtils *snappingUtils;
57+
//! Map units/pixel ratio from map canvas. Needed for
58+
double mapUnitsPerPixel;
59+
60+
//! Constraint for X coordinate
61+
AlignMapPointConstraint xConstraint;
62+
//! Constraint for Y coordinate
63+
AlignMapPointConstraint yConstraint;
64+
//! Constraint for distance
65+
AlignMapPointConstraint distanceConstraint;
66+
//! Constraint for angle
67+
AlignMapPointConstraint angleConstraint;
68+
//! Constraint for soft lock to a common angle
69+
AlignMapPointConstraint commonAngleConstraint;
70+
71+
//! List of recent CAD points in map coordinates. These are used to turn relative constraints to absolute.
72+
//! First point is the most recent point. Currently using only "previous" point (index 1) and "penultimate"
73+
//! point (index 2) for alignment purposes.
74+
QList<QgsPointXY> cadPointList;
75+
76+
void dump() const;
77+
};
78+
79+
//! Structure returned from alignMapPoint() method
80+
struct AlignMapPointOutput
81+
{
82+
//! Whether the combination of constraints is actually valid
83+
bool valid;
84+
85+
//! map point aligned according to the constraints
86+
QgsPointXY finalMapPoint;
87+
88+
//! Snapped segment - only valid if actually used for something
89+
QgsPointLocator::Match edgeMatch;
90+
91+
//! Angle (in degrees) to which we have soft-locked ourselves (if not set it is -1)
92+
int softLockCommonAngle;
93+
};
94+
95+
/**
96+
* Applies X/Y/angle/distance constraints from the given context to a map point.
97+
* Returns a structure containing aligned map point, whether the constraints are valid and
98+
* some extra information.
99+
*/
100+
static AlignMapPointOutput alignMapPoint( const QgsPointXY &originalMapPoint, const AlignMapPointContext &ctx );
101+
102+
};
103+
104+
#endif // QGSCADUTILS_H

‎src/gui/qgsadvanceddigitizingdockwidget.cpp

Lines changed: 68 additions & 325 deletions
Large diffs are not rendered by default.

‎src/gui/qgsadvanceddigitizingdockwidget.h

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,6 @@ class QgsMapTool;
3232
class QgsMapToolAdvancedDigitizing;
3333
class QgsPointXY;
3434

35-
// tolerances for soft constraints (last values, and common angles)
36-
// for angles, both tolerance in pixels and degrees are used for better performance
37-
static const double SOFT_CONSTRAINT_TOLERANCE_PIXEL = 15 SIP_SKIP;
38-
static const double SOFT_CONSTRAINT_TOLERANCE_DEGREES = 10 SIP_SKIP;
39-
4035
/** \ingroup gui
4136
* \brief The QgsAdvancedDigitizingDockWidget class is a dockable widget
4237
* used to handle the CAD tools on top of a selection of map tools.
@@ -188,10 +183,6 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
188183
double mValue;
189184
};
190185

191-
//! performs the intersection of a circle and a line
192-
//! \note from the two solutions, the intersection will be set to the closest point
193-
static bool lineCircleIntersection( const QgsPointXY &center, const double radius, const QList<QgsPointXY> &segment, QgsPointXY &intersection );
194-
195186
/**
196187
* Create an advanced digitizing dock widget
197188
* \param canvas The map canvas on which the widget operates
@@ -393,15 +384,12 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
393384
//! defines the additional constraint to be used (no/parallel/perpendicular)
394385
void lockAdditionalConstraint( AdditionalConstraint constraint );
395386

396-
QList<QgsPointXY> snapSegment( const QgsPointLocator::Match &snapMatch );
397-
398387
/**
399-
* Returns the first snapped segment. Will try to snap a segment according to the event's snapping mode.
388+
* Returns the first snapped segment. Will try to snap a segment using all layers
400389
* \param originalMapPoint point to be snapped (in map coordinates)
401390
* \param snapped if given, determines if a segment has been snapped
402-
* \param allLayers if true, override snapping mode
403391
*/
404-
QList<QgsPointXY> snapSegment( const QgsPointXY &originalMapPoint, bool *snapped = nullptr, bool allLayers = false ) const;
392+
QList<QgsPointXY> snapSegmentToAllLayers( const QgsPointXY &originalMapPoint, bool *snapped = nullptr ) const;
405393

406394
//! update the current point in the CAD point list
407395
void updateCurrentPoint( const QgsPointXY &point );
@@ -432,6 +420,9 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private
432420
*/
433421
void updateConstraintValue( CadConstraint *constraint, const QString &textValue, bool convertExpression = false );
434422

423+
//! Updates values of constraints that are not locked based on the current point
424+
void updateUnlockedConstraintValues( const QgsPointXY &point );
425+
435426
QgsMapCanvas *mMapCanvas = nullptr;
436427
QgsAdvancedDigitizingCanvasItem *mCadPaintItem = nullptr;
437428

‎tests/src/core/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ SET(TESTS
7777
testqgsauthconfig.cpp
7878
testqgsauthmanager.cpp
7979
testqgsblendmodes.cpp
80+
testqgscadutils.cpp
8081
testqgsclipper.cpp
8182
testqgscolorscheme.cpp
8283
testqgscolorschemeregistry.cpp

‎tests/src/core/testqgscadutils.cpp

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
/***************************************************************************
2+
testqgscadutils.cpp
3+
--------------------------------------
4+
Date : September 2017
5+
Copyright : (C) 2017 by Martin Dobias
6+
Email : wonder.sk at gmail.com
7+
***************************************************************************
8+
* *
9+
* This program is free software; you can redistribute it and/or modify *
10+
* it under the terms of the GNU General Public License as published by *
11+
* the Free Software Foundation; either version 2 of the License, or *
12+
* (at your option) any later version. *
13+
* *
14+
***************************************************************************/
15+
16+
#include "qgstest.h"
17+
#include <QObject>
18+
#include <QString>
19+
20+
#include "qgscadutils.h"
21+
#include "qgsproject.h"
22+
#include "qgssnappingutils.h"
23+
#include "qgsvectorlayer.h"
24+
25+
/** \ingroup UnitTests
26+
* This is a unit test for the QgsCadUtils class.
27+
*/
28+
class TestQgsCadUtils : public QObject
29+
{
30+
Q_OBJECT
31+
public:
32+
TestQgsCadUtils()
33+
{}
34+
~TestQgsCadUtils()
35+
{
36+
}
37+
38+
private slots:
39+
void initTestCase();// will be called before the first testfunction is executed.
40+
void cleanupTestCase();// will be called after the last testfunction was executed.
41+
void init() {} // will be called before each testfunction is executed.
42+
void cleanup() {} // will be called after every testfunction.
43+
44+
void testBasic();
45+
void testXY();
46+
void testAngle();
47+
void testCommonAngle();
48+
void testDistance();
49+
void testEdge();
50+
51+
private:
52+
53+
QgsCadUtils::AlignMapPointContext baseContext()
54+
{
55+
QgsCadUtils::AlignMapPointContext context;
56+
context.snappingUtils = mSnappingUtils;
57+
context.mapUnitsPerPixel = mMapSettings.mapUnitsPerPixel();
58+
context.cadPointList << QgsPointXY() << QgsPointXY( 30, 20 ) << QgsPointXY( 30, 30 );
59+
return context;
60+
}
61+
62+
QString mTestDataDir;
63+
QgsVectorLayer *mLayerPolygon = nullptr;
64+
QgsSnappingUtils *mSnappingUtils = nullptr;
65+
QgsMapSettings mMapSettings;
66+
};
67+
68+
69+
//runs before all tests
70+
void TestQgsCadUtils::initTestCase()
71+
{
72+
// init QGIS's paths - true means that all path will be inited from prefix
73+
QgsApplication::init();
74+
QgsApplication::initQgis();
75+
76+
mLayerPolygon = new QgsVectorLayer( "Polygon?crs=EPSG:27700", "layer polygon", "memory" );
77+
QVERIFY( mLayerPolygon->isValid() );
78+
79+
QgsPolygon polygon1;
80+
QgsPolyline polygon1exterior;
81+
polygon1exterior << QgsPointXY( 10, 10 ) << QgsPointXY( 30, 10 ) << QgsPointXY( 10, 20 ) << QgsPointXY( 10, 10 );
82+
polygon1 << polygon1exterior;
83+
QgsFeature polygonF1;
84+
polygonF1.setGeometry( QgsGeometry::fromPolygon( polygon1 ) );
85+
86+
mLayerPolygon->startEditing();
87+
mLayerPolygon->addFeature( polygonF1 );
88+
89+
QgsProject::instance()->addMapLayer( mLayerPolygon );
90+
91+
QgsSnappingConfig snapConfig;
92+
snapConfig.setEnabled( true );
93+
snapConfig.setMode( QgsSnappingConfig::AllLayers );
94+
snapConfig.setType( QgsSnappingConfig::VertexAndSegment );
95+
snapConfig.setTolerance( 1.0 );
96+
97+
mMapSettings.setExtent( QgsRectangle( 0, 0, 100, 100 ) );
98+
mMapSettings.setOutputSize( QSize( 100, 100 ) );
99+
mMapSettings.setLayers( QList<QgsMapLayer *>() << mLayerPolygon );
100+
101+
mSnappingUtils = new QgsSnappingUtils;
102+
mSnappingUtils->setConfig( snapConfig );
103+
mSnappingUtils->setMapSettings( mMapSettings );
104+
}
105+
106+
//runs after all tests
107+
void TestQgsCadUtils::cleanupTestCase()
108+
{
109+
delete mSnappingUtils;
110+
111+
QgsApplication::exitQgis();
112+
}
113+
114+
void TestQgsCadUtils::testBasic()
115+
{
116+
QgsCadUtils::AlignMapPointContext context( baseContext() );
117+
118+
// no snap
119+
QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 5, 5 ), context );
120+
QVERIFY( res0.valid );
121+
QCOMPARE( res0.finalMapPoint, QgsPointXY( 5, 5 ) );
122+
123+
// simple snap to vertex
124+
QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 9.5, 9.5 ), context );
125+
QVERIFY( res1.valid );
126+
QCOMPARE( res1.finalMapPoint, QgsPointXY( 10, 10 ) );
127+
}
128+
129+
void TestQgsCadUtils::testXY()
130+
{
131+
QgsCadUtils::AlignMapPointContext context( baseContext() );
132+
133+
// x absolute
134+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 20 );
135+
QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context );
136+
QVERIFY( res0.valid );
137+
QCOMPARE( res0.finalMapPoint, QgsPointXY( 20, 29 ) );
138+
139+
// x relative
140+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -5 );
141+
QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context );
142+
QVERIFY( res1.valid );
143+
QCOMPARE( res1.finalMapPoint, QgsPointXY( 25, 29 ) );
144+
145+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint();
146+
147+
// y absolute
148+
context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 20 );
149+
QgsCadUtils::AlignMapPointOutput res2 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context );
150+
QVERIFY( res2.valid );
151+
QCOMPARE( res2.finalMapPoint, QgsPointXY( 29, 20 ) );
152+
153+
// y relative
154+
context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -5 );
155+
QgsCadUtils::AlignMapPointOutput res3 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context );
156+
QVERIFY( res3.valid );
157+
QCOMPARE( res3.finalMapPoint, QgsPointXY( 29, 15 ) );
158+
159+
// x and y (relative)
160+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 32 );
161+
context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 22 );
162+
QgsCadUtils::AlignMapPointOutput res4 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context );
163+
QVERIFY( res4.valid );
164+
QCOMPARE( res4.finalMapPoint, QgsPointXY( 32, 22 ) );
165+
166+
// x and y (relative)
167+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -2 );
168+
context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -2 );
169+
QgsCadUtils::AlignMapPointOutput res5 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context );
170+
QVERIFY( res5.valid );
171+
QCOMPARE( res5.finalMapPoint, QgsPointXY( 28, 18 ) );
172+
}
173+
174+
void TestQgsCadUtils::testAngle()
175+
{
176+
QgsCadUtils::AlignMapPointContext context( baseContext() );
177+
178+
// angle abs
179+
context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 );
180+
QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20 ), context );
181+
QVERIFY( res0.valid );
182+
QCOMPARE( res0.finalMapPoint, QgsPointXY( 35, 25 ) );
183+
184+
// angle rel
185+
context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, 45 );
186+
QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 30, 30 ), context );
187+
QVERIFY( res1.valid );
188+
QCOMPARE( res1.finalMapPoint, QgsPointXY( 25, 25 ) );
189+
190+
// angle + x abs
191+
context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 );
192+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 38 );
193+
QgsCadUtils::AlignMapPointOutput res2 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20 ), context );
194+
QVERIFY( res2.valid );
195+
QCOMPARE( res2.finalMapPoint, QgsPointXY( 38, 28 ) );
196+
197+
// angle + y rel
198+
context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 );
199+
context.xConstraint = QgsCadUtils::AlignMapPointConstraint();
200+
context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 17 );
201+
QgsCadUtils::AlignMapPointOutput res3 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20 ), context );
202+
QVERIFY( res3.valid );
203+
QCOMPARE( res3.finalMapPoint, QgsPointXY( 27, 17 ) );
204+
}
205+
206+
void TestQgsCadUtils::testCommonAngle()
207+
{
208+
QgsCadUtils::AlignMapPointContext context( baseContext() );
209+
210+
// without common angle
211+
QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20.1 ), context );
212+
QVERIFY( res0.valid );
213+
QCOMPARE( res0.softLockCommonAngle, -1 );
214+
QCOMPARE( res0.finalMapPoint, QgsPointXY( 40, 20.1 ) );
215+
216+
// common angle
217+
context.commonAngleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 90 );
218+
QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20.1 ), context );
219+
QVERIFY( res1.valid );
220+
QCOMPARE( res1.softLockCommonAngle, 0 );
221+
QCOMPARE( res1.finalMapPoint, QgsPointXY( 40, 20 ) );
222+
223+
// common angle + angle (make sure that angle constraint has priority)
224+
context.commonAngleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 90 );
225+
context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 );
226+
QgsCadUtils::AlignMapPointOutput res2 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20.1 ), context );
227+
QVERIFY( res2.valid );
228+
QCOMPARE( res2.softLockCommonAngle, -1 );
229+
QCOMPARE( res2.finalMapPoint, QgsPointXY( 35.05, 25.05 ) );
230+
231+
// common angle rel
232+
context.angleConstraint = QgsCadUtils::AlignMapPointConstraint();
233+
context.commonAngleConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, 90 );
234+
context.cadPointList[1] = QgsPointXY( 40, 20 );
235+
QgsCadUtils::AlignMapPointOutput res3 = QgsCadUtils::alignMapPoint( QgsPointXY( 50.1, 29.9 ), context );
236+
QVERIFY( res3.valid );
237+
QCOMPARE( res3.softLockCommonAngle, 90 );
238+
QCOMPARE( res3.finalMapPoint, QgsPointXY( 50, 30 ) );
239+
}
240+
241+
void TestQgsCadUtils::testDistance()
242+
{
243+
// TODO:
244+
// dist
245+
// dist+x / dist+y
246+
// dist+angle
247+
}
248+
249+
void TestQgsCadUtils::testEdge()
250+
{
251+
// TODO:
252+
// x+edge / y+edge
253+
// angle+edge
254+
// distance+edge
255+
}
256+
257+
QGSTEST_MAIN( TestQgsCadUtils )
258+
259+
#include "testqgscadutils.moc"

0 commit comments

Comments
 (0)
Please sign in to comment.