001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.data.validation.tests;
003
004import static org.openstreetmap.josm.tools.I18n.marktr;
005import static org.openstreetmap.josm.tools.I18n.tr;
006
007import java.util.Arrays;
008import java.util.Collections;
009import java.util.HashSet;
010import java.util.Set;
011
012import org.openstreetmap.josm.data.osm.OsmPrimitive;
013import org.openstreetmap.josm.data.osm.OsmUtils;
014import org.openstreetmap.josm.data.osm.Relation;
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;
019import org.openstreetmap.josm.gui.mappaint.ElemStyles;
020
021/**
022 * Check area type ways for errors
023 *
024 * @author stoecker
025 * @since 3669
026 */
027public class UnclosedWays extends Test {
028
029    /**
030     * Constructs a new {@code UnclosedWays} test.
031     */
032    public UnclosedWays() {
033        super(tr("Unclosed Ways"), tr("This tests if ways which should be circular are closed."));
034    }
035
036    /**
037     * A check performed by UnclosedWays test.
038     * @since 6390
039     */
040    private static class UnclosedWaysCheck {
041        /** The unique numeric code for this check */
042        public final int code;
043        /** The OSM key checked */
044        public final String key;
045        /** The English message */
046        private final String engMessage;
047        /** The special values, to be ignored if ignore is set to true; to be considered only if ignore is set to false */
048        private final Set<String> specialValues;
049        /** The boolean indicating if special values must be ignored or considered only */
050        private final boolean ignore;
051
052        /**
053         * Constructs a new {@code UnclosedWaysCheck}.
054         * @param code The unique numeric code for this check
055         * @param key The OSM key checked
056         * @param engMessage The English message
057         */
058        UnclosedWaysCheck(int code, String key, String engMessage) {
059            this(code, key, engMessage, Collections.<String>emptySet());
060        }
061
062        /**
063         * Constructs a new {@code UnclosedWaysCheck}.
064         * @param code The unique numeric code for this check
065         * @param key The OSM key checked
066         * @param engMessage The English message
067         * @param ignoredValues The ignored values.
068         */
069        UnclosedWaysCheck(int code, String key, String engMessage, Set<String> ignoredValues) {
070            this(code, key, engMessage, ignoredValues, true);
071        }
072
073        /**
074         * Constructs a new {@code UnclosedWaysCheck}.
075         * @param code The unique numeric code for this check
076         * @param key The OSM key checked
077         * @param engMessage The English message
078         * @param specialValues The special values, to be ignored if ignore is set to true; to be considered only if ignore is set to false
079         * @param ignore indicates if special values must be ignored or considered only
080         */
081        UnclosedWaysCheck(int code, String key, String engMessage, Set<String> specialValues, boolean ignore) {
082            this.code = code;
083            this.key = key;
084            this.engMessage = engMessage;
085            this.specialValues = specialValues;
086            this.ignore = ignore;
087        }
088
089        /**
090         * Returns the test error of the given way, if any.
091         * @param w The way to check
092         * @param test parent test
093         * @return The test error if the way is erroneous, {@code null} otherwise
094         */
095        public final TestError getTestError(Way w, UnclosedWays test) {
096            String value = w.get(key);
097            if (isValueErroneous(value)) {
098                return TestError.builder(test, Severity.WARNING, code)
099                        .message(tr("Unclosed way"), engMessage, engMessage.contains("{0}") ? new Object[]{value} : new Object[]{})
100                        .primitives(w)
101                        .highlight(Arrays.asList(w.firstNode(), w.lastNode()))
102                        .build();
103            }
104            return null;
105        }
106
107        protected boolean isValueErroneous(String value) {
108            return value != null && ignore != specialValues.contains(value);
109        }
110    }
111
112    /**
113     * A check performed by UnclosedWays test where the key is treated as boolean.
114     * @since 6390
115     */
116    private static final class UnclosedWaysBooleanCheck extends UnclosedWaysCheck {
117
118        /**
119         * Constructs a new {@code UnclosedWaysBooleanCheck}.
120         * @param code The unique numeric code for this check
121         * @param key The OSM key checked
122         * @param engMessage The English message
123         */
124        UnclosedWaysBooleanCheck(int code, String key, String engMessage) {
125            super(code, key, engMessage);
126        }
127
128        @Override
129        protected boolean isValueErroneous(String value) {
130            Boolean btest = OsmUtils.getOsmBoolean(value);
131            // Not a strict boolean comparison to handle building=house like a building=yes
132            return (btest != null && btest) || (btest == null && value != null);
133        }
134    }
135
136    private static final UnclosedWaysCheck[] checks = {
137        // CHECKSTYLE.OFF: SingleSpaceSeparator
138        // list contains natural tag allowed on unclosed ways as well as those only allowed on nodes to avoid
139        // duplicate warnings
140        new UnclosedWaysCheck(1101, "natural", marktr("natural type {0}"),
141            new HashSet<>(Arrays.asList("arete", "bay", "cave", "cliff", "coastline", "gorge", "gully", "peak",
142                            "ridge", "saddle", "strait", "tree", "tree_row", "valley", "volcano"))),
143
144        new UnclosedWaysCheck(1102, "landuse", marktr("landuse type {0}")),
145        new UnclosedWaysCheck(1103, "amenities", marktr("amenities type {0}")),
146        new UnclosedWaysCheck(1104, "sport",     marktr("sport type {0}"),
147                new HashSet<>(Arrays.asList("water_slide", "climbing", "skiing", "toboggan", "bobsleigh"))),
148        new UnclosedWaysCheck(1105, "tourism",   marktr("tourism type {0}"),
149                new HashSet<>(Arrays.asList("attraction", "artwork"))),
150        new UnclosedWaysCheck(1106, "shop",      marktr("shop type {0}")),
151        new UnclosedWaysCheck(1107, "leisure",   marktr("leisure type {0}"),
152                new HashSet<>(Arrays.asList("track", "slipway"))),
153        new UnclosedWaysCheck(1108, "waterway",  marktr("waterway type {0}"),
154                new HashSet<>(Arrays.asList("riverbank")), false),
155        new UnclosedWaysCheck(1109, "boundary", marktr("boundary type {0}")),
156        new UnclosedWaysCheck(1110, "area:highway", marktr("area:highway type {0}")),
157        new UnclosedWaysBooleanCheck(1120, "building", marktr("building")),
158        new UnclosedWaysBooleanCheck(1130, "area",     marktr("area")),
159        // 1131: Area style way is not closed
160        // CHECKSTYLE.ON: SingleSpaceSeparator
161    };
162
163    /**
164     * Returns the set of checked OSM keys.
165     * @return The set of checked OSM keys.
166     * @since 6390
167     */
168    public Set<String> getCheckedKeys() {
169        Set<String> keys = new HashSet<>();
170        for (UnclosedWaysCheck c : checks) {
171            keys.add(c.key);
172        }
173        return keys;
174    }
175
176    @Override
177    public void visit(Way w) {
178
179        if (!w.isUsable() || w.isArea())
180            return;
181
182        for (OsmPrimitive parent: w.getReferrers()) {
183            if (parent instanceof Relation && ((Relation) parent).isMultipolygon())
184                return;
185        }
186
187        for (UnclosedWaysCheck c : checks) {
188            TestError error = c.getTestError(w, this);
189            if (error != null) {
190                errors.add(error);
191                return;
192            }
193        }
194        // code 1131: other area style ways
195        if (ElemStyles.hasOnlyAreaElements(w) && !w.getNodes().isEmpty()) {
196            errors.add(TestError.builder(this, Severity.WARNING, 1131)
197                    .message(tr("Unclosed way"), marktr("Area style way is not closed"), new Object())
198                    .primitives(w)
199                    .highlight(Arrays.asList(w.firstNode(), w.lastNode()))
200                    .build());
201        }
202    }
203}