Skip to content

Commit

Permalink
Support backward and forward snapping lines.
Browse files Browse the repository at this point in the history
  • Loading branch information
mohsenD98 committed Nov 13, 2024
1 parent c233436 commit afa4554
Showing 1 changed file with 97 additions and 57 deletions.
154 changes: 97 additions & 57 deletions src/qml/CoordinateLocator.qml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}
Expand Down Expand Up @@ -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
}
}
}
}
Expand Down Expand Up @@ -327,29 +359,33 @@ 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;
}
const MINIMAL_PIXEL_DISTANCE_TRESHOLD = 20;
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)
Expand All @@ -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;
}
Expand All @@ -377,31 +413,34 @@ 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);
angleValue += Math.atan2(previousPoint.y - penultimatePoint.y, previousPoint.x - penultimatePoint.x);
}
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;
}

Expand All @@ -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;
Expand All @@ -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
Expand Down

0 comments on commit afa4554

Please sign in to comment.