From afa455473fa7a1b21bb9c978d18ea03e98cec282 Mon Sep 17 00:00:00 2001 From: Mohsen Date: Wed, 13 Nov 2024 18:12:54 +0330 Subject: [PATCH] Support backward and forward snapping lines. --- src/qml/CoordinateLocator.qml | 154 +++++++++++++++++++++------------- 1 file changed, 97 insertions(+), 57 deletions(-) diff --git a/src/qml/CoordinateLocator.qml b/src/qml/CoordinateLocator.qml index 879d6b6082..c799a0dfcf 100644 --- a/src/qml/CoordinateLocator.qml +++ b/src/qml/CoordinateLocator.qml @@ -39,7 +39,6 @@ Item { property variant sourceLocation: undefined // Screen coordinate readonly property variant currentCoordinate: !!overrideLocation ? overrideLocation : snappingUtils.snappedCoordinate - property var commonAngleInDegrees: null // some trickery here: the first part (!mapSettings.visibleExtent) is only there to get a signal when // the map canvas extent changes (user pans/zooms) and the calculation of the display position is retriggered @@ -64,15 +63,33 @@ Item { // Get the current crosshair location in screen coordinates. If `undefined`, then we use the center of the screen as input point. const location = sourceLocation === undefined ? Qt.point(locator.width / 2, locator.height / 2) : sourceLocation; if (snapToCommonAngleButton.isSnapToCommonAngleEnabled) { - locator.commonAngleInDegrees = getCommonAngleInDegrees(location, locator.rubberbandModel, snapToCommonAngleButton.snapToCommonAngleDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative); - const coords = calculateSnapToAngleLineEndCoords(snappedPoint, locator.commonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative, 1000); - snapToCommonAngleLines.endCoordX = coords.x || 0; - snapToCommonAngleLines.endCoordY = coords.y || 0; - return snapPointToCommonAngle(location, locator.rubberbandModel, locator.commonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative); + const backwardCommonAngleInDegrees = getCommonAngleInDegrees(location, locator.rubberbandModel, snapToCommonAngleButton.snapToCommonAngleDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative); + const forwardCommonAngleInDegrees = getCommonAngleInDegrees(location, locator.rubberbandModel, snapToCommonAngleButton.snapToCommonAngleDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative, true); + const backwardCoords = calculateSnapToAngleLineEndCoords(snappedPoint, backwardCommonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative, 1000); + const forwardCoords = calculateSnapToAngleLineEndCoords(snappedPoint, forwardCommonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative, 1000, -1); + snappingLinesModel.setProperty(0, "endCoordX", backwardCoords.x || 0); + snappingLinesModel.setProperty(0, "endCoordY", backwardCoords.y || 0); + snappingLinesModel.setProperty(0, "snappedToAngle", backwardCommonAngleInDegrees !== undefined); + snappingLinesModel.setProperty(1, "endCoordX", forwardCoords.x || 0); + snappingLinesModel.setProperty(1, "endCoordY", forwardCoords.y || 0); + snappingLinesModel.setProperty(1, "snappedToAngle", forwardCommonAngleInDegrees !== undefined); + if (backwardCommonAngleInDegrees !== undefined && forwardCommonAngleInDegrees !== undefined) { + var backwardSnappedPoint = snapPointToCommonAngle(location, locator.rubberbandModel, backwardCommonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative); + var forwardSnappedPoint = snapPointToCommonAngle(location, locator.rubberbandModel, forwardCommonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative, true); + return { + "x": (backwardSnappedPoint.x + forwardSnappedPoint.x) / 2, + "y": (backwardSnappedPoint.y + forwardSnappedPoint.y) / 2 + }; + } else if (backwardCommonAngleInDegrees !== undefined) + return snapPointToCommonAngle(location, locator.rubberbandModel, backwardCommonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative); + else if (forwardCommonAngleInDegrees !== undefined) + return snapPointToCommonAngle(location, locator.rubberbandModel, forwardCommonAngleInDegrees, snapToCommonAngleButton.isSnapToCommonAngleRelative, true); } else { - locator.commonAngleInDegrees = null; - snapToCommonAngleLines.endCoordX = 0; - snapToCommonAngleLines.endCoordY = 0; + for (let i = 0; i < snappingLinesModel.count; ++i) { + snappingLinesModel.setProperty(i, "endCoordX", 0); + snappingLinesModel.setProperty(i, "endCoordY", 0); + snappingLinesModel.setProperty(i, "snappedToAngle", false); + } } return location; } @@ -235,47 +252,62 @@ Item { } } - Shape { - id: snapToCommonAngleLines - - property double endCoordX: 0 - property double endCoordY: 0 - - visible: !!locator.commonAngleInDegrees || (endCoordX !== 0 && endCoordY !== 0) + Repeater { + id: snappingLines width: parent.width height: parent.height anchors.centerIn: parent - opacity: 0.5 - // outer line - ShapePath { - id: outerLine - - strokeWidth: 4 - strokeColor: "#fff" - strokeStyle: ShapePath.DashLine - dashPattern: [5, 3] - startX: snappedPoint.x - startY: snappedPoint.y - - PathLine { - x: snapToCommonAngleLines.endCoordX - y: snapToCommonAngleLines.endCoordY + model: ListModel { + id: snappingLinesModel + ListElement { + endCoordX: 0 + endCoordY: 0 + snappedToAngle: false + } + ListElement { + endCoordX: 0 + endCoordY: 0 + snappedToAngle: false } } - // inner line - ShapePath { - strokeWidth: outerLine.strokeWidth / 2 - strokeColor: "#000" - strokeStyle: ShapePath.DashLine - dashPattern: outerLine.dashPattern.map(v => v * 2) - startX: snappedPoint.x - startY: snappedPoint.y + delegate: Shape { + visible: !!snappedToAngle || (endCoordX !== 0 && endCoordY !== 0) + width: parent.width + height: parent.height + anchors.centerIn: parent + opacity: 0.5 + + // outer line + ShapePath { + id: outerLine + strokeWidth: 4 + strokeColor: "#fff" + strokeStyle: ShapePath.DashLine + dashPattern: [5, 3] + startX: snappedPoint.x + startY: snappedPoint.y + + PathLine { + x: endCoordX + y: endCoordY + } + } - PathLine { - x: snapToCommonAngleLines.endCoordX - y: snapToCommonAngleLines.endCoordY + // inner line + ShapePath { + strokeWidth: outerLine.strokeWidth / 2 + strokeColor: "#000" + strokeStyle: ShapePath.DashLine + dashPattern: outerLine.dashPattern.map(v => v * 2) + startX: snappedPoint.x + startY: snappedPoint.y + + PathLine { + x: endCoordX + y: endCoordY + } } } } @@ -327,8 +359,9 @@ Item { * @param {number} commonAngleStepDeg - the common angle in degrees we are trying to snap to * @param {boolean} isRelativeAngle - whether the angle should be calculated relative to the last geometry segment * @returns {number|undefined} - the closest common angle. Returns`undefined` if there is no nearby point that forms a common angle. + * @param {boolean} forwardMode - true: snap to firstPoint and false: snap to previousPoint */ - function getCommonAngleInDegrees(currentPoint, rubberbandModel, commonAngleStepDeg, isRelativeAngle) { + function getCommonAngleInDegrees(currentPoint, rubberbandModel, commonAngleStepDeg, isRelativeAngle, forwardMode = false) { if (!rubberbandModel) { return; } @@ -336,20 +369,23 @@ Item { const SOFT_CONSTRAINT_TOLERANCE_DEGREES = 20; const SOFT_CONSTRAINT_TOLERANCE_PIXEL = 40; const previousPoint = mapCanvas.mapSettings.coordinateToScreen(rubberbandModel.lastCoordinate); + const firstPoint = mapCanvas.mapSettings.coordinateToScreen(rubberbandModel.firstCoordinate); const rubberbandPointsCount = rubberbandModel.vertexCount; + const targetPoint = forwardMode ? firstPoint : previousPoint; + const minimumDigitizedPoints = forwardMode ? 3 : 2; - // we need at least 1 alredy digitized point (and the other one is wanna be digitized) - if (rubberbandPointsCount < 2) { + // we need at least [minimumDigitizedPoints - 1] alredy digitized point (and the other one is wanna be digitized) + if (rubberbandPointsCount < minimumDigitizedPoints) { return; } - const distanceFromLastPoint = Math.sqrt((currentPoint.x - previousPoint.x) ** 2 + (currentPoint.y - previousPoint.y) ** 2); + const distanceFromLastPoint = Math.sqrt((currentPoint.x - targetPoint.x) ** 2 + (currentPoint.y - targetPoint.y) ** 2); if (distanceFromLastPoint < MINIMAL_PIXEL_DISTANCE_TRESHOLD) { return; } const commonAngle = commonAngleStepDeg * Math.PI / 180; // see if soft common angle constraint should be performed // only if not in HardLock mode - let softAngle = Math.atan2(currentPoint.y - previousPoint.y, currentPoint.x - previousPoint.x); + let softAngle = Math.atan2(currentPoint.y - targetPoint.y, currentPoint.x - targetPoint.x); let deltaAngle = 0; if (isRelativeAngle && rubberbandPointsCount >= 3) { // compute the angle relative to the last segment (0° is aligned with last segment) @@ -363,7 +399,7 @@ Item { 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 dist = Math.abs(Math.cos(softAngle + deltaAngle) * (previousPoint.y - currentPoint.y) - Math.sin(softAngle + deltaAngle) * (previousPoint.x - currentPoint.x)); + const dist = Math.abs(Math.cos(softAngle + deltaAngle) * (targetPoint.y - currentPoint.y) - Math.sin(softAngle + deltaAngle) * (targetPoint.x - currentPoint.x)); if (dist < SOFT_CONSTRAINT_TOLERANCE_PIXEL) { return 180.0 / Math.PI * softAngle; } @@ -377,21 +413,24 @@ Item { * @param {Rubberband} rubberbandModel - holds all previously added points in the current digitizing session * @param {number} commonAngleDegrees - the common angle in degrees we are snapping to * @param {boolean} isRelativeAngle - whether the angle should be calculated relative to the last geometry segment + * @param {boolean} forwardMode - true: snap to firstPoint and false: snap to previousPoint * @returns {QPointF} - the resulting snapped to common angle point. If no snapping was possible, then `currentPoint` is returned. */ - function snapPointToCommonAngle(currentPoint, rubberbandModel, commonAngleDegrees, isRelativeAngle) { + function snapPointToCommonAngle(currentPoint, rubberbandModel, commonAngleDegrees, isRelativeAngle, forwardMode = false) { if (!rubberbandModel) { return currentPoint; } // if null or undefined, no common angle is determined - if (commonAngleDegrees == null) { + if (commonAngleDegrees === null) { return currentPoint; } let angleValue = commonAngleDegrees * Math.PI / 180; const returnPoint = currentPoint; const previousPoint = mapCanvas.mapSettings.coordinateToScreen(rubberbandModel.lastCoordinate); + const firstPoint = mapCanvas.mapSettings.coordinateToScreen(rubberbandModel.firstCoordinate); const rubberbandPointsCount = rubberbandModel.vertexCount; + const targetPoint = forwardMode ? firstPoint : previousPoint; if (isRelativeAngle && rubberbandPointsCount >= 3) { // compute the angle relative to the last segment (0° is aligned with last segment) const penultimatePoint = mapCanvas.mapSettings.coordinateToScreen(rubberbandModel.penultimateCoordinate); @@ -399,9 +438,9 @@ Item { } const cosa = Math.cos(angleValue); const sina = Math.sin(angleValue); - const v = (currentPoint.x - previousPoint.x) * cosa + (currentPoint.y - previousPoint.y) * sina; - returnPoint.x = (previousPoint.x + cosa * v); - returnPoint.y = (previousPoint.y + sina * v); + const v = (currentPoint.x - targetPoint.x) * cosa + (currentPoint.y - targetPoint.y) * sina; + returnPoint.x = (targetPoint.x + cosa * v); + returnPoint.y = (targetPoint.y + sina * v); return returnPoint; } @@ -411,15 +450,16 @@ Item { * @param {number} angleDegrees - angle of the line in degrees. * @param {boolean} isRelativeAngle - whether the angle should be calculated relative to the last geometry segment * @param {number} screenSize - size of the screen. Used to make sure the end of the line is outside the screen. + * @param {number} direction - direction of line. 1: from current point to screen edges, -1: from current point to first point. */ - function calculateSnapToAngleLineEndCoords(currentPoint, angleDegrees, isRelativeAngle, screenSize) { + function calculateSnapToAngleLineEndCoords(currentPoint, angleDegrees, isRelativeAngle, screenSize, direction = 1) { if (rubberbandModel == null) { return {}; } const rubberbandPointsCount = rubberbandModel.vertexCount; // if the angle is null or undefined, return empty coordinate map - if (angleDegrees == null) { + if (angleDegrees === null) { return {}; } let deltaAngle = 0; @@ -432,8 +472,8 @@ Item { const x1 = currentPoint.x; const y1 = currentPoint.y; const angleRadians = angleDegrees * Math.PI / 180 + deltaAngle; - const x2 = x1 + screenSize * Math.cos(angleRadians); - const y2 = y1 + screenSize * Math.sin(angleRadians); + const x2 = x1 + screenSize * Math.cos(angleRadians) * direction; + const y2 = y1 + screenSize * Math.sin(angleRadians) * direction; return { "x": x2, "y": y2