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}