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