diff --git a/doc/api_break.dox b/doc/api_break.dox index 3aa00f7424f..49f495e3ca7 100644 --- a/doc/api_break.dox +++ b/doc/api_break.dox @@ -476,6 +476,7 @@ QgsAdvancedDigitizingDockWidget {#qgis_api_break_3_0_QgsAdvancedDigitizin - canvasPressEvent(), canvasReleaseEvent(), canvasMoveEvent() were removed. Handling of events is done in QgsMapToolAdvancedDigitizing. - snappingMode() was removed. Advanced digitizing now always uses project's snapping configuration. +- lineCircleIntersection() was removed QgsApplication {#qgis_api_break_3_0_QgsApplication} diff --git a/python/gui/qgsadvanceddigitizingdockwidget.sip b/python/gui/qgsadvanceddigitizingdockwidget.sip index a62b6994441..2ea305d0d46 100644 --- a/python/gui/qgsadvanceddigitizingdockwidget.sip +++ b/python/gui/qgsadvanceddigitizingdockwidget.sip @@ -11,7 +11,6 @@ - class QgsAdvancedDigitizingDockWidget : QgsDockWidget { %Docstring @@ -151,14 +150,6 @@ class QgsAdvancedDigitizingDockWidget : QgsDockWidget }; - static bool lineCircleIntersection( const QgsPointXY ¢er, const double radius, const QList &segment, QgsPointXY &intersection ); -%Docstring -.. note:: - - from the two solutions, the intersection will be set to the closest point - :rtype: bool -%End - explicit QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent = 0 ); %Docstring Create an advanced digitizing dock widget diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 6177717e0ab..feca1a6b8fa 100755 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -139,6 +139,7 @@ SET(QGIS_CORE_SRCS qgscachedfeatureiterator.cpp qgscacheindex.cpp qgscacheindexfeatureid.cpp + qgscadutils.cpp qgsclipper.cpp qgscolorramp.cpp qgscolorscheme.cpp @@ -792,6 +793,7 @@ SET(QGIS_CORE_HDRS qgscachedfeatureiterator.h qgscacheindex.h qgscacheindexfeatureid.h + qgscadutils.h qgsclipper.h qgscolorramp.h qgscolorscheme.h diff --git a/src/core/qgscadutils.cpp b/src/core/qgscadutils.cpp new file mode 100644 index 00000000000..7586e17d0a5 --- /dev/null +++ b/src/core/qgscadutils.cpp @@ -0,0 +1,368 @@ +/*************************************************************************** + qgscadutils.cpp + ------------------- + begin : September 2017 + copyright : (C) 2017 by Martin Dobias + email : wonder dot sk at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgscadutils.h" + +#include "qgslogger.h" +#include "qgssnappingutils.h" + +// tolerances for soft constraints (last values, and common angles) +// for angles, both tolerance in pixels and degrees are used for better performance +static const double SOFT_CONSTRAINT_TOLERANCE_PIXEL = 15; +static const double SOFT_CONSTRAINT_TOLERANCE_DEGREES = 10; + + +/// @cond PRIVATE +struct EdgesOnlyFilter : public QgsPointLocator::MatchFilter +{ + bool acceptMatch( const QgsPointLocator::Match &m ) override { return m.hasEdge(); } +}; +/// @endcond + + +// TODO: move to geometry utils (if not already there) +static bool lineCircleIntersection( const QgsPointXY ¢er, const double radius, const QgsPointXY &edgePt0, const QgsPointXY &edgePt1, QgsPointXY &intersection ) +{ + // formula taken from http://mathworld.wolfram.com/Circle-LineIntersection.html + + const double x1 = edgePt0.x() - center.x(); + const double y1 = edgePt0.y() - center.y(); + const double x2 = edgePt1.x() - center.x(); + const double y2 = edgePt1.y() - center.y(); + const double dx = x2 - x1; + const double dy = y2 - y1; + + const double dr = std::sqrt( std::pow( dx, 2 ) + std::pow( dy, 2 ) ); + const double d = x1 * y2 - x2 * y1; + + const double disc = std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 ); + + if ( disc < 0 ) + { + //no intersection or tangent + return false; + } + else + { + // two solutions + const int sgnDy = dy < 0 ? -1 : 1; + + 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 ) ); + 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 ) ); + const QgsPointXY p1( ax, ay ); + + 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 ) ); + 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 ) ); + const QgsPointXY p2( bx, by ); + + // snap to nearest intersection + + if ( intersection.sqrDist( p1 ) < intersection.sqrDist( p2 ) ) + { + intersection.set( p1.x(), p1.y() ); + } + else + { + intersection.set( p2.x(), p2.y() ); + } + return true; + } +} + + + +QgsCadUtils::AlignMapPointOutput QgsCadUtils::alignMapPoint( const QgsPointXY &originalMapPoint, const QgsCadUtils::AlignMapPointContext &ctx ) +{ + QgsCadUtils::AlignMapPointOutput res; + res.valid = true; + res.softLockCommonAngle = -1; + + // try to snap to anything + QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint ); + QgsPointXY point = snapMatch.isValid() ? snapMatch.point() : originalMapPoint; + + // try to snap explicitly to a segment - useful for some constraints + QgsPointXY edgePt0, edgePt1; + EdgesOnlyFilter edgesOnlyFilter; + QgsPointLocator::Match edgeMatch = ctx.snappingUtils->snapToMap( originalMapPoint, &edgesOnlyFilter ); + if ( edgeMatch.hasEdge() ) + edgeMatch.edgePoints( edgePt0, edgePt1 ); + + res.edgeMatch = edgeMatch; + + QgsPointXY previousPt, penultimatePt; + if ( ctx.cadPointList.count() >= 2 ) + previousPt = ctx.cadPointList.at( 1 ); + if ( ctx.cadPointList.count() >= 3 ) + penultimatePt = ctx.cadPointList.at( 2 ); + + // ***************************** + // ---- X constraint + if ( ctx.xConstraint.locked ) + { + if ( !ctx.xConstraint.relative ) + { + point.setX( ctx.xConstraint.value ); + } + else if ( ctx.cadPointList.count() >= 2 ) + { + point.setX( previousPt.x() + ctx.xConstraint.value ); + } + if ( edgeMatch.hasEdge() && !ctx.yConstraint.locked ) + { + // intersect with snapped segment line at X ccordinate + const double dx = edgePt1.x() - edgePt0.x(); + if ( dx == 0 ) + { + point.setY( edgePt0.y() ); + } + else + { + const double dy = edgePt1.y() - edgePt0.y(); + point.setY( edgePt0.y() + ( dy * ( point.x() - edgePt0.x() ) ) / dx ); + } + } + } + + // ***************************** + // ---- Y constraint + if ( ctx.yConstraint.locked ) + { + if ( !ctx.yConstraint.relative ) + { + point.setY( ctx.yConstraint.value ); + } + else if ( ctx.cadPointList.count() >= 2 ) + { + point.setY( previousPt.y() + ctx.yConstraint.value ); + } + if ( edgeMatch.hasEdge() && !ctx.xConstraint.locked ) + { + // intersect with snapped segment line at Y ccordinate + const double dy = edgePt1.y() - edgePt0.y(); + if ( dy == 0 ) + { + point.setX( edgePt0.x() ); + } + else + { + const double dx = edgePt1.x() - edgePt0.x(); + point.setX( edgePt0.x() + ( dx * ( point.y() - edgePt0.y() ) ) / dy ); + } + } + } + + // ***************************** + // ---- Common Angle constraint + if ( !ctx.angleConstraint.locked && ctx.cadPointList.count() >= 2 && ctx.commonAngleConstraint.locked && ctx.commonAngleConstraint.value != 0 ) + { + double commonAngle = ctx.commonAngleConstraint.value * M_PI / 180; + // see if soft common angle constraint should be performed + // only if not in HardLock mode + double softAngle = std::atan2( point.y() - previousPt.y(), + point.x() - previousPt.x() ); + double deltaAngle = 0; + if ( ctx.commonAngleConstraint.relative && ctx.cadPointList.count() >= 3 ) + { + // compute the angle relative to the last segment (0° is aligned with last segment) + deltaAngle = std::atan2( previousPt.y() - penultimatePt.y(), + previousPt.x() - penultimatePt.x() ); + softAngle -= deltaAngle; + } + int quo = std::round( softAngle / commonAngle ); + if ( std::fabs( softAngle - quo * commonAngle ) * 180.0 * M_1_PI <= SOFT_CONSTRAINT_TOLERANCE_DEGREES ) + { + // also check the distance in pixel to the line, otherwise it's too sticky at long ranges + softAngle = quo * commonAngle; + // http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html + // use the direction vector (cos(a),sin(a)) from previous point. |x2-x1|=1 since sin2+cos2=1 + const double dist = std::fabs( std::cos( softAngle + deltaAngle ) * ( previousPt.y() - point.y() ) + - std::sin( softAngle + deltaAngle ) * ( previousPt.x() - point.x() ) ); + if ( dist / ctx.mapUnitsPerPixel < SOFT_CONSTRAINT_TOLERANCE_PIXEL ) + { + res.softLockCommonAngle = 180.0 / M_PI * softAngle; + } + } + } + + // angle can be locked in one of the two ways: + // 1. "hard" lock defined by the user + // 2. "soft" lock from common angle (e.g. 45 degrees) + bool angleLocked = false, angleRelative = false; + int angleValueDeg = 0; + if ( ctx.angleConstraint.locked ) + { + angleLocked = true; + angleRelative = ctx.angleConstraint.relative; + angleValueDeg = ctx.angleConstraint.value; + } + else if ( res.softLockCommonAngle != -1 ) + { + angleLocked = true; + angleRelative = ctx.commonAngleConstraint.relative; + angleValueDeg = res.softLockCommonAngle; + } + + // ***************************** + // ---- Angle constraint + // input angles are in degrees + if ( angleLocked ) + { + double angleValue = angleValueDeg * M_PI / 180; + if ( angleRelative && ctx.cadPointList.count() >= 3 ) + { + // compute the angle relative to the last segment (0° is aligned with last segment) + angleValue += std::atan2( previousPt.y() - penultimatePt.y(), + previousPt.x() - penultimatePt.x() ); + } + + double cosa = std::cos( angleValue ); + double sina = std::sin( angleValue ); + double v = ( point.x() - previousPt.x() ) * cosa + ( point.y() - previousPt.y() ) * sina; + if ( ctx.xConstraint.locked && ctx.yConstraint.locked ) + { + // do nothing if both X,Y are already locked + } + else if ( ctx.xConstraint.locked ) + { + if ( qgsDoubleNear( cosa, 0.0 ) ) + { + res.valid = false; + } + else + { + double x = ctx.xConstraint.value; + if ( !ctx.xConstraint.relative ) + { + x -= previousPt.x(); + } + point.setY( previousPt.y() + x * sina / cosa ); + } + } + else if ( ctx.yConstraint.locked ) + { + if ( qgsDoubleNear( sina, 0.0 ) ) + { + res.valid = false; + } + else + { + double y = ctx.yConstraint.value; + if ( !ctx.yConstraint.relative ) + { + y -= previousPt.y(); + } + point.setX( previousPt.x() + y * cosa / sina ); + } + } + else + { + point.setX( previousPt.x() + cosa * v ); + point.setY( previousPt.y() + sina * v ); + } + + if ( edgeMatch.hasEdge() && !ctx.distanceConstraint.locked ) + { + // magnetize to the intersection of the snapped segment and the lockedAngle + + // line of previous point + locked angle + const double x1 = previousPt.x(); + const double y1 = previousPt.y(); + const double x2 = previousPt.x() + cosa; + const double y2 = previousPt.y() + sina; + // line of snapped segment + const double x3 = edgePt0.x(); + const double y3 = edgePt0.y(); + const double x4 = edgePt1.x(); + const double y4 = edgePt1.y(); + + const double d = ( x1 - x2 ) * ( y3 - y4 ) - ( y1 - y2 ) * ( x3 - x4 ); + + // do not compute intersection if lines are almost parallel + // this threshold might be adapted + if ( std::fabs( d ) > 0.01 ) + { + point.setX( ( ( x3 - x4 ) * ( x1 * y2 - y1 * x2 ) - ( x1 - x2 ) * ( x3 * y4 - y3 * x4 ) ) / d ); + point.setY( ( ( y3 - y4 ) * ( x1 * y2 - y1 * x2 ) - ( y1 - y2 ) * ( x3 * y4 - y3 * x4 ) ) / d ); + } + } + } + + // ***************************** + // ---- Distance constraint + if ( ctx.distanceConstraint.locked && ctx.cadPointList.count() >= 2 ) + { + if ( ctx.xConstraint.locked || ctx.yConstraint.locked ) + { + // perform both to detect errors in constraints + if ( ctx.xConstraint.locked ) + { + QgsPointXY verticalPt0( ctx.xConstraint.value, point.y() ); + QgsPointXY verticalPt1( ctx.xConstraint.value, point.y() + 1 ); + res.valid &= lineCircleIntersection( previousPt, ctx.distanceConstraint.value, verticalPt0, verticalPt1, point ); + } + if ( ctx.yConstraint.locked ) + { + QgsPointXY horizontalPt0( point.x(), ctx.yConstraint.value ); + QgsPointXY horizontalPt1( point.x() + 1, ctx.yConstraint.value ); + res.valid &= lineCircleIntersection( previousPt, ctx.distanceConstraint.value, horizontalPt0, horizontalPt1, point ); + } + } + else + { + const double dist = std::sqrt( point.sqrDist( previousPt ) ); + if ( dist == 0 ) + { + // handle case where mouse is over origin and distance constraint is enabled + // take arbitrary horizontal line + point.set( previousPt.x() + ctx.distanceConstraint.value, previousPt.y() ); + } + else + { + const double vP = ctx.distanceConstraint.value / dist; + point.set( previousPt.x() + ( point.x() - previousPt.x() ) * vP, + previousPt.y() + ( point.y() - previousPt.y() ) * vP ); + } + + if ( edgeMatch.hasEdge() && !ctx.angleConstraint.locked ) + { + // we will magnietize to the intersection of that segment and the lockedDistance ! + res.valid &= lineCircleIntersection( previousPt, ctx.distanceConstraint.value, edgePt0, edgePt1, point ); + } + } + } + + // ***************************** + // ---- calculate CAD values + QgsDebugMsgLevel( QString( "point: %1 %2" ).arg( point.x() ).arg( point.y() ), 4 ); + QgsDebugMsgLevel( QString( "previous point: %1 %2" ).arg( previousPt.x() ).arg( previousPt.y() ), 4 ); + QgsDebugMsgLevel( QString( "penultimate point: %1 %2" ).arg( penultimatePt.x() ).arg( penultimatePt.y() ), 4 ); + //QgsDebugMsg( QString( "dx: %1 dy: %2" ).arg( point.x() - previousPt.x() ).arg( point.y() - previousPt.y() ) ); + //QgsDebugMsg( QString( "ddx: %1 ddy: %2" ).arg( previousPt.x() - penultimatePt.x() ).arg( previousPt.y() - penultimatePt.y() ) ); + + res.finalMapPoint = point; + + return res; +} + +void QgsCadUtils::AlignMapPointContext::dump() const +{ + QgsDebugMsg( "Constraints (locked / relative / value" ); + QgsDebugMsg( QString( "Angle: %1 %2 %3" ).arg( angleConstraint.locked ).arg( angleConstraint.relative ).arg( angleConstraint.value ) ); + QgsDebugMsg( QString( "Distance: %1 %2 %3" ).arg( distanceConstraint.locked ).arg( distanceConstraint.relative ).arg( distanceConstraint.value ) ); + QgsDebugMsg( QString( "X: %1 %2 %3" ).arg( xConstraint.locked ).arg( xConstraint.relative ).arg( xConstraint.value ) ); + QgsDebugMsg( QString( "Y: %1 %2 %3" ).arg( yConstraint.locked ).arg( yConstraint.relative ).arg( yConstraint.value ) ); +} diff --git a/src/core/qgscadutils.h b/src/core/qgscadutils.h new file mode 100644 index 00000000000..3146f135ae4 --- /dev/null +++ b/src/core/qgscadutils.h @@ -0,0 +1,104 @@ +/*************************************************************************** + qgscadutils.h + ------------------- + begin : September 2017 + copyright : (C) 2017 by Martin Dobias + email : wonder dot sk at gmail dot com + ***************************************************************************/ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef QGSCADUTILS_H +#define QGSCADUTILS_H + +#include "qgis_core.h" + +#include "qgspointlocator.h" + +class QgsSnappingUtils; + +/** + * The QgsCadUtils class provides routines for CAD editing. + * + * \since QGIS 3.0 + */ +class CORE_EXPORT QgsCadUtils +{ + public: + + //! Structure with details of one constraint + struct AlignMapPointConstraint + { + AlignMapPointConstraint( bool locked = false, bool relative = false, double value = 0 ) + : locked( locked ) + , relative( relative ) + , value( value ) + {} + + //! Whether the constraint is active, i.e. should be considered + bool locked; + //! Whether the value is relative to previous value + bool relative; + //! Numeric value of the constraint (coordinate/distance in map units or angle in degrees) + double value; + }; + + //! Structure defining all constraints for alignMapPoint() method + struct AlignMapPointContext + { + //! Snapping utils that will be used to snap point to map. Must not be null + QgsSnappingUtils *snappingUtils; + //! Map units/pixel ratio from map canvas. Needed for + double mapUnitsPerPixel; + + //! Constraint for X coordinate + AlignMapPointConstraint xConstraint; + //! Constraint for Y coordinate + AlignMapPointConstraint yConstraint; + //! Constraint for distance + AlignMapPointConstraint distanceConstraint; + //! Constraint for angle + AlignMapPointConstraint angleConstraint; + //! Constraint for soft lock to a common angle + AlignMapPointConstraint commonAngleConstraint; + + //! List of recent CAD points in map coordinates. These are used to turn relative constraints to absolute. + //! First point is the most recent point. Currently using only "previous" point (index 1) and "penultimate" + //! point (index 2) for alignment purposes. + QList cadPointList; + + void dump() const; + }; + + //! Structure returned from alignMapPoint() method + struct AlignMapPointOutput + { + //! Whether the combination of constraints is actually valid + bool valid; + + //! map point aligned according to the constraints + QgsPointXY finalMapPoint; + + //! Snapped segment - only valid if actually used for something + QgsPointLocator::Match edgeMatch; + + //! Angle (in degrees) to which we have soft-locked ourselves (if not set it is -1) + int softLockCommonAngle; + }; + + /** + * Applies X/Y/angle/distance constraints from the given context to a map point. + * Returns a structure containing aligned map point, whether the constraints are valid and + * some extra information. + */ + static AlignMapPointOutput alignMapPoint( const QgsPointXY &originalMapPoint, const AlignMapPointContext &ctx ); + +}; + +#endif // QGSCADUTILS_H diff --git a/src/gui/qgsadvanceddigitizingdockwidget.cpp b/src/gui/qgsadvanceddigitizingdockwidget.cpp index 047c1247f9c..9ef7599059d 100644 --- a/src/gui/qgsadvanceddigitizingdockwidget.cpp +++ b/src/gui/qgsadvanceddigitizingdockwidget.cpp @@ -20,6 +20,7 @@ #include "qgsadvanceddigitizingdockwidget.h" #include "qgsadvanceddigitizingcanvasitem.h" #include "qgsapplication.h" +#include "qgscadutils.h" #include "qgsexpression.h" #include "qgslogger.h" #include "qgsmapcanvas.h" @@ -33,64 +34,6 @@ #include "qgssnappingutils.h" #include "qgsproject.h" -/// @cond PRIVATE -struct EdgesOnlyFilter : public QgsPointLocator::MatchFilter -{ - bool acceptMatch( const QgsPointLocator::Match &m ) override { return m.hasEdge(); } -}; -/// @endcond - - -bool QgsAdvancedDigitizingDockWidget::lineCircleIntersection( const QgsPointXY ¢er, const double radius, const QList &segment, QgsPointXY &intersection ) -{ - Q_ASSERT( segment.count() == 2 ); - - // formula taken from http://mathworld.wolfram.com/Circle-LineIntersection.html - - const double x1 = segment[0].x() - center.x(); - const double y1 = segment[0].y() - center.y(); - const double x2 = segment[1].x() - center.x(); - const double y2 = segment[1].y() - center.y(); - const double dx = x2 - x1; - const double dy = y2 - y1; - - const double dr = std::sqrt( std::pow( dx, 2 ) + std::pow( dy, 2 ) ); - const double d = x1 * y2 - x2 * y1; - - const double disc = std::pow( radius, 2 ) * std::pow( dr, 2 ) - std::pow( d, 2 ); - - if ( disc < 0 ) - { - //no intersection or tangent - return false; - } - else - { - // two solutions - const int sgnDy = dy < 0 ? -1 : 1; - - 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 ) ); - 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 ) ); - const QgsPointXY p1( ax, ay ); - - 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 ) ); - 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 ) ); - const QgsPointXY p2( bx, by ); - - // snap to nearest intersection - - if ( intersection.sqrDist( p1 ) < intersection.sqrDist( p2 ) ) - { - intersection.set( p1.x(), p1.y() ); - } - else - { - intersection.set( p2.x(), p2.y() ); - } - return true; - } -} - QgsAdvancedDigitizingDockWidget::QgsAdvancedDigitizingDockWidget( QgsMapCanvas *canvas, QWidget *parent ) : QgsDockWidget( parent ) @@ -563,251 +506,53 @@ void QgsAdvancedDigitizingDockWidget::updateCapacity( bool updateUIwithoutChange } +static QgsCadUtils::AlignMapPointConstraint _constraint( QgsAdvancedDigitizingDockWidget::CadConstraint *c ) +{ + QgsCadUtils::AlignMapPointConstraint constr; + constr.locked = c->lockMode() == QgsAdvancedDigitizingDockWidget::CadConstraint::HardLock; + constr.relative = c->relative(); + constr.value = c->value(); + return constr; +} + bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e ) { - bool res = true; + QgsCadUtils::AlignMapPointContext context; + context.snappingUtils = mMapCanvas->snappingUtils(); + context.mapUnitsPerPixel = mMapCanvas->mapUnitsPerPixel(); + context.xConstraint = _constraint( mXConstraint.get() ); + context.yConstraint = _constraint( mYConstraint.get() ); + context.distanceConstraint = _constraint( mDistanceConstraint.get() ); + context.angleConstraint = _constraint( mAngleConstraint.get() ); + context.cadPointList = mCadPointList; - QgsDebugMsgLevel( "Constraints (locked / relative / value", 4 ); - QgsDebugMsgLevel( QString( "Angle: %1 %2 %3" ).arg( mAngleConstraint->isLocked() ).arg( mAngleConstraint->relative() ).arg( mAngleConstraint->value() ), 4 ); - QgsDebugMsgLevel( QString( "Distance: %1 %2 %3" ).arg( mDistanceConstraint->isLocked() ).arg( mDistanceConstraint->relative() ).arg( mDistanceConstraint->value() ), 4 ); - QgsDebugMsgLevel( QString( "X: %1 %2 %3" ).arg( mXConstraint->isLocked() ).arg( mXConstraint->relative() ).arg( mXConstraint->value() ), 4 ); - QgsDebugMsgLevel( QString( "Y: %1 %2 %3" ).arg( mYConstraint->isLocked() ).arg( mYConstraint->relative() ).arg( mYConstraint->value() ), 4 ); + context.commonAngleConstraint.locked = true; + context.commonAngleConstraint.relative = context.angleConstraint.relative; + context.commonAngleConstraint.value = mCommonAngleConstraint; - QgsPointXY point = e->snapPoint(); + QgsCadUtils::AlignMapPointOutput output = QgsCadUtils::alignMapPoint( e->originalMapPoint(), context ); - mSnappedSegment = snapSegment( e->originalMapPoint() ); - - bool previousPointExist, penulPointExist; - QgsPointXY previousPt = previousPoint( &previousPointExist ); - QgsPointXY penultimatePt = penultimatePoint( &penulPointExist ); - - // ***************************** - // ---- X constraint - if ( mXConstraint->isLocked() ) + bool res = output.valid; + QgsPointXY point = output.finalMapPoint; + mSnappedSegment.clear(); + if ( output.edgeMatch.hasEdge() ) { - if ( !mXConstraint->relative() ) - { - point.setX( mXConstraint->value() ); - } - else if ( mCapacities.testFlag( RelativeCoordinates ) ) - { - point.setX( previousPt.x() + mXConstraint->value() ); - } - if ( !mSnappedSegment.isEmpty() && !mYConstraint->isLocked() ) - { - // intersect with snapped segment line at X ccordinate - const double dx = mSnappedSegment.at( 1 ).x() - mSnappedSegment.at( 0 ).x(); - if ( dx == 0 ) - { - point.setY( mSnappedSegment.at( 0 ).y() ); - } - else - { - const double dy = mSnappedSegment.at( 1 ).y() - mSnappedSegment.at( 0 ).y(); - point.setY( mSnappedSegment.at( 0 ).y() + ( dy * ( point.x() - mSnappedSegment.at( 0 ).x() ) ) / dx ); - } - } + QgsPointXY edgePt0, edgePt1; + output.edgeMatch.edgePoints( edgePt0, edgePt1 ); + mSnappedSegment << edgePt0 << edgePt1; } - // ***************************** - // ---- Y constraint - if ( mYConstraint->isLocked() ) + if ( mAngleConstraint->lockMode() != CadConstraint::HardLock ) { - if ( !mYConstraint->relative() ) + if ( output.softLockCommonAngle != -1 ) { - point.setY( mYConstraint->value() ); - } - else if ( mCapacities.testFlag( RelativeCoordinates ) ) - { - point.setY( previousPt.y() + mYConstraint->value() ); - } - if ( !mSnappedSegment.isEmpty() && !mXConstraint->isLocked() ) - { - // intersect with snapped segment line at Y ccordinate - const double dy = mSnappedSegment.at( 1 ).y() - mSnappedSegment.at( 0 ).y(); - if ( dy == 0 ) - { - point.setX( mSnappedSegment.at( 0 ).x() ); - } - else - { - const double dx = mSnappedSegment.at( 1 ).x() - mSnappedSegment.at( 0 ).x(); - point.setX( mSnappedSegment.at( 0 ).x() + ( dx * ( point.y() - mSnappedSegment.at( 0 ).y() ) ) / dy ); - } - } - } - // ***************************** - // ---- Angle constraint - // input angles are in degrees - if ( mAngleConstraint->lockMode() == CadConstraint::SoftLock ) - { - // reset the lock - mAngleConstraint->setLockMode( CadConstraint::NoLock ); - } - if ( !mAngleConstraint->isLocked() && mCapacities.testFlag( AbsoluteAngle ) && mCommonAngleConstraint != 0 ) - { - double commonAngle = mCommonAngleConstraint * M_PI / 180; - // see if soft common angle constraint should be performed - // only if not in HardLock mode - double softAngle = std::atan2( point.y() - previousPt.y(), - point.x() - previousPt.x() ); - double deltaAngle = 0; - if ( mAngleConstraint->relative() && mCapacities.testFlag( RelativeAngle ) ) - { - // compute the angle relative to the last segment (0° is aligned with last segment) - deltaAngle = std::atan2( previousPt.y() - penultimatePt.y(), - previousPt.x() - penultimatePt.x() ); - softAngle -= deltaAngle; - } - int quo = std::round( softAngle / commonAngle ); - if ( std::fabs( softAngle - quo * commonAngle ) * 180.0 * M_1_PI <= SOFT_CONSTRAINT_TOLERANCE_DEGREES ) - { - // also check the distance in pixel to the line, otherwise it's too sticky at long ranges - softAngle = quo * commonAngle; - // http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html - // use the direction vector (cos(a),sin(a)) from previous point. |x2-x1|=1 since sin2+cos2=1 - const double dist = std::fabs( std::cos( softAngle + deltaAngle ) * ( previousPt.y() - point.y() ) - - std::sin( softAngle + deltaAngle ) * ( previousPt.x() - point.x() ) ); - if ( dist / mMapCanvas->mapSettings().mapUnitsPerPixel() < SOFT_CONSTRAINT_TOLERANCE_PIXEL ) - { - mAngleConstraint->setLockMode( CadConstraint::SoftLock ); - mAngleConstraint->setValue( 180.0 / M_PI * softAngle ); - } - } - } - if ( mAngleConstraint->isLocked() ) - { - double angleValue = mAngleConstraint->value() * M_PI / 180; - if ( mAngleConstraint->relative() && mCapacities.testFlag( RelativeAngle ) ) - { - // compute the angle relative to the last segment (0° is aligned with last segment) - angleValue += std::atan2( previousPt.y() - penultimatePt.y(), - previousPt.x() - penultimatePt.x() ); - } - - double cosa = std::cos( angleValue ); - double sina = std::sin( angleValue ); - double v = ( point.x() - previousPt.x() ) * cosa + ( point.y() - previousPt.y() ) * sina; - if ( mXConstraint->isLocked() && mYConstraint->isLocked() ) - { - // do nothing if both X,Y are already locked - } - else if ( mXConstraint->isLocked() ) - { - if ( qgsDoubleNear( cosa, 0.0 ) ) - { - res = false; - } - else - { - double x = mXConstraint->value(); - if ( !mXConstraint->relative() ) - { - x -= previousPt.x(); - } - point.setY( previousPt.y() + x * sina / cosa ); - } - } - else if ( mYConstraint->isLocked() ) - { - if ( qgsDoubleNear( sina, 0.0 ) ) - { - res = false; - } - else - { - double y = mYConstraint->value(); - if ( !mYConstraint->relative() ) - { - y -= previousPt.y(); - } - point.setX( previousPt.x() + y * cosa / sina ); - } + mAngleConstraint->setLockMode( CadConstraint::SoftLock ); + mAngleConstraint->setValue( output.softLockCommonAngle ); } else { - point.setX( previousPt.x() + cosa * v ); - point.setY( previousPt.y() + sina * v ); - } - - if ( !mSnappedSegment.isEmpty() && !mDistanceConstraint->isLocked() ) - { - // magnetize to the intersection of the snapped segment and the lockedAngle - - // line of previous point + locked angle - const double x1 = previousPt.x(); - const double y1 = previousPt.y(); - const double x2 = previousPt.x() + cosa; - const double y2 = previousPt.y() + sina; - // line of snapped segment - const double x3 = mSnappedSegment.at( 0 ).x(); - const double y3 = mSnappedSegment.at( 0 ).y(); - const double x4 = mSnappedSegment.at( 1 ).x(); - const double y4 = mSnappedSegment.at( 1 ).y(); - - const double d = ( x1 - x2 ) * ( y3 - y4 ) - ( y1 - y2 ) * ( x3 - x4 ); - - // do not compute intersection if lines are almost parallel - // this threshold might be adapted - if ( std::fabs( d ) > 0.01 ) - { - point.setX( ( ( x3 - x4 ) * ( x1 * y2 - y1 * x2 ) - ( x1 - x2 ) * ( x3 * y4 - y3 * x4 ) ) / d ); - point.setY( ( ( y3 - y4 ) * ( x1 * y2 - y1 * x2 ) - ( y1 - y2 ) * ( x3 * y4 - y3 * x4 ) ) / d ); - } + mAngleConstraint->setLockMode( CadConstraint::NoLock ); } } - // ***************************** - // ---- Distance constraint - if ( mDistanceConstraint->isLocked() && previousPointExist ) - { - if ( mXConstraint->isLocked() || mYConstraint->isLocked() ) - { - // perform both to detect errors in constraints - if ( mXConstraint->isLocked() ) - { - const QList verticalSegment = QList() - << QgsPointXY( mXConstraint->value(), point.y() ) - << QgsPointXY( mXConstraint->value(), point.y() + 1 ); - res &= lineCircleIntersection( previousPt, mDistanceConstraint->value(), verticalSegment, point ); - } - if ( mYConstraint->isLocked() ) - { - const QList horizontalSegment = QList() - << QgsPointXY( point.x(), mYConstraint->value() ) - << QgsPointXY( point.x() + 1, mYConstraint->value() ); - res &= lineCircleIntersection( previousPt, mDistanceConstraint->value(), horizontalSegment, point ); - } - } - else - { - const double dist = std::sqrt( point.sqrDist( previousPt ) ); - if ( dist == 0 ) - { - // handle case where mouse is over origin and distance constraint is enabled - // take arbitrary horizontal line - point.set( previousPt.x() + mDistanceConstraint->value(), previousPt.y() ); - } - else - { - const double vP = mDistanceConstraint->value() / dist; - point.set( previousPt.x() + ( point.x() - previousPt.x() ) * vP, - previousPt.y() + ( point.y() - previousPt.y() ) * vP ); - } - - if ( !mSnappedSegment.isEmpty() && !mAngleConstraint->isLocked() ) - { - // we will magnietize to the intersection of that segment and the lockedDistance ! - res &= lineCircleIntersection( previousPt, mDistanceConstraint->value(), snappedSegment(), point ); - } - } - } - - // ***************************** - // ---- calculate CAD values - QgsDebugMsgLevel( QString( "point: %1 %2" ).arg( point.x() ).arg( point.y() ), 4 ); - QgsDebugMsgLevel( QString( "previous point: %1 %2" ).arg( previousPt.x() ).arg( previousPt.y() ), 4 ); - QgsDebugMsgLevel( QString( "penultimate point: %1 %2" ).arg( penultimatePt.x() ).arg( penultimatePt.y() ), 4 ); - //QgsDebugMsg( QString( "dx: %1 dy: %2" ).arg( point.x() - previousPt.x() ).arg( point.y() - previousPt.y() ) ); - //QgsDebugMsg( QString( "ddx: %1 ddy: %2" ).arg( previousPt.x() - penultimatePt.x() ).arg( previousPt.y() - penultimatePt.y() ) ); // set the point coordinates in the map event e->setMapPoint( point ); @@ -815,8 +560,27 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e ) // update the point list updateCurrentPoint( point ); - // ***************************** - // ---- update the GUI with the values + updateUnlockedConstraintValues( point ); + + if ( res ) + { + emit popWarning(); + } + else + { + emit pushWarning( tr( "Some constraints are incompatible. Resulting point might be incorrect." ) ); + } + + return res; +} + + +void QgsAdvancedDigitizingDockWidget::updateUnlockedConstraintValues( const QgsPointXY &point ) +{ + bool previousPointExist, penulPointExist; + QgsPointXY previousPt = previousPoint( &previousPointExist ); + QgsPointXY penultimatePt = penultimatePoint( &penulPointExist ); + // --- angle if ( !mAngleConstraint->isLocked() && previousPointExist ) { @@ -863,49 +627,28 @@ bool QgsAdvancedDigitizingDockWidget::applyConstraints( QgsMapMouseEvent *e ) mYConstraint->setValue( point.y() ); } } - - if ( res ) - { - emit popWarning(); - } - else - { - emit pushWarning( tr( "Some constraints are incompatible. Resulting point might be incorrect." ) ); - } - - return res; } - -QList QgsAdvancedDigitizingDockWidget::snapSegment( const QgsPointXY &originalMapPoint, bool *snapped, bool allLayers ) const +QList QgsAdvancedDigitizingDockWidget::snapSegmentToAllLayers( const QgsPointXY &originalMapPoint, bool *snapped ) const { QList segment; QgsPointXY pt1, pt2; QgsPointLocator::Match match; - if ( !allLayers ) - { - // run snapToMap with only segments - EdgesOnlyFilter filter; - match = mMapCanvas->snappingUtils()->snapToMap( originalMapPoint, &filter ); - } - else - { - // run snapToMap with only edges on all layers - QgsSnappingUtils *snappingUtils = mMapCanvas->snappingUtils(); + QgsSnappingUtils *snappingUtils = mMapCanvas->snappingUtils(); - QgsSnappingConfig canvasConfig = snappingUtils->config(); - QgsSnappingConfig localConfig = snappingUtils->config(); + QgsSnappingConfig canvasConfig = snappingUtils->config(); + QgsSnappingConfig localConfig = snappingUtils->config(); - localConfig.setMode( QgsSnappingConfig::AllLayers ); - localConfig.setType( QgsSnappingConfig::Segment ); - snappingUtils->setConfig( localConfig ); + localConfig.setMode( QgsSnappingConfig::AllLayers ); + localConfig.setType( QgsSnappingConfig::Segment ); + snappingUtils->setConfig( localConfig ); - match = snappingUtils->snapToMap( originalMapPoint ); + match = snappingUtils->snapToMap( originalMapPoint ); + + snappingUtils->setConfig( canvasConfig ); - snappingUtils->setConfig( canvasConfig ); - } if ( match.isValid() && match.hasEdge() ) { match.edgePoints( pt1, pt2 ); @@ -930,7 +673,7 @@ bool QgsAdvancedDigitizingDockWidget::alignToSegment( QgsMapMouseEvent *e, CadCo bool previousPointExist, penulPointExist, snappedSegmentExist; QgsPointXY previousPt = previousPoint( &previousPointExist ); QgsPointXY penultimatePt = penultimatePoint( &penulPointExist ); - mSnappedSegment = snapSegment( e->originalMapPoint(), &snappedSegmentExist, true ); + mSnappedSegment = snapSegmentToAllLayers( e->originalMapPoint(), &snappedSegmentExist ); if ( !previousPointExist || !snappedSegmentExist ) { diff --git a/src/gui/qgsadvanceddigitizingdockwidget.h b/src/gui/qgsadvanceddigitizingdockwidget.h index a9c2780a1d4..b60db85fbcf 100644 --- a/src/gui/qgsadvanceddigitizingdockwidget.h +++ b/src/gui/qgsadvanceddigitizingdockwidget.h @@ -32,11 +32,6 @@ class QgsMapTool; class QgsMapToolAdvancedDigitizing; class QgsPointXY; -// tolerances for soft constraints (last values, and common angles) -// for angles, both tolerance in pixels and degrees are used for better performance -static const double SOFT_CONSTRAINT_TOLERANCE_PIXEL = 15 SIP_SKIP; -static const double SOFT_CONSTRAINT_TOLERANCE_DEGREES = 10 SIP_SKIP; - /** \ingroup gui * \brief The QgsAdvancedDigitizingDockWidget class is a dockable widget * 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 double mValue; }; - //! performs the intersection of a circle and a line - //! \note from the two solutions, the intersection will be set to the closest point - static bool lineCircleIntersection( const QgsPointXY ¢er, const double radius, const QList &segment, QgsPointXY &intersection ); - /** * Create an advanced digitizing dock widget * \param canvas The map canvas on which the widget operates @@ -393,15 +384,12 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private //! defines the additional constraint to be used (no/parallel/perpendicular) void lockAdditionalConstraint( AdditionalConstraint constraint ); - QList snapSegment( const QgsPointLocator::Match &snapMatch ); - /** - * Returns the first snapped segment. Will try to snap a segment according to the event's snapping mode. + * Returns the first snapped segment. Will try to snap a segment using all layers * \param originalMapPoint point to be snapped (in map coordinates) * \param snapped if given, determines if a segment has been snapped - * \param allLayers if true, override snapping mode */ - QList snapSegment( const QgsPointXY &originalMapPoint, bool *snapped = nullptr, bool allLayers = false ) const; + QList snapSegmentToAllLayers( const QgsPointXY &originalMapPoint, bool *snapped = nullptr ) const; //! update the current point in the CAD point list void updateCurrentPoint( const QgsPointXY &point ); @@ -432,6 +420,9 @@ class GUI_EXPORT QgsAdvancedDigitizingDockWidget : public QgsDockWidget, private */ void updateConstraintValue( CadConstraint *constraint, const QString &textValue, bool convertExpression = false ); + //! Updates values of constraints that are not locked based on the current point + void updateUnlockedConstraintValues( const QgsPointXY &point ); + QgsMapCanvas *mMapCanvas = nullptr; QgsAdvancedDigitizingCanvasItem *mCadPaintItem = nullptr; diff --git a/tests/src/core/CMakeLists.txt b/tests/src/core/CMakeLists.txt index 74049f68383..a1bed2afdce 100755 --- a/tests/src/core/CMakeLists.txt +++ b/tests/src/core/CMakeLists.txt @@ -77,6 +77,7 @@ SET(TESTS testqgsauthconfig.cpp testqgsauthmanager.cpp testqgsblendmodes.cpp + testqgscadutils.cpp testqgsclipper.cpp testqgscolorscheme.cpp testqgscolorschemeregistry.cpp diff --git a/tests/src/core/testqgscadutils.cpp b/tests/src/core/testqgscadutils.cpp new file mode 100644 index 00000000000..ba05d8b6a71 --- /dev/null +++ b/tests/src/core/testqgscadutils.cpp @@ -0,0 +1,259 @@ +/*************************************************************************** + testqgscadutils.cpp + -------------------------------------- + Date : September 2017 + Copyright : (C) 2017 by Martin Dobias + Email : wonder.sk at gmail.com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgstest.h" +#include +#include + +#include "qgscadutils.h" +#include "qgsproject.h" +#include "qgssnappingutils.h" +#include "qgsvectorlayer.h" + +/** \ingroup UnitTests + * This is a unit test for the QgsCadUtils class. + */ +class TestQgsCadUtils : public QObject +{ + Q_OBJECT + public: + TestQgsCadUtils() + {} + ~TestQgsCadUtils() + { + } + + private slots: + void initTestCase();// will be called before the first testfunction is executed. + void cleanupTestCase();// will be called after the last testfunction was executed. + void init() {} // will be called before each testfunction is executed. + void cleanup() {} // will be called after every testfunction. + + void testBasic(); + void testXY(); + void testAngle(); + void testCommonAngle(); + void testDistance(); + void testEdge(); + + private: + + QgsCadUtils::AlignMapPointContext baseContext() + { + QgsCadUtils::AlignMapPointContext context; + context.snappingUtils = mSnappingUtils; + context.mapUnitsPerPixel = mMapSettings.mapUnitsPerPixel(); + context.cadPointList << QgsPointXY() << QgsPointXY( 30, 20 ) << QgsPointXY( 30, 30 ); + return context; + } + + QString mTestDataDir; + QgsVectorLayer *mLayerPolygon = nullptr; + QgsSnappingUtils *mSnappingUtils = nullptr; + QgsMapSettings mMapSettings; +}; + + +//runs before all tests +void TestQgsCadUtils::initTestCase() +{ + // init QGIS's paths - true means that all path will be inited from prefix + QgsApplication::init(); + QgsApplication::initQgis(); + + mLayerPolygon = new QgsVectorLayer( "Polygon?crs=EPSG:27700", "layer polygon", "memory" ); + QVERIFY( mLayerPolygon->isValid() ); + + QgsPolygon polygon1; + QgsPolyline polygon1exterior; + polygon1exterior << QgsPointXY( 10, 10 ) << QgsPointXY( 30, 10 ) << QgsPointXY( 10, 20 ) << QgsPointXY( 10, 10 ); + polygon1 << polygon1exterior; + QgsFeature polygonF1; + polygonF1.setGeometry( QgsGeometry::fromPolygon( polygon1 ) ); + + mLayerPolygon->startEditing(); + mLayerPolygon->addFeature( polygonF1 ); + + QgsProject::instance()->addMapLayer( mLayerPolygon ); + + QgsSnappingConfig snapConfig; + snapConfig.setEnabled( true ); + snapConfig.setMode( QgsSnappingConfig::AllLayers ); + snapConfig.setType( QgsSnappingConfig::VertexAndSegment ); + snapConfig.setTolerance( 1.0 ); + + mMapSettings.setExtent( QgsRectangle( 0, 0, 100, 100 ) ); + mMapSettings.setOutputSize( QSize( 100, 100 ) ); + mMapSettings.setLayers( QList() << mLayerPolygon ); + + mSnappingUtils = new QgsSnappingUtils; + mSnappingUtils->setConfig( snapConfig ); + mSnappingUtils->setMapSettings( mMapSettings ); +} + +//runs after all tests +void TestQgsCadUtils::cleanupTestCase() +{ + delete mSnappingUtils; + + QgsApplication::exitQgis(); +} + +void TestQgsCadUtils::testBasic() +{ + QgsCadUtils::AlignMapPointContext context( baseContext() ); + + // no snap + QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 5, 5 ), context ); + QVERIFY( res0.valid ); + QCOMPARE( res0.finalMapPoint, QgsPointXY( 5, 5 ) ); + + // simple snap to vertex + QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 9.5, 9.5 ), context ); + QVERIFY( res1.valid ); + QCOMPARE( res1.finalMapPoint, QgsPointXY( 10, 10 ) ); +} + +void TestQgsCadUtils::testXY() +{ + QgsCadUtils::AlignMapPointContext context( baseContext() ); + + // x absolute + context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 20 ); + QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context ); + QVERIFY( res0.valid ); + QCOMPARE( res0.finalMapPoint, QgsPointXY( 20, 29 ) ); + + // x relative + context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -5 ); + QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context ); + QVERIFY( res1.valid ); + QCOMPARE( res1.finalMapPoint, QgsPointXY( 25, 29 ) ); + + context.xConstraint = QgsCadUtils::AlignMapPointConstraint(); + + // y absolute + context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 20 ); + QgsCadUtils::AlignMapPointOutput res2 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context ); + QVERIFY( res2.valid ); + QCOMPARE( res2.finalMapPoint, QgsPointXY( 29, 20 ) ); + + // y relative + context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -5 ); + QgsCadUtils::AlignMapPointOutput res3 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context ); + QVERIFY( res3.valid ); + QCOMPARE( res3.finalMapPoint, QgsPointXY( 29, 15 ) ); + + // x and y (relative) + context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 32 ); + context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 22 ); + QgsCadUtils::AlignMapPointOutput res4 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context ); + QVERIFY( res4.valid ); + QCOMPARE( res4.finalMapPoint, QgsPointXY( 32, 22 ) ); + + // x and y (relative) + context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -2 ); + context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, -2 ); + QgsCadUtils::AlignMapPointOutput res5 = QgsCadUtils::alignMapPoint( QgsPointXY( 29, 29 ), context ); + QVERIFY( res5.valid ); + QCOMPARE( res5.finalMapPoint, QgsPointXY( 28, 18 ) ); +} + +void TestQgsCadUtils::testAngle() +{ + QgsCadUtils::AlignMapPointContext context( baseContext() ); + + // angle abs + context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 ); + QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20 ), context ); + QVERIFY( res0.valid ); + QCOMPARE( res0.finalMapPoint, QgsPointXY( 35, 25 ) ); + + // angle rel + context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, 45 ); + QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 30, 30 ), context ); + QVERIFY( res1.valid ); + QCOMPARE( res1.finalMapPoint, QgsPointXY( 25, 25 ) ); + + // angle + x abs + context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 ); + context.xConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 38 ); + QgsCadUtils::AlignMapPointOutput res2 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20 ), context ); + QVERIFY( res2.valid ); + QCOMPARE( res2.finalMapPoint, QgsPointXY( 38, 28 ) ); + + // angle + y rel + context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 ); + context.xConstraint = QgsCadUtils::AlignMapPointConstraint(); + context.yConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 17 ); + QgsCadUtils::AlignMapPointOutput res3 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20 ), context ); + QVERIFY( res3.valid ); + QCOMPARE( res3.finalMapPoint, QgsPointXY( 27, 17 ) ); +} + +void TestQgsCadUtils::testCommonAngle() +{ + QgsCadUtils::AlignMapPointContext context( baseContext() ); + + // without common angle + QgsCadUtils::AlignMapPointOutput res0 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20.1 ), context ); + QVERIFY( res0.valid ); + QCOMPARE( res0.softLockCommonAngle, -1 ); + QCOMPARE( res0.finalMapPoint, QgsPointXY( 40, 20.1 ) ); + + // common angle + context.commonAngleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 90 ); + QgsCadUtils::AlignMapPointOutput res1 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20.1 ), context ); + QVERIFY( res1.valid ); + QCOMPARE( res1.softLockCommonAngle, 0 ); + QCOMPARE( res1.finalMapPoint, QgsPointXY( 40, 20 ) ); + + // common angle + angle (make sure that angle constraint has priority) + context.commonAngleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 90 ); + context.angleConstraint = QgsCadUtils::AlignMapPointConstraint( true, false, 45 ); + QgsCadUtils::AlignMapPointOutput res2 = QgsCadUtils::alignMapPoint( QgsPointXY( 40, 20.1 ), context ); + QVERIFY( res2.valid ); + QCOMPARE( res2.softLockCommonAngle, -1 ); + QCOMPARE( res2.finalMapPoint, QgsPointXY( 35.05, 25.05 ) ); + + // common angle rel + context.angleConstraint = QgsCadUtils::AlignMapPointConstraint(); + context.commonAngleConstraint = QgsCadUtils::AlignMapPointConstraint( true, true, 90 ); + context.cadPointList[1] = QgsPointXY( 40, 20 ); + QgsCadUtils::AlignMapPointOutput res3 = QgsCadUtils::alignMapPoint( QgsPointXY( 50.1, 29.9 ), context ); + QVERIFY( res3.valid ); + QCOMPARE( res3.softLockCommonAngle, 90 ); + QCOMPARE( res3.finalMapPoint, QgsPointXY( 50, 30 ) ); +} + +void TestQgsCadUtils::testDistance() +{ + // TODO: + // dist + // dist+x / dist+y + // dist+angle +} + +void TestQgsCadUtils::testEdge() +{ + // TODO: + // x+edge / y+edge + // angle+edge + // distance+edge +} + +QGSTEST_MAIN( TestQgsCadUtils ) + +#include "testqgscadutils.moc"