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