Index: data/defaultpresets.xml
===================================================================
--- data/defaultpresets.xml (revision 15556)
+++ data/defaultpresets.xml (working copy)
@@ -7580,6 +7580,17 @@
+ -
+
+
+
+
+
+
+
+
+
+
-
Index: images/presets/transport/way/relation_connectivity.svg
===================================================================
--- images/presets/transport/way/relation_connectivity.svg (nonexistent)
+++ images/presets/transport/way/relation_connectivity.svg (working copy)
@@ -0,0 +1,5 @@
+
Index: src/org/openstreetmap/josm/data/validation/OsmValidator.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/OsmValidator.java (revision 15556)
+++ src/org/openstreetmap/josm/data/validation/OsmValidator.java (working copy)
@@ -42,6 +42,7 @@
import org.openstreetmap.josm.data.validation.tests.BarriersEntrances;
import org.openstreetmap.josm.data.validation.tests.Coastlines;
import org.openstreetmap.josm.data.validation.tests.ConditionalKeys;
+import org.openstreetmap.josm.data.validation.tests.ConnectivityRelations;
import org.openstreetmap.josm.data.validation.tests.CrossingWays;
import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
@@ -150,6 +151,7 @@
PublicTransportRouteTest.class, // 3600 .. 3699
RightAngleBuildingTest.class, // 3700 .. 3799
SharpAngles.class, // 3800 .. 3899
+ ConnectivityRelations.class, // 3900 .. 3999
};
/**
Index: src/org/openstreetmap/josm/data/validation/tests/ConnectivityRelations.java
===================================================================
--- src/org/openstreetmap/josm/data/validation/tests/ConnectivityRelations.java (nonexistent)
+++ src/org/openstreetmap/josm/data/validation/tests/ConnectivityRelations.java (working copy)
@@ -0,0 +1,257 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.regex.Pattern;
+
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+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.data.validation.Severity;
+import org.openstreetmap.josm.data.validation.Test;
+import org.openstreetmap.josm.data.validation.TestError;
+import org.openstreetmap.josm.tools.Logging;
+
+/**
+ * Check for inconsistencies in lane information between relation and members.
+ */
+public class ConnectivityRelations extends Test {
+
+ protected static final int INCONSISTENT_LANE_COUNT = 3900;
+
+ protected static final int UNKNOWN_CONNECTIVITY_ROLE = 3901;
+
+ protected static final int NO_CONNECTIVITY_TAG = 3902;
+
+ protected static final int MALFORMED_CONNECTIVITY_TAG = 3903;
+
+ protected static final int TOO_MANY_ROLES = 3904;
+
+ protected static final int MISSING_ROLE = 3905;
+
+ protected static final int MEMBER_MISSING_LANES = 3906;
+
+ private static final String CONNECTIVITY_TAG = "connectivity";
+ private static final String VIA = "via";
+ private static final String TO = "to";
+ private static final String FROM = "from";
+ private static final int BW = -1000;
+ private static final Pattern OPTIONAL_LANE_PATTERN = Pattern.compile("\\([0-9-]+\\)");
+ private static final Pattern TO_LANE_PATTERN = Pattern.compile("\\p{Zs}*[,:;]\\p{Zs}*");
+
+ /**
+ * Constructor
+ */
+ public ConnectivityRelations() {
+ super(tr("Connectivity Relation Check"), tr("Checks that lane count of relation matches with lanes of members"));
+ }
+
+ /**
+ * Convert the connectivity tag into a map of values
+ *
+ * @param relation A relation with a {@code connectivity} tag.
+ * @return A Map in the form of {@code Map>} May contain nulls when errors are encountered
+ * @since xxx
+ */
+ public static Map> parseConnectivityTag(Relation relation) {
+ final String joined = relation.get(CONNECTIVITY_TAG).replaceAll("bw", Integer.toString(BW));
+
+ if (joined == null) {
+ return Collections.emptyMap();
+ }
+
+ final Map> result = new HashMap<>();
+ String[] lanes = joined.split("\\|", -1);
+ for (int i = 0; i < lanes.length; i++) {
+ String[] lane = lanes[i].split(":", -1);
+
+ int laneNumber;
+ //Ignore connections from bw, since we cannot derive a lane number from bw
+ if (!lane[0].equals("bw")) {
+ laneNumber = Integer.parseInt(lane[0].trim());
+ } else {
+ laneNumber = BW;
+ }
+ Map connections = new HashMap<>();
+ String[] toLanes = TO_LANE_PATTERN.split(lane[1]);
+ for (int j = 0; j < toLanes.length; j++) {
+ String toLane = toLanes[j].trim();
+ try {
+ if (OPTIONAL_LANE_PATTERN.matcher(toLane).matches()) {
+ toLane = toLane.replace("(", "").replace(")", "").trim();
+ if (!toLane.equals("bw")) {
+ connections.put(Integer.parseInt(toLane), Boolean.TRUE);
+ } else
+ connections.put(BW, Boolean.TRUE);
+ } else {
+ if (!toLane.contains("bw")) {
+ connections.put(Integer.parseInt(toLane), Boolean.FALSE);
+ } else {
+ connections.put(BW, Boolean.FALSE);
+ }
+ }
+ } catch (NumberFormatException e) {
+ Logging.debug(e);
+ connections.put(null, null);
+ }
+ }
+ result.put(laneNumber, connections);
+
+ }
+ return result;
+ }
+
+ @Override
+ public void visit(Relation r) {
+ if (r.hasTag("type", CONNECTIVITY_TAG)) {
+ if (!r.hasKey(CONNECTIVITY_TAG)) {
+ errors.add(TestError.builder(this, Severity.WARNING, NO_CONNECTIVITY_TAG)
+ .message(tr("No connectivity tag in connectivity relation")).primitives(r).build());
+ } else if (!r.hasIncompleteMembers()) {
+ boolean badRole = checkForBadRole(r);
+ boolean missingRole = checkForMissingRole(r);
+ if (!badRole && !missingRole) {
+ checkForInconsistentLanes(r);
+ } else if (missingRole) {
+ createMissingRole(r);
+ }
+ }
+ }
+ }
+
+ private void checkForInconsistentLanes(Relation relation) {
+ String lanelessRoles = "";
+ // Lane count from connectivity tag
+ Map> connTagLanes = parseConnectivityTag(relation);
+ // Lane count from member tags
+ Map roleLanes = new HashMap<>();
+
+
+ for (RelationMember rM : relation.getMembers()) {
+ // Check lanes
+ if (rM.getType() == OsmPrimitiveType.WAY) {
+ OsmPrimitive prim = rM.getMember();
+ if (!VIA.equals(rM.getRole())) {
+ if (prim.hasKey("lanes"))
+ roleLanes.put(rM.getRole(), Integer.parseInt(prim.get("lanes")));
+ else {
+ String addString = "'" + rM.getRole() + "'";
+ if (!lanelessRoles.isEmpty()) {
+ addString = ", " + addString;
+ }
+ lanelessRoles += addString;
+ }
+ }
+ }
+ }
+
+ if (lanelessRoles.isEmpty()) {
+ boolean fromCheck = roleLanes.get(FROM) < Collections
+ .max(connTagLanes.entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
+ boolean toCheck = false;
+ for (Entry> to : connTagLanes.entrySet()) {
+ if (!to.getValue().containsKey(null)) {
+ toCheck = roleLanes.get(TO) < Collections
+ .max(to.getValue().entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
+ } else {
+ errors.add(TestError.builder(this, Severity.ERROR, MALFORMED_CONNECTIVITY_TAG)
+ .message(tr("Connectivity tag contains unusual data")).primitives(relation)
+ .build());
+ }
+ }
+ if (fromCheck || toCheck) {
+ errors.add(TestError.builder(this, Severity.WARNING, INCONSISTENT_LANE_COUNT)
+ .message(tr("Inconsistent lane numbering between relation and members")).primitives(relation)
+ .build());
+ }
+ } else {
+ errors.add(TestError.builder(this, Severity.OTHER, MEMBER_MISSING_LANES)
+ .message(tr("Relation {0} member(s) missing lanes tag", lanelessRoles)).primitives(relation)
+ .build());
+ }
+ }
+
+ private boolean checkForBadRole(Relation relation) {
+ // Check role names
+ int viaWays = 0;
+ int viaNodes = 0;
+ int toWays = 0;
+ int fromWays = 0;
+ for (RelationMember relationMember : relation.getMembers()) {
+ if (relationMember.getMember() instanceof Way) {
+ if (relationMember.hasRole(FROM))
+ fromWays++;
+ else if (relationMember.hasRole(TO))
+ toWays++;
+ else if (relationMember.hasRole(VIA))
+ viaWays++;
+ else {
+ createUnknownRole(relation, relationMember.getMember());
+ return true;
+ }
+ } else if (relationMember.getMember() instanceof Node) {
+ if (!relationMember.hasRole(VIA)) {
+ createUnknownRole(relation, relationMember.getMember());
+ return true;
+ }
+ viaNodes++;
+ }
+ }
+ return mixedViaNodeAndWay(relation, viaWays, viaNodes, toWays, fromWays);
+ }
+
+ private boolean checkForMissingRole(Relation relation) {
+ List necessaryRoles = new ArrayList<>();
+ necessaryRoles.add(FROM);
+ necessaryRoles.add(VIA);
+ necessaryRoles.add(TO);
+
+ List roleList = new ArrayList<>();
+ for (RelationMember relationMember: relation.getMembers()) {
+ roleList.add(relationMember.getRole());
+ }
+
+
+ return !roleList.containsAll(necessaryRoles);
+ }
+
+ private boolean mixedViaNodeAndWay(Relation relation, int viaWays, int viaNodes, int toWays, int fromWays) {
+ String message = "";
+ if ((viaWays != 0 && viaNodes != 0) || viaNodes > 1) {
+ message = tr("Relation contains {1} {0} roles.", VIA, viaWays + viaNodes);
+ } else if (toWays != 1) {
+ message = tr("Relation contains too many {0} roles", TO);
+ } else if (fromWays != 1) {
+ message = tr("Relation contains too many {0} roles", FROM);
+ }
+ if (message.isEmpty()) {
+ return false;
+ } else {
+ errors.add(TestError.builder(this, Severity.WARNING, TOO_MANY_ROLES)
+ .message(message).primitives(relation).build());
+ return true;
+ }
+ }
+
+ private void createUnknownRole(Relation relation, OsmPrimitive primitive) {
+ errors.add(TestError.builder(this, Severity.WARNING, UNKNOWN_CONNECTIVITY_ROLE)
+ .message(tr("Unkown role in connectivity relation")).primitives(relation).highlight(primitive).build());
+ }
+
+ private void createMissingRole(Relation relation) {
+ errors.add(TestError.builder(this, Severity.WARNING, MISSING_ROLE)
+ .message(tr("Connectivity relation is missing at least one necessary role")).primitives(relation)
+ .build());
+ }
+}
Index: test/unit/org/openstreetmap/josm/data/validation/tests/ConnectivityRelationsTest.java
===================================================================
--- test/unit/org/openstreetmap/josm/data/validation/tests/ConnectivityRelationsTest.java (nonexistent)
+++ test/unit/org/openstreetmap/josm/data/validation/tests/ConnectivityRelationsTest.java (working copy)
@@ -0,0 +1,116 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.validation.tests;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.openstreetmap.josm.JOSMFixture;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+
+/**
+ * Test the ConnectivityRelations validation test
+ *
+ * @author Taylor Smock
+ */
+public class ConnectivityRelationsTest {
+ private ConnectivityRelations check;
+ private static final String CONNECTIVITY = "connectivity";
+ /**
+ * Setup test.
+ *
+ * @throws Exception if an error occurs
+ */
+ @Before
+ public void setUp() throws Exception {
+ JOSMFixture.createUnitTestFixture().init();
+ check = new ConnectivityRelations();
+ }
+
+ private Relation createDefaultTestRelation() {
+ Node connection = new Node(new LatLon(0, 0));
+ return TestUtils.newRelation("type=connectivity connectivity=1:1",
+ new RelationMember("from", TestUtils.newWay("lanes=4", new Node(new LatLon(-0.1, -0.1)), connection)),
+ new RelationMember("via", connection),
+ new RelationMember("to", TestUtils.newWay("lanes=4", connection, new Node(new LatLon(0.1, 0.1)))));
+ }
+
+ /**
+ * Test for connectivity relations without a connectivity tag
+ */
+ @Test
+ public void testNoConnectivityTag() {
+ Relation relation = createDefaultTestRelation();
+ check.visit(relation);
+
+ Assert.assertEquals(0, check.getErrors().size());
+
+ relation.remove(CONNECTIVITY);
+ check.visit(relation);
+ Assert.assertEquals(1, check.getErrors().size());
+ }
+
+ /**
+ * Check for lanes that don't make sense
+ */
+ @Test
+ public void testMisMatchedLanes() {
+ Relation relation = createDefaultTestRelation();
+ check.visit(relation);
+ int expectedFailures = 0;
+
+ Assert.assertEquals(expectedFailures, check.getErrors().size());
+
+ relation.put(CONNECTIVITY, "45000:1");
+ check.visit(relation);
+ Assert.assertEquals(++expectedFailures, check.getErrors().size());
+
+ relation.put(CONNECTIVITY, "1:45000");
+ check.visit(relation);
+ Assert.assertEquals(++expectedFailures, check.getErrors().size());
+
+ relation.put(CONNECTIVITY, "1:1,2");
+ check.visit(relation);
+ Assert.assertEquals(expectedFailures, check.getErrors().size());
+
+ relation.put(CONNECTIVITY, "1:1,(2)");
+ check.visit(relation);
+ Assert.assertEquals(expectedFailures, check.getErrors().size());
+
+ relation.put(CONNECTIVITY, "1:1,(20000)");
+ check.visit(relation);
+ Assert.assertEquals(++expectedFailures, check.getErrors().size());
+ }
+
+ /**
+ * Check for bad roles (not from/via/to)
+ */
+ @Test
+ public void testForBadRole() {
+ Relation relation = createDefaultTestRelation();
+ check.visit(relation);
+ int expectedFailures = 0;
+
+ Assert.assertEquals(expectedFailures, check.getErrors().size());
+
+ for (int i = 0; i < relation.getMembers().size(); i++) {
+ String tRole = replaceMember(relation, i, "badRole");
+ check.visit(relation);
+ Assert.assertEquals(++expectedFailures, check.getErrors().size());
+ replaceMember(relation, i, tRole);
+ check.visit(relation);
+ Assert.assertEquals(expectedFailures, check.getErrors().size());
+ }
+ }
+
+ private String replaceMember(Relation relation, int index, String replacementRole) {
+ RelationMember relationMember = relation.getMember(index);
+ String currentRole = relationMember.getRole();
+ relation.removeMember(index);
+ relation.addMember(index, new RelationMember(replacementRole, relationMember.getMember()));
+ return currentRole;
+ }
+}