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