Refactor CAD editing alignment logic to new QgsCadUtils class

This commit is contained in:
Martin Dobias 2017-09-12 16:52:59 +02:00
parent 68bb68d176
commit 878dfddd3b
9 changed files with 806 additions and 346 deletions

View File

@ -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}

View File

@ -11,7 +11,6 @@
class QgsAdvancedDigitizingDockWidget : QgsDockWidget
{
%Docstring
@ -151,14 +150,6 @@ class QgsAdvancedDigitizingDockWidget : QgsDockWidget
};
static bool lineCircleIntersection( const QgsPointXY &center, const double radius, const QList<QgsPointXY> &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

View File

@ -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

368
src/core/qgscadutils.cpp Normal file
View File

@ -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 &center, 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 ) );
}

104
src/core/qgscadutils.h Normal file
View File

@ -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<QgsPointXY> 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

View File

@ -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 &center, const double radius, const QList<QgsPointXY> &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<QgsPointXY> verticalSegment = QList<QgsPointXY>()
<< QgsPointXY( mXConstraint->value(), point.y() )
<< QgsPointXY( mXConstraint->value(), point.y() + 1 );
res &= lineCircleIntersection( previousPt, mDistanceConstraint->value(), verticalSegment, point );
}
if ( mYConstraint->isLocked() )
{
const QList<QgsPointXY> horizontalSegment = QList<QgsPointXY>()
<< 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<QgsPointXY> QgsAdvancedDigitizingDockWidget::snapSegment( const QgsPointXY &originalMapPoint, bool *snapped, bool allLayers ) const
QList<QgsPointXY> QgsAdvancedDigitizingDockWidget::snapSegmentToAllLayers( const QgsPointXY &originalMapPoint, bool *snapped ) const
{
QList<QgsPointXY> 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 )
{

View File

@ -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 &center, const double radius, const QList<QgsPointXY> &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<QgsPointXY> 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<QgsPointXY> snapSegment( const QgsPointXY &originalMapPoint, bool *snapped = nullptr, bool allLayers = false ) const;
QList<QgsPointXY> 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;

View File

@ -77,6 +77,7 @@ SET(TESTS
testqgsauthconfig.cpp
testqgsauthmanager.cpp
testqgsblendmodes.cpp
testqgscadutils.cpp
testqgsclipper.cpp
testqgscolorscheme.cpp
testqgscolorschemeregistry.cpp

View File

@ -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 <QObject>
#include <QString>
#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<QgsMapLayer *>() << 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"