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