Ticket #17528: intersectionissues_v8.patch

File intersectionissues_v8.patch, 30.0 KB (added by taylor.smock, 5 years ago)

Add tests along with some modifications to catch some edge cases -- dependent upon #17616, but can be switched back to using GPXDistance methods if necessary.

  • 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.Way;
     17import org.openstreetmap.josm.data.osm.WaySegment;
     18import org.openstreetmap.josm.data.validation.Severity;
     19import org.openstreetmap.josm.data.validation.Test;
     20import org.openstreetmap.josm.data.validation.TestError;
     21import org.openstreetmap.josm.gui.progress.ProgressMonitor;
     22import org.openstreetmap.josm.tools.Geometry;
     23import org.openstreetmap.josm.tools.Logging;
     24
     25/**
     26 * Finds issues with highway intersections
     27 * @author Taylor Smock
     28 * @since xxx
     29 */
     30public class IntersectionIssues extends Test {
     31    private static final int INTERSECTIONISSUESCODE = 3800;
     32    /** The code for an intersection which briefly interrupts a road */
     33    public static final int SHORT_DISCONNECT = INTERSECTIONISSUESCODE + 0;
     34    /** The code for a node that is almost on a way */
     35    public static final int NEARBY_NODE = INTERSECTIONISSUESCODE + 1;
     36    /** The distance to consider for nearby nodes/short disconnects */
     37    public static final double MAX_DISTANCE = 5.0; // meters
     38    /** The distance to consider for nearby nodes with tags */
     39    public static final double MAX_DISTANCE_NODE_INFORMATION = MAX_DISTANCE / 5.0; // meters
     40    /** The maximum angle for almost overlapping ways */
     41    public static final double MAX_ANGLE = 15.0; // degrees
     42    /** The maximum length to consider for almost overlapping ways */
     43    public static final double MAX_LENGTH = 5.0; // meters
     44
     45    private HashMap<String, ArrayList<Way>> ways;
     46    ArrayList<Way> allWays;
     47
     48    /**
     49     * Construct a new {@code IntersectionIssues} object
     50     */
     51    public IntersectionIssues() {
     52        super(tr("Intersection Issues"), tr("Check for issues at intersections"), OverlappingWays.class);
     53    }
     54
     55    @Override
     56    public void startTest(ProgressMonitor monitor) {
     57        super.startTest(monitor);
     58        ways = new HashMap<>();
     59        allWays = new ArrayList<>();
     60    }
     61
     62    @Override
     63    public void endTest() {
     64        Way pWay = null;
     65        try {
     66            for (ArrayList<Way> comparison : ways.values()) {
     67                pWay = comparison.get(0);
     68                checkNearbyEnds(comparison);
     69            }
     70            for (Way way : allWays) {
     71                pWay = way;
     72                for (Way way2 : allWays) {
     73                    if (way2.equals(way)) continue;
     74                    pWay = way2;
     75                    if (way.getBBox().intersects(way2.getBBox())) {
     76                        checkNearbyNodes(way, way2);
     77                    }
     78                }
     79            }
     80        } catch (Exception e) {
     81            if (pWay != null) {
     82                Logging.debug("Way https://osm.org/way/{0} caused an error", pWay.getOsmId());
     83            }
     84            Logging.warn(e);
     85        }
     86        ways = null;
     87        allWays = null;
     88        super.endTest();
     89    }
     90
     91    @Override
     92    public void visit(Way way) {
     93        if (!way.isUsable()) return;
     94        String HIGHWAY = "highway";
     95        if (way.hasKey(HIGHWAY) && !way.get(HIGHWAY).contains("_link") &&
     96                !way.get(HIGHWAY).contains("proposed")) {
     97            String[] identityTags = new String[] {"name", "ref"};
     98            for (String tag : identityTags) {
     99                if (way.hasKey(tag)) {
     100                    ArrayList<Way> similar = ways.get(way.get(tag)) == null ? new ArrayList<>() : ways.get(way.get(tag));
     101                    if (!similar.contains(way)) similar.add(way);
     102                    ways.put(way.get(tag), similar);
     103                }
     104            }
     105            if (!allWays.contains(way)) allWays.add(way);
     106        }
     107    }
     108
     109    /**
     110     * Check for ends that are nearby but not directly connected
     111     * @param comparison Ways to look at
     112     */
     113    public void checkNearbyEnds(List<Way> comparison) {
     114        ArrayList<Way> errored = new ArrayList<>();
     115        for (Way one : comparison) {
     116            LatLon oneLast = one.lastNode().getCoor();
     117            LatLon oneFirst = one.firstNode().getCoor();
     118            for (Way two : comparison) {
     119                if (one.equals(two)) continue;
     120                if (one.isFirstLastNode(two.firstNode()) || one.isFirstLastNode(two.lastNode()) ||
     121                        (errored.contains(one) && errored.contains(two))) continue;
     122                LatLon twoLast = two.lastNode().getCoor();
     123                LatLon twoFirst = two.firstNode().getCoor();
     124                int nearCase = getNearCase(oneFirst, oneLast, twoFirst, twoLast);
     125                if (nearCase != 0) {
     126                    for (Way way : two.lastNode().getParentWays()) {
     127                        if (way.equals(two)) continue;
     128                        if (one.hasKey("name") && way.hasKey("name") && way.get("name").equals(one.get("name")) ||
     129                                one.hasKey("ref") && way.hasKey("ref") && way.get("ref").equals(one.get("ref"))) {
     130                            return;
     131                        }
     132                    }
     133                    for (Way way : two.firstNode().getParentWays()) {
     134                        if (way.equals(two)) continue;
     135                        if (one.hasKey("name") && way.hasKey("name") && way.get("name").equals(one.get("name")) ||
     136                                one.hasKey("ref") && way.hasKey("ref") && way.get("ref").equals(one.get("ref"))) {
     137                            return;
     138                        }
     139                    }
     140                }
     141                if (nearCase > 0) {
     142                    List<Way> nearby = new ArrayList<>();
     143                    nearby.add(one);
     144                    nearby.add(two);
     145                    List<WaySegment> segments = new ArrayList<>();
     146                    if ((nearCase & 1) != 0) {
     147                        segments.add(new WaySegment(two, two.getNodesCount() - 2));
     148                        segments.add(new WaySegment(one, one.getNodesCount() - 2));
     149                    }
     150                    if ((nearCase & 2) != 0) {
     151                        segments.add(new WaySegment(two, two.getNodesCount() - 2));
     152                        segments.add(new WaySegment(one, 0));
     153                    }
     154                    if ((nearCase & 4) != 0) {
     155                        segments.add(new WaySegment(two, 0));
     156                        segments.add(new WaySegment(one, one.getNodesCount() - 2));
     157                    }
     158                    if ((nearCase & 8) != 0) {
     159                        segments.add(new WaySegment(two, 0));
     160                        segments.add(new WaySegment(one, 0));
     161                    }
     162                    errored.addAll(nearby);
     163                    allWays.removeAll(errored);
     164                    TestError.Builder testError = TestError.builder(this, Severity.WARNING, SHORT_DISCONNECT)
     165                            .primitives(nearby)
     166                            .highlightWaySegments(segments)
     167                            .message(tr("Disconnected road"));
     168                    errors.add(testError.build());
     169                }
     170            }
     171        }
     172    }
     173
     174    /**
     175     * Get nearby cases
     176     * @param oneFirst The {@code LatLon} of the the first node of the first way
     177     * @param oneLast The {@code LatLon} of the the last node of the first way
     178     * @param twoFirst The {@code LatLon} of the the first node of the second way
     179     * @param twoLast The {@code LatLon} of the the last node of the second way
     180     * @return A bitwise int (8421 -> twoFirst/oneFirst, twoFirst/oneLast, twoLast/oneFirst, twoLast/oneLast)
     181     *
     182     */
     183    private int getNearCase(LatLon oneFirst, LatLon oneLast, LatLon twoFirst, LatLon twoLast) {
     184        int returnInt = 0;
     185        if (twoLast.greatCircleDistance(oneLast) <= MAX_DISTANCE) {
     186            returnInt = returnInt | 1;
     187        }
     188        if (twoLast.greatCircleDistance(oneFirst) <= MAX_DISTANCE) {
     189            returnInt = returnInt | 2;
     190        }
     191        if (twoFirst.greatCircleDistance(oneLast) <= MAX_DISTANCE) {
     192            returnInt = returnInt | 4;
     193        }
     194        if (twoFirst.greatCircleDistance(oneFirst) <= MAX_DISTANCE) {
     195            returnInt = returnInt | 8;
     196        }
     197        return returnInt;
     198    }
     199
     200    /**
     201     * Check nearby nodes to an intersection of two ways
     202     * @param way1 A way to check an almost intersection with
     203     * @param way2 A way to check an almost intersection with
     204     */
     205    public void checkNearbyNodes(Way way1, Way way2) {
     206        Collection<Node> intersectingNodes = getIntersectingNode(way1, way2);
     207        if (intersectingNodes.isEmpty() ||
     208                (way1.isOneway() != 0 && way2.isOneway() != 0 &&
     209                (way1.hasKey("name") && way1.get("name").equals(way2.get("name")) ||
     210                 way1.hasKey("ref") && way1.get("ref").equals(way2.get("ref"))))) return;
     211        for (Node intersectingNode : intersectingNodes) {
     212            checkNearbyNodes(way1, way2, intersectingNode);
     213            checkNearbyNodes(way2, way1, intersectingNode);
     214        }
     215    }
     216
     217    private void checkNearbyNodes(Way way1, Way way2, Node nearby) {
     218        for (Node node : way1.getNeighbours(nearby)) {
     219            if (node.equals(nearby) || way2.containsNode(node)) continue;
     220            double distance = Geometry.getDistance(way2, node);
     221            double angle = getSmallestAngle(way2, nearby, node);
     222            if (((distance < MAX_DISTANCE && !node.isTagged())
     223                    || (distance < MAX_DISTANCE_NODE_INFORMATION && node.isTagged()))
     224                    && angle < MAX_ANGLE) {
     225                List<Way> primitiveIssues = new ArrayList<>();
     226                primitiveIssues.add(way1);
     227                primitiveIssues.add(way2);
     228                List<TestError> tErrors = new ArrayList<>();
     229                if (previousErrors != null) tErrors.addAll(previousErrors);
     230                tErrors.addAll(getErrors());
     231                for (TestError error : tErrors) {
     232                    int code = error.getCode();
     233                    if ((code == SHORT_DISCONNECT || code == NEARBY_NODE
     234                            || code == OverlappingWays.OVERLAPPING_HIGHWAY
     235                            || code == OverlappingWays.DUPLICATE_WAY_SEGMENT
     236                            || code == OverlappingWays.OVERLAPPING_HIGHWAY_AREA
     237                            || code == OverlappingWays.OVERLAPPING_WAY
     238                            || code == OverlappingWays.OVERLAPPING_WAY_AREA
     239                            || code == OverlappingWays.OVERLAPPING_RAILWAY
     240                            || code == OverlappingWays.OVERLAPPING_RAILWAY_AREA)
     241                            && primitiveIssues.containsAll(error.getPrimitives())) {
     242                        return;
     243                    }
     244                }
     245                List<WaySegment> waysegmentsOne = new ArrayList<>();
     246                int index = way1.getNodes().indexOf(nearby);
     247                if (index >= way1.getNodesCount() - 1) index--;
     248                waysegmentsOne.add(new WaySegment(way1, index));
     249                if (index > 0) waysegmentsOne.add(new WaySegment(way1, index - 1));
     250                index = way2.getNodes().indexOf(nearby);
     251                List<WaySegment> waysegmentsTwo = new ArrayList<>();
     252                if (index >= way2.getNodesCount() - 1) index--;
     253                waysegmentsTwo.add(new WaySegment(way2, index));
     254                if (index > 0) waysegmentsTwo.add(new WaySegment(way2, index - 1));
     255                List<WaySegment> waysegments = new ArrayList<>();
     256                for (WaySegment twoSegment : waysegmentsTwo) {
     257                    if (angle == getSmallestAngle(twoSegment.toWay(), nearby, node)) {
     258                        waysegments.add(twoSegment);
     259                        break;
     260                    }
     261                }
     262                for (WaySegment oneSegment: waysegmentsOne) {
     263                    if (oneSegment.toWay().containsNode(node)) {
     264                        waysegments.add(oneSegment);
     265                        break;
     266                    }
     267                }
     268                TestError.Builder testError = TestError.builder(this, Severity.WARNING, NEARBY_NODE)
     269                        .primitives(primitiveIssues)
     270                        .highlightWaySegments(waysegments)
     271                        .message(tr("Sharp angle"));
     272                errors.add(testError.build());
     273            }
     274        }
     275    }
     276
     277    /**
     278     * Get the intersecting node of two ways
     279     * @param way1 A way that (hopefully) intersects with way2
     280     * @param way2 A way to find an intersection with
     281     * @return A collection of nodes where the ways intersect
     282     */
     283    public Collection<Node> getIntersectingNode(Way way1, Way way2) {
     284        HashSet<Node> nodes = new HashSet<>();
     285        for (Node node : way1.getNodes()) {
     286            if (way2.containsNode(node)) {
     287                nodes.add(node);
     288            }
     289        }
     290        return nodes;
     291    }
     292
     293    /**
     294     * Get the corner angle between nodes
     295     * @param way The way with additional nodes
     296     * @param intersection The node to get angles around
     297     * @param comparison The node to get angles from
     298     * @return The angle for comparison->intersection->(additional node) (normalized degrees)
     299     */
     300    public double getSmallestAngle(Way way, Node intersection, Node comparison) {
     301        Set<Node> neighbours = way.getNeighbours(intersection);
     302        double angle = Double.MAX_VALUE;
     303        EastNorth eastNorthIntersection = intersection.getEastNorth();
     304        EastNorth eastNorthComparison = comparison.getEastNorth();
     305        for (Node node : neighbours) {
     306            EastNorth eastNorthNode = node.getEastNorth();
     307            double tAngle = Geometry.getCornerAngle(eastNorthComparison, eastNorthIntersection, eastNorthNode);
     308            if (Math.abs(tAngle) < angle) angle = Math.abs(tAngle);
     309        }
     310        return Geometry.getNormalizedAngleInDegrees(angle);
     311    }
     312}
  • 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.Way;
     17import org.openstreetmap.josm.data.validation.TestError;
     18import org.openstreetmap.josm.testutils.JOSMTestRules;
     19import org.openstreetmap.josm.tools.Utils;
     20
     21import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
     22
     23/**
     24 * JUnit Test of "Long Segment" validation test.
     25 */
     26public class IntersectionIssuesTest {
     27
     28    /**
     29     * Setup test.
     30     */
     31    @Rule
     32    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
     33    public JOSMTestRules test = new JOSMTestRules().projection();
     34
     35    private static List<TestError> test(DataSet ds) throws Exception {
     36        IntersectionIssues test = new IntersectionIssues();
     37        test.initialize();
     38        test.startTest(null);
     39        for (Way w : ds.getWays()) {
     40            test.visit(w);
     41        }
     42        test.endTest();
     43        return test.getErrors();
     44    }
     45
     46    /**
     47     * This test area has 3 ways, where two have the same name and one has a
     48     * different name where there is a short disconnect between the two with
     49     * the same name.
     50     * @return The collection of ways to test.
     51     */
     52    private static Collection<OsmPrimitive> getTestArea1() {
     53        Node node1 = new Node(new LatLon(43.85619540309, 18.36535033094));
     54        Node node2 = new Node(new LatLon(43.85658651031, 18.36534961159));
     55        Node node3 = new Node(new LatLon(43.85662897034, 18.36534953349));
     56        Node node4 = new Node(new LatLon(43.85694640771, 18.36534894963));
     57        Node node5 = new Node(new LatLon(43.85658576291, 18.36456808743));
     58        Node node6 = new Node(new LatLon(43.8566296379, 18.36604757608));
     59
     60        Way way1 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     61                node1, node2);
     62        Way way2 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     63                node3, node4);
     64        Way way3 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     65                node5, node2, node3, node6);
     66
     67        HashSet<OsmPrimitive> collection = new HashSet<>();
     68        collection.addAll(way1.getNodes());
     69        collection.addAll(way2.getNodes());
     70        collection.addAll(way3.getNodes());
     71        collection.add(way1);
     72        collection.add(way2);
     73        collection.add(way3);
     74        return collection;
     75    }
     76
     77    private static Collection<OsmPrimitive> getTestArea2() {
     78        Node node1 = new Node(new LatLon(43.85641709632, 18.36725849681));
     79        Node node2 = new Node(new LatLon(43.85680820208, 18.36725777746));
     80        Node node3 = new Node(new LatLon(43.85685066196, 18.36725769936));
     81        Node node4 = new Node(new LatLon(43.85716809815, 18.3672571155));
     82        Node node5 = new Node(new LatLon(43.85680745469, 18.3664762533));
     83        Node node6 = new Node(new LatLon(43.85685132951, 18.36795574195));
     84        Way way1 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     85                node1, node2);
     86        Way way2 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     87                node2, node3);
     88        Way way3 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     89                node3, node4);
     90        Way way4 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     91                node5, node2);
     92        Way way5 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     93                node3, node6);
     94
     95        HashSet<OsmPrimitive> collection = new HashSet<>();
     96        collection.addAll(way1.getNodes());
     97        collection.addAll(way2.getNodes());
     98        collection.addAll(way3.getNodes());
     99        collection.addAll(way4.getNodes());
     100        collection.addAll(way5.getNodes());
     101        collection.add(way1);
     102        collection.add(way2);
     103        collection.add(way3);
     104        collection.add(way4);
     105        collection.add(way5);
     106        return collection;
     107    }
     108
     109    private static Collection<OsmPrimitive> getTestArea3() {
     110        Node node1 = new Node(new LatLon(43.85570051259, 18.36651114378));
     111        Node node2 = new Node(new LatLon(43.85613408344, 18.36651034633));
     112        Node node3 = new Node(new LatLon(43.85645152344, 18.36650976248));
     113        Node node4 = new Node(new LatLon(43.85609087565, 18.36572890027));
     114        Node node5 = new Node(new LatLon(43.85609162303, 18.3665104064));
     115        Node node6 = new Node(new LatLon(43.85613475101, 18.36720838893));
     116        Way way1 = TestUtils.newWay("highway=residential name=\"Test Road 1\"",
     117                node1, node2, node3);
     118        Way way2 = TestUtils.newWay("highway=residential name=\"Test Road 2\"",
     119                node4, node5, node2, node6);
     120
     121        Collection<OsmPrimitive> collection = new HashSet<>();
     122        collection.addAll(way1.getNodes());
     123        collection.addAll(way2.getNodes());
     124        collection.add(way1);
     125        collection.add(way2);
     126        return collection;
     127    }
     128
     129    private static Collection<OsmPrimitive> getTestAreaRealWorld1() {
     130        // This was at https://www.openstreetmap.org/node/6123937677
     131        Node node1 = new Node(new LatLon(16.4151329, -95.0267841));
     132        Node node2 = new Node(new LatLon(16.4150313, -95.0267948));
     133        Node node3 = new Node(new LatLon(16.4149297, -95.0268057));
     134        Way way1 = TestUtils.newWay("highway=residential name=Calle Los Olivos",
     135                node1, node3);
     136        Way way2 = TestUtils.newWay(
     137                "highway=residential name=Calle Camino Carretero (La Amistad)",
     138                node1, node2, node3);
     139
     140        Collection<OsmPrimitive> collection = new HashSet<>();
     141        collection.addAll(way1.getNodes());
     142        collection.addAll(way2.getNodes());
     143        collection.add(way1);
     144        collection.add(way2);
     145        return collection;
     146    }
     147
     148    private static DataSet createDataSet(Collection<OsmPrimitive> primitives) {
     149        DataSet ds = new DataSet();
     150        for (Node node : Utils.filteredCollection(primitives, Node.class)) {
     151            if (ds.containsNode(node)) continue;
     152            ds.addPrimitive(node);
     153        }
     154        for (Way way : Utils.filteredCollection(primitives, Way.class)) {
     155            for (Node node : way.getNodes()) {
     156                if (ds.containsNode(node)) continue;
     157                ds.addPrimitive(node);
     158            }
     159            if (ds.containsWay(way)) continue;
     160            ds.addPrimitive(way);
     161        }
     162        return ds;
     163    }
     164
     165    /**
     166     * Unit test for {@link IntersectionIssues#checkNearbyEnds}
     167     * @throws Exception if any error occurs
     168     */
     169    @Test
     170    public void testCheckNearbyEnds() throws Exception {
     171        DataSet area1 = createDataSet(getTestArea1());
     172        List<TestError> testResults = test(area1);
     173        Assert.assertEquals(1, testResults.size());
     174        Assert.assertEquals(IntersectionIssues.SHORT_DISCONNECT, testResults.get(0).getCode());
     175
     176        DataSet area2 = createDataSet(getTestArea2());
     177        testResults = test(area2);
     178        Assert.assertEquals(1, testResults.size());
     179        Assert.assertEquals(IntersectionIssues.SHORT_DISCONNECT, testResults.get(0).getCode());
     180
     181        area1.mergeFrom(area2);
     182        testResults = test(area1);
     183        Assert.assertEquals(2, testResults.size());
     184        for (TestError error : testResults) {
     185            Assert.assertEquals(IntersectionIssues.SHORT_DISCONNECT, error.getCode());
     186        }
     187    }
     188
     189    /**
     190     * Unit test for {@link IntersectionIssues#checkNearbyNodes}
     191     * @throws Exception if any error occurs
     192     */
     193    @Test
     194    public void testCheckAlmostOverlappingWays() throws Exception {
     195        Collection<OsmPrimitive> area1 = getTestArea1();
     196        List<TestError> testResults = test(createDataSet(area1));
     197        Assert.assertEquals(1, testResults.size());
     198        Assert.assertNotEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     199
     200        Collection<OsmPrimitive> area3 = getTestArea3();
     201        testResults = test(createDataSet(area3));
     202        Assert.assertEquals(1, testResults.size());
     203        Assert.assertEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     204
     205        Collection<OsmPrimitive> realWorldArea1 = getTestAreaRealWorld1();
     206        testResults = test(createDataSet(realWorldArea1));
     207        Assert.assertEquals(1, testResults.size());
     208        Assert.assertEquals(IntersectionIssues.NEARBY_NODE, testResults.get(0).getCode());
     209    }
     210}