QList<QgsPointXY> QgsAdvancedDigitizingDockWidget::snapSegmentToAllLayers( const QgsPointXY &originalMapPoint, bool *snapped ) const { QList<QgsPointXY> segment; QgsPointXY pt1, pt2; QgsPointLocator::Match match; QgsSnappingUtils *snappingUtils = mMapCanvas->snappingUtils(); QgsSnappingConfig canvasConfig = snappingUtils->config(); QgsSnappingConfig localConfig = snappingUtils->config(); localConfig.setMode( QgsSnappingConfig::AllLayers ); localConfig.setType( QgsSnappingConfig::Segment ); snappingUtils->setConfig( localConfig ); match = snappingUtils->snapToMap( originalMapPoint ); snappingUtils->setConfig( canvasConfig ); if ( match.isValid() && match.hasEdge() ) { match.edgePoints( pt1, pt2 ); segment << pt1 << pt2; } if ( snapped ) { *snapped = segment.count() == 2; } return segment; }
void QgsMapToolOffsetCurve::canvasPressEvent( QMouseEvent* e ) { deleteRubberBandAndGeometry(); mGeometryModified = false; mForceCopy = false; if ( !mCanvas ) { return; } //get selected features or snap to nearest feature if no selection QgsVectorLayer* layer = currentVectorLayer(); if ( !layer ) { notifyNotVectorLayer(); return; } QgsSnappingUtils* snapping = mCanvas->snappingUtils(); // store previous settings int oldType; double oldSearchRadius; QgsTolerance::UnitType oldSearchRadiusUnit; QgsSnappingUtils::SnapToMapMode oldMode = snapping->snapToMapMode(); snapping->defaultSettings( oldType, oldSearchRadius, oldSearchRadiusUnit ); // setup new settings (temporary) QSettings settings; snapping->setSnapToMapMode( QgsSnappingUtils::SnapAllLayers ); snapping->setDefaultSettings( QgsPointLocator::Edge, settings.value( "/Qgis/digitizing/search_radius_vertex_edit", 10 ).toDouble(), ( QgsTolerance::UnitType ) settings.value( "/Qgis/digitizing/search_radius_vertex_edit_unit", QgsTolerance::Pixels ).toInt() ); QgsPointLocator::Match match = snapping->snapToMap( e->pos() ); // restore old settings snapping->setSnapToMapMode( oldMode ); snapping->setDefaultSettings( oldType, oldSearchRadius, oldSearchRadiusUnit ); if ( match.hasEdge() && match.layer() ) { mSourceLayerId = match.layer()->id(); QgsFeature fet; if ( match.layer()->getFeatures( QgsFeatureRequest( match.featureId() ) ).nextFeature( fet ) ) { mForceCopy = ( e->modifiers() & Qt::ControlModifier ); //no geometry modification if ctrl is pressed mOriginalGeometry = createOriginGeometry( match.layer(), match, fet ); mRubberBand = createRubberBand(); if ( mRubberBand ) { mRubberBand->setToGeometry( mOriginalGeometry, layer ); } mModifiedFeature = fet.id(); createDistanceItem(); } } }
QList<QgsPoint> QgsMapMouseEvent::snapSegment( SnappingMode snappingMode, bool* snapped , bool allLayers ) const { QList<QgsPoint> segment; QgsPoint pt1, pt2; // If there's a cached snapping result we use it if ( snappingMode == mSnappingMode && mSnapMatch.hasEdge() ) { mSnapMatch.edgePoints( pt1, pt2 ); segment << pt1 << pt2; } else if ( snappingMode != NoSnapping ) { QgsPointLocator::Match match; if ( snappingMode == SnapProjectConfig && !allLayers ) { // run snapToMap with only segments EdgesOnlyFilter filter; match = mMapCanvas->snappingUtils()->snapToMap( mOriginalMapPoint, &filter ); } else if ( snappingMode == SnapAllLayers || allLayers ) { // run snapToMap with only edges on all layers QgsSnappingUtils* snappingUtils = mMapCanvas->snappingUtils(); QgsSnappingUtils::SnapToMapMode canvasMode = snappingUtils->snapToMapMode(); int type; double tolerance; QgsTolerance::UnitType unit; snappingUtils->defaultSettings( type, tolerance, unit ); snappingUtils->setSnapToMapMode( QgsSnappingUtils::SnapAllLayers ); snappingUtils->setDefaultSettings( QgsPointLocator::Edge, tolerance, unit ); match = snappingUtils->snapToMap( mOriginalMapPoint ); snappingUtils->setSnapToMapMode( canvasMode ); snappingUtils->setDefaultSettings( type, tolerance, unit ); } if ( match.isValid() && match.hasEdge() ) { match.edgePoints( pt1, pt2 ); segment << pt1 << pt2; } } if ( snapped ) { *snapped = segment.count() == 2; } return segment; }
void testNearestEdge() { QgsPointLocator loc( mVL ); QgsPointXY pt( 1.1, 0.5 ); QgsPointLocator::Match m = loc.nearestEdge( pt, 999 ); QVERIFY( m.isValid() ); QVERIFY( m.hasEdge() ); QCOMPARE( m.layer(), mVL ); QCOMPARE( m.featureId(), ( QgsFeatureId )1 ); QCOMPARE( m.point(), QgsPointXY( 1, 0.5 ) ); QCOMPARE( m.distance(), 0.1 ); QCOMPARE( m.vertexIndex(), 1 ); QgsPointXY pt1, pt2; m.edgePoints( pt1, pt2 ); QCOMPARE( pt1, QgsPointXY( 1, 0 ) ); QCOMPARE( pt2, QgsPointXY( 1, 1 ) ); }
bool acceptMatch( const QgsPointLocator::Match& m ) override { return m.hasEdge(); }
void QgsMapToolOffsetCurve::canvasReleaseEvent( QgsMapMouseEvent *e ) { mCtrlHeldOnFirstClick = false; if ( e->button() == Qt::RightButton ) { cancel(); return; } if ( mOriginalGeometry.isNull() ) { // first click, get feature to modify deleteRubberBandAndGeometry(); mGeometryModified = false; QgsPointLocator::Match match = mCanvas->snappingUtils()->snapToCurrentLayer( e->pos(), QgsPointLocator::Types( QgsPointLocator::Edge | QgsPointLocator::Area ) ); if ( ( match.hasEdge() || match.hasArea() ) && match.layer() ) { mLayer = match.layer(); QgsFeature fet; if ( match.layer()->getFeatures( QgsFeatureRequest( match.featureId() ) ).nextFeature( fet ) ) { mCtrlHeldOnFirstClick = ( e->modifiers() & Qt::ControlModifier ); //no geometry modification if ctrl is pressed prepareGeometry( match, fet ); mRubberBand = createRubberBand(); if ( mRubberBand ) { mRubberBand->setToGeometry( mManipulatedGeometry, match.layer() ); } mModifiedFeature = fet.id(); createUserInputWidget(); bool hasZ = QgsWkbTypes::hasZ( mLayer->wkbType() ); bool hasM = QgsWkbTypes::hasZ( mLayer->wkbType() ); if ( hasZ || hasM ) { emit messageEmitted( QStringLiteral( "layer %1 has %2%3%4 geometry. %2%3%4 values be set to 0 when using offset tool." ) .arg( mLayer->name() ) .arg( hasZ ? "Z" : "" ) .arg( hasZ && hasM ? "/" : "" ) .arg( hasM ? "M" : "" ) , Qgis::Warning ); } } } if ( mOriginalGeometry.isNull() ) { emit messageEmitted( tr( "Could not find a nearby feature in any vector layer." ) ); cancel(); notifyNotVectorLayer(); } } else { // second click - apply changes double offset = calculateOffset( e->snapPoint() ); applyOffset( offset, e->modifiers() ); } }
void QgsMapToolOffsetCurve::prepareGeometry( const QgsPointLocator::Match &match, QgsFeature &snappedFeature ) { QgsVectorLayer *vl = match.layer(); if ( !vl ) { return; } mOriginalGeometry = QgsGeometry(); mManipulatedGeometry = QgsGeometry(); mModifiedPart = -1; mModifiedRing = -1; //assign feature part by vertex number (snap to vertex) or by before vertex number (snap to segment) QgsGeometry geom = snappedFeature.geometry(); if ( geom.isNull() ) { return; } mOriginalGeometry = geom; QgsWkbTypes::Type geomType = geom.wkbType(); if ( QgsWkbTypes::geometryType( geomType ) == QgsWkbTypes::LineGeometry ) { if ( !match.hasEdge() ) { return; } if ( !geom.isMultipart() ) { mManipulatedGeometry = geom; } else { int vertex = match.vertexIndex(); QgsVertexId vertexId; geom.vertexIdFromVertexNr( vertex, vertexId ); mModifiedPart = vertexId.part; QgsMultiPolylineXY multiLine = geom.asMultiPolyline(); mManipulatedGeometry = QgsGeometry::fromPolylineXY( multiLine.at( mModifiedPart ) ); } } else if ( QgsWkbTypes::geometryType( geomType ) == QgsWkbTypes::PolygonGeometry ) { if ( !match.hasEdge() && match.hasArea() ) { if ( !geom.isMultipart() ) { mManipulatedGeometry = geom; } else { // get the correct part QgsMultiPolygonXY mpolygon = geom.asMultiPolygon(); for ( int part = 0; part < mpolygon.count(); part++ ) // go through the polygons { const QgsPolygonXY &polygon = mpolygon[part]; QgsGeometry partGeo = QgsGeometry::fromPolygonXY( polygon ); const QgsPointXY layerCoords = match.point(); if ( partGeo.contains( &layerCoords ) ) { mModifiedPart = part; mManipulatedGeometry = partGeo; } } } } else if ( match.hasEdge() ) { int vertex = match.vertexIndex(); QgsVertexId vertexId; geom.vertexIdFromVertexNr( vertex, vertexId ); QgsDebugMsg( QStringLiteral( "%1" ).arg( vertexId.ring ) ); if ( !geom.isMultipart() ) { QgsPolygonXY poly = geom.asPolygon(); // if has rings if ( poly.count() > 0 ) { mModifiedRing = vertexId.ring; mManipulatedGeometry = QgsGeometry::fromPolygonXY( QgsPolygonXY() << poly.at( mModifiedRing ) ); } else { mManipulatedGeometry = QgsGeometry::fromPolygonXY( poly ); } } else { mModifiedPart = vertexId.part; // get part, get ring QgsMultiPolygonXY multiPoly = geom.asMultiPolygon(); // if has rings if ( multiPoly.at( mModifiedPart ).count() > 0 ) { mModifiedRing = vertexId.ring; mManipulatedGeometry = QgsGeometry::fromPolygonXY( QgsPolygonXY() << multiPoly.at( mModifiedPart ).at( mModifiedRing ) ); } else { mManipulatedGeometry = QgsGeometry::fromPolygonXY( multiPoly.at( mModifiedPart ) ); } } } } }
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; }