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