Branch data Line data Source code
1 : : /*************************************************************************** 2 : : qgscadutils.cpp 3 : : ------------------- 4 : : begin : September 2017 5 : : copyright : (C) 2017 by Martin Dobias 6 : : email : wonder dot sk at gmail dot com 7 : : ***************************************************************************/ 8 : : /*************************************************************************** 9 : : * * 10 : : * This program is free software; you can redistribute it and/or modify * 11 : : * it under the terms of the GNU General Public License as published by * 12 : : * the Free Software Foundation; either version 2 of the License, or * 13 : : * (at your option) any later version. * 14 : : * * 15 : : ***************************************************************************/ 16 : : 17 : : #include "qgscadutils.h" 18 : : 19 : : #include "qgslogger.h" 20 : : #include "qgssnappingutils.h" 21 : : #include "qgsgeometryutils.h" 22 : : 23 : : // tolerances for soft constraints (last values, and common angles) 24 : : // for angles, both tolerance in pixels and degrees are used for better performance 25 : : static const double SOFT_CONSTRAINT_TOLERANCE_PIXEL = 15; 26 : : static const double SOFT_CONSTRAINT_TOLERANCE_DEGREES = 10; 27 : : 28 : : 29 : : /// @cond PRIVATE 30 : : struct EdgesOnlyFilter : public QgsPointLocator::MatchFilter 31 : : { 32 : : bool acceptMatch( const QgsPointLocator::Match &m ) override { return m.hasEdge(); } 33 : : }; 34 : : /// @endcond 35 : : 36 : : 37 : 0 : QgsCadUtils::AlignMapPointOutput QgsCadUtils::alignMapPoint( const QgsPointXY &originalMapPoint, const QgsCadUtils::AlignMapPointContext &ctx ) 38 : : { 39 : 0 : QgsCadUtils::AlignMapPointOutput res; 40 : 0 : res.valid = true; 41 : 0 : res.softLockCommonAngle = -1; 42 : : 43 : : // try to snap to anything 44 : 0 : QgsPointLocator::Match snapMatch = ctx.snappingUtils->snapToMap( originalMapPoint, nullptr, true ); 45 : 0 : res.snapMatch = snapMatch; 46 : 0 : QgsPointXY point = snapMatch.isValid() ? snapMatch.point() : originalMapPoint; 47 : 0 : QgsPointXY edgePt0, edgePt1; 48 : 0 : if ( snapMatch.hasEdge() ) 49 : : { 50 : 0 : snapMatch.edgePoints( edgePt0, edgePt1 ); 51 : : // note : res.edgeMatch should be removed, as we can just check snapMatch.hasEdge() 52 : 0 : res.edgeMatch = snapMatch; 53 : 0 : } 54 : : else 55 : : { 56 : 0 : res.edgeMatch = QgsPointLocator::Match(); 57 : : } 58 : : 59 : 0 : QgsPointXY previousPt, penultimatePt; 60 : 0 : if ( ctx.cadPointList.count() >= 2 ) 61 : 0 : previousPt = ctx.cadPointList.at( 1 ); 62 : 0 : if ( ctx.cadPointList.count() >= 3 ) 63 : 0 : penultimatePt = ctx.cadPointList.at( 2 ); 64 : : 65 : : // ***************************** 66 : : // ---- X constraint 67 : 0 : if ( ctx.xConstraint.locked ) 68 : : { 69 : 0 : if ( !ctx.xConstraint.relative ) 70 : : { 71 : 0 : point.setX( ctx.xConstraint.value ); 72 : 0 : } 73 : 0 : else if ( ctx.cadPointList.count() >= 2 ) 74 : : { 75 : 0 : point.setX( previousPt.x() + ctx.xConstraint.value ); 76 : 0 : } 77 : 0 : if ( snapMatch.hasEdge() && !ctx.yConstraint.locked ) 78 : : { 79 : : // intersect with snapped segment line at X coordinate 80 : 0 : const double dx = edgePt1.x() - edgePt0.x(); 81 : 0 : if ( dx == 0 ) 82 : : { 83 : 0 : point.setY( edgePt0.y() ); 84 : 0 : } 85 : : else 86 : : { 87 : 0 : const double dy = edgePt1.y() - edgePt0.y(); 88 : 0 : point.setY( edgePt0.y() + ( dy * ( point.x() - edgePt0.x() ) ) / dx ); 89 : : } 90 : 0 : } 91 : 0 : } 92 : : 93 : : // ***************************** 94 : : // ---- Y constraint 95 : 0 : if ( ctx.yConstraint.locked ) 96 : : { 97 : 0 : if ( !ctx.yConstraint.relative ) 98 : : { 99 : 0 : point.setY( ctx.yConstraint.value ); 100 : 0 : } 101 : 0 : else if ( ctx.cadPointList.count() >= 2 ) 102 : : { 103 : 0 : point.setY( previousPt.y() + ctx.yConstraint.value ); 104 : 0 : } 105 : 0 : if ( snapMatch.hasEdge() && !ctx.xConstraint.locked ) 106 : : { 107 : : // intersect with snapped segment line at Y coordinate 108 : 0 : const double dy = edgePt1.y() - edgePt0.y(); 109 : 0 : if ( dy == 0 ) 110 : : { 111 : 0 : point.setX( edgePt0.x() ); 112 : 0 : } 113 : : else 114 : : { 115 : 0 : const double dx = edgePt1.x() - edgePt0.x(); 116 : 0 : point.setX( edgePt0.x() + ( dx * ( point.y() - edgePt0.y() ) ) / dy ); 117 : : } 118 : 0 : } 119 : 0 : } 120 : : 121 : : // ***************************** 122 : : // ---- Common Angle constraint 123 : 0 : if ( !ctx.angleConstraint.locked && ctx.cadPointList.count() >= 2 && ctx.commonAngleConstraint.locked && ctx.commonAngleConstraint.value != 0 ) 124 : : { 125 : 0 : double commonAngle = ctx.commonAngleConstraint.value * M_PI / 180; 126 : : // see if soft common angle constraint should be performed 127 : : // only if not in HardLock mode 128 : 0 : double softAngle = std::atan2( point.y() - previousPt.y(), 129 : 0 : point.x() - previousPt.x() ); 130 : 0 : double deltaAngle = 0; 131 : 0 : if ( ctx.commonAngleConstraint.relative && ctx.cadPointList.count() >= 3 ) 132 : : { 133 : : // compute the angle relative to the last segment (0° is aligned with last segment) 134 : 0 : deltaAngle = std::atan2( previousPt.y() - penultimatePt.y(), 135 : 0 : previousPt.x() - penultimatePt.x() ); 136 : 0 : softAngle -= deltaAngle; 137 : 0 : } 138 : 0 : int quo = std::round( softAngle / commonAngle ); 139 : 0 : if ( std::fabs( softAngle - quo * commonAngle ) * 180.0 * M_1_PI <= SOFT_CONSTRAINT_TOLERANCE_DEGREES ) 140 : : { 141 : : // also check the distance in pixel to the line, otherwise it's too sticky at long ranges 142 : 0 : softAngle = quo * commonAngle; 143 : : // http://mathworld.wolfram.com/Point-LineDistance2-Dimensional.html 144 : : // use the direction vector (cos(a),sin(a)) from previous point. |x2-x1|=1 since sin2+cos2=1 145 : 0 : const double dist = std::fabs( std::cos( softAngle + deltaAngle ) * ( previousPt.y() - point.y() ) 146 : 0 : - std::sin( softAngle + deltaAngle ) * ( previousPt.x() - point.x() ) ); 147 : 0 : if ( dist / ctx.mapUnitsPerPixel < SOFT_CONSTRAINT_TOLERANCE_PIXEL ) 148 : : { 149 : 0 : res.softLockCommonAngle = 180.0 / M_PI * softAngle; 150 : 0 : } 151 : 0 : } 152 : 0 : } 153 : : 154 : : // angle can be locked in one of the two ways: 155 : : // 1. "hard" lock defined by the user 156 : : // 2. "soft" lock from common angle (e.g. 45 degrees) 157 : 0 : bool angleLocked = false, angleRelative = false; 158 : 0 : double angleValueDeg = 0; 159 : 0 : if ( ctx.angleConstraint.locked ) 160 : : { 161 : 0 : angleLocked = true; 162 : 0 : angleRelative = ctx.angleConstraint.relative; 163 : 0 : angleValueDeg = ctx.angleConstraint.value; 164 : 0 : } 165 : 0 : else if ( res.softLockCommonAngle != -1 ) 166 : : { 167 : 0 : angleLocked = true; 168 : 0 : angleRelative = ctx.commonAngleConstraint.relative; 169 : 0 : angleValueDeg = res.softLockCommonAngle; 170 : 0 : } 171 : : 172 : : // ***************************** 173 : : // ---- Angle constraint 174 : : // input angles are in degrees 175 : 0 : if ( angleLocked ) 176 : : { 177 : 0 : double angleValue = angleValueDeg * M_PI / 180; 178 : 0 : if ( angleRelative && ctx.cadPointList.count() >= 3 ) 179 : : { 180 : : // compute the angle relative to the last segment (0° is aligned with last segment) 181 : 0 : angleValue += std::atan2( previousPt.y() - penultimatePt.y(), 182 : 0 : previousPt.x() - penultimatePt.x() ); 183 : 0 : } 184 : : 185 : 0 : double cosa = std::cos( angleValue ); 186 : 0 : double sina = std::sin( angleValue ); 187 : 0 : double v = ( point.x() - previousPt.x() ) * cosa + ( point.y() - previousPt.y() ) * sina; 188 : 0 : if ( ctx.xConstraint.locked && ctx.yConstraint.locked ) 189 : : { 190 : : // do nothing if both X,Y are already locked 191 : 0 : } 192 : 0 : else if ( ctx.xConstraint.locked ) 193 : : { 194 : 0 : if ( qgsDoubleNear( cosa, 0.0 ) ) 195 : : { 196 : 0 : res.valid = false; 197 : 0 : } 198 : : else 199 : : { 200 : 0 : double x = ctx.xConstraint.value; 201 : 0 : if ( !ctx.xConstraint.relative ) 202 : : { 203 : 0 : x -= previousPt.x(); 204 : 0 : } 205 : 0 : point.setY( previousPt.y() + x * sina / cosa ); 206 : : } 207 : 0 : } 208 : 0 : else if ( ctx.yConstraint.locked ) 209 : : { 210 : 0 : if ( qgsDoubleNear( sina, 0.0 ) ) 211 : : { 212 : 0 : res.valid = false; 213 : 0 : } 214 : : else 215 : : { 216 : 0 : double y = ctx.yConstraint.value; 217 : 0 : if ( !ctx.yConstraint.relative ) 218 : : { 219 : 0 : y -= previousPt.y(); 220 : 0 : } 221 : 0 : point.setX( previousPt.x() + y * cosa / sina ); 222 : : } 223 : 0 : } 224 : : else 225 : : { 226 : 0 : point.setX( previousPt.x() + cosa * v ); 227 : 0 : point.setY( previousPt.y() + sina * v ); 228 : : } 229 : : 230 : 0 : if ( snapMatch.hasEdge() && !ctx.distanceConstraint.locked ) 231 : : { 232 : : // magnetize to the intersection of the snapped segment and the lockedAngle 233 : : 234 : : // line of previous point + locked angle 235 : 0 : const double x1 = previousPt.x(); 236 : 0 : const double y1 = previousPt.y(); 237 : 0 : const double x2 = previousPt.x() + cosa; 238 : 0 : const double y2 = previousPt.y() + sina; 239 : : // line of snapped segment 240 : 0 : const double x3 = edgePt0.x(); 241 : 0 : const double y3 = edgePt0.y(); 242 : 0 : const double x4 = edgePt1.x(); 243 : 0 : const double y4 = edgePt1.y(); 244 : : 245 : 0 : const double d = ( x1 - x2 ) * ( y3 - y4 ) - ( y1 - y2 ) * ( x3 - x4 ); 246 : : 247 : : // do not compute intersection if lines are almost parallel 248 : : // this threshold might be adapted 249 : 0 : if ( std::fabs( d ) > 0.01 ) 250 : : { 251 : 0 : point.setX( ( ( x3 - x4 ) * ( x1 * y2 - y1 * x2 ) - ( x1 - x2 ) * ( x3 * y4 - y3 * x4 ) ) / d ); 252 : 0 : point.setY( ( ( y3 - y4 ) * ( x1 * y2 - y1 * x2 ) - ( y1 - y2 ) * ( x3 * y4 - y3 * x4 ) ) / d ); 253 : 0 : } 254 : 0 : } 255 : 0 : } 256 : : 257 : : // ***************************** 258 : : // ---- Distance constraint 259 : 0 : if ( ctx.distanceConstraint.locked && ctx.cadPointList.count() >= 2 ) 260 : : { 261 : 0 : if ( ctx.xConstraint.locked || ctx.yConstraint.locked ) 262 : : { 263 : : // perform both to detect errors in constraints 264 : 0 : if ( ctx.xConstraint.locked ) 265 : : { 266 : 0 : QgsPointXY verticalPt0( ctx.xConstraint.value, point.y() ); 267 : 0 : QgsPointXY verticalPt1( ctx.xConstraint.value, point.y() + 1 ); 268 : 0 : res.valid &= QgsGeometryUtils::lineCircleIntersection( previousPt, ctx.distanceConstraint.value, verticalPt0, verticalPt1, point ); 269 : 0 : } 270 : 0 : if ( ctx.yConstraint.locked ) 271 : : { 272 : 0 : QgsPointXY horizontalPt0( point.x(), ctx.yConstraint.value ); 273 : 0 : QgsPointXY horizontalPt1( point.x() + 1, ctx.yConstraint.value ); 274 : 0 : res.valid &= QgsGeometryUtils::lineCircleIntersection( previousPt, ctx.distanceConstraint.value, horizontalPt0, horizontalPt1, point ); 275 : 0 : } 276 : 0 : } 277 : : else 278 : : { 279 : 0 : const double dist = std::sqrt( point.sqrDist( previousPt ) ); 280 : 0 : if ( dist == 0 ) 281 : : { 282 : : // handle case where mouse is over origin and distance constraint is enabled 283 : : // take arbitrary horizontal line 284 : 0 : point.set( previousPt.x() + ctx.distanceConstraint.value, previousPt.y() ); 285 : 0 : } 286 : : else 287 : : { 288 : 0 : const double vP = ctx.distanceConstraint.value / dist; 289 : 0 : point.set( previousPt.x() + ( point.x() - previousPt.x() ) * vP, 290 : 0 : previousPt.y() + ( point.y() - previousPt.y() ) * vP ); 291 : : } 292 : : 293 : 0 : if ( snapMatch.hasEdge() && !ctx.angleConstraint.locked ) 294 : : { 295 : : // we will magnietize to the intersection of that segment and the lockedDistance ! 296 : 0 : res.valid &= QgsGeometryUtils::lineCircleIntersection( previousPt, ctx.distanceConstraint.value, edgePt0, edgePt1, point ); 297 : 0 : } 298 : : } 299 : 0 : } 300 : : 301 : : // ***************************** 302 : : // ---- calculate CAD values 303 : 0 : QgsDebugMsgLevel( QStringLiteral( "point: %1 %2" ).arg( point.x() ).arg( point.y() ), 4 ); 304 : 0 : QgsDebugMsgLevel( QStringLiteral( "previous point: %1 %2" ).arg( previousPt.x() ).arg( previousPt.y() ), 4 ); 305 : 0 : QgsDebugMsgLevel( QStringLiteral( "penultimate point: %1 %2" ).arg( penultimatePt.x() ).arg( penultimatePt.y() ), 4 ); 306 : : //QgsDebugMsg( QStringLiteral( "dx: %1 dy: %2" ).arg( point.x() - previousPt.x() ).arg( point.y() - previousPt.y() ) ); 307 : : //QgsDebugMsg( QStringLiteral( "ddx: %1 ddy: %2" ).arg( previousPt.x() - penultimatePt.x() ).arg( previousPt.y() - penultimatePt.y() ) ); 308 : : 309 : 0 : res.finalMapPoint = point; 310 : : 311 : 0 : return res; 312 : : } 313 : : 314 : 0 : void QgsCadUtils::AlignMapPointContext::dump() const 315 : : { 316 : 0 : QgsDebugMsg( QStringLiteral( "Constraints (locked / relative / value" ) ); 317 : 0 : QgsDebugMsg( QStringLiteral( "Angle: %1 %2 %3" ).arg( angleConstraint.locked ).arg( angleConstraint.relative ).arg( angleConstraint.value ) ); 318 : 0 : QgsDebugMsg( QStringLiteral( "Distance: %1 %2 %3" ).arg( distanceConstraint.locked ).arg( distanceConstraint.relative ).arg( distanceConstraint.value ) ); 319 : 0 : QgsDebugMsg( QStringLiteral( "X: %1 %2 %3" ).arg( xConstraint.locked ).arg( xConstraint.relative ).arg( xConstraint.value ) ); 320 : 0 : QgsDebugMsg( QStringLiteral( "Y: %1 %2 %3" ).arg( yConstraint.locked ).arg( yConstraint.relative ).arg( yConstraint.value ) ); 321 : 0 : }