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.Collections; 009import java.util.List; 010 011import org.openstreetmap.josm.data.osm.Node; 012import org.openstreetmap.josm.data.osm.OsmPrimitive; 013import org.openstreetmap.josm.data.osm.Relation; 014import org.openstreetmap.josm.data.osm.RelationMember; 015import org.openstreetmap.josm.data.osm.Way; 016import org.openstreetmap.josm.data.validation.Severity; 017import org.openstreetmap.josm.data.validation.Test; 018import org.openstreetmap.josm.data.validation.TestError; 019 020/** 021 * Checks if turnrestrictions are valid 022 * @since 3669 023 */ 024public class TurnrestrictionTest extends Test { 025 026 protected static final int NO_VIA = 1801; 027 protected static final int NO_FROM = 1802; 028 protected static final int NO_TO = 1803; 029 protected static final int MORE_VIA = 1804; 030 protected static final int MORE_FROM = 1805; 031 protected static final int MORE_TO = 1806; 032 protected static final int UNKNOWN_ROLE = 1807; 033 protected static final int UNKNOWN_TYPE = 1808; 034 protected static final int FROM_VIA_NODE = 1809; 035 protected static final int TO_VIA_NODE = 1810; 036 protected static final int FROM_VIA_WAY = 1811; 037 protected static final int TO_VIA_WAY = 1812; 038 protected static final int MIX_VIA = 1813; 039 protected static final int UNCONNECTED_VIA = 1814; 040 protected static final int SUPERFLUOUS = 1815; 041 protected static final int FROM_EQUALS_TO = 1816; 042 043 /** 044 * Constructs a new {@code TurnrestrictionTest}. 045 */ 046 public TurnrestrictionTest() { 047 super(tr("Turnrestrictions"), tr("This test checks if turnrestrictions are valid.")); 048 } 049 050 @Override 051 public void visit(Relation r) { 052 if (!"restriction".equals(r.get("type"))) 053 return; 054 055 Way fromWay = null; 056 Way toWay = null; 057 List<OsmPrimitive> via = new ArrayList<>(); 058 059 boolean morefrom = false; 060 boolean moreto = false; 061 boolean morevia = false; 062 boolean mixvia = false; 063 064 /* find the "from", "via" and "to" elements */ 065 for (RelationMember m : r.getMembers()) { 066 if (m.getMember().isIncomplete()) 067 return; 068 069 List<OsmPrimitive> l = new ArrayList<>(); 070 l.add(r); 071 l.add(m.getMember()); 072 if (m.isWay()) { 073 Way w = m.getWay(); 074 if (w.getNodesCount() < 2) { 075 continue; 076 } 077 078 switch (m.getRole()) { 079 case "from": 080 if (fromWay != null) { 081 morefrom = true; 082 } else { 083 fromWay = w; 084 } 085 break; 086 case "to": 087 if (toWay != null) { 088 moreto = true; 089 } else { 090 toWay = w; 091 } 092 break; 093 case "via": 094 if (!via.isEmpty() && via.get(0) instanceof Node) { 095 mixvia = true; 096 } else { 097 via.add(w); 098 } 099 break; 100 default: 101 errors.add(new TestError(this, Severity.WARNING, tr("Unknown role"), UNKNOWN_ROLE, 102 l, Collections.singletonList(m))); 103 } 104 } else if (m.isNode()) { 105 Node n = m.getNode(); 106 if ("via".equals(m.getRole())) { 107 if (!via.isEmpty()) { 108 if (via.get(0) instanceof Node) { 109 morevia = true; 110 } else { 111 mixvia = true; 112 } 113 } else { 114 via.add(n); 115 } 116 } else { 117 errors.add(new TestError(this, Severity.WARNING, tr("Unknown role"), UNKNOWN_ROLE, 118 l, Collections.singletonList(m))); 119 } 120 } else { 121 errors.add(new TestError(this, Severity.WARNING, tr("Unknown member type"), UNKNOWN_TYPE, 122 l, Collections.singletonList(m))); 123 } 124 } 125 if (morefrom) { 126 errors.add(new TestError(this, Severity.ERROR, tr("More than one \"from\" way found"), MORE_FROM, r)); 127 } 128 if (moreto) { 129 errors.add(new TestError(this, Severity.ERROR, tr("More than one \"to\" way found"), MORE_TO, r)); 130 } 131 if (morevia) { 132 errors.add(new TestError(this, Severity.ERROR, tr("More than one \"via\" node found"), MORE_VIA, r)); 133 } 134 if (mixvia) { 135 errors.add(new TestError(this, Severity.ERROR, tr("Cannot mix node and way for role \"via\""), MIX_VIA, r)); 136 } 137 138 if (fromWay == null) { 139 errors.add(new TestError(this, Severity.ERROR, tr("No \"from\" way found"), NO_FROM, r)); 140 return; 141 } 142 if (toWay == null) { 143 errors.add(new TestError(this, Severity.ERROR, tr("No \"to\" way found"), NO_TO, r)); 144 return; 145 } 146 if (fromWay.equals(toWay)) { 147 errors.add(new TestError(this, r.hasTag("restriction", "no_u_turn") ? Severity.OTHER : Severity.WARNING, 148 tr("\"from\" way equals \"to\" way"), FROM_EQUALS_TO, r)); 149 } 150 if (via.isEmpty()) { 151 errors.add(new TestError(this, Severity.ERROR, tr("No \"via\" node or way found"), NO_VIA, r)); 152 return; 153 } 154 155 if (via.get(0) instanceof Node) { 156 final Node viaNode = (Node) via.get(0); 157 final Way viaPseudoWay = new Way(); 158 viaPseudoWay.addNode(viaNode); 159 checkIfConnected(fromWay, viaPseudoWay, 160 tr("The \"from\" way does not start or end at a \"via\" node."), FROM_VIA_NODE); 161 if (toWay.isOneway() != 0 && viaNode.equals(toWay.lastNode(true))) { 162 errors.add(new TestError(this, Severity.WARNING, tr("Superfluous turnrestriction as \"to\" way is oneway"), SUPERFLUOUS, r)); 163 return; 164 } 165 checkIfConnected(viaPseudoWay, toWay, 166 tr("The \"to\" way does not start or end at a \"via\" node."), TO_VIA_NODE); 167 } else { 168 // check if consecutive ways are connected: from/via[0], via[i-1]/via[i], via[last]/to 169 checkIfConnected(fromWay, (Way) via.get(0), 170 tr("The \"from\" and the first \"via\" way are not connected."), FROM_VIA_WAY); 171 if (via.size() > 1) { 172 for (int i = 1; i < via.size(); i++) { 173 Way previous = (Way) via.get(i - 1); 174 Way current = (Way) via.get(i); 175 checkIfConnected(previous, current, 176 tr("The \"via\" ways are not connected."), UNCONNECTED_VIA); 177 } 178 } 179 if (toWay.isOneway() != 0 && ((Way) via.get(via.size() - 1)).isFirstLastNode(toWay.lastNode(true))) { 180 errors.add(new TestError(this, Severity.WARNING, tr("Superfluous turnrestriction as \"to\" way is oneway"), SUPERFLUOUS, r)); 181 return; 182 } 183 checkIfConnected((Way) via.get(via.size() - 1), toWay, 184 tr("The last \"via\" and the \"to\" way are not connected."), TO_VIA_WAY); 185 } 186 } 187 188 private static boolean isFullOneway(Way w) { 189 return w.isOneway() != 0 && !"no".equals(w.get("oneway:bicycle")); 190 } 191 192 private void checkIfConnected(Way previous, Way current, String msg, int code) { 193 boolean c; 194 if (isFullOneway(previous) && isFullOneway(current)) { 195 // both oneways: end/start node must be equal 196 c = previous.lastNode(true).equals(current.firstNode(true)); 197 } else if (isFullOneway(previous)) { 198 // previous way is oneway: end of previous must be start/end of current 199 c = current.isFirstLastNode(previous.lastNode(true)); 200 } else if (isFullOneway(current)) { 201 // current way is oneway: start of current must be start/end of previous 202 c = previous.isFirstLastNode(current.firstNode(true)); 203 } else { 204 // otherwise: start/end of previous must be start/end of current 205 c = current.isFirstLastNode(previous.firstNode()) || current.isFirstLastNode(previous.lastNode()); 206 } 207 if (!c) { 208 errors.add(new TestError(this, Severity.ERROR, msg, code, Arrays.asList(previous, current))); 209 } 210 } 211}