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.Collection; 008import java.util.EnumSet; 009import java.util.HashMap; 010import java.util.LinkedHashMap; 011import java.util.LinkedList; 012import java.util.Map; 013import java.util.stream.Collectors; 014 015import org.openstreetmap.josm.command.Command; 016import org.openstreetmap.josm.command.DeleteCommand; 017import org.openstreetmap.josm.data.osm.OsmPrimitive; 018import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 019import org.openstreetmap.josm.data.osm.Relation; 020import org.openstreetmap.josm.data.osm.RelationMember; 021import org.openstreetmap.josm.data.validation.OsmValidator; 022import org.openstreetmap.josm.data.validation.Severity; 023import org.openstreetmap.josm.data.validation.Test; 024import org.openstreetmap.josm.data.validation.TestError; 025import org.openstreetmap.josm.gui.progress.ProgressMonitor; 026import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; 027import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem; 028import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType; 029import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets; 030import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem; 031import org.openstreetmap.josm.gui.tagging.presets.items.Roles; 032import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role; 033import org.openstreetmap.josm.tools.Utils; 034 035/** 036 * Check for wrong relations. 037 * @since 3669 038 */ 039public class RelationChecker extends Test { 040 041 // CHECKSTYLE.OFF: SingleSpaceSeparator 042 /** Role {0} unknown in templates {1} */ 043 public static final int ROLE_UNKNOWN = 1701; 044 /** Empty role type found when expecting one of {0} */ 045 public static final int ROLE_EMPTY = 1702; 046 /** Role member does not match expression {0} in template {1} */ 047 public static final int WRONG_TYPE = 1703; 048 /** Number of {0} roles too high ({1}) */ 049 public static final int HIGH_COUNT = 1704; 050 /** Number of {0} roles too low ({1}) */ 051 public static final int LOW_COUNT = 1705; 052 /** Role {0} missing */ 053 public static final int ROLE_MISSING = 1706; 054 /** Relation type is unknown */ 055 public static final int RELATION_UNKNOWN = 1707; 056 /** Relation is empty */ 057 public static final int RELATION_EMPTY = 1708; 058 // CHECKSTYLE.ON: SingleSpaceSeparator 059 060 /** 061 * Error message used to group errors related to role problems. 062 * @since 6731 063 */ 064 public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem"); 065 private boolean ignoreMultiPolygons; 066 067 /** 068 * Constructor 069 */ 070 public RelationChecker() { 071 super(tr("Relation checker"), 072 tr("Checks for errors in relations.")); 073 } 074 075 @Override 076 public void initialize() { 077 initializePresets(); 078 } 079 080 private static final Collection<TaggingPreset> relationpresets = new LinkedList<>(); 081 082 /** 083 * Reads the presets data. 084 */ 085 public static synchronized void initializePresets() { 086 if (!relationpresets.isEmpty()) { 087 // the presets have already been initialized 088 return; 089 } 090 for (TaggingPreset p : TaggingPresets.getTaggingPresets()) { 091 for (TaggingPresetItem i : p.data) { 092 if (i instanceof Roles) { 093 relationpresets.add(p); 094 break; 095 } 096 } 097 } 098 } 099 100 private static class RoleInfo { 101 private int total; 102 } 103 104 @Override 105 public void startTest(ProgressMonitor progressMonitor) { 106 super.startTest(progressMonitor); 107 108 for (Test t : OsmValidator.getEnabledTests(false)) { 109 if (t instanceof MultipolygonTest) { 110 ignoreMultiPolygons = true; 111 break; 112 } 113 } 114 } 115 116 @Override 117 public void visit(Relation n) { 118 Map<String, RoleInfo> map = buildRoleInfoMap(n); 119 if (map.isEmpty()) { 120 errors.add(TestError.builder(this, Severity.ERROR, RELATION_EMPTY) 121 .message(tr("Relation is empty")) 122 .primitives(n) 123 .build()); 124 } 125 if (ignoreMultiPolygons && n.isMultipolygon()) { 126 // see #17010: don't report same problem twice 127 return; 128 } 129 Map<Role, String> allroles = buildAllRoles(n); 130 if (allroles.isEmpty() && n.hasTag("type", "route") 131 && n.hasTag("route", "train", "subway", "monorail", "tram", "bus", "trolleybus", "aerialway", "ferry")) { 132 errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN) 133 .message(tr("Route scheme is unspecified. Add {0} ({1}=public_transport; {2}=legacy)", "public_transport:version", "2", "1")) 134 .primitives(n) 135 .build()); 136 } else if (allroles.isEmpty()) { 137 errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN) 138 .message(tr("Relation type is unknown")) 139 .primitives(n) 140 .build()); 141 } 142 143 if (!map.isEmpty() && !allroles.isEmpty()) { 144 checkRoles(n, allroles, map); 145 } 146 } 147 148 private static Map<String, RoleInfo> buildRoleInfoMap(Relation n) { 149 Map<String, RoleInfo> map = new HashMap<>(); 150 for (RelationMember m : n.getMembers()) { 151 map.computeIfAbsent(m.getRole(), k -> new RoleInfo()).total++; 152 } 153 return map; 154 } 155 156 // return Roles grouped by key 157 private static Map<Role, String> buildAllRoles(Relation n) { 158 Map<Role, String> allroles = new LinkedHashMap<>(); 159 160 for (TaggingPreset p : relationpresets) { 161 final boolean matches = TaggingPresetItem.matches(Utils.filteredCollection(p.data, KeyedItem.class), n.getKeys()); 162 final Roles r = Utils.find(p.data, Roles.class); 163 if (matches && r != null) { 164 for (Role role: r.roles) { 165 allroles.put(role, p.name); 166 } 167 } 168 } 169 return allroles; 170 } 171 172 private static boolean checkMemberType(Role r, RelationMember member) { 173 if (r.types != null) { 174 switch (member.getDisplayType()) { 175 case NODE: 176 return r.types.contains(TaggingPresetType.NODE); 177 case CLOSEDWAY: 178 return r.types.contains(TaggingPresetType.CLOSEDWAY); 179 case WAY: 180 return r.types.contains(TaggingPresetType.WAY); 181 case MULTIPOLYGON: 182 return r.types.contains(TaggingPresetType.MULTIPOLYGON); 183 case RELATION: 184 return r.types.contains(TaggingPresetType.RELATION); 185 default: // not matching type 186 return false; 187 } 188 } else { 189 // if no types specified, then test is passed 190 return true; 191 } 192 } 193 194 /** 195 * get all role definition for specified key and check, if some definition matches 196 * 197 * @param allroles containing list of possible role presets of the member 198 * @param member to be verified 199 * @param n relation to be verified 200 * @return <code>true</code> if member passed any of definition within preset 201 * 202 */ 203 private boolean checkMemberExpressionAndType(Map<Role, String> allroles, RelationMember member, Relation n) { 204 String role = member.getRole(); 205 String name = null; 206 // Set of all accepted types in template 207 Collection<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class); 208 TestError possibleMatchError = null; 209 // iterate through all of the role definition within preset 210 // and look for any matching definition 211 for (Map.Entry<Role, String> e : allroles.entrySet()) { 212 Role r = e.getKey(); 213 if (!r.isRole(role)) { 214 continue; 215 } 216 name = e.getValue(); 217 types.addAll(r.types); 218 if (checkMemberType(r, member)) { 219 // member type accepted by role definition 220 if (r.memberExpression == null) { 221 // no member expression - so all requirements met 222 return true; 223 } else { 224 // verify if preset accepts such member 225 OsmPrimitive primitive = member.getMember(); 226 if (!primitive.isUsable()) { 227 // if member is not usable (i.e. not present in working set) 228 // we can't verify expression - so we just skip it 229 return true; 230 } else { 231 // verify expression 232 if (r.memberExpression.match(primitive)) { 233 return true; 234 } else { 235 // possible match error 236 // we still need to iterate further, as we might have 237 // different present, for which memberExpression will match 238 // but stash the error in case no better reason will be found later 239 possibleMatchError = TestError.builder(this, Severity.WARNING, WRONG_TYPE) 240 .message(ROLE_VERIF_PROBLEM_MSG, 241 marktr("Role of relation member does not match expression ''{0}'' in template {1}"), 242 r.memberExpression, name) 243 .primitives(member.getMember().isUsable() ? member.getMember() : n) 244 .build(); 245 } 246 } 247 } 248 } else if (OsmPrimitiveType.RELATION == member.getType() && !member.getMember().isUsable() 249 && r.types.contains(TaggingPresetType.MULTIPOLYGON)) { 250 // if relation is incomplete we cannot verify if it's a multipolygon - so we just skip it 251 return true; 252 } 253 } 254 255 if (name == null) { 256 return true; 257 } else if (possibleMatchError != null) { 258 // if any error found, then assume that member type was correct 259 // and complain about not matching the memberExpression 260 // (the only failure, that we could gather) 261 errors.add(possibleMatchError); 262 } else { 263 // no errors found till now. So member at least failed at matching the type 264 // it could also fail at memberExpression, but we can't guess at which 265 266 // Do not raise an error for incomplete ways for which we expect them to be closed, as we cannot know 267 boolean ignored = member.getMember().isIncomplete() && OsmPrimitiveType.WAY == member.getType() 268 && !types.contains(TaggingPresetType.WAY) && types.contains(TaggingPresetType.CLOSEDWAY); 269 if (!ignored) { 270 // convert in localization friendly way to string of accepted types 271 String typesStr = types.stream().map(x -> tr(x.getName())).collect(Collectors.joining("/")); 272 273 errors.add(TestError.builder(this, Severity.WARNING, WRONG_TYPE) 274 .message(ROLE_VERIF_PROBLEM_MSG, 275 marktr("Type ''{0}'' of relation member with role ''{1}'' does not match accepted types ''{2}'' in template {3}"), 276 member.getType(), member.getRole(), typesStr, name) 277 .primitives(member.getMember().isUsable() ? member.getMember() : n) 278 .build()); 279 } 280 } 281 return false; 282 } 283 284 /** 285 * 286 * @param n relation to validate 287 * @param allroles contains presets for specified relation 288 * @param map contains statistics of occurrences of specified role types in relation 289 */ 290 private void checkRoles(Relation n, Map<Role, String> allroles, Map<String, RoleInfo> map) { 291 // go through all members of relation 292 for (RelationMember member: n.getMembers()) { 293 // error reporting done inside 294 checkMemberExpressionAndType(allroles, member, n); 295 } 296 297 // verify role counts based on whole role sets 298 for (Role r: allroles.keySet()) { 299 String keyname = r.key; 300 if (keyname.isEmpty()) { 301 keyname = tr("<empty>"); 302 } 303 checkRoleCounts(n, r, keyname, map.get(r.key)); 304 } 305 if ("network".equals(n.get("type")) && !"bicycle".equals(n.get("route"))) { 306 return; 307 } 308 // verify unwanted members 309 for (String key : map.keySet()) { 310 boolean found = false; 311 for (Role r: allroles.keySet()) { 312 if (r.isRole(key)) { 313 found = true; 314 break; 315 } 316 } 317 318 if (!found) { 319 String templates = allroles.keySet().stream().map(r -> r.key).collect(Collectors.joining("/")); 320 321 if (!key.isEmpty()) { 322 errors.add(TestError.builder(this, Severity.WARNING, ROLE_UNKNOWN) 323 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' unknown in templates ''{1}''"), key, templates) 324 .primitives(n) 325 .build()); 326 } else { 327 errors.add(TestError.builder(this, Severity.WARNING, ROLE_EMPTY) 328 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Empty role type found when expecting one of ''{0}''"), templates) 329 .primitives(n) 330 .build()); 331 } 332 } 333 } 334 } 335 336 private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) { 337 long count = (ri == null) ? 0 : ri.total; 338 long vc = r.getValidCount(count); 339 if (count != vc) { 340 if (count == 0) { 341 errors.add(TestError.builder(this, Severity.WARNING, ROLE_MISSING) 342 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' missing"), keyname) 343 .primitives(n) 344 .build()); 345 } else if (vc > count) { 346 errors.add(TestError.builder(this, Severity.WARNING, LOW_COUNT) 347 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too low ({1})"), keyname, count) 348 .primitives(n) 349 .build()); 350 } else { 351 errors.add(TestError.builder(this, Severity.WARNING, HIGH_COUNT) 352 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too high ({1})"), keyname, count) 353 .primitives(n) 354 .build()); 355 } 356 } 357 } 358 359 @Override 360 public Command fixError(TestError testError) { 361 Collection<? extends OsmPrimitive> primitives = testError.getPrimitives(); 362 if (isFixable(testError) && !primitives.iterator().next().isDeleted()) { 363 return new DeleteCommand(primitives); 364 } 365 return null; 366 } 367 368 @Override 369 public boolean isFixable(TestError testError) { 370 Collection<? extends OsmPrimitive> primitives = testError.getPrimitives(); 371 return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew(); 372 } 373}