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 (!r.hasTag("type", "restriction"))
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 && !w.hasTag("oneway:bicycle", "no");
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}