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