Ticket #18138: 18138.7.patch

File 18138.7.patch, 27.4 KB (added by Traaker_L, 5 years ago)

More clarification for warning messages about multiple/mixed via members, removal of redundant role checks covered by relation checker, changing of "inconsistent lane warning" severity to WARNING, assume that way has at least one way and don't throw error if connectivity relation only seems to deal with one way.

  • data/defaultpresets.xml

     
    75807580                <role key="to" text="to way" requisite="required" count="1" type="way" />
    75817581            </roles>
    75827582        </item> <!-- Turn Restriction -->
     7583        <item name="Lane Connectivity" type="relation" preset_name_label="true" icon="presets/transport/way/relation_connectivity.svg">
     7584            <link wiki="Relation:connectivity" />
     7585            <space />
     7586            <key key="type" value="connectivity" />
     7587            <text key="connectivity" text="Lane Connectivity" />
     7588            <roles>
     7589                <role key="from" text="from way" requisite="required" count="1" type="way" />
     7590                <role key="via" text="via node or ways" requisite="required" type="way,node" />
     7591                <role key="to" text="to way" requisite="required" count="1" type="way" />
     7592            </roles>
     7593        </item> <!-- Lane Connectivity -->
    75837594        <item name="Enforcement" icon="presets/vehicle/restriction/speed_camera.svg" type="relation" preset_name_label="true">
    75847595            <link wiki="Relation:enforcement" />
    75857596            <space />
  • images/presets/transport/way/relation_connectivity.svg

     
     1<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 16 16" width="16" height="16">
     2  <path d="M0,0 h16v16h-16z" fill="#8d8"/>
     3  <path d="M1.5,16v-6.5l4,-4v-6l8,0v16.5" stroke="#fff" fill="#888"/>
     4  <path d="M9.5,1v2m0,2v2m0,2v2m0,2v2m-4,0v-2m0,-2v-2" stroke="#fff"/>
     5</svg>
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

     
    4242import org.openstreetmap.josm.data.validation.tests.BarriersEntrances;
    4343import org.openstreetmap.josm.data.validation.tests.Coastlines;
    4444import org.openstreetmap.josm.data.validation.tests.ConditionalKeys;
     45import org.openstreetmap.josm.data.validation.tests.ConnectivityRelations;
    4546import org.openstreetmap.josm.data.validation.tests.CrossingWays;
    4647import org.openstreetmap.josm.data.validation.tests.DuplicateNode;
    4748import org.openstreetmap.josm.data.validation.tests.DuplicateRelation;
     
    150151        PublicTransportRouteTest.class, // 3600 .. 3699
    151152        RightAngleBuildingTest.class, // 3700 .. 3799
    152153        SharpAngles.class, // 3800 .. 3899
     154        ConnectivityRelations.class, // 3900 .. 3999
    153155    };
    154156
    155157    /**
  • src/org/openstreetmap/josm/data/validation/tests/ConnectivityRelations.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import static org.openstreetmap.josm.tools.I18n.tr;
     5import static org.openstreetmap.josm.tools.I18n.trn;
     6
     7import java.util.ArrayList;
     8import java.util.Collections;
     9import java.util.Comparator;
     10import java.util.HashMap;
     11import java.util.List;
     12import java.util.Map;
     13import java.util.Map.Entry;
     14import java.util.Set;
     15import java.util.regex.Pattern;
     16
     17import org.openstreetmap.josm.data.osm.Node;
     18import org.openstreetmap.josm.data.osm.OsmPrimitive;
     19import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
     20import org.openstreetmap.josm.data.osm.Relation;
     21import org.openstreetmap.josm.data.osm.RelationMember;
     22import org.openstreetmap.josm.data.osm.Way;
     23import org.openstreetmap.josm.data.validation.Severity;
     24import org.openstreetmap.josm.data.validation.Test;
     25import org.openstreetmap.josm.data.validation.TestError;
     26import org.openstreetmap.josm.tools.Logging;
     27
     28/**
     29 * Check for inconsistencies in lane information between relation and members.
     30 */
     31public class ConnectivityRelations extends Test {
     32
     33    protected static final int INCONSISTENT_LANE_COUNT = 3900;
     34
     35    protected static final int UNKNOWN_CONNECTIVITY_ROLE = 3901;
     36
     37    protected static final int NO_CONNECTIVITY_TAG = 3902;
     38
     39    protected static final int MALFORMED_CONNECTIVITY_TAG = 3903;
     40
     41    protected static final int MISSING_COMMA_CONNECTIVITY_TAG = 3904;
     42
     43    protected static final int TOO_MANY_ROLES = 3905;
     44
     45    protected static final int MISSING_ROLE = 3906;
     46
     47    protected static final int MEMBER_MISSING_LANES = 3907;
     48
     49    protected static final int CONNECTIVITY_IMPLIED = 3908;
     50
     51    private static final String CONNECTIVITY_TAG = "connectivity";
     52    private static final String VIA = "via";
     53    private static final String TO = "to";
     54    private static final String FROM = "from";
     55    private static final int BW = -1000;
     56    private static final Pattern OPTIONAL_LANE_PATTERN = Pattern.compile("\\([0-9-]+\\)");
     57    private static final Pattern TO_LANE_PATTERN = Pattern.compile("\\p{Zs}*[,:;]\\p{Zs}*");
     58    private static final Pattern MISSING_COMMA_PATTERN = Pattern.compile("[0-9]+\\([0-9]+\\)|\\([0-9]+\\)[0-9]+");
     59    private static final Pattern LANE_TAG_PATTERN = Pattern.compile(".*:lanes");
     60
     61    /**
     62    * Constructor
     63    */
     64    public ConnectivityRelations() {
     65        super(tr("Connectivity Relation Check"), tr("Checks that lane count of relation matches with lanes of members"));
     66    }
     67
     68    /**
     69     * Convert the connectivity tag into a map of values
     70     *
     71     * @param relation A relation with a {@code connectivity} tag.
     72     * @return A Map in the form of {@code Map<Lane From, Map<Lane To, Optional>>} May contain nulls when errors are encountered
     73     * @since xxx
     74     */
     75    public static Map<Integer, Map<Integer, Boolean>> parseConnectivityTag(Relation relation) {
     76        final String joined = relation.get(CONNECTIVITY_TAG).replace("bw", Integer.toString(BW));
     77
     78        if (joined == null) {
     79            return Collections.emptyMap();
     80        }
     81
     82        final Map<Integer, Map<Integer, Boolean>> result = new HashMap<>();
     83        String[] lanes = joined.split("\\|", -1);
     84        for (int i = 0; i < lanes.length; i++) {
     85            String[] lane = lanes[i].split(":", -1);
     86            int laneNumber;
     87            //Ignore connections from bw, since we cannot derive a lane number from bw
     88            if (!"bw".equals(lane[0])) {
     89                laneNumber = Integer.parseInt(lane[0].trim());
     90            } else {
     91                laneNumber = BW;
     92            }
     93            Map<Integer, Boolean> connections = new HashMap<>();
     94            String[] toLanes = TO_LANE_PATTERN.split(lane[1]);
     95            for (int j = 0; j < toLanes.length; j++) {
     96                String toLane = toLanes[j].trim();
     97                try {
     98                    if (OPTIONAL_LANE_PATTERN.matcher(toLane).matches()) {
     99                        toLane = toLane.replace("(", "").replace(")", "").trim();
     100                        if (!toLane.equals("bw")) {
     101                            connections.put(Integer.parseInt(toLane), Boolean.TRUE);
     102                        } else
     103                            connections.put(BW, Boolean.TRUE);
     104                    } else {
     105                        if (!toLane.contains("bw")) {
     106                            connections.put(Integer.parseInt(toLane), Boolean.FALSE);
     107                        } else {
     108                            connections.put(BW, Boolean.FALSE);
     109                        }
     110                    }
     111                } catch (NumberFormatException e) {
     112                    if (MISSING_COMMA_PATTERN.matcher(toLane).matches()) {
     113                        connections.put(null, true);
     114                    } else {
     115                        connections.put(null, null);
     116                    }
     117                }
     118            }
     119            result.put(laneNumber, connections);
     120        }
     121        return result;
     122    }
     123
     124    @Override
     125    public void visit(Relation r) {
     126        if (r.hasTag("type", CONNECTIVITY_TAG)) {
     127            if (!r.hasKey(CONNECTIVITY_TAG)) {
     128                errors.add(TestError.builder(this, Severity.WARNING, NO_CONNECTIVITY_TAG)
     129                        .message(tr("No 'connectivity' tag in connectivity relation")).primitives(r).build());
     130            } else if (!r.hasIncompleteMembers()) {
     131                boolean badRole = checkForBadRole(r);
     132                boolean missingRole = checkForMissingRole(r);
     133                if (!badRole && !missingRole) {
     134                    Map<String, Integer> roleLanes = checkForInconsistentLanes(r);
     135                    checkForImpliedConnectivity(r, roleLanes);
     136                } else if (missingRole) {
     137                    createMissingRole(r);
     138                }
     139            }
     140        }
     141    }
     142
     143    /**
     144     * Compare lane tags of members to values in the {@code connectivity} tag of the relation
     145     *
     146     * @param relation A relation with a {@code connectivity} tag.
     147     * @return A Map in the form of {@code Map<Role, Lane Count>}
     148     * @since xxx
     149     */
     150    private Map<String, Integer> checkForInconsistentLanes(Relation relation) {
     151        StringBuilder lanelessRoles = new StringBuilder();
     152        int lanelessRolesCount = 0;
     153        // Lane count from connectivity tag
     154        Map<Integer, Map<Integer, Boolean>> connTagLanes = parseConnectivityTag(relation);
     155        //If the ways involved in the connectivity tag are assuming a standard 2-way bi-directional highway
     156        boolean defaultLanes = true;
     157        for (Entry<Integer, Map<Integer, Boolean>> thisEntry : connTagLanes.entrySet()) {
     158            for(Entry<Integer, Boolean> thisEntry2 : thisEntry.getValue().entrySet()) {
     159                Logging.debug("Checking: " + thisEntry2.toString());
     160                if (thisEntry2.getKey() != null && thisEntry2.getKey() > 1) {
     161                    defaultLanes = false;
     162                    break;
     163                }
     164            }
     165            if (!defaultLanes) {
     166                break;
     167            }
     168        }
     169        // Lane count from member tags
     170        Map<String, Integer> roleLanes = new HashMap<>();
     171        for (RelationMember rM : relation.getMembers()) {
     172            // Check lanes
     173            if (rM.getType() == OsmPrimitiveType.WAY) {
     174                OsmPrimitive prim = rM.getMember();
     175                if (!VIA.equals(rM.getRole())) {
     176                    Map<String,String> primKeys = prim.getKeys();
     177                    List<Long> laneCounts = new ArrayList<>();
     178                    long maxLaneCount;
     179                    if (prim.hasTag("lanes")) {
     180                        laneCounts.add(Long.parseLong(prim.get("lanes")));
     181                    }
     182                    for (Entry<String,String> entry : primKeys.entrySet()) {
     183                        String thisKey = entry.getKey();
     184                        String thisValue = entry.getValue();
     185                        if (LANE_TAG_PATTERN.matcher(thisKey).matches()) {
     186                            //Count bar characters
     187                            long count = thisValue.chars().filter(ch -> ch == '|').count() + 1;
     188                            laneCounts.add(count);
     189                        }
     190                    }
     191
     192                    if (!laneCounts.equals(Collections.emptyList())) {
     193                        maxLaneCount = Collections.max(laneCounts);
     194                        roleLanes.put(rM.getRole(), (int)maxLaneCount);
     195                    }else {
     196                        String addString = "'" + rM.getRole() + "'";
     197                        StringBuilder sb = new StringBuilder(addString);
     198                        if (lanelessRoles.length() > 0) {
     199                            sb.insert(0, " and ");
     200                        }
     201                        lanelessRoles.append(sb.toString());
     202                        lanelessRolesCount++;
     203                    }
     204                }
     205            }
     206        }
     207
     208        if (lanelessRoles.toString().isEmpty()) {
     209            boolean fromCheck = roleLanes.get(FROM) < Collections
     210                    .max(connTagLanes.entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
     211            boolean toCheck = false;
     212            for (Entry<Integer, Map<Integer, Boolean>> to : connTagLanes.entrySet()) {
     213                if (!to.getValue().containsKey(null)) {
     214                    toCheck = roleLanes.get(TO) < Collections
     215                            .max(to.getValue().entrySet(), Comparator.comparingInt(Map.Entry::getKey)).getKey();
     216                } else {
     217                    if (to.getValue().containsValue(true)) {
     218                        errors.add(TestError.builder(this, Severity.ERROR, MISSING_COMMA_CONNECTIVITY_TAG)
     219                                .message(tr("Connectivity tag missing comma between optional and non-optional values")).primitives(relation)
     220                                .build());
     221                    }else {
     222                        errors.add(TestError.builder(this, Severity.ERROR, MALFORMED_CONNECTIVITY_TAG)
     223                                .message(tr("Connectivity tag contains unusual data")).primitives(relation)
     224                                .build());
     225                    }
     226                }
     227            }
     228            if (fromCheck || toCheck) {
     229                errors.add(TestError.builder(this, Severity.WARNING, INCONSISTENT_LANE_COUNT)
     230                        .message(tr("Inconsistent lane numbering between relation and member tags")).primitives(relation)
     231                        .build());
     232            }
     233        } else if (!defaultLanes){
     234            errors.add(TestError.builder(this, Severity.WARNING, MEMBER_MISSING_LANES)
     235                    .message(trn("Relation {0} member missing lanes tag", "Relation {0} members missing 'lanes' or '*:lanes' tag",
     236                            lanelessRolesCount, lanelessRoles)).primitives(relation)
     237                    .build());
     238        }
     239        return roleLanes;
     240    }
     241
     242    /**
     243     * Check the relation to see if the connectivity described is already implied by other data
     244     *
     245     * @param relation A relation with a {@code connectivity} tag.
     246     * @param roleLanes The lane counts for each relation role
     247     * @since xxx
     248     */
     249    private void checkForImpliedConnectivity(Relation relation, Map<String, Integer> roleLanes) {
     250        boolean connImplied = true;
     251        Map<Integer, Map<Integer, Boolean>> connTagLanes = parseConnectivityTag(relation);
     252        // Don't flag connectivity as already implied when:
     253        // - Lane counts are different on the roads
     254        // - Placement tags convey the connectivity
     255        // - The relation passes through an intersection
     256        //   - If via member is a node, it's connected to ways not in the relation
     257        //   - If a via member is a way, ways not in the relation connect to its nodes
     258        // - Highways that appear to be merging have a different cumulative number of lanes than
     259        //   the highway that they're merging into
     260
     261        connImplied = checkMemberTagsForImpliedConnectivity(relation, roleLanes) && !checkForIntersectionAtMembers(relation);
     262        // Check if connectivity tag implies default connectivity
     263        if (connImplied) {
     264            for (Entry<Integer, Map<Integer, Boolean>> to : connTagLanes.entrySet()) {
     265                int fromLane = to.getKey();
     266                for (Entry<Integer, Boolean> lane : to.getValue().entrySet()) {
     267                    if (lane.getKey() != null && fromLane != lane.getKey()) {
     268                        connImplied = false;
     269                        break;
     270                    }
     271                }
     272                if (!connImplied)
     273                    break;
     274            }
     275        }
     276
     277        if (connImplied) {
     278            errors.add(TestError.builder(this, Severity.WARNING, CONNECTIVITY_IMPLIED)
     279                    .message(tr("This connectivity may already be implied")).primitives(relation)
     280                    .build());
     281        }
     282    }
     283
     284    /**
     285     * Check to see if there is an intersection present at the via member
     286     *
     287     * @param relation A relation with a {@code connectivity} tag.
     288     * @return A Boolean that indicates whether an intersection is present at the via member
     289     * @since xxx
     290     */
     291    private boolean checkForIntersectionAtMembers(Relation relation) {
     292        OsmPrimitive viaPrim = relation.findRelationMembers("via").get(0);
     293        Set<OsmPrimitive> relationMembers = relation.getMemberPrimitives();
     294
     295        if (viaPrim.getType() == OsmPrimitiveType.NODE) {
     296            Node viaNode = (Node)viaPrim;
     297            List<Way> parentWays = viaNode.getParentWays();
     298            if( parentWays.size() > 2) {
     299                for (Way thisWay : parentWays) {
     300                    if (!relationMembers.contains(thisWay) && thisWay.hasTag("highway")) {
     301                        return true;
     302                    }
     303                }
     304            }
     305        } else if (viaPrim.getType() == OsmPrimitiveType.WAY){
     306            Way viaWay = (Way)viaPrim;
     307            for (Node thisNode : viaWay.getNodes()) {
     308                List<Way> parentWays = thisNode.getParentWays();
     309                if( parentWays.size() > 2) {
     310                    for (Way thisWay : parentWays) {
     311                        if (!relationMembers.contains(thisWay) && thisWay.hasTag("highway")) {
     312                            return true;
     313                        }
     314                    }
     315                }
     316            }
     317        }
     318        return false;
     319    }
     320
     321    /**
     322     * Check the relation to see if the connectivity described is already implied by the relation members' tags
     323     *
     324     * @param relation A relation with a {@code connectivity} tag.
     325     * @param roleLanes The lane counts for each relation role
     326     * @return Whether connectivity is already implied by tags on relation members
     327     * @since xxx
     328     */
     329    private boolean checkMemberTagsForImpliedConnectivity(Relation relation, Map<String, Integer> roleLanes) {
     330        // The members have different lane counts
     331        if (roleLanes.containsKey(TO) && roleLanes.containsKey(FROM) && (!roleLanes.get(TO).equals(roleLanes.get(FROM)))) {
     332            return false;
     333        }
     334
     335        // The members don't have placement tags defining the connectivity
     336        List<RelationMember> members = relation.getMembers();
     337        Map<String,OsmPrimitive> toFromMembers = new HashMap<>();
     338        for (RelationMember mem : members) {
     339            if ( mem.getRole().equals(FROM)) {
     340                toFromMembers.put(FROM, mem.getMember());
     341            } else if (mem.getRole().equals(TO)){
     342                toFromMembers.put(TO, mem.getMember());
     343            }
     344        }
     345
     346        return toFromMembers.get(TO).hasKey("placement") || toFromMembers.get(FROM).hasKey("placement");
     347    }
     348
     349    /**
     350     * Check the relation to see if the connectivity described is already implied by the relation members' tags
     351     *
     352     * @param relation A relation with a {@code connectivity} tag.
     353     * @return Whether one or more of the relation's members has an unusual role
     354     * @since xxx
     355     */
     356    private boolean checkForBadRole(Relation relation) {
     357        // Check role names
     358        int viaWays = 0;
     359        int viaNodes = 0;
     360        for (RelationMember relationMember : relation.getMembers()) {
     361            if (relationMember.getMember() instanceof Way) {
     362                if (relationMember.hasRole(VIA))
     363                    viaWays++;
     364                else if (!relationMember.hasRole(FROM) && !relationMember.hasRole(TO)){
     365                    createUnknownRole(relation, relationMember.getMember());
     366                    return true;
     367                }
     368            } else if (relationMember.getMember() instanceof Node) {
     369                if (!relationMember.hasRole(VIA)) {
     370                    createUnknownRole(relation, relationMember.getMember());
     371                    return true;
     372                }
     373                viaNodes++;
     374            }
     375        }
     376        return mixedViaNodeAndWay(relation, viaWays, viaNodes);
     377    }
     378
     379    /**
     380     * Check the relation to see if the connectivity described is already implied by the relation members' tags
     381     *
     382     * @param relation A relation with a {@code connectivity} tag.
     383     * @return Whether the relation is missing one or more of the critical {@code from}, {@code via}, or {@code to} roles
     384     * @since xxx
     385     */
     386    private boolean checkForMissingRole(Relation relation) {
     387        List<String> necessaryRoles = new ArrayList<>();
     388        necessaryRoles.add(FROM);
     389        necessaryRoles.add(VIA);
     390        necessaryRoles.add(TO);
     391
     392        List<String> roleList = new ArrayList<>();
     393        for (RelationMember relationMember: relation.getMembers()) {
     394            roleList.add(relationMember.getRole());
     395        }
     396
     397        return !roleList.containsAll(necessaryRoles);
     398    }
     399
     400    /**
     401     * Check the relation to see if the connectivity described is already implied by the relation members' tags
     402     *
     403     * @param relation A relation with a {@code connectivity} tag.
     404     * @param viaWays The number of ways in the relation with the {@code via} role
     405     * @param viaNodes The number of nodes in the relation with the {@code via} role
     406     * @return Whether the relation is missing one or more of the critical 'from', 'via', or 'to' roles
     407     * @since xxx
     408     */
     409    private boolean mixedViaNodeAndWay(Relation relation, int viaWays, int viaNodes) {
     410        String message = "";
     411        if (viaNodes > 1) {
     412            if (viaWays > 0) {
     413                message = tr("Relation should not contain mixed 'via' ways and nodes");
     414            } else {
     415                message = tr("Multiple 'via' roles only allowed with ways");
     416            }
     417        }
     418        if (message.isEmpty()) {
     419            return false;
     420        } else {
     421            errors.add(TestError.builder(this, Severity.WARNING, TOO_MANY_ROLES)
     422                    .message(message).primitives(relation).build());
     423            return true;
     424        }
     425    }
     426
     427    /**
     428     * Create a warning with the {@code UNKNOWN_CONNECTIVITY_ROLE} code
     429     *
     430     * @param relation A relation with a {@code connectivity} tag.
     431     * @param primitive The primitive with the unknown role
     432     * @since xxx
     433     */
     434    private void createUnknownRole(Relation relation, OsmPrimitive primitive) {
     435        errors.add(TestError.builder(this, Severity.WARNING, UNKNOWN_CONNECTIVITY_ROLE)
     436                .message(tr("Unkown role in connectivity relation")).primitives(relation).highlight(primitive).build());
     437    }
     438
     439    /**
     440     * Create a warning with the {@code MISSING_ROLE} code
     441     *
     442     * @param relation A relation with a {@code connectivity} tag.
     443     * @since xxx
     444     */
     445    private void createMissingRole(Relation relation) {
     446        errors.add(TestError.builder(this, Severity.WARNING, MISSING_ROLE)
     447                .message(tr("Connectivity relation is missing at least one necessary role")).primitives(relation)
     448                .build());
     449    }
     450}
  • test/unit/org/openstreetmap/josm/data/validation/tests/ConnectivityRelationsTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import org.junit.Assert;
     5import org.junit.Before;
     6import org.junit.Test;
     7import org.openstreetmap.josm.JOSMFixture;
     8import org.openstreetmap.josm.TestUtils;
     9import org.openstreetmap.josm.data.coor.LatLon;
     10import org.openstreetmap.josm.data.osm.Node;
     11import org.openstreetmap.josm.data.osm.Relation;
     12import org.openstreetmap.josm.data.osm.RelationMember;
     13
     14/**
     15 * Test the ConnectivityRelations validation test
     16 *
     17 * @author Taylor Smock
     18 */
     19public class ConnectivityRelationsTest {
     20    private ConnectivityRelations check;
     21    private static final String CONNECTIVITY = "connectivity";
     22    /**
     23     * Setup test.
     24     *
     25     * @throws Exception if an error occurs
     26     */
     27    @Before
     28    public void setUp() throws Exception {
     29        JOSMFixture.createUnitTestFixture().init();
     30        check = new ConnectivityRelations();
     31    }
     32
     33    private Relation createDefaultTestRelation() {
     34        Node connection = new Node(new LatLon(0, 0));
     35        return TestUtils.newRelation("type=connectivity connectivity=1:1",
     36                new RelationMember("from", TestUtils.newWay("lanes=4", new Node(new LatLon(-0.1, -0.1)), connection)),
     37                new RelationMember("via", connection),
     38                new RelationMember("to", TestUtils.newWay("lanes=4", connection, new Node(new LatLon(0.1, 0.1)))));
     39    }
     40
     41    /**
     42     * Test for connectivity relations without a connectivity tag
     43     */
     44    @Test
     45    public void testNoConnectivityTag() {
     46        Relation relation = createDefaultTestRelation();
     47        check.visit(relation);
     48
     49        Assert.assertEquals(0, check.getErrors().size());
     50
     51        relation.remove(CONNECTIVITY);
     52        check.visit(relation);
     53        Assert.assertEquals(1, check.getErrors().size());
     54    }
     55
     56    /**
     57     * Check for lanes that don't make sense
     58     */
     59    @Test
     60    public void testMisMatchedLanes() {
     61        Relation relation = createDefaultTestRelation();
     62        check.visit(relation);
     63        int expectedFailures = 0;
     64
     65        Assert.assertEquals(expectedFailures, check.getErrors().size());
     66
     67        relation.put(CONNECTIVITY, "45000:1");
     68        check.visit(relation);
     69        Assert.assertEquals(++expectedFailures, check.getErrors().size());
     70
     71        relation.put(CONNECTIVITY, "1:45000");
     72        check.visit(relation);
     73        Assert.assertEquals(++expectedFailures, check.getErrors().size());
     74
     75        relation.put(CONNECTIVITY, "1:1,2");
     76        check.visit(relation);
     77        Assert.assertEquals(expectedFailures, check.getErrors().size());
     78
     79        relation.put(CONNECTIVITY, "1:1,(2)");
     80        check.visit(relation);
     81        Assert.assertEquals(expectedFailures, check.getErrors().size());
     82
     83        relation.put(CONNECTIVITY, "1:1,(20000)");
     84        check.visit(relation);
     85        Assert.assertEquals(++expectedFailures, check.getErrors().size());
     86    }
     87
     88    /**
     89     * Check for bad roles (not from/via/to)
     90     */
     91    @Test
     92    public void testForBadRole() {
     93        Relation relation = createDefaultTestRelation();
     94        check.visit(relation);
     95        int expectedFailures = 0;
     96
     97        Assert.assertEquals(expectedFailures, check.getErrors().size());
     98
     99        for (int i = 0; i < relation.getMembers().size(); i++) {
     100            String tRole = replaceMember(relation, i, "badRole");
     101            check.visit(relation);
     102            Assert.assertEquals(++expectedFailures, check.getErrors().size());
     103            replaceMember(relation, i, tRole);
     104            check.visit(relation);
     105            Assert.assertEquals(expectedFailures, check.getErrors().size());
     106        }
     107    }
     108
     109    private String replaceMember(Relation relation, int index, String replacementRole) {
     110        RelationMember relationMember = relation.getMember(index);
     111        String currentRole = relationMember.getRole();
     112        relation.removeMember(index);
     113        relation.addMember(index, new RelationMember(replacementRole, relationMember.getMember()));
     114        return currentRole;
     115    }
     116}