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