Skip to content

Commit

Permalink
Fix #21856: Split way: Wrong position of new member in PTv2 relation …
Browse files Browse the repository at this point in the history
…splitting a loop

git-svn-id: https://josm.openstreetmap.de/svn/trunk@18539 0c6e7542-c601-0410-84e7-c038aed88b3b
  • Loading branch information
taylor.smock committed Aug 16, 2022
1 parent 37a1972 commit 0f1bb9b
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 33 deletions.
43 changes: 33 additions & 10 deletions src/org/openstreetmap/josm/command/SplitWayCommand.java
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ static Analysis analyseSplit(Way way,
if (isOrderedRelation) {
if (way.lastNode() == way.firstNode()) {
// Self-closing way.
direction = Direction.IRRELEVANT;
direction = direction.merge(Direction.IRRELEVANT);
} else {
// For ordered relations, looking beyond the nearest neighbour members is not required,
// and can even cause the wrong direction to be guessed (with closed loops).
Expand All @@ -497,9 +497,9 @@ static Analysis analyseSplit(Way way,
missingWays.add(w);
else {
if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
direction = Direction.FORWARDS;
direction = direction.merge(Direction.FORWARDS);
} else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
direction = Direction.BACKWARDS;
direction = direction.merge(Direction.BACKWARDS);
}
}
}
Expand All @@ -509,9 +509,9 @@ static Analysis analyseSplit(Way way,
missingWays.add(w);
else {
if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
direction = Direction.BACKWARDS;
direction = direction.merge(Direction.BACKWARDS);
} else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
direction = Direction.FORWARDS;
direction = direction.merge(Direction.FORWARDS);
}
}
}
Expand All @@ -528,18 +528,18 @@ static Analysis analyseSplit(Way way,
if (ir - k >= 0 && r.getMember(ir - k).isWay()) {
Way w = r.getMember(ir - k).getWay();
if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
direction = Direction.FORWARDS;
direction = direction.merge(Direction.FORWARDS);
} else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
direction = Direction.BACKWARDS;
direction = direction.merge(Direction.BACKWARDS);
}
break;
}
if (ir + k < r.getMembersCount() && r.getMember(ir + k).isWay()) {
Way w = r.getMember(ir + k).getWay();
if (w.lastNode() == way.firstNode() || w.firstNode() == way.firstNode()) {
direction = Direction.BACKWARDS;
direction = direction.merge(Direction.BACKWARDS);
} else if (w.firstNode() == way.lastNode() || w.lastNode() == way.lastNode()) {
direction = Direction.FORWARDS;
direction = direction.merge(Direction.FORWARDS);
}
break;
}
Expand Down Expand Up @@ -951,7 +951,30 @@ enum Direction {
FORWARDS,
BACKWARDS,
UNKNOWN,
IRRELEVANT
IRRELEVANT;

/**
* Merge directions (this helps avoid overriding {@link #FORWARDS} with {@link #BACKWARDS}).
* @param other The other direction to merge. {@link #UNKNOWN} will be overridden.
* @return The merged direction
*/
Direction merge(Direction other) {
if (this == other) {
return this;
}
if (this == IRRELEVANT || other == IRRELEVANT ||
(this == FORWARDS && other == BACKWARDS) ||
(other == FORWARDS && this == BACKWARDS)) {
return IRRELEVANT;
}
if (this == UNKNOWN) {
return other;
}
if (other == UNKNOWN) {
return this;
}
return UNKNOWN;
}
}

enum WarningType {
Expand Down
9 changes: 7 additions & 2 deletions src/org/openstreetmap/josm/data/osm/DataSet.java
Original file line number Diff line number Diff line change
Expand Up @@ -530,11 +530,16 @@ public void addPrimitive(OsmPrimitive primitive) {
* @since 17981
*/
public void addPrimitiveRecursive(OsmPrimitive primitive) {
Stream<OsmPrimitive> children;
if (primitive instanceof Way) {
((Way) primitive).getNodes().forEach(n -> addPrimitiveRecursive(n));
children = ((Way) primitive).getNodes().stream().map(OsmPrimitive.class::cast);
} else if (primitive instanceof Relation) {
((Relation) primitive).getMembers().forEach(m -> addPrimitiveRecursive(m.getMember()));
children = ((Relation) primitive).getMembers().stream().map(RelationMember::getMember);
} else {
children = Stream.empty();
}
// Relations may have the same member multiple times.
children.filter(p -> !Objects.equals(this, p.getDataSet())).forEach(this::addPrimitiveRecursive);
addPrimitive(primitive);
}

Expand Down
106 changes: 85 additions & 21 deletions test/unit/org/openstreetmap/josm/command/SplitWayCommandTest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.command;

import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
Expand All @@ -11,10 +12,16 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.openstreetmap.josm.TestUtils;
import org.openstreetmap.josm.command.SplitWayCommand.Strategy;
import org.openstreetmap.josm.data.UndoRedoHandler;
Expand All @@ -26,6 +33,8 @@
import org.openstreetmap.josm.data.osm.Relation;
import org.openstreetmap.josm.data.osm.RelationMember;
import org.openstreetmap.josm.data.osm.Way;
import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
import org.openstreetmap.josm.io.IllegalDataException;
import org.openstreetmap.josm.io.OsmReader;
import org.openstreetmap.josm.testutils.JOSMTestRules;
Expand All @@ -42,7 +51,7 @@ final class SplitWayCommandTest {
*/
@RegisterExtension
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
public JOSMTestRules test = new JOSMTestRules().main().projection().preferences();
static JOSMTestRules test = new JOSMTestRules().main().projection().preferences();

/**
* Unit test of {@link SplitWayCommand#findVias}.
Expand Down Expand Up @@ -76,22 +85,21 @@ void testFindVias() {
assertEquals(Collections.singletonList(via), SplitWayCommand.findVias(r, "destination_sign"));
}

static Stream<Arguments> testRouteRelation() {
Stream.Builder<Arguments> builder = Stream.builder();
for (int i = 0; i < 4; i++) {
builder.add(Arguments.of(false, i));
builder.add(Arguments.of(true, i));
}
return builder.build();
}

/**
* Unit tests of route relations.
*/
@Test
void testRouteRelation() {
doTestRouteRelation(false, 0);
doTestRouteRelation(false, 1);
doTestRouteRelation(false, 2);
doTestRouteRelation(false, 3);
doTestRouteRelation(true, 0);
doTestRouteRelation(true, 1);
doTestRouteRelation(true, 2);
doTestRouteRelation(true, 3);
}

void doTestRouteRelation(final boolean wayIsReversed, final int indexOfWayToKeep) {
@ParameterizedTest
@MethodSource
void testRouteRelation(final boolean wayIsReversed, final int indexOfWayToKeep) {
final DataSet dataSet = new DataSet();
final Node n1 = new Node(new LatLon(1, 0));
final Node n2 = new Node(new LatLon(2, 0));
Expand Down Expand Up @@ -186,18 +194,21 @@ void testOneMemberOrderedRelationShowsWarningTest() {
assertFalse(result.isPresent());
}

@Test
void testDoIncompleteMembersOrderedRelationCorrectOrderTest() {
static Stream<Arguments> testIncompleteMembersOrderedRelationCorrectOrderTest() {
Stream.Builder<Arguments> builder = Stream.builder();
for (int i = 0; i < 2; i++) {
// All these permutations should result in a split that keeps the new parts in order.
doIncompleteMembersOrderedRelationCorrectOrderTest(false, false, i);
doIncompleteMembersOrderedRelationCorrectOrderTest(true, false, i);
doIncompleteMembersOrderedRelationCorrectOrderTest(true, true, i);
doIncompleteMembersOrderedRelationCorrectOrderTest(false, true, i);
builder.add(Arguments.of(false, false, i));
builder.add(Arguments.of(true, false, i));
builder.add(Arguments.of(true, true, i));
builder.add(Arguments.of(false, true, i));
}
return builder.build();
}

private void doIncompleteMembersOrderedRelationCorrectOrderTest(final boolean reverseWayOne,
@ParameterizedTest
@MethodSource
void testIncompleteMembersOrderedRelationCorrectOrderTest(final boolean reverseWayOne,
final boolean reverseWayTwo,
final int indexOfWayToKeep) {
final DataSet dataSet = new DataSet();
Expand Down Expand Up @@ -433,4 +444,57 @@ void testTicket20163() throws IOException, IllegalDataException {
}
}

/**
* Test case: smart ordering in routes
* See #21856
*/
@ParameterizedTest
@ValueSource(booleans = {false, true})
void testTicket21856(boolean reverse) {
Way way1 = TestUtils.newWay("highway=residential", TestUtils.newNode(""), TestUtils.newNode(""));
way1.setOsmId(23_968_090, 1);
way1.lastNode().setOsmId(6_823_898_683L, 1);
Way way2 = TestUtils.newWay("highway=residential", way1.lastNode(), TestUtils.newNode(""));
way2.setOsmId(728_199_307, 1);
way2.lastNode().setOsmId(6_823_898_684L, 1);
Node splitNode = TestUtils.newNode("");
splitNode.setOsmId(6_823_906_290L, 1);
Way splitWay = TestUtils.newWay("highway=service", way2.firstNode(), splitNode, TestUtils.newNode(""), way2.lastNode());
// The behavior should be the same regardless of the direction of the way
if (reverse) {
List<Node> nodes = new ArrayList<>(splitWay.getNodes());
Collections.reverse(nodes);
splitWay.setNodes(nodes);
}
splitWay.setOsmId(728_199_306, 1);
Relation route = TestUtils.newRelation("type=route route=bus", new RelationMember("", way1), new RelationMember("", splitWay),
new RelationMember("", way2), new RelationMember("", way1));
DataSet dataSet = new DataSet();
dataSet.addPrimitiveRecursive(route);
dataSet.setSelected(splitNode);
// Sanity check (preconditions -- the route should be well-formed already)
WayConnectionTypeCalculator connectionTypeCalculator = new WayConnectionTypeCalculator();
List<WayConnectionType> links = connectionTypeCalculator.updateLinks(route, route.getMembers());
assertAll("All links should be connected (forward)",
links.subList(0, links.size() - 2).stream().map(link -> () -> assertTrue(link.linkNext)));
assertAll("All links should be connected (backward)",
links.subList(1, links.size() - 1).stream().map(link -> () -> assertTrue(link.linkPrev)));
final Optional<SplitWayCommand> result = SplitWayCommand.splitWay(
splitWay,
SplitWayCommand.buildSplitChunks(splitWay, Collections.singletonList(splitNode)),
new ArrayList<>(),
Strategy.keepLongestChunk(),
// This split requires additional downloads but problem occured before the download
SplitWayCommand.WhenRelationOrderUncertain.SPLIT_ANYWAY
);
assertTrue(result.isPresent());
result.get().executeCommand();
// Actual check
connectionTypeCalculator = new WayConnectionTypeCalculator();
links = connectionTypeCalculator.updateLinks(route, route.getMembers());
assertAll("All links should be connected (forward)",
links.subList(0, links.size() - 2).stream().map(link -> () -> assertTrue(link.linkNext)));
assertAll("All links should be connected (backward)",
links.subList(1, links.size() - 1).stream().map(link -> () -> assertTrue(link.linkPrev)));
}
}

0 comments on commit 0f1bb9b

Please sign in to comment.