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