mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-13 00:03:09 -04:00
Refactor CAD editing alignment logic to new QgsCadUtils class
This commit is contained in:
parent
68bb68d176
commit
878dfddd3b
@ -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}
|
||||
|
@ -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<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
|
||||
|
@ -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
368
src/core/qgscadutils.cpp
Normal 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 ¢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 ) );
|
||||
}
|
104
src/core/qgscadutils.h
Normal file
104
src/core/qgscadutils.h
Normal 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
|
@ -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<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 )
|
||||
{
|
||||
|
@ -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<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;
|
||||
|
||||
|
@ -77,6 +77,7 @@ SET(TESTS
|
||||
testqgsauthconfig.cpp
|
||||
testqgsauthmanager.cpp
|
||||
testqgsblendmodes.cpp
|
||||
testqgscadutils.cpp
|
||||
testqgsclipper.cpp
|
||||
testqgscolorscheme.cpp
|
||||
testqgscolorschemeregistry.cpp
|
||||
|
259
tests/src/core/testqgscadutils.cpp
Normal file
259
tests/src/core/testqgscadutils.cpp
Normal 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"
|
Loading…
x
Reference in New Issue
Block a user