Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 335 additions & 0 deletions test/qml/tst_geometryeditor_vertex.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
import QtQuick
import QtTest
import org.qfield
import org.qgis
import Theme
import "../../src/qml/geometryeditors" as GeometryEditors

// tests for the vertex editor tool. The vertex model itself is tested in
// test_vertexmodel.cpp, so here we only check the tool around it: init wiring,
// applyChanges and the autoSave branch, cancel, remove, and undo.
//
// canvasClicked is not tested here on purpose. It turns a screen point into a
// map coordinate through mapSettings.screenToCoordinate, which only makes sense
// with a real rendered canvas. without one the screen-to-map math is meaningless,
// so a click test would just be asserting made up numbers, so that path belongs in
// an integration (spix) test, not here.
//
// the tool reads a few things from its parent scope in the app, so we provide
// them here as plain items the same way tst_featureForm.qml does.
TestCase {
id: testCase
name: "GeometryEditorVertex"

property var fieldsLayer: qgisProject.mapLayersByName("Fields")[0]
property var tracksLayer: qgisProject.mapLayersByName("Tracks")[0]

// reset between tests. the tests assert on the vertex model geometry and do
// not commit, so this only needs to clear editing state and roll back any
// stray edit buffer.
function cleanup() {
vertexEditor.cancel();
if (fieldsLayer.editBuffer()) {
fieldsLayer.rollBack();
}
if (tracksLayer.editBuffer()) {
tracksLayer.rollBack();
}
qfieldSettings.autoSave = false;
}

function makeFeatureModel(layer, fid) {
featureModel.currentLayer = layer;
featureModel.feature = layer.getFeature(fid);
featureModel.applyGeometryToVertexModel();
return featureModel;
}

// Fields/39 is a simple polygon. its vertex model interleaves segment
// candidates with real vertices, so the real (existing) vertices sit at the
// odd indices and 1 is the first of them.
function makeFieldsModel() {
return makeFeatureModel(fieldsLayer, "39");
}

// select the first real vertex and move it, so the model goes dirty like it
// would after a drag on the canvas. the layer is in meters so a meter-scale
// move is well above the tools ignore-tiny-moves threshold.
function selectAndMoveFirstVertex() {
const vertexModel = featureModel.vertexModel;
vertexModel.editingMode = VertexModel.EditVertex;
vertexModel.currentVertexIndex = 1;
const point = vertexModel.currentPoint;
vertexModel.currentPoint = GeometryUtils.point(point.x + 5, point.y + 5);
}

// pin a known extent and output size so mapUnitsPerPixel is deterministic on
// every platform. the vertex model ignores moves smaller than one pixel, so
// without a fixed map scale a move that registers locally can be dropped on a
// headless CI where the default map scale differs.
MapSettings {
id: mapSettingsItem
destinationCrs: CoordinateReferenceSystemUtils.fromDescription("EPSG:3857")
outputSize: Qt.size(600, 600)
extent: GeometryUtils.createRectangleFromPoints(GeometryUtils.point(1030800, 5911300), GeometryUtils.point(1031200, 5911700))
}

FeatureModel {
id: featureModel
project: qgisProject

vertexModel: VertexModel {
id: geometryEditingVertexModel
}
}

GeometryEditors.VertexEditor {
id: vertexEditor
featureModel: featureModel
mapSettings: mapSettingsItem
}

function test_initWiresFeatureModelAndResetsCurrentVertex() {
const model = makeFieldsModel();
model.vertexModel.currentVertexIndex = 3;

vertexEditor.init(model, mapSettingsItem, null, null);

compare(vertexEditor.featureModel, model);
// init should always start with no vertex selected
compare(model.vertexModel.currentVertexIndex, -1);
}

function test_blockingFollowsVertexModelDirtyState() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);
compare(vertexEditor.blocking, false);

selectAndMoveFirstVertex();

// blocking is just dirty, so an unsaved edit should block
compare(model.vertexModel.dirty, true);
compare(vertexEditor.blocking, true);
}

// returns how many points in list a are not present (within tolerance) in
// list b. Comparing both directions makes this independent of vertex order or
// ring rotation, which a vertex move can introduce.
function pointsNotIn(a, b) {
let missing = 0;
for (let i = 0; i < a.length; ++i) {
let found = false;
for (let j = 0; j < b.length; ++j) {
if (Math.abs(a[i].x - b[j].x) < 0.001 && Math.abs(a[i].y - b[j].y) < 0.001) {
found = true;
break;
}
}
if (!found)
missing++;
}
return missing;
}

// reads the vertices out of a geometry's WKT as a plain array of points, so
// tests can compare geometries vertex by vertex. Uses asWkt only, which is the
// same accessor the other tests rely on. For a polygon ring the closing vertex
// repeats the first one, so we drop that duplicate to count each vertex once.
function geometryPoints(geometry) {
const points = [];
const wkt = geometry.asWkt(6);
// pull the coordinate pairs from inside the parentheses
const inner = wkt.substring(wkt.indexOf("("), wkt.lastIndexOf(")") + 1);
const pairs = inner.replace(/[()]/g, " ").trim().split(",");
for (let i = 0; i < pairs.length; ++i) {
const nums = pairs[i].trim().split(/\s+/);
if (nums.length >= 2 && nums[0] !== "") {
points.push({
x: parseFloat(nums[0]),
y: parseFloat(nums[1])
});
}
}
// drop a repeated closing vertex (polygons repeat the first point at the end)
if (points.length > 1) {
const first = points[0];
const last = points[points.length - 1];
if (Math.abs(first.x - last.x) < 0.001 && Math.abs(first.y - last.y) < 0.001) {
points.pop();
}
}
return points;
}
Comment thread
kaustuvpokharel marked this conversation as resolved.

function test_movedVertexProducesExpectedGeometry() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);
const before = geometryPoints(model.vertexModel.geometry);

selectAndMoveFirstVertex();

// a single vertex move produces the same polygon with exactly one vertex
// relocated: one original point gone, one new point present, count unchanged
const moved = model.vertexModel.geometry;
verify(!moved.isNull);
compare(moved.type, Qgis.GeometryType.Polygon);

const after = geometryPoints(moved);
compare(after.length, before.length);
compare(pointsNotIn(before, after), 1);
compare(pointsNotIn(after, before), 1);
}

function test_applyChangesWithAutoSaveOffDoesNotApply() {
qfieldSettings.autoSave = false;
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);

selectAndMoveFirstVertex();
const before = model.feature.geometry.asWkt();

// the next/prev/add buttons call applyChanges(autoSave). with autoSave off
// the edit stays in the model and is not pushed to the feature yet
vertexEditor.applyChanges(qfieldSettings.autoSave);

compare(model.feature.geometry.asWkt(), before);
compare(model.vertexModel.dirty, true);
}

function test_applyChangesNoOpWhenNotDirty() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);
compare(model.vertexModel.dirty, false);

const before = model.feature.geometry.asWkt();

// nothing changed, so apply should do nothing
vertexEditor.applyChanges(true);

compare(model.feature.geometry.asWkt(), before);
}

function test_cancelResetsEditingModeAndClearsDirty() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);
selectAndMoveFirstVertex();
compare(model.vertexModel.dirty, true);

vertexEditor.cancel();

// cancel throws away the edit and leaves editing mode
compare(model.vertexModel.editingMode, VertexModel.NoEditing);
compare(model.vertexModel.dirty, false);
}

function test_removeVertexReducesCountAndSetsDirty() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);
const vertexModel = model.vertexModel;

vertexModel.editingMode = VertexModel.EditVertex;
vertexModel.currentVertexIndex = 1;
// the polygon has plenty of real vertices, well above the minimum, so
// removing one is allowed
verify(vertexModel.canRemoveVertex);
const countBefore = vertexModel.vertexCount;

vertexModel.removeCurrentVertex();

// removing one existing vertex drops it and its paired segment candidate, so
// the row count goes down by exactly two, and the edit is marked dirty
compare(vertexModel.vertexCount, countBefore - 2);
compare(vertexModel.dirty, true);
}

function test_canRemoveVertexFalseWithoutEditMode() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);

// canRemoveVertex only holds while a vertex is being edited, never before
compare(model.vertexModel.editingMode, VertexModel.NoEditing);
compare(model.vertexModel.canRemoveVertex, false);
}

function test_undoRestoresGeometryAfterMove() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);
const vertexModel = model.vertexModel;

const before = vertexModel.currentPoint;
selectAndMoveFirstVertex();
verify(vertexModel.canUndo);

vertexModel.undoHistory();

// undo should put the moved vertex back where it started
vertexModel.currentVertexIndex = 1;
const restored = vertexModel.currentPoint;
compare(restored.x, before.x);
compare(restored.y, before.y);
}

function test_undoIsNoOpWithEmptyHistory() {
const model = makeFieldsModel();
vertexEditor.init(model, mapSettingsItem, null, null);

// nothing has been edited, so there is nothing to undo
compare(model.vertexModel.canUndo, false);
model.vertexModel.undoHistory();
compare(model.vertexModel.dirty, false);
}

function test_movedVertexProducesExpectedGeometryOnLine() {
// same path on a line feature, to cover the line geometry branch
const model = makeFeatureModel(tracksLayer, "1");
vertexEditor.init(model, mapSettingsItem, null, null);
const vertexModel = model.vertexModel;

const before = geometryPoints(vertexModel.geometry);

// on a line the first real vertex is also at an odd index
vertexModel.editingMode = VertexModel.EditVertex;
vertexModel.currentVertexIndex = 1;
const point = vertexModel.currentPoint;
vertexModel.currentPoint = GeometryUtils.point(point.x + 5, point.y + 5);
compare(vertexModel.dirty, true);

// the vertex model geometry reflects the move directly, one vertex relocated
const moved = vertexModel.geometry;
verify(!moved.isNull);
compare(moved.type, Qgis.GeometryType.Line);

const after = geometryPoints(moved);
compare(after.length, before.length);
compare(pointsNotIn(before, after), 1);
compare(pointsNotIn(after, before), 1);
}
Comment thread
kaustuvpokharel marked this conversation as resolved.

// scope objects the tool expects from the app
Item {
id: mainWindowItem
}

Item {
id: qfieldSettings
property bool autoSave: false
}

Item {
id: coordinateLocator
property var currentCoordinate: GeometryUtils.point(0, 0)
property string positionInformation: ""
property string topSnappingResult: ""
property bool positionLocked: false
}

Item {
id: projectInfo
property string cloudUserInformation: ""
}

Item {
id: positionSource
property string positionInformation: ""
}
}
Loading