Ticket #16803: 16803-v2.patch

File 16803-v2.patch, 10.5 KB (added by GerdP, 5 years ago)
  • src/org/openstreetmap/josm/data/validation/tests/Highways.java

     
    88import java.util.Arrays;
    99import java.util.HashMap;
    1010import java.util.HashSet;
     11import java.util.Iterator;
    1112import java.util.List;
    1213import java.util.Locale;
    1314import java.util.Map;
    1415import java.util.Set;
    15 import java.util.stream.Collectors;
    1616
    1717import org.openstreetmap.josm.command.ChangePropertyCommand;
    1818import org.openstreetmap.josm.data.osm.Node;
     
    2222import org.openstreetmap.josm.data.validation.Severity;
    2323import org.openstreetmap.josm.data.validation.Test;
    2424import org.openstreetmap.josm.data.validation.TestError;
     25import org.openstreetmap.josm.tools.Geometry;
    2526import org.openstreetmap.josm.tools.Logging;
    2627import org.openstreetmap.josm.tools.Utils;
    2728
     
    4142
    4243    protected static final String SOURCE_MAXSPEED = "source:maxspeed";
    4344
     45    // CHECKSTYLE.OFF: SingleSpaceSeparator
     46    private static final Set<String> LINK_TO_HIGHWAYS = new HashSet<>(Arrays.asList(
     47            "motorway",  "motorway_link",
     48            "trunk",     "trunk_link",
     49            "primary",   "primary_link",
     50            "secondary", "secondary_link",
     51            "tertiary",  "tertiary_link"
     52            ));
     53
    4454    /**
    4555     * Classified highways in order of importance
    4656     */
    47     // CHECKSTYLE.OFF: SingleSpaceSeparator
    4857    private static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList(
    4958            "motorway",  "motorway_link",
    5059            "trunk",     "trunk_link",
     
    5665            "living_street");
    5766    // CHECKSTYLE.ON: SingleSpaceSeparator
    5867
     68
    5969    private static final Set<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = new HashSet<>(Arrays.asList(
    6070            "urban", "rural", "zone", "zone20", "zone:20", "zone30", "zone:30", "zone40",
    6171            "nsl_single", "nsl_dual", "motorway", "trunk", "living_street", "bicycle_road"));
     
    98108        if (w.isUsable()) {
    99109            if (w.isClosed() && w.hasTag(HIGHWAY, CLASSIFIED_HIGHWAYS) && w.hasTag("junction", "roundabout")
    100110                    && IN_DOWNLOADED_AREA_STRICT.test(w)) {
    101                 // TODO: find out how to handle splitted roundabouts (see #12841)
     111                // TODO: find out how to handle split roundabouts (see #12841)
    102112                testWrongRoundabout(w);
    103113            }
    104114            if (w.hasKey(SOURCE_MAXSPEED)) {
     
    164174     */
    165175    public static boolean isHighwayLinkOkay(final Way way) {
    166176        final String highway = way.get(HIGHWAY);
    167         if (highway == null || !highway.endsWith("_link")
    168                 || !IN_DOWNLOADED_AREA.test(way.getNode(0)) || !IN_DOWNLOADED_AREA.test(way.getNode(way.getNodesCount()-1))) {
     177        if (highway == null || !highway.endsWith("_link")) {
    169178            return true;
    170179        }
    171180
    172         final Set<OsmPrimitive> referrers = new HashSet<>();
     181        // check if connected to a high class road where the link must match the higher class
     182        String highClass = null;
     183        for (int i = 0; i < way.getNodesCount(); i++) {
     184            Node n = way.getNode(i);
     185            if (!IN_DOWNLOADED_AREA.test(n))
     186                return true;
     187            Set<Way> otherWays = new HashSet<>();
     188            otherWays.addAll(Utils.filteredCollection(n.getReferrers(), Way.class));
     189            if (otherWays.size() == 1)
     190                continue;
     191            Iterator<Way> iter = otherWays.iterator();
     192            while (iter.hasNext()) {
     193                Way w = iter.next();
     194                final String hw2 = w.get(HIGHWAY);
     195                if (way == w || w.getNodesCount() < 2 || !w.isUsable() || hw2 == null)
     196                    iter.remove();
     197                else {
     198                    if ("motorway".equals(hw2)) {
     199                        highClass = "motorway";
     200                        break;
     201                    } else if ("trunk".equals(hw2))
     202                        highClass = "trunk";
     203                }
     204            }
     205        }
    173206
    174         if (way.isClosed()) {
    175             // for closed way we need to check all adjacent ways
    176             for (Node n: way.getNodes()) {
    177                 referrers.addAll(n.getReferrers());
     207        if (highClass != null && !highway.equals(highClass + "_link")) {
     208            return false;
     209        }
     210
     211        for (int i = 0; i < way.getNodesCount(); i++) {
     212            Node n = way.getNode(i);
     213            Set<Way> otherWays = new HashSet<>();
     214            otherWays.addAll(Utils.filteredCollection(n.getReferrers(), Way.class));
     215            if (otherWays.size() == 1)
     216                continue;
     217            otherWays.removeIf(w -> w == way || !highway.startsWith(w.get(HIGHWAY)) || !LINK_TO_HIGHWAYS.contains(w.get(HIGHWAY)));
     218            if (otherWays.isEmpty())
     219                continue;
     220
     221            //TODO: ignore ways which are not allowed because of turn restrictions, oneway attributes or access rules?
     222            HashSet<Way> sameTag = new HashSet<>();
     223            for (Way ow : otherWays) {
     224                if (highway.equals(ow.get(HIGHWAY)))
     225                    sameTag.add(ow);
     226                else
     227                    return true;
    178228            }
     229            // we have way(s) with the same _link tag, ignore those with a sharp angle
     230            final int pos = i;
     231            sameTag.removeIf(w -> isSharpAngle(way, pos, w));
     232            if (!sameTag.isEmpty())
     233                return true;
     234        }
     235        return false;
     236
     237    }
     238
     239    /**
     240     * Check if the two given connected ways form a sharp angle.
     241     * @param way 1st way
     242     * @param nodePos node position of connecting node in 1st way
     243     * @param otherWay the 2nd way
     244     * @return true if angle is sharp or way cannot be travelled because of oneway attributes
     245     */
     246    private static boolean isSharpAngle(Way way, int nodePos, Way otherWay) {
     247        Node n = way.getNode(nodePos);
     248        int oneway = way.isOneway();
     249        if (oneway == 0) {
     250            if ("roundabout".equals(way.get("junction"))) {
     251                oneway = 1;
     252            }
     253        }
     254
     255        if (oneway != 1) {
     256            Node prev = getPrevNode(way, nodePos);
     257            if (prev != null && !onlySharpAngle(n, prev, otherWay))
     258                return false;
     259        }
     260        if (oneway != -1) {
     261            Node next = getNextNode(way, nodePos);
     262            if (next != null && !onlySharpAngle(n, next, otherWay))
     263                return false;
     264        }
     265        return true;
     266    }
     267
     268    private static Node getNextNode(Way way, int nodePos) {
     269        if (nodePos + 1 > way.getNodesCount()) {
     270            if (way.isClosed())
     271                return way.getNode(1);
     272            return null;
    179273        } else {
    180             referrers.addAll(way.firstNode().getReferrers());
    181             referrers.addAll(way.lastNode().getReferrers());
     274            return way.getNode(nodePos + 1);
    182275        }
     276    }
    183277
    184         // Find ways of same class (exact class of class_link)
    185         List<Way> sameClass = Utils.filteredCollection(referrers, Way.class).stream().filter(
    186                 otherWay -> !way.equals(otherWay) && otherWay.hasTag(HIGHWAY, highway, highway.replaceAll("_link$", "")))
    187                 .collect(Collectors.toList());
    188         if (sameClass.size() > 1) {
    189             // It is possible to have a class_link between 2 segments of same class
    190             // in roundabout designs that physically separate a specific turn from the main roundabout
    191             // But if we have more than a single adjacent class, and one of them is a roundabout, that's an error
    192             for (Way w : sameClass) {
    193                 if (w.hasTag("junction", "roundabout")) {
    194                     return false;
     278    private static Node getPrevNode(Way way, int nodePos) {
     279        if (nodePos == 0) {
     280            if (way.isClosed())
     281                return way.getNode(way.getNodesCount()-2);
     282            return null;
     283        } else {
     284            return way.getNode(nodePos - 1);
     285        }
     286    }
     287
     288    private static boolean onlySharpAngle(Node common, Node from, Way toWay) {
     289        int oneway = toWay.isOneway();
     290        if (oneway == 0) {
     291            if ("roundabout".equals(toWay.get("junction"))) {
     292                oneway = 1;
     293            }
     294        }
     295
     296        for (int i = 0; i < toWay.getNodesCount(); i++) {
     297            if (common == toWay.getNode(i)) {
     298
     299                if (oneway != 1) {
     300                    Node to = getNextNode(toWay, i);
     301                    if (to != null && !isSharpAngle(from, common, to))
     302                        return false;
    195303                }
     304                if (oneway != -1) {
     305                    Node to = getPrevNode(toWay, i);
     306                    if (to != null && !isSharpAngle(from, common, to))
     307                        return false;
     308                }
     309                break;
    196310            }
    197311        }
    198         // Link roads should always at least one adjacent segment of same class
    199         return !sameClass.isEmpty();
     312        return true;
    200313    }
    201314
     315    /**
     316     * Returns true if angle of a corner defined with 3 point coordinates is < 60.
     317     *
     318     * @param n1 first node
     319     * @param n2 Common node
     320     * @param n3 third node
     321     * @return true if angle is below 60.
     322     */
     323
     324    private static boolean isSharpAngle(Node n1, Node n2, Node n3) {
     325        double angle = Geometry.getNormalizedAngleInDegrees(
     326                Geometry.getCornerAngle(n1.getEastNorth(), n2.getEastNorth(), n3.getEastNorth()));
     327        return angle < 60;
     328    }
     329
    202330    private void testHighwayLink(final Way way) {
    203331        if (!isHighwayLinkOkay(way)) {
    204332            errors.add(TestError.builder(this, Severity.WARNING, SOURCE_WRONG_LINK)
  • test/unit/org/openstreetmap/josm/data/validation/tests/HighwaysTest.java

     
    116116            if (!test.getErrors().isEmpty()) {
    117117                fail(test.getErrors().get(0).getMessage());
    118118            }
    119             Way w1 = ways.stream().filter(w -> 28508494 == w.getId()).findFirst().get();
    120             Way w2 = ways.stream().filter(w -> 28508493 == w.getId()).findFirst().get();
    121             test.visit(w1);
    122             test.visit(w2);
    123             assertEquals(2, test.getErrors().size());
    124119        }
    125120    }
    126121}