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}