001// License: GPL. See LICENSE file for details. 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 042 /** 043 * Constructs a new {@code TurnrestrictionTest}. 044 */ 045 public TurnrestrictionTest() { 046 super(tr("Turnrestrictions"), tr("This test checks if turnrestrictions are valid.")); 047 } 048 049 @Override 050 public void visit(Relation r) { 051 if (!"restriction".equals(r.get("type"))) 052 return; 053 054 Way fromWay = null; 055 Way toWay = null; 056 List<OsmPrimitive> via = new ArrayList<>(); 057 058 boolean morefrom = false; 059 boolean moreto = false; 060 boolean morevia = false; 061 boolean mixvia = false; 062 063 /* find the "from", "via" and "to" elements */ 064 for (RelationMember m : r.getMembers()) { 065 if (m.getMember().isIncomplete()) 066 return; 067 068 List<OsmPrimitive> l = new ArrayList<>(); 069 l.add(r); 070 l.add(m.getMember()); 071 if (m.isWay()) { 072 Way w = m.getWay(); 073 if (w.getNodesCount() < 2) { 074 continue; 075 } 076 077 switch (m.getRole()) { 078 case "from": 079 if (fromWay != null) { 080 morefrom = true; 081 } else { 082 fromWay = w; 083 } 084 break; 085 case "to": 086 if (toWay != null) { 087 moreto = true; 088 } else { 089 toWay = w; 090 } 091 break; 092 case "via": 093 if (!via.isEmpty() && via.get(0) instanceof Node) { 094 mixvia = true; 095 } else { 096 via.add(w); 097 } 098 break; 099 default: 100 errors.add(new TestError(this, Severity.WARNING, tr("Unknown role"), UNKNOWN_ROLE, 101 l, Collections.singletonList(m))); 102 } 103 } else if (m.isNode()) { 104 Node n = m.getNode(); 105 if ("via".equals(m.getRole())) { 106 if (!via.isEmpty()) { 107 if (via.get(0) instanceof Node) { 108 morevia = true; 109 } else { 110 mixvia = true; 111 } 112 } else { 113 via.add(n); 114 } 115 } else { 116 errors.add(new TestError(this, Severity.WARNING, tr("Unknown role"), UNKNOWN_ROLE, 117 l, Collections.singletonList(m))); 118 } 119 } else { 120 errors.add(new TestError(this, Severity.WARNING, tr("Unknown member type"), UNKNOWN_TYPE, 121 l, Collections.singletonList(m))); 122 } 123 } 124 if (morefrom) { 125 errors.add(new TestError(this, Severity.ERROR, tr("More than one \"from\" way found"), MORE_FROM, r)); 126 } 127 if (moreto) { 128 errors.add(new TestError(this, Severity.ERROR, tr("More than one \"to\" way found"), MORE_TO, r)); 129 } 130 if (morevia) { 131 errors.add(new TestError(this, Severity.ERROR, tr("More than one \"via\" node found"), MORE_VIA, r)); 132 } 133 if (mixvia) { 134 errors.add(new TestError(this, Severity.ERROR, tr("Cannot mix node and way for role \"via\""), MIX_VIA, r)); 135 } 136 137 if (fromWay == null) { 138 errors.add(new TestError(this, Severity.ERROR, tr("No \"from\" way found"), NO_FROM, r)); 139 return; 140 } 141 if (toWay == null) { 142 errors.add(new TestError(this, Severity.ERROR, tr("No \"to\" way found"), NO_TO, r)); 143 return; 144 } 145 if (via.isEmpty()) { 146 errors.add(new TestError(this, Severity.ERROR, tr("No \"via\" node or way found"), NO_VIA, r)); 147 return; 148 } 149 150 if (via.get(0) instanceof Node) { 151 final Node viaNode = (Node) via.get(0); 152 final Way viaPseudoWay = new Way(); 153 viaPseudoWay.addNode(viaNode); 154 checkIfConnected(fromWay, viaPseudoWay, 155 tr("The \"from\" way does not start or end at a \"via\" node."), FROM_VIA_NODE); 156 if (toWay.isOneway() != 0 && viaNode.equals(toWay.lastNode(true))) { 157 errors.add(new TestError(this, Severity.WARNING, tr("Superfluous turnrestriction as \"to\" way is oneway"), SUPERFLUOUS, r)); 158 return; 159 } 160 checkIfConnected(viaPseudoWay, toWay, 161 tr("The \"to\" way does not start or end at a \"via\" node."), TO_VIA_NODE); 162 } else { 163 // check if consecutive ways are connected: from/via[0], via[i-1]/via[i], via[last]/to 164 checkIfConnected(fromWay, (Way) via.get(0), 165 tr("The \"from\" and the first \"via\" way are not connected."), FROM_VIA_WAY); 166 if (via.size() > 1) { 167 for (int i = 1; i < via.size(); i++) { 168 Way previous = (Way) via.get(i - 1); 169 Way current = (Way) via.get(i); 170 checkIfConnected(previous, current, 171 tr("The \"via\" ways are not connected."), UNCONNECTED_VIA); 172 } 173 } 174 if (toWay.isOneway() != 0 && ((Way) via.get(via.size() - 1)).isFirstLastNode(toWay.lastNode(true))) { 175 errors.add(new TestError(this, Severity.WARNING, tr("Superfluous turnrestriction as \"to\" way is oneway"), SUPERFLUOUS, r)); 176 return; 177 } 178 checkIfConnected((Way) via.get(via.size() - 1), toWay, 179 tr("The last \"via\" and the \"to\" way are not connected."), TO_VIA_WAY); 180 181 } 182 } 183 184 private void checkIfConnected(Way previous, Way current, String msg, int code) { 185 boolean c; 186 if (previous.isOneway() != 0 && current.isOneway() != 0) { 187 // both oneways: end/start node must be equal 188 c = previous.lastNode(true).equals(current.firstNode(true)); 189 } else if (previous.isOneway() != 0) { 190 // previous way is oneway: end of previous must be start/end of current 191 c = current.isFirstLastNode(previous.lastNode(true)); 192 } else if (current.isOneway() != 0) { 193 // current way is oneway: start of current must be start/end of previous 194 c = previous.isFirstLastNode(current.firstNode(true)); 195 } else { 196 // otherwise: start/end of previous must be start/end of current 197 c = current.isFirstLastNode(previous.firstNode()) || current.isFirstLastNode(previous.lastNode()); 198 } 199 if (!c) { 200 errors.add(new TestError(this, Severity.ERROR, msg, code, Arrays.asList(previous, current))); 201 } 202 } 203}