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", "alternate")) { 049 errors.add(TestError.builder(this, Severity.ERROR, 3601) 050 .message(tr("Route relation contains a ''{0}'' role", "forward/backward/alternate")) 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}