Ticket #17528: intersectionissues_v9.patch

File intersectionissues_v9.patch, 37.0 KB (added by taylor.smock, 6 years ago)

Filter out roundabouts for the nearly connected roads check, refactor to reduce method complexities, fix failing tests (I need to use this for awhile to see if there are significant false positives)

  • src/org/openstreetmap/josm/actions/ValidateAction.java

     
    116116        private boolean canceled;
    117117        private List<TestError> errors;
    118118
     119        private List<Class<? extends Test>> runTests;
     120
    119121        /**
    120122         * Constructs a new {@code ValidationTask}
    121123         * @param tests  the tests to run
     
    153155        @Override
    154156        protected void realRun() throws SAXException, IOException,
    155157        OsmTransferException {
     158            runTests = new ArrayList<>();
    156159            if (tests == null || tests.isEmpty())
    157160                return;
    158161            errors = new ArrayList<>(200);
    159162            getProgressMonitor().setTicksCount(tests.size() * validatedPrimitives.size());
    160             int testCounter = 0;
     163            runTests(tests, 0);
     164            tests = null;
     165            if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
     166                getProgressMonitor().setCustomText("");
     167                getProgressMonitor().subTask(tr("Updating ignored errors ..."));
     168                for (TestError error : errors) {
     169                    if (canceled) return;
     170                    error.updateIgnored();
     171                }
     172            }
     173        }
     174
     175        protected int runTests(Collection<Test> tests, int testCounter) {
     176            ArrayList<Test> remaining = new ArrayList<>();
    161177            for (Test test : tests) {
    162178                if (canceled)
    163                     return;
     179                    return testCounter;
     180                if (test.getAfterClass() != null && !runTests.contains(test.getAfterClass())) {
     181                    remaining.add(test);
     182                    continue;
     183                }
    164184                testCounter++;
    165                 getProgressMonitor().setCustomText(tr("Test {0}/{1}: Starting {2}", testCounter, tests.size(), test.getName()));
     185                getProgressMonitor().setCustomText(tr("Test {0}/{1}: Starting {2}", testCounter, this.tests.size(), test.getName()));
    166186                test.setPartialSelection(formerValidatedPrimitives != null);
     187                test.setPreviousErrors(errors);
    167188                test.startTest(getProgressMonitor().createSubTaskMonitor(validatedPrimitives.size(), false));
    168189                test.visit(validatedPrimitives);
    169190                test.endTest();
    170191                errors.addAll(test.getErrors());
    171192                test.clear();
     193                runTests.add(test.getClass());
    172194            }
    173             tests = null;
    174             if (ValidatorPrefHelper.PREF_USE_IGNORE.get()) {
    175                 getProgressMonitor().setCustomText("");
    176                 getProgressMonitor().subTask(tr("Updating ignored errors ..."));
    177                 for (TestError error : errors) {
    178                     if (canceled) return;
    179                     error.updateIgnored();
    180                 }
     195            if (!remaining.isEmpty()) {
     196                testCounter = runTests(remaining, testCounter);
    181197            }
     198            return testCounter;
    182199        }
    183200    }
    184201}
  • src/org/openstreetmap/josm/data/validation/OsmValidator.java

     
    4949import org.openstreetmap.josm.data.validation.tests.DuplicatedWayNodes;
    5050import org.openstreetmap.josm.data.validation.tests.Highways;
    5151import org.openstreetmap.josm.data.validation.tests.InternetTags;
     52import org.openstreetmap.josm.data.validation.tests.IntersectionIssues;
    5253import org.openstreetmap.josm.data.validation.tests.Lanes;
    5354import org.openstreetmap.josm.data.validation.tests.LongSegment;
    5455import org.openstreetmap.josm.data.validation.tests.MapCSSTagChecker;
     
    148149        LongSegment.class, // 3500 .. 3599
    149150        PublicTransportRouteTest.class, // 3600 .. 3699
    150151        RightAngleBuildingTest.class, // 3700 .. 3799
     152        IntersectionIssues.class, // 3800 .. 3899
    151153    };
    152154
    153155    /**
  • src/org/openstreetmap/josm/data/validation/Test.java

     
    4646    /** Name of the test */
    4747    protected final String name;
    4848
     49    /** Test to run after */
     50    protected Class<? extends Test> afterTest;
     51
    4952    /** Description of the test */
    5053    protected final String description;
    5154
     
    6770    /** The list of errors */
    6871    protected List<TestError> errors = new ArrayList<>(30);
    6972
     73    /** The list of previously found errors */
     74    protected List<TestError> previousErrors;
     75
    7076    /** Whether the test is run on a partial selection data */
    7177    protected boolean partialSelection;
    7278
     
    8490     * @param description Description of the test
    8591     */
    8692    public Test(String name, String description) {
     93        this(name, description, null);
     94    }
     95
     96    /**
     97     * Constructor
     98     * @param name Name of the test
     99     * @param description Description of the test
     100     * @param afterTest Ensure the test is run after a test with this name
     101     *
     102     * @since xxx
     103     */
     104    public Test(String name, String description, Class<? extends Test> afterTest) {
    87105        this.name = name;
    88106        this.description = description;
     107        this.afterTest = afterTest;
    89108    }
    90109
    91110    /**
     
    178197    }
    179198
    180199    /**
     200     * Set the validation errors accumulated by other tests until this moment
     201     * For validation errors accumulated by this test, use {@code getErrors()}
     202     * @param errors The errors from previous tests
     203     */
     204    public void setPreviousErrors(List<TestError> errors) {
     205        previousErrors = errors;
     206    }
     207
     208    /**
    181209     * Notification of the end of the test. The tester may perform additional
    182210     * actions and destroy the used structures.
    183211     * <p>
     
    319347    }
    320348
    321349    /**
     350     * Get the class that the test must run after
     351     * @return A class that extends {@code Test}
     352     *
     353     * @since xxx
     354     */
     355    public Class<? extends Test> getAfterClass() {
     356        return afterTest;
     357    }
     358
     359    /**
    322360     * Determines if the test has been canceled.
    323361     * @return {@code true} if the test has been canceled, {@code false} otherwise
    324362     */
  • src/org/openstreetmap/josm/data/validation/tests/IntersectionIssues.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;
     5
     6import java.util.ArrayList;
     7import java.util.Collection;
     8import java.util.HashMap;
     9import java.util.HashSet;
     10import java.util.List;
     11import java.util.Set;
     12
     13import org.openstreetmap.josm.data.coor.EastNorth;
     14import org.openstreetmap.josm.data.coor.LatLon;
     15import org.openstreetmap.josm.data.osm.Node;
     16import org.openstreetmap.josm.data.osm.OsmPrimitive;
     17import org.openstreetmap.josm.data.osm.Way;
     18import org.openstreetmap.josm.data.osm.WaySegment;
     19import org.openstreetmap.josm.data.validation.Severity;
     20import org.openstreetmap.josm.data.validation.Test;
     21import org.openstreetmap.josm.data.validation.TestError;
     22import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     23import org.openstreetmap.josm.tools.Geometry;
     24import org.openstreetmap.josm.tools.Logging;
     25import org.openstreetmap.josm.tools.Utils;
     26
     27/**
     28 * Finds issues with highway intersections
     29 * @author Taylor Smock
     30 * @since xxx
     31 */
     32public class IntersectionIssues extends Test {
     33    private static final int INTERSECTIONISSUESCODE = 3800;
     34    /** The code for an intersection which briefly interrupts a road */
     35    protected static final int SHORT_DISCONNECT = INTERSECTIONISSUESCODE + 0;
     36    /** The code for a node that is almost on a way */
     37    protected static final int NEARBY_NODE = INTERSECTIONISSUESCODE + 1;
     38    /** The distance to consider for nearby nodes/short disconnects */
     39    protected static final double MAX_DISTANCE = 5.0; // meters
     40    /** The distance to consider for nearby nodes with tags */
     41    protected static final double MAX_DISTANCE_NODE_INFORMATION = MAX_DISTANCE / 5.0; // meters
     42    /** The maximum angle for almost overlapping ways */
     43    protected static final double MAX_ANGLE = 15.0; // degrees
     44    /** The maximum distance to consider for almost overlapping ways.
     45     * Please note that lane width should be at least 2.6m (if it is a full-size lane) */
     46    protected static final double MAX_DISTANCE_OVERLAPPING = 1.0; // meters
     47
     48    private HashMap<String, ArrayList<Way>> ways;
     49    ArrayList<Way> allWays;
     50
     51    /**
     52     * Construct a new {@code IntersectionIssues} object
     53     */
     54    public IntersectionIssues() {
     55        super(tr("Intersection Issues"), tr("Check for issues at intersections"), OverlappingWays.class);
     56    }
     57
     58    @Override
     59    public void startTest(ProgressMonitor monitor) {
     60        super.startTest(monitor);
     61        ways = new HashMap<>();
     62        allWays = new ArrayList<>();
     63    }
     64
     65    @Override
     66    public void endTest() {
     67        Way pWay = null;
     68        try {
     69            for (ArrayList<Way> comparison : ways.values()) {
     70                pWay = comparison.get(0);
     71                checkNearbyEnds(comparison);
     72            }
     73            for (Way way : allWays) {
     74                pWay = way;
     75                for (Way way2 : allWays) {
     76                    if (way2.equals(way)) continue;
     77                    pWay = way2;
     78                    if (way.getBBox().intersects(way2.getBBox())) {
     79                        checkNearbyNodes(way, way2);
     80                    }
     81                }
     82            }
     83        } catch (Exception e) {
     84            if (pWay != null) {
     85                Logging.debug("Way https://osm.org/way/{0} caused an error", pWay.getOsmId());
     86            }
     87            Logging.warn(e);
     88        }
     89        ways = null;
     90        allWays = null;
     91        super.endTest();
     92    }
     93
     94    @Override
     95    public void visit(Way way) {
     96        if (!way.isUsable()) return;
     97        String highway = "highway";
     98        if (way.hasKey(highway) && !way.get(highway).contains("_link") &&
     99                !way.get(highway).contains("proposed")) {
     100            String[] identityTags = new String[] {"name", "ref"};
     101            for (String tag : identityTags) {
     102                if (way.hasKey(tag)) {
     103                    ArrayList<Way> similar = ways.get(way.get(tag)) == null ? new ArrayList<>() : ways.get(way.get(tag));
     104                    if (!similar.contains(way)) similar.add(way);
     105                    ways.put(way.get(tag), similar);
     106                }
     107            }
     108            if (!allWays.contains(way)) allWays.add(way);
     109        }
     110    }
     111
     112    /**
     113     * Check for ends that are nearby but not directly connected
     114     * @param comparison Ways to look at
     115     */
     116    public void checkNearbyEnds(List<Way> comparison) {
     117        ArrayList<Way> errored = new ArrayList<>();
     118        for (Way one : comparison) {
     119            LatLon oneLast = one.lastNode().getCoor();
     120            LatLon oneFirst = one.firstNode().getCoor();
     121            for (Way two : comparison) {
     122                if (one.equals(two) || one.isFirstLastNode(two.firstNode())
     123                        || one.isFirstLastNode(two.lastNode()) ||
     124                        (errored.contains(one) && errored.contains(two))) continue;
     125                LatLon twoLast = two.lastNode().getCoor();
     126                LatLon twoFirst = two.firstNode().getCoor();
     127                int nearCase = getNearCase(oneFirst, oneLast, twoFirst, twoLast);
     128                if (nearCase != 0 && !checkForConnection(one, two, nearCase)) {
     129                    createCheckNearbyEndsError(nearCase, errored, one, two);
     130                    return;
     131                }
     132            }
     133        }
     134    }
     135
     136    // 8421 -> twoFirst/oneFirst, twoFirst/oneLast, twoLast/oneFirst, twoLast/oneLast
     137    private boolean checkForConnection(Way one, Way two, int nearCase) {
     138        List<Node> nodes = new ArrayList<>();
     139        if ((nearCase & 1) == 1) {
     140            nodes.add(one.lastNode());
     141            nodes.add(two.lastNode());
     142        }
     143        if ((nearCase & 2) == 2) {
     144            nodes.add(one.firstNode());
     145            nodes.add(two.lastNode());
     146        }
     147        if ((nearCase & 4) == 4) {
     148            nodes.add(one.lastNode());
     149            nodes.add(two.firstNode());
     150        }
     151        if ((nearCase & 8) == 8) {
     152            nodes.add(one.firstNode());
     153            nodes.add(two.firstNode());
     154        }
     155        for (Node node : nodes) {
     156            Collection<Way> parents = node.getParentWays();
     157            parents.remove(one);
     158            parents.remove(two);
     159            for (Way way : parents) {
     160                if ((one.hasKey("name") && way.hasKey("name") && way.get("name").equals(one.get("name"))) ||
     161                        (one.hasKey("ref") && way.hasKey("ref") && way.get("ref").equals(one.get("ref")))) {
     162                    return true;
     163                }
     164            }
     165        }
     166        return false;
     167    }
     168
     169    /**
     170     * Returns true if an exception is found
     171     * @param nodes Nodes that may be parts of roundabouts or other exceptions.
     172     * @return true if an exception is found
     173     */
     174    private boolean checkForExceptions(Node... nodes) {
     175        Collection<String> exceptions = new HashSet<>();
     176        exceptions.add("junction");
     177        for (Node node : nodes) {
     178            for (Way way : Utils.filteredCollection(node.getReferrers(), Way.class)) {
     179                for (String exceptionKey : exceptions) {
     180                    if (way.hasKey(exceptionKey)) return true;
     181                }
     182            }
     183        }
     184        return false;
     185    }
     186
     187    private void createCheckNearbyEndsError(int nearCase, List<Way> errored, Way one, Way two) {
     188        if (nearCase <= 0) return;
     189        List<Way> nearby = new ArrayList<>();
     190        nearby.add(one);
     191        nearby.add(two);
     192        List<WaySegment> segments = new ArrayList<>();
     193        if ((nearCase & 1) != 0 && !checkForExceptions(one.lastNode(), two.lastNode())) {
     194            segments.add(new WaySegment(two, two.getNodesCount() - 2));
     195            segments.add(new WaySegment(one, one.getNodesCount() - 2));
     196        }
     197        if ((nearCase & 2) != 0 && !checkForExceptions(two.lastNode(), one.firstNode())) {
     198            segments.add(new WaySegment(two, two.getNodesCount() - 2));
     199            segments.add(new WaySegment(one, 0));
     200        }
     201        if ((nearCase & 4) != 0 && !checkForExceptions(two.firstNode(), one.lastNode())) {
     202            segments.add(new WaySegment(two, 0));
     203            segments.add(new WaySegment(one, one.getNodesCount() - 2));
     204        }
     205        if ((nearCase & 8) != 0 && !checkForExceptions(two.firstNode(), one.firstNode())) {
     206            segments.add(new WaySegment(two, 0));
     207            segments.add(new WaySegment(one, 0));
     208        }
     209        errored.addAll(nearby);
     210        allWays.removeAll(errored);
     211        TestError.Builder testError = TestError.builder(this, Severity.WARNING, SHORT_DISCONNECT)
     212                .primitives(nearby)
     213                .highlightWaySegments(segments)
     214                .message(tr("Disconnected road"));
     215        errors.add(testError.build());
     216    }
     217
     218    /**
     219     * Get nearby cases
     220     * @param oneFirst The {@code LatLon} of the the first node of the first way
     221     * @param oneLast The {@code LatLon} of the the last node of the first way
     222     * @param twoFirst The {@code LatLon} of the the first node of the second way
     223     * @param twoLast The {@code LatLon} of the the last node of the second way
     224     * @return A bitwise int (8421 -> twoFirst/oneFirst, twoFirst/oneLast, twoLast/oneFirst, twoLast/oneLast)
     225     *
     226     */
     227    private int getNearCase(LatLon oneFirst, LatLon oneLast, LatLon twoFirst, LatLon twoLast) {
     228        int returnInt = 0;
     229        if (twoLast.greatCircleDistance(oneLast) <= MAX_DISTANCE) {
     230            returnInt = returnInt | 1;
     231        }
     232        if (twoLast.greatCircleDistance(oneFirst) <= MAX_DISTANCE) {
     233            returnInt = returnInt | 2;
     234        }
     235        if (twoFirst.greatCircleDistance(oneLast) <= MAX_DISTANCE) {
     236            returnInt = returnInt | 4;
     237        }
     238        if (twoFirst.greatCircleDistance(oneFirst) <= MAX_DISTANCE) {
     239            returnInt = returnInt | 8;
     240        }
     241        return returnInt;
     242    }
     243
     244    /**
     245     * Check nearby nodes to an intersection of two ways
     246     * @param way1 A way to check an almost intersection with
     247     * @param way2 A way to check an almost intersection with
     248     */
     249    public void checkNearbyNodes(Way way1, Way way2) {
     250        Collection<Node> intersectingNodes = getIntersectingNode(way1, way2);
     251        if (intersectingNodes.isEmpty() ||
     252                (way1.isOneway() != 0 && way2.isOneway() != 0 &&
     253                ((way1.hasKey("name") && way1.get("name").equals(way2.get("name"))) ||
     254                 (way1.hasKey("ref") && way1.get("ref").equals(way2.get("ref")))))) return;
     255        for (Node intersectingNode : intersectingNodes) {
     256            checkNearbyNodes(way1, way2, intersectingNode);
     257            checkNearbyNodes(way2, way1, intersectingNode);
     258        }
     259    }
     260
     261    private void checkNearbyNodes(Way way1, Way way2, Node nearby) {
     262        for (Node node : way1.getNeighbours(nearby)) {
     263            if (node.equals(nearby) || way2.containsNode(node)) continue;
     264            double distance = Geometry.getDistance(way2, node);
     265            double angle = getSmallestAngle(way2, nearby, node);
     266            if (((distance < MAX_DISTANCE && !node.isTagged())
     267                    || (distance < MAX_DISTANCE_NODE_INFORMATION && node.isTagged()))
     268                    && angle < MAX_ANGLE) {
     269                List<Way> primitiveIssues = new ArrayList<>();
     270                primitiveIssues.add(way1);
     271                primitiveIssues.add(way2);
     272                if (alreadyFoundInRelevantTest(primitiveIssues)) return;
     273
     274                List<WaySegment> waysegmentsOne = buildWaySegmentAroundNode(way1, nearby);
     275                List<WaySegment> waysegmentsTwo = buildWaySegmentAroundNode(way2, nearby);
     276                List<WaySegment> waysegments = new ArrayList<>();
     277                Node twoNear = null;
     278                Node oneNear = null;
     279                for (WaySegment twoSegment : waysegmentsTwo) {
     280                    if (angle == getSmallestAngle(twoSegment.toWay(), nearby, node)) {
     281                        waysegments.add(twoSegment);
     282                        twoNear = getNearNode(twoSegment, nearby);
     283                        break;
     284                    }
     285                }
     286                for (WaySegment oneSegment: waysegmentsOne) {
     287                    if (oneSegment.toWay().containsNode(node)) {
     288                        waysegments.add(oneSegment);
     289                        oneNear = getNearNode(oneSegment, nearby);
     290                        break;
     291                    }
     292                }
     293                double distance1 = Geometry.getDistance(waysegments.get(0).toWay(), twoNear);
     294                double distance2 = Geometry.getDistance(waysegments.get(1).toWay(), oneNear);
     295                if (Math.min(distance1, distance2) < MAX_DISTANCE_OVERLAPPING)
     296                    createNearlyOverlappingError(primitiveIssues, waysegments);
     297            }
     298        }
     299    }
     300
     301    private void createNearlyOverlappingError(Collection<? extends OsmPrimitive> primitiveIssues, Collection<WaySegment> waysegments) {
     302        TestError.Builder testError = TestError.builder(this, Severity.WARNING, NEARBY_NODE)
     303                .primitives(primitiveIssues)
     304                .highlightWaySegments(waysegments)
     305                .message(tr("Nearly overlapping ways"));
     306        errors.add(testError.build());
     307    }
     308
     309    private Node getNearNode(WaySegment segment, Node nearby) {
     310        return segment.getFirstNode() != nearby ? segment.getFirstNode() : segment.getSecondNode();
     311    }
     312
     313    private List<WaySegment> buildWaySegmentAroundNode(Way way, Node node) {
     314        List<WaySegment> waysegments = new ArrayList<>();
     315        int index = way.getNodes().indexOf(node);
     316        if (index >= way.getNodesCount() - 1) index--;
     317        waysegments.add(new WaySegment(way, index));
     318        if (index > 0) waysegments.add(new WaySegment(way, index - 1));
     319        return waysegments;
     320    }
     321
     322    private boolean alreadyFoundInRelevantTest(Collection<? extends OsmPrimitive> primitiveIssues) {
     323        List<TestError> tErrors = new ArrayList<>();
     324        if (previousErrors != null) tErrors.addAll(previousErrors);
     325        tErrors.addAll(getErrors());
     326        for (TestError error : tErrors) {
     327            int code = error.getCode();
     328            if ((code == SHORT_DISCONNECT || code == NEARBY_NODE
     329                    || code == OverlappingWays.OVERLAPPING_HIGHWAY
     330                    || code == OverlappingWays.DUPLICATE_WAY_SEGMENT
     331                    || code == OverlappingWays.OVERLAPPING_HIGHWAY_AREA
     332                    || code == OverlappingWays.OVERLAPPING_WAY
     333                    || code == OverlappingWays.OVERLAPPING_WAY_AREA
     334                    || code == OverlappingWays.OVERLAPPING_RAILWAY
     335                    || code == OverlappingWays.OVERLAPPING_RAILWAY_AREA)
     336                    && primitiveIssues.containsAll(error.getPrimitives())) {
     337                return true;
     338            }
     339        }
     340        return false;
     341    }
     342
     343    /**
     344     * Get the intersecting node of two ways
     345     * @param way1 A way that (hopefully) intersects with way2
     346     * @param way2 A way to find an intersection with
     347     * @return A collection of nodes where the ways intersect
     348     */
     349    public Collection<Node> getIntersectingNode(Way way1, Way way2) {
     350        HashSet<Node> nodes = new HashSet<>();
     351        for (Node node : way1.getNodes()) {
     352            if (way2.containsNode(node)) {
     353                nodes.add(node);
     354            }
     355        }
     356        return nodes;
     357    }
     358
     359    /**
     360     * Get the corner angle between nodes
     361     * @param way The way with additional nodes
     362     * @param intersection The node to get angles around
     363     * @param comparison The node to get angles from
     364     * @return The angle for comparison->intersection->(additional node) (normalized degrees)
     365     */
     366    public double getSmallestAngle(Way way, Node intersection, Node comparison) {
     367        Set<Node> neighbours = way.getNeighbours(intersection);
     368        double angle = Double.MAX_VALUE;
     369        EastNorth eastNorthIntersection = intersection.getEastNorth();
     370        EastNorth eastNorthComparison = comparison.getEastNorth();
     371        for (Node node : neighbours) {
     372            EastNorth eastNorthNode = node.getEastNorth();
     373            double tAngle = Geometry.getCornerAngle(eastNorthComparison, eastNorthIntersection, eastNorthNode);
     374            if (Math.abs(tAngle) < angle) angle = Math.abs(tAngle);
     375        }
     376        return Geometry.getNormalizedAngleInDegrees(angle);
     377    }
     378}
  • test/unit/org/openstreetmap/josm/data/validation/tests/IntersectionIssuesTest.java

     
     1// License: GPL. For details, see LICENSE file.
     2package org.openstreetmap.josm.data.validation.tests;
     3
     4import java.util.Collection;
     5import java.util.HashSet;
     6import java.util.List;
     7
     8import org.junit.Assert;
     9import org.junit.Rule;
     10import org.junit.Test;
     11import org.openstreetmap.josm.TestUtils;
     12import org.openstreetmap.josm.data.coor.LatLon;
     13import org.openstreetmap.josm.data.osm.DataSet;
     14import org.openstreetmap.josm.data.osm.Node;
     15import org.openstreetmap.josm.data.osm.OsmPrimitive;
     16import org.openstreetmap.josm.data.osm.Relation;
     17import org.openstreetmap.josm.data.osm.Way;
     18import org.openstreetmap.josm.data.validation.TestError;
     19import org.openstreetmap.josm.testutils.JOSMTestRules;
     20import org.openstreetmap.josm.tools.Utils;
     21
     22import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     23
     24/**
     25 * JUnit Test of "Intersection Issues" validation test.
     26 */
     27public class IntersectionIssuesTest {
     28
     29    /**
     30     * Setup test.
     31     */
     32    @Rule
     33    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     34    public JOSMTestRules test = new JOSMTestRules().projection();
     35
     36    private static List<TestError> test(DataSet ds) throws Exception {
     37        IntersectionIssues test = new IntersectionIssues();
     38        test.initialize();
     39        test.startTest(null);
     40        for (Way w : ds.getWays()) {
     41            test.visit(w);
     42        }
     43        test.endTest();
     44        return test.getErrors();
     45    }
     46
     47    private static Collection<OsmPrimitive> createCollection(OsmPrimitive... osmPrimitives) {
     48        HashSet<OsmPrimitive> collection = new HashSet<>();
     49        for (OsmPrimitive primitive : osmPrimitives) {
     50            if (collection.contains(primitive)) continue;
     51            if (primitive instanceof Node) {
     52                collection.add(primitive);
     53            } else if (primitive instanceof Way) {
     54                collection.addAll(((Way) primitive).getNodes());
     55                collection.add(primitive);
     56            } else if (primitive instanceof Relation) {
     57                Relation relation = (Relation) primitive;
     58                collection.add(relation);
     59                for (OsmPrimitive tPrim : relation.getMemberPrimitives()) {
     60                    collection.addAll(createCollection(tPrim));
     61                }
     62            }
     63        }
     64        return collection;
     65    }
     66
     67    /**
     68     * This test area has 3 ways, where two have the same name and one has a
     69     * different name where there is a short disconnect between the two with
     70     * the same name.
     71     * @return The collection of ways to test.
     72     */
     73    private static Collection<OsmPrimitive> getTestArea1() {
     74        Node node1 = new Node(new LatLon(43.85619540309, 18.36535033094));
     75        Node node2 = new Node(new LatLon(43.85658651031, 18.36534961159));
     76        Node node3 = new Node(new LatLon(43.85662897034, 18.36534953349));
     77        Node node4 = new Node(new LatLon(43.85694640771, 18.36534894963));
     78        Node node5 = new Node(new LatLon(43.85658576291, 18.36456808743));
     79        Node node6 = new Node(new LatLon(43.8566296379, 18.36604757608));
     80
     81        Way way1 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     82                node1, node2);
     83        Way way2 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     84                node3, node4);
     85        Way way3 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     86                node5, node2, node3, node6);
     87
     88        return createCollection(way1, way2, way3);
     89    }
     90
     91    private static Collection<OsmPrimitive> getTestArea2() {
     92        Node node1 = new Node(new LatLon(43.85641709632, 18.36725849681));
     93        Node node2 = new Node(new LatLon(43.85680820208, 18.36725777746));
     94        Node node3 = new Node(new LatLon(43.85685066196, 18.36725769936));
     95        Node node4 = new Node(new LatLon(43.85716809815, 18.3672571155));
     96        Node node5 = new Node(new LatLon(43.85680745469, 18.3664762533));
     97        Node node6 = new Node(new LatLon(43.85685132951, 18.36795574195));
     98        Way way1 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     99                node1, node2);
     100        Way way2 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     101                node2, node3);
     102        Way way3 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     103                node3, node4);
     104        Way way4 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     105                node5, node2);
     106        Way way5 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     107                node3, node6);
     108
     109        return createCollection(way1, way2, way3, way4, way5);
     110    }
     111
     112    private static Collection<OsmPrimitive> getTestArea3() {
     113        Node node1 = new Node(new LatLon(43.85570051259, 18.36651114378));
     114        Node node2 = new Node(new LatLon(43.85613408344, 18.36651034633));
     115        Node node3 = new Node(new LatLon(43.85645152344, 18.36650976248));
     116        Node node4 = new Node(new LatLon(43.85609087565, 18.36572890027));
     117        Node node5 = new Node(new LatLon(43.85609162303, 18.3665104064));
     118        Node node6 = new Node(new LatLon(43.85613475101, 18.36720838893));
     119        Way way1 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     120                node1, node2, node3);
     121        Way way2 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     122                node4, node5, node2, node6);
     123
     124        return createCollection(way1, way2);
     125    }
     126
     127    /**
     128     * This is a section of road that should be found (almost overlapping)
     129     * @return A collection of ways and nodes to be tested
     130     */
     131    private static Collection<OsmPrimitive> getTestAreaRealWorld1() {
     132        // This was at https://www.openstreetmap.org/node/6123937677
     133        Node node1 = new Node(new LatLon(16.4151329, -95.0267841));
     134        Node node2 = new Node(new LatLon(16.4150313, -95.0267948));
     135        Node node3 = new Node(new LatLon(16.4149297, -95.0268057));
     136        Way way1 = TestUtils.newWay("highway=residential name=Calle Los Olivos",
     137                node1, node3);
     138        Way way2 = TestUtils.newWay(
     139                "highway=residential name=Calle Camino Carretero (La Amistad)",
     140                node1, node2, node3);
     141
     142        return createCollection(way1, way2);
     143    }
     144
     145    /**
     146     * This is a section of road that is almost overlapping
     147     * @return A collection of ways and nodes to be tested
     148     */
     149    private static Collection<OsmPrimitive> getTestAreaRealWorld2() {
     150        // this was at https://www.openstreetmap.org/way/435783782
     151        Node node1 = new Node(new LatLon(38.9881335, 31.1968857));
     152        Node node2 = new Node(new LatLon(38.9879932, 31.1968928));
     153        Node node3 = new Node(new LatLon(38.9877181, 31.1969176));
     154        Node node4 = new Node(new LatLon(38.9873522, 31.1969247));
     155        Node node5 = new Node(new LatLon(38.9870915, 31.1969116));
     156
     157        Way way1 = TestUtils.newWay("highway=track", node1, node2, node3, node4, node5);
     158        Way way2 = TestUtils.newWay("highway=track", node1, node5);
     159
     160        return createCollection(way1, way2);
     161    }
     162
     163    /**
     164     * This is a roundabout that happened to be a false positive for the
     165     * disconnected road test
     166     * @return A collection of ways and nodes to be tested
     167     */
     168    private static Collection<OsmPrimitive> getTestAreaRealWorld3() {
     169        // this was at https://www.openstreetmap.org/way/50635342
     170        Node node1 = new Node(new LatLon(38.3260143, 26.3085291));
     171
     172        // This probably should have been a junction=roundabout, but I don't know that for certain
     173        Way way1 = TestUtils.newWay("highway=secondary name=\"Atatürk Blv.\" oneway=yes ref=D300", node1);
     174        way1.addNode(new Node(new LatLon(38.3260353, 26.308504)));
     175        way1.addNode(new Node(new LatLon(38.3260507, 26.308473)));
     176        way1.addNode(new Node(new LatLon(38.3260596, 26.308438)));
     177        way1.addNode(new Node(new LatLon(38.3260612, 26.3083978)));
     178        way1.addNode(new Node(new LatLon(38.3260542, 26.3083586)));
     179        way1.addNode(new Node(new LatLon(38.326039, 26.3083233)));
     180        way1.addNode(new Node(new LatLon(38.3260169, 26.3082945)));
     181        way1.addNode(new Node(new LatLon(38.3259895, 26.3082745)));
     182        way1.addNode(new Node(new LatLon(38.3259562, 26.3082644)));
     183        way1.addNode(new Node(new LatLon(38.325922, 26.3082673)));
     184        way1.addNode(new Node(new LatLon(38.3258899, 26.3082829)));
     185        way1.addNode(new Node(new LatLon(38.3258629, 26.3083099)));
     186        way1.addNode(new Node(new LatLon(38.3258434, 26.3083458)));
     187        way1.addNode(new Node(new LatLon(38.325833, 26.3083874)));
     188        way1.addNode(new Node(new LatLon(38.3258326, 26.3084308)));
     189        way1.addNode(new Node(new LatLon(38.3258422, 26.3084723)));
     190        way1.addNode(new Node(new LatLon(38.3258609, 26.3085085)));
     191        way1.addNode(new Node(new LatLon(38.3258821, 26.3085321)));
     192        way1.addNode(new Node(new LatLon(38.3259071, 26.3085484)));
     193        way1.addNode(new Node(new LatLon(38.3259345, 26.3085564)));
     194        way1.addNode(new Node(new LatLon(38.3259626, 26.3085558)));
     195        way1.addNode(new Node(new LatLon(38.3259898, 26.3085465)));
     196        way1.addNode(node1);
     197
     198        Way way2 = TestUtils.newWay("highway=secondary name=\"Atatürk Blv.\" oneway=yes ref=D300", node1);
     199        way2.addNode(new Node(new LatLon(38.3259914, 26.3086145)));
     200        way2.addNode(new Node(new LatLon(38.3259765, 26.3087124)));
     201        way2.addNode(new Node(new LatLon(38.3259728, 26.3087982)));
     202
     203        return createCollection(way1, way2);
     204    }
     205
     206    private static Collection<OsmPrimitive> getTestAreaRealWorld4() {
     207        // This was at https://www.openstreetmap.org/way/584296023
     208        Node node1 = new Node(new LatLon(14.3272233, 120.9600503));
     209        Node node2 = new Node(new LatLon(14.326624, 120.9605805));
     210        Way way1 = TestUtils.newWay("highway=service oneway=yes name=\'Congressional Avenue Transit Lane\"", node1,
     211                new Node(new LatLon(14.3270856, 120.9601202)),
     212                new Node(new LatLon(14.326801, 120.9603738)), node2);
     213        Way way2 = TestUtils.newWay("highway=tertiary name=\"Congressional Avenue\" oneway=yes", node1, node2);
     214
     215        return createCollection(way1, way2);
     216    }
     217
     218    private static DataSet createDataSet(Collection<OsmPrimitive> primitives) {
     219        DataSet ds = new DataSet();
     220        for (Node node : Utils.filteredCollection(primitives, Node.class)) {
     221            if (ds.containsNode(node)) continue;
     222            ds.addPrimitive(node);
     223        }
     224        for (Way way : Utils.filteredCollection(primitives, Way.class)) {
     225            for (Node node : way.getNodes()) {
     226                if (ds.containsNode(node)) continue;
     227                ds.addPrimitive(node);
     228            }
     229            if (ds.containsWay(way)) continue;
     230            ds.addPrimitive(way);
     231        }
     232        return ds;
     233    }
     234
     235    /**
     236     * Unit test for {@link IntersectionIssues#checkNearbyEnds}
     237     * @throws Exception if any error occurs
     238     */
     239    @Test
     240    public void testCheckNearbyEnds() throws Exception {
     241        /** TODO add the following real world areas:
     242         * https://www.openstreetmap.org/way/261432285
     243         */
     244        DataSet area1 = createDataSet(getTestArea1());
     245        List<TestError> testResults = test(area1);
     246        Assert.assertEquals(1, testResults.size());
     247        Assert.assertEquals(IntersectionIssues.SHORT_DISCONNECT, testResults.get(0).getCode());
     248
     249        DataSet area2 = createDataSet(getTestArea2());
     250        testResults = test(area2);
     251        Assert.assertEquals(1, testResults.size());
     252        Assert.assertEquals(IntersectionIssues.SHORT_DISCONNECT, testResults.get(0).getCode());
     253
     254        area1.mergeFrom(area2);
     255        testResults = test(area1);
     256        Assert.assertEquals(2, testResults.size());
     257        for (TestError error : testResults) {
     258            Assert.assertEquals(IntersectionIssues.SHORT_DISCONNECT, error.getCode());
     259        }
     260
     261        testResults = test(createDataSet(getTestAreaRealWorld2()));
     262        Assert.assertEquals(1, testResults.size());
     263        Assert.assertNotEquals(IntersectionIssues.SHORT_DISCONNECT, testResults.get(0).getCode());
     264
     265        testResults = test(createDataSet(getTestAreaRealWorld3()));
     266        if (testResults.size() == 1)
     267            Assert.assertNotEquals(IntersectionIssues.SHORT_DISCONNECT, testResults.get(0).getCode());
     268        Assert.assertTrue(testResults.size() <= 1);
     269    }
     270
     271    /**
     272     * Unit test for {@link IntersectionIssues#checkNearbyNodes}
     273     * @throws Exception if any error occurs
     274     */
     275    @Test
     276    public void testCheckAlmostOverlappingWays() throws Exception {
     277        List<TestError> testResults = test(createDataSet(getTestArea1()));
     278        Assert.assertEquals(1, testResults.size());
     279        Assert.assertNotEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     280
     281        testResults = test(createDataSet(getTestArea3()));
     282        Assert.assertEquals(1, testResults.size());
     283        Assert.assertEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     284
     285        testResults = test(createDataSet(getTestAreaRealWorld1()));
     286        Assert.assertEquals(1, testResults.size());
     287        Assert.assertEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     288
     289        DataSet area4 = createDataSet(getTestAreaRealWorld2());
     290        testResults = test(area4);
     291        Assert.assertEquals(1, testResults.size());
     292        Assert.assertEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     293
     294        testResults = test(createDataSet(getTestAreaRealWorld4()));
     295        Assert.assertEquals(0, testResults.size());
     296    }
     297}