001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.data.validation.tests.CrossingWays.HIGHWAY;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.util.List;
008
009import org.openstreetmap.josm.data.osm.Node;
010import org.openstreetmap.josm.data.osm.OsmPrimitive;
011import org.openstreetmap.josm.data.osm.Relation;
012import org.openstreetmap.josm.data.osm.Way;
013import org.openstreetmap.josm.data.validation.Severity;
014import org.openstreetmap.josm.data.validation.Test;
015import org.openstreetmap.josm.data.validation.TestError;
016import org.openstreetmap.josm.gui.mappaint.ElemStyles;
017
018/**
019 * Checks for ways connected to areas.
020 * @since 4682
021 */
022public class WayConnectedToArea extends Test {
023
024    /**
025     * Constructs a new {@code WayConnectedToArea} test.
026     */
027    public WayConnectedToArea() {
028        super(tr("Way connected to Area"), tr("Checks for ways connected to areas."));
029    }
030
031    @Override
032    public void visit(Way w) {
033        if (!w.isUsable() || w.isClosed() || !w.hasKey(HIGHWAY)) {
034            return;
035        }
036
037        List<OsmPrimitive> r = w.firstNode().getReferrers();
038        boolean hasway = r.stream().anyMatch(p -> p != w && p.hasKey(HIGHWAY));
039        if (!hasway) {
040            for (OsmPrimitive p : r) {
041                testForError(w, w.firstNode(), p);
042            }
043        }
044        r = w.lastNode().getReferrers();
045        hasway = r.stream().anyMatch(p -> p != w && p.hasKey(HIGHWAY));
046        if (!hasway) {
047            for (OsmPrimitive p : r) {
048                testForError(w, w.lastNode(), p);
049            }
050        }
051    }
052
053    private void testForError(Way w, Node wayNode, OsmPrimitive p) {
054        if (wayNode.isOutsideDownloadArea()
055                || wayNode.getReferrers().stream().anyMatch(p1 -> p1.hasTag("route", "ferry"))) {
056            return;
057        } else if (isArea(p)) {
058            addPossibleError(w, wayNode, p, p);
059        } else {
060            p.referrers(Relation.class)
061                    .filter(r -> r.isMultipolygon() && isArea(r))
062                    .findFirst()
063                    .ifPresent(r -> addPossibleError(w, wayNode, p, r));
064        }
065    }
066
067    private static boolean isArea(OsmPrimitive p) {
068        return p.hasKey("landuse", "natural") && ElemStyles.hasAreaElemStyle(p, false);
069    }
070
071    private void addPossibleError(Way w, Node wayNode, OsmPrimitive p, OsmPrimitive area) {
072        // Avoid "legal" cases (see #10655)
073        if (w.hasKey(HIGHWAY) && wayNode.hasTag("leisure", "slipway") && area.hasTag("natural", "water")) {
074            return;
075        }
076        if (wayNode.hasTag("noexit", "yes")) {
077            // Avoid "legal" case (see #17036)
078            return;
079        }
080        errors.add(TestError.builder(this, Severity.WARNING, 2301)
081                .message(tr("Way terminates on Area"))
082                .primitives(w, p)
083                .highlight(wayNode)
084                .build());
085    }
086}