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; 011import java.util.stream.Collectors; 012 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 final Severity severity; 099 // see #20455: raise severity to error when we are sure that tag key must describe an area 100 if ((ignore && !specialValues.isEmpty()) || "boundary".equals(key)) { 101 severity = Severity.WARNING; 102 } else { 103 severity = Severity.ERROR; 104 } 105 return TestError.builder(test, severity, code) 106 .message(tr("Unclosed way"), engMessage, engMessage.contains("{0}") ? new Object[]{value} : new Object[]{}) 107 .primitives(w) 108 .highlight(Arrays.asList(w.firstNode(), w.lastNode())) 109 .build(); 110 } 111 return null; 112 } 113 114 protected boolean isValueErroneous(String value) { 115 return value != null && ignore != specialValues.contains(value); 116 } 117 } 118 119 /** 120 * A check performed by UnclosedWays test where the key is treated as boolean. 121 * @since 6390 122 */ 123 private static final class UnclosedWaysBooleanCheck extends UnclosedWaysCheck { 124 125 /** 126 * Constructs a new {@code UnclosedWaysBooleanCheck}. 127 * @param code The unique numeric code for this check 128 * @param key The OSM key checked 129 * @param engMessage The English message 130 */ 131 UnclosedWaysBooleanCheck(int code, String key, String engMessage) { 132 super(code, key, engMessage); 133 } 134 135 @Override 136 protected boolean isValueErroneous(String value) { 137 Boolean btest = OsmUtils.getOsmBoolean(value); 138 // Not a strict boolean comparison to handle building=house like a building=yes 139 return (btest != null && btest) || (btest == null && value != null); 140 } 141 } 142 143 private static final UnclosedWaysCheck[] checks = { 144 // CHECKSTYLE.OFF: SingleSpaceSeparator 145 // list contains natural tag allowed on unclosed ways as well as those only allowed on nodes to avoid 146 // duplicate warnings 147 new UnclosedWaysCheck(1101, "natural", marktr("natural type {0}"), 148 new HashSet<>(Arrays.asList("arete", "bay", "cave", "cliff", "coastline", "earth_bank", "gorge", "gully", 149 "mountain_range", "peak", "ridge", "saddle", "strait", "tree", "tree_row", "valley", "volcano"))), 150 151 new UnclosedWaysCheck(1102, "landuse", marktr("landuse type {0}")), 152 new UnclosedWaysCheck(1103, "amenity", marktr("amenity type {0}"), 153 new HashSet<>(Arrays.asList("bench", "bicycle_parking", "weighbridge"))), 154 new UnclosedWaysCheck(1104, "sport", marktr("sport type {0}"), 155 new HashSet<>(Arrays.asList("water_slide", "climbing", "skiing", "toboggan", "bobsleigh", "karting", "motor", "motocross", 156 "cycling"))), 157 new UnclosedWaysCheck(1105, "tourism", marktr("tourism type {0}"), 158 new HashSet<>(Arrays.asList("attraction", "artwork"))), 159 new UnclosedWaysCheck(1106, "shop", marktr("shop type {0}")), 160 new UnclosedWaysCheck(1107, "leisure", marktr("leisure type {0}"), 161 new HashSet<>(Arrays.asList("track", "slipway", "barefoot"))), 162 new UnclosedWaysCheck(1108, "waterway", marktr("waterway type {0}"), 163 new HashSet<>(Arrays.asList("riverbank")), false), 164 new UnclosedWaysCheck(1109, "boundary", marktr("boundary type {0}")), 165 new UnclosedWaysCheck(1110, "area:highway", marktr("area:highway type {0}")), 166 new UnclosedWaysCheck(1111, "place", marktr("place type {0}")), 167 new UnclosedWaysBooleanCheck(1120, "building", marktr("building")), 168 new UnclosedWaysBooleanCheck(1130, "area", marktr("area")), 169 // 1131: Area style way is not closed 170 // CHECKSTYLE.ON: SingleSpaceSeparator 171 }; 172 173 /** 174 * Returns the set of checked OSM keys. 175 * @return The set of checked OSM keys. 176 * @since 6390 177 */ 178 public Set<String> getCheckedKeys() { 179 return Arrays.stream(checks).map(c -> c.key).collect(Collectors.toSet()); 180 } 181 182 @Override 183 public void visit(Way w) { 184 185 if (!w.isUsable() || w.isArea()) 186 return; 187 188 for (UnclosedWaysCheck c : checks) { 189 if ("boundary".equals(c.key) && w.referrers(Relation.class).anyMatch(Relation::isMultipolygon)) 190 return; 191 TestError error = c.getTestError(w, this); 192 if (error != null) { 193 errors.add(error); 194 return; 195 } 196 } 197 // code 1131: other area style ways 198 if (ElemStyles.hasOnlyAreaElements(w) && !w.getNodes().isEmpty()) { 199 errors.add(TestError.builder(this, Severity.WARNING, 1131) 200 .message(tr("Unclosed way"), marktr("Area style way is not closed"), new Object()) 201 .primitives(w) 202 .highlight(Arrays.asList(w.firstNode(), w.lastNode())) 203 .build()); 204 } 205 } 206}