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