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