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.List; 009 010import org.openstreetmap.josm.data.osm.Node; 011import org.openstreetmap.josm.data.osm.OsmPrimitive; 012import org.openstreetmap.josm.data.osm.Relation; 013import org.openstreetmap.josm.data.osm.RelationMember; 014import org.openstreetmap.josm.data.osm.Way; 015import org.openstreetmap.josm.data.validation.Severity; 016import org.openstreetmap.josm.data.validation.Test; 017import org.openstreetmap.josm.data.validation.TestError; 018 019/** 020 * Checks if turn restrictions are valid 021 * @since 3669 022 */ 023public class TurnrestrictionTest extends Test { 024 025 protected static final int NO_VIA = 1801; 026 protected static final int NO_FROM = 1802; 027 protected static final int NO_TO = 1803; 028 protected static final int MORE_VIA = 1804; 029 protected static final int MORE_FROM = 1805; 030 protected static final int MORE_TO = 1806; 031 protected static final int UNEXPECTED_ROLE = 1807; 032 protected static final int UNEXPECTED_TYPE = 1808; 033 protected static final int FROM_VIA_NODE = 1809; 034 protected static final int TO_VIA_NODE = 1810; 035 protected static final int FROM_VIA_WAY = 1811; 036 protected static final int TO_VIA_WAY = 1812; 037 protected static final int MIX_VIA = 1813; 038 protected static final int UNCONNECTED_VIA = 1814; 039 protected static final int SUPERFLUOUS = 1815; 040 protected static final int FROM_EQUALS_TO = 1816; 041 protected static final int UNKNOWN_RESTRICTION = 1817; 042 protected static final int TO_CLOSED_WAY = 1818; 043 protected static final int FROM_CLOSED_WAY = 1819; 044 045 private static final List<String> SUPPORTED_RESTRICTIONS = Arrays.asList( 046 "no_right_turn", "no_left_turn", "no_u_turn", "no_straight_on", 047 "only_right_turn", "only_left_turn", "only_straight_on", 048 "no_entry", "no_exit" 049 ); 050 051 /** 052 * Constructs a new {@code TurnrestrictionTest}. 053 */ 054 public TurnrestrictionTest() { 055 super(tr("Turn restrictions"), tr("This test checks if turn restrictions are valid.")); 056 } 057 058 @Override 059 public void visit(Relation r) { 060 if (!r.hasTag("type", "restriction")) 061 return; 062 063 if (!r.hasTag("restriction", SUPPORTED_RESTRICTIONS)) { 064 errors.add(TestError.builder(this, Severity.ERROR, UNKNOWN_RESTRICTION) 065 .message(tr("Unknown turn restriction")) 066 .primitives(r) 067 .build()); 068 return; 069 } 070 071 Way fromWay = null; 072 Way toWay = null; 073 List<OsmPrimitive> via = new ArrayList<>(); 074 075 boolean morefrom = false; 076 boolean moreto = false; 077 boolean morevia = false; 078 boolean mixvia = false; 079 080 /* find the "from", "via" and "to" elements */ 081 for (RelationMember m : r.getMembers()) { 082 if (m.getMember().isIncomplete()) 083 return; 084 085 List<OsmPrimitive> l = new ArrayList<>(); 086 l.add(r); 087 l.add(m.getMember()); 088 if (m.isWay()) { 089 Way w = m.getWay(); 090 if (w.getNodesCount() < 2) { 091 continue; 092 } 093 094 switch (m.getRole()) { 095 case "from": 096 if (fromWay != null) { 097 morefrom = true; 098 } else { 099 fromWay = w; 100 } 101 break; 102 case "to": 103 if (toWay != null) { 104 moreto = true; 105 } else { 106 toWay = w; 107 } 108 break; 109 case "via": 110 if (!via.isEmpty() && via.get(0) instanceof Node) { 111 mixvia = true; 112 } else { 113 via.add(w); 114 } 115 break; 116 default: 117 errors.add(TestError.builder(this, Severity.WARNING, UNEXPECTED_ROLE) 118 .message(tr("Unexpected role ''{0}'' in turn restriction", m.getRole())) 119 .primitives(l) 120 .highlight(m.getMember()) 121 .build()); 122 } 123 } else if (m.isNode()) { 124 Node n = m.getNode(); 125 if ("via".equals(m.getRole())) { 126 if (!via.isEmpty()) { 127 if (via.get(0) instanceof Node) { 128 morevia = true; 129 } else { 130 mixvia = true; 131 } 132 } else { 133 via.add(n); 134 } 135 } else { 136 errors.add(TestError.builder(this, Severity.WARNING, UNEXPECTED_ROLE) 137 .message(tr("Unexpected role ''{0}'' in turn restriction", m.getRole())) 138 .primitives(l) 139 .highlight(m.getMember()) 140 .build()); 141 } 142 } else { 143 errors.add(TestError.builder(this, Severity.WARNING, UNEXPECTED_TYPE) 144 .message(tr("Unexpected member type in turn restriction")) 145 .primitives(l) 146 .highlight(m.getMember()) 147 .build()); 148 } 149 } 150 if (morefrom) { 151 errors.add(TestError.builder(this, Severity.ERROR, MORE_FROM) 152 .message(tr("More than one \"from\" way found")) 153 .primitives(r) 154 .build()); 155 return; 156 } 157 if (moreto) { 158 errors.add(TestError.builder(this, Severity.ERROR, MORE_TO) 159 .message(tr("More than one \"to\" way found")) 160 .primitives(r) 161 .build()); 162 return; 163 } 164 if (morevia) { 165 errors.add(TestError.builder(this, Severity.ERROR, MORE_VIA) 166 .message(tr("More than one \"via\" node found")) 167 .primitives(r) 168 .build()); 169 return; 170 } 171 if (mixvia) { 172 errors.add(TestError.builder(this, Severity.ERROR, MIX_VIA) 173 .message(tr("Cannot mix node and way for role \"via\"")) 174 .primitives(r) 175 .build()); 176 return; 177 } 178 179 if (fromWay == null) { 180 errors.add(TestError.builder(this, Severity.ERROR, NO_FROM) 181 .message(tr("No \"from\" way found")) 182 .primitives(r) 183 .build()); 184 return; 185 } else if (fromWay.isClosed()) { 186 errors.add(TestError.builder(this, Severity.ERROR, FROM_CLOSED_WAY) 187 .message(tr("\"from\" way is a closed way")) 188 .primitives(r) 189 .highlight(fromWay) 190 .build()); 191 return; 192 } 193 194 if (toWay == null) { 195 errors.add(TestError.builder(this, Severity.ERROR, NO_TO) 196 .message(tr("No \"to\" way found")) 197 .primitives(r) 198 .build()); 199 return; 200 } else if (toWay.isClosed()) { 201 errors.add(TestError.builder(this, Severity.ERROR, TO_CLOSED_WAY) 202 .message(tr("\"to\" way is a closed way")) 203 .primitives(r) 204 .highlight(toWay) 205 .build()); 206 return; 207 } 208 if (fromWay.equals(toWay)) { 209 Severity severity = r.hasTag("restriction", "no_u_turn") ? Severity.OTHER : Severity.WARNING; 210 errors.add(TestError.builder(this, severity, FROM_EQUALS_TO) 211 .message(tr("\"from\" way equals \"to\" way")) 212 .primitives(r) 213 .build()); 214 } 215 if (via.isEmpty()) { 216 errors.add(TestError.builder(this, Severity.ERROR, NO_VIA) 217 .message(tr("No \"via\" node or way found")) 218 .primitives(r) 219 .build()); 220 return; 221 } 222 223 if (via.get(0) instanceof Node) { 224 final Node viaNode = (Node) via.get(0); 225 if (isFullOneway(toWay) && viaNode.equals(toWay.lastNode(true))) { 226 errors.add(TestError.builder(this, Severity.WARNING, SUPERFLUOUS) 227 .message(tr("Superfluous turn restriction as \"to\" way is oneway")) 228 .primitives(r) 229 .highlight(toWay) 230 .build()); 231 return; 232 } 233 if (isFullOneway(fromWay) && viaNode.equals(fromWay.firstNode(true))) { 234 errors.add(TestError.builder(this, Severity.WARNING, SUPERFLUOUS) 235 .message(tr("Superfluous turn restriction as \"from\" way is oneway")) 236 .primitives(r) 237 .highlight(fromWay) 238 .build()); 239 return; 240 } 241 final Way viaPseudoWay = new Way(); 242 viaPseudoWay.addNode(viaNode); 243 checkIfConnected(r, fromWay, viaPseudoWay, 244 tr("The \"from\" way does not start or end at a \"via\" node."), FROM_VIA_NODE); 245 checkIfConnected(r, viaPseudoWay, toWay, 246 tr("The \"to\" way does not start or end at a \"via\" node."), TO_VIA_NODE); 247 } else { 248 if (isFullOneway(toWay) && ((Way) via.get(via.size() - 1)).isFirstLastNode(toWay.lastNode(true))) { 249 errors.add(TestError.builder(this, Severity.WARNING, SUPERFLUOUS) 250 .message(tr("Superfluous turn restriction as \"to\" way is oneway")) 251 .primitives(r) 252 .highlight(toWay) 253 .build()); 254 return; 255 } 256 if (isFullOneway(fromWay) && ((Way) via.get(0)).isFirstLastNode(fromWay.firstNode(true))) { 257 errors.add(TestError.builder(this, Severity.WARNING, SUPERFLUOUS) 258 .message(tr("Superfluous turn restriction as \"from\" way is oneway")) 259 .primitives(r) 260 .highlight(fromWay) 261 .build()); 262 return; 263 } 264 // check if consecutive ways are connected: from/via[0], via[i-1]/via[i], via[last]/to 265 checkIfConnected(r, fromWay, (Way) via.get(0), 266 tr("The \"from\" and the first \"via\" way are not connected."), FROM_VIA_WAY); 267 if (via.size() > 1) { 268 for (int i = 1; i < via.size(); i++) { 269 Way previous = (Way) via.get(i - 1); 270 Way current = (Way) via.get(i); 271 checkIfConnected(r, previous, current, 272 tr("The \"via\" ways are not connected."), UNCONNECTED_VIA); 273 } 274 } 275 checkIfConnected(r, (Way) via.get(via.size() - 1), toWay, 276 tr("The last \"via\" and the \"to\" way are not connected."), TO_VIA_WAY); 277 } 278 } 279 280 private static boolean isFullOneway(Way w) { 281 return w.isOneway() != 0 && !w.hasTag("oneway:bicycle", "no"); 282 } 283 284 private void checkIfConnected(Relation r, Way previous, Way current, String msg, int code) { 285 boolean c; 286 if (isFullOneway(previous) && isFullOneway(current)) { 287 // both oneways: end/start node must be equal 288 c = previous.lastNode(true).equals(current.firstNode(true)); 289 } else if (isFullOneway(previous)) { 290 // previous way is oneway: end of previous must be start/end of current 291 c = current.isFirstLastNode(previous.lastNode(true)); 292 } else if (isFullOneway(current)) { 293 // current way is oneway: start of current must be start/end of previous 294 c = previous.isFirstLastNode(current.firstNode(true)); 295 } else { 296 // otherwise: start/end of previous must be start/end of current 297 c = current.isFirstLastNode(previous.firstNode()) || current.isFirstLastNode(previous.lastNode()); 298 } 299 if (!c) { 300 List<OsmPrimitive> hilite = new ArrayList<>(); 301 if (previous.getNodesCount() == 1 && previous.isNew()) 302 hilite.add(previous.firstNode()); 303 else 304 hilite.add(previous); 305 if (current.getNodesCount() == 1 && current.isNew()) 306 hilite.add(current.firstNode()); 307 else 308 hilite.add(current); 309 List<OsmPrimitive> primitives = new ArrayList<>(); 310 primitives.add(r); 311 primitives.addAll(hilite); 312 errors.add(TestError.builder(this, Severity.ERROR, code) 313 .message(msg) 314 .primitives(primitives) 315 .highlight(hilite) 316 .build()); 317 } 318 } 319}