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