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