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.HashMap;
009import java.util.HashSet;
010import java.util.Iterator;
011import java.util.List;
012import java.util.Locale;
013import java.util.Map;
014
015import org.openstreetmap.josm.command.ChangePropertyCommand;
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.OsmUtils;
020import org.openstreetmap.josm.data.osm.Way;
021import org.openstreetmap.josm.data.validation.Severity;
022import org.openstreetmap.josm.data.validation.Test;
023import org.openstreetmap.josm.data.validation.TestError;
024import org.openstreetmap.josm.tools.Utils;
025
026/**
027 * Test that performs semantic checks on highways.
028 * @since 5902
029 */
030public class Highways extends Test {
031
032    protected static final int WRONG_ROUNDABOUT_HIGHWAY = 2701;
033    protected static final int MISSING_PEDESTRIAN_CROSSING = 2702;
034    protected static final int SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE = 2703;
035    protected static final int SOURCE_MAXSPEED_UNKNOWN_CONTEXT = 2704;
036    protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_MAXSPEED = 2705;
037    protected static final int SOURCE_MAXSPEED_CONTEXT_MISMATCH_VS_HIGHWAY = 2706;
038
039    /**
040     * Classified highways in order of importance
041     */
042    protected static final List<String> CLASSIFIED_HIGHWAYS = Arrays.asList(
043            "motorway",  "motorway_link",
044            "trunk",     "trunk_link",
045            "primary",   "primary_link",
046            "secondary", "secondary_link",
047            "tertiary",  "tertiary_link",
048            "unclassified",
049            "residential",
050            "living_street");
051
052    protected static final List<String> KNOWN_SOURCE_MAXSPEED_CONTEXTS = Arrays.asList(
053            "urban", "rural", "zone", "zone30", "zone:30", "nsl_single", "nsl_dual", "motorway", "trunk", "living_street");
054
055    protected static final List<String> ISO_COUNTRIES = Arrays.asList(Locale.getISOCountries());
056
057    boolean leftByPedestrians = false;
058    boolean leftByCyclists = false;
059    boolean leftByCars = false;
060    int pedestrianWays = 0;
061    int cyclistWays = 0;
062    int carsWays = 0;
063
064    /**
065     * Constructs a new {@code Highways} test.
066     */
067    public Highways() {
068        super(tr("Highways"), tr("Performs semantic checks on highways."));
069    }
070
071    protected class WrongRoundaboutHighway extends TestError {
072
073        public final String correctValue;
074
075        public WrongRoundaboutHighway(Way w, String key) {
076            super(Highways.this, Severity.WARNING,
077                    tr("Incorrect roundabout (highway: {0} instead of {1})", w.get("highway"), key),
078                    WRONG_ROUNDABOUT_HIGHWAY, w);
079            this.correctValue = key;
080        }
081    }
082
083    @Override
084    public void visit(Node n) {
085        if (n.isUsable()) {
086            if (!n.hasTag("highway", "crossing") && !n.hasTag("crossing", "no") && n.isReferredByWays(2)) {
087                testMissingPedestrianCrossing(n);
088            }
089            if (n.hasKey("source:maxspeed")) {
090                // Check maxspeed but not context against highway for nodes as maxspeed is not set on highways here but on signs, speed cameras, etc.
091                testSourceMaxspeed(n, false);
092            }
093        }
094    }
095
096    @Override
097    public void visit(Way w) {
098        if (w.isUsable()) {
099            if (w.hasKey("highway") && w.hasKey("junction") && w.get("junction").equals("roundabout")) {
100                testWrongRoundabout(w);
101            }
102            if (w.hasKey("source:maxspeed")) {
103                // Check maxspeed, including context against highway
104                testSourceMaxspeed(w, true);
105            }
106        }
107    }
108
109    private void testWrongRoundabout(Way w) {
110        Map<String, List<Way>> map = new HashMap<String, List<Way>>();
111        // Count all highways (per type) connected to this roundabout
112        // As roundabouts are closed ways, take care of not processing the first/last node twice
113        for (Node n : new HashSet<Node>(w.getNodes())) {
114            for (Way h : Utils.filteredCollection(n.getReferrers(), Way.class)) {
115                if (h != w && h.hasKey("highway")) {
116                    List<Way> list = map.get(h.get("highway"));
117                    if (list == null) {
118                        map.put(h.get("highway"), list = new ArrayList<Way>());
119                    }
120                    list.add(h);
121                }
122            }
123        }
124        // The roundabout should carry the highway tag of its two biggest highways
125        for (String s : CLASSIFIED_HIGHWAYS) {
126            List<Way> list = map.get(s);
127            if (list != null && list.size() >= 2) {
128                // Except when a single road is connected, but with two oneway segments
129                Boolean oneway1 = OsmUtils.getOsmBoolean(list.get(0).get("oneway"));
130                Boolean oneway2 = OsmUtils.getOsmBoolean(list.get(1).get("oneway"));
131                if (list.size() > 2 || oneway1 == null || oneway2 == null || !oneway1 || !oneway2) {
132                    // Error when the highway tags do not match
133                    if (!w.get("highway").equals(s)) {
134                        errors.add(new WrongRoundaboutHighway(w, s));
135                    }
136                    break;
137                }
138            }
139        }
140    }
141
142    private void testMissingPedestrianCrossing(Node n) {
143        leftByPedestrians = false;
144        leftByCyclists = false;
145        leftByCars = false;
146        pedestrianWays = 0;
147        cyclistWays = 0;
148        carsWays = 0;
149
150        for (Way w : OsmPrimitive.getFilteredList(n.getReferrers(), Way.class)) {
151            String highway = w.get("highway");
152            if (highway != null) {
153                if (highway.equals("footway") || highway.equals("path")) {
154                    handlePedestrianWay(n, w);
155                    if (w.hasTag("bicycle", "yes", "designated")) {
156                        handleCyclistWay(n, w);
157                    }
158                } else if (highway.equals("cycleway")) {
159                    handleCyclistWay(n, w);
160                    if (w.hasTag("foot", "yes", "designated")) {
161                        handlePedestrianWay(n, w);
162                    }
163                } else if (CLASSIFIED_HIGHWAYS.contains(highway)) {
164                    // Only look at classified highways for now:
165                    // - service highways support is TBD (see #9141 comments)
166                    // - roads should be determined first. Another warning is raised anyway
167                    handleCarWay(n, w);
168                }
169                if ((leftByPedestrians || leftByCyclists) && leftByCars) {
170                    errors.add(new TestError(this, Severity.OTHER, tr("Missing pedestrian crossing information"), MISSING_PEDESTRIAN_CROSSING, n));
171                    return;
172                }
173            }
174        }
175    }
176
177    private void handleCarWay(Node n, Way w) {
178        carsWays++;
179        if (!w.isFirstLastNode(n) || carsWays > 1) {
180            leftByCars = true;
181        }
182    }
183
184    private void handleCyclistWay(Node n, Way w) {
185        cyclistWays++;
186        if (!w.isFirstLastNode(n) || cyclistWays > 1) {
187            leftByCyclists = true;
188        }
189    }
190
191    private void handlePedestrianWay(Node n, Way w) {
192        pedestrianWays++;
193        if (!w.isFirstLastNode(n) || pedestrianWays > 1) {
194            leftByPedestrians = true;
195        }
196    }
197    
198    private void testSourceMaxspeed(OsmPrimitive p, boolean testContextHighway) {
199        String value = p.get("source:maxspeed");
200        if (value.matches("[A-Z]{2}:.+")) {
201            int index = value.indexOf(':');
202            // Check country
203            String country = value.substring(0, index);
204            if (!ISO_COUNTRIES.contains(country)) {
205                errors.add(new TestError(this, Severity.WARNING, tr("Unknown country code: {0}", country), SOURCE_MAXSPEED_UNKNOWN_COUNTRY_CODE, p));
206            }
207            // Check context
208            String context = value.substring(index+1);
209            if (!KNOWN_SOURCE_MAXSPEED_CONTEXTS.contains(context)) {
210                errors.add(new TestError(this, Severity.WARNING, tr("Unknown source:maxspeed context: {0}", context), SOURCE_MAXSPEED_UNKNOWN_CONTEXT, p));
211            }
212            // TODO: Check coherence of context against maxspeed
213            // TODO: Check coherence of context against highway
214        }
215    }
216
217    @Override
218    public boolean isFixable(TestError testError) {
219        return testError instanceof WrongRoundaboutHighway;
220    }
221
222    @Override
223    public Command fixError(TestError testError) {
224        if (testError instanceof WrongRoundaboutHighway) {
225            // primitives list can be empty if all primitives have been purged
226            Iterator<? extends OsmPrimitive> it = testError.getPrimitives().iterator();
227            if (it.hasNext()) {
228                return new ChangePropertyCommand(it.next(),
229                        "highway", ((WrongRoundaboutHighway) testError).correctValue);
230            }
231        }
232        return null;
233    }
234}