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