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