001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.HashSet;
009import java.util.List;
010import java.util.Set;
011
012import org.openstreetmap.josm.data.osm.Node;
013import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
014import org.openstreetmap.josm.data.osm.Relation;
015import org.openstreetmap.josm.data.osm.RelationMember;
016import org.openstreetmap.josm.data.validation.Severity;
017import org.openstreetmap.josm.data.validation.Test;
018import org.openstreetmap.josm.data.validation.TestError;
019import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionType;
020import org.openstreetmap.josm.gui.dialogs.relation.sort.WayConnectionTypeCalculator;
021
022/**
023 * Tests for <a href="https://wiki.openstreetmap.org/wiki/Proposed_features/Public_Transport">public transport routes</a>.
024 */
025public class PublicTransportRouteTest extends Test {
026
027    private final WayConnectionTypeCalculator connectionTypeCalculator = new WayConnectionTypeCalculator();
028
029    /**
030     * Constructs a new {@code PublicTransportRouteTest}.
031     */
032    public PublicTransportRouteTest() {
033        super(tr("Public Transport Route"));
034    }
035
036    @Override
037    public void visit(Relation r) {
038        final boolean skip = r.hasIncompleteMembers()
039                || !r.hasTag("type", "route")
040                || !r.hasKey("route")
041                || !r.hasTag("public_transport:version", "2");
042        if (skip) {
043            return;
044        }
045
046        final List<RelationMember> membersToCheck = new ArrayList<>();
047        final Set<Node> routeNodes = new HashSet<>();
048        for (RelationMember member : r.getMembers()) {
049            if (member.hasRole("forward", "backward")) {
050                errors.add(new TestError(this, Severity.WARNING, tr("Route relation contains a ''{0}'' role", "forward/backward"), 3601, r));
051                return;
052            } else if (member.hasRole("") && OsmPrimitiveType.WAY.equals(member.getType())) {
053                membersToCheck.add(member);
054                routeNodes.addAll(member.getWay().getNodes());
055            }
056        }
057        if (membersToCheck.isEmpty()) {
058            return;
059        }
060
061        final List<WayConnectionType> links = connectionTypeCalculator.updateLinks(membersToCheck);
062        for (int i = 0; i < links.size(); i++) {
063            final WayConnectionType link = links.get(i);
064            final boolean hasError = !(i == 0 || link.linkPrev)
065                    || !(i == links.size() - 1 || link.linkNext)
066                    || link.direction == null
067                    || WayConnectionType.Direction.NONE.equals(link.direction);
068            if (hasError) {
069                errors.add(new TestError(this, Severity.WARNING, tr("Route relation contains a gap"), 3602, r));
070                return;
071            }
072        }
073
074        for (RelationMember member : r.getMembers()) {
075            if (member.hasRole("stop", "stop_exit_only", "stop_entry_only")
076                    && OsmPrimitiveType.NODE.equals(member.getType())
077                    && !routeNodes.contains(member.getNode())) {
078                errors.add(new TestError(this, Severity.WARNING,
079                        tr("Stop position not part of route"), 3603, Arrays.asList(member.getMember(), r)));
080            }
081        }
082    }
083}