// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.data.validation.tests; import static org.openstreetmap.josm.tools.I18n.marktr; import static org.openstreetmap.josm.tools.I18n.tr; import java.util.Collection; import java.util.EnumSet; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.DeleteCommand; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.OsmPrimitiveType; import org.openstreetmap.josm.data.osm.Relation; import org.openstreetmap.josm.data.osm.RelationMember; import org.openstreetmap.josm.data.validation.Severity; import org.openstreetmap.josm.data.validation.Test; import org.openstreetmap.josm.data.validation.TestError; import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset; import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem; import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType; import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets; import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem; import org.openstreetmap.josm.gui.tagging.presets.items.Roles; import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role; import org.openstreetmap.josm.tools.Utils; /** * Check for wrong relations. * @since 3669 */ public class RelationChecker extends Test { // CHECKSTYLE.OFF: SingleSpaceSeparator /** Role {0} unknown in templates {1} */ public static final int ROLE_UNKNOWN = 1701; /** Empty role type found when expecting one of {0} */ public static final int ROLE_EMPTY = 1702; /** Role member does not match expression {0} in template {1} */ public static final int WRONG_TYPE = 1703; /** Number of {0} roles too high ({1}) */ public static final int HIGH_COUNT = 1704; /** Number of {0} roles too low ({1}) */ public static final int LOW_COUNT = 1705; /** Role {0} missing */ public static final int ROLE_MISSING = 1706; /** Relation type is unknown */ public static final int RELATION_UNKNOWN = 1707; /** Relation is empty */ public static final int RELATION_EMPTY = 1708; // CHECKSTYLE.ON: SingleSpaceSeparator /** * Error message used to group errors related to role problems. * @since 6731 */ public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem"); /** * Constructor */ public RelationChecker() { super(tr("Relation checker"), tr("Checks for errors in relations.")); } @Override public void initialize() { initializePresets(); } private static final Collection<TaggingPreset> relationpresets = new LinkedList<>(); /** * Reads the presets data. */ public static synchronized void initializePresets() { if (!relationpresets.isEmpty()) { // the presets have already been initialized return; } for (TaggingPreset p : TaggingPresets.getTaggingPresets()) { for (TaggingPresetItem i : p.data) { if (i instanceof Roles) { relationpresets.add(p); break; } } } } private static class RolePreset { private final List<Role> roles; private final String name; RolePreset(List<Role> roles, String name) { this.roles = roles; this.name = name; } } private static class RoleInfo { private int total; } @Override public void visit(Relation n) { Map<String, RolePreset> allroles = buildAllRoles(n); if (allroles.isEmpty() && n.hasTag("type", "route") && n.hasTag("route", "train", "subway", "monorail", "tram", "bus", "trolleybus", "aerialway", "ferry")) { errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN) .message(tr("Route scheme is unspecified. Add {0} ({1}=public_transport; {2}=legacy)", "public_transport:version", "2", "1")) .primitives(n) .build()); } else if (allroles.isEmpty()) { errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN) .message(tr("Relation type is unknown")) .primitives(n) .build()); } Map<String, RoleInfo> map = buildRoleInfoMap(n); if (map.isEmpty()) { errors.add(TestError.builder(this, Severity.ERROR, RELATION_EMPTY) .message(tr("Relation is empty")) .primitives(n) .build()); } else if (!allroles.isEmpty()) { checkRoles(n, allroles, map); } } private static Map<String, RoleInfo> buildRoleInfoMap(Relation n) { Map<String, RoleInfo> map = new HashMap<>(); for (RelationMember m : n.getMembers()) { String role = m.getRole(); RoleInfo ri = map.get(role); if (ri == null) { ri = new RoleInfo(); map.put(role, ri); } ri.total++; } return map; } // return Roles grouped by key private static Map<String, RolePreset> buildAllRoles(Relation n) { Map<String, RolePreset> allroles = new HashMap<>(); for (TaggingPreset p : relationpresets) { final boolean matches = TaggingPresetItem.matches(Utils.filteredCollection(p.data, KeyedItem.class), n.getKeys()); final Roles r = Utils.find(p.data, Roles.class); if (matches && r != null) { for (Role role: r.roles) { String key = role.key; List<Role> roleGroup; if (allroles.containsKey(key)) { roleGroup = allroles.get(key).roles; } else { roleGroup = new LinkedList<>(); allroles.put(key, new RolePreset(roleGroup, p.name)); } roleGroup.add(role); } } } return allroles; } private boolean checkMemberType(Role r, RelationMember member) { if (r.types != null) { switch (member.getDisplayType()) { case NODE: return r.types.contains(TaggingPresetType.NODE); case CLOSEDWAY: return r.types.contains(TaggingPresetType.CLOSEDWAY); case WAY: return r.types.contains(TaggingPresetType.WAY); case MULTIPOLYGON: return r.types.contains(TaggingPresetType.MULTIPOLYGON); case RELATION: return r.types.contains(TaggingPresetType.RELATION); default: // not matching type return false; } } else { // if no types specified, then test is passed return true; } } /** * get all role definition for specified key and check, if some definition matches * * @param rolePreset containing preset for role of the member * @param member to be verified * @param n relation to be verified * @return <tt>true</tt> if member passed any of definition within preset * */ private boolean checkMemberExpressionAndType(RolePreset rolePreset, RelationMember member, Relation n) { if (rolePreset == null || rolePreset.roles == null) { // no restrictions on role types return true; } TestError possibleMatchError = null; // iterate through all of the role definition within preset // and look for any matching definition for (Role r: rolePreset.roles) { if (checkMemberType(r, member)) { // member type accepted by role definition if (r.memberExpression == null) { // no member expression - so all requirements met return true; } else { // verify if preset accepts such member OsmPrimitive primitive = member.getMember(); if (!primitive.isUsable()) { // if member is not usable (i.e. not present in working set) // we can't verify expression - so we just skip it return true; } else { // verify expression if (r.memberExpression.match(primitive)) { return true; } else { // possible match error // we still need to iterate further, as we might have // different present, for which memberExpression will match // but stash the error in case no better reason will be found later possibleMatchError = TestError.builder(this, Severity.WARNING, WRONG_TYPE) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role of relation member does not match expression ''{0}'' in template {1}"), r.memberExpression, rolePreset.name) .primitives(member.getMember().isUsable() ? member.getMember() : n) .build(); } } } } else if (OsmPrimitiveType.RELATION.equals(member.getType()) && !member.getMember().isUsable() && r.types.contains(TaggingPresetType.MULTIPOLYGON)) { // if relation is incomplete we cannot verify if it's a multipolygon - so we just skip it return true; } } if (possibleMatchError != null) { // if any error found, then assume that member type was correct // and complain about not matching the memberExpression // (the only failure, that we could gather) errors.add(possibleMatchError); } else { // no errors found till now. So member at least failed at matching the type // it could also fail at memberExpression, but we can't guess at which // prepare Set of all accepted types in template Collection<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class); for (Role r: rolePreset.roles) { types.addAll(r.types); } // convert in localization friendly way to string of accepted types String typesStr = types.stream().map(x -> tr(x.getName())).collect(Collectors.joining("/")); errors.add(TestError.builder(this, Severity.WARNING, WRONG_TYPE) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Type ''{0}'' of relation member with role ''{1}'' does not match accepted types ''{2}'' in template {3}"), member.getType(), member.getRole(), typesStr, rolePreset.name) .primitives(member.getMember().isUsable() ? member.getMember() : n) .build()); } return false; } /** * * @param n relation to validate * @param allroles contains presets for specified relation * @param map contains statistics of occurances of specified role types in relation */ private void checkRoles(Relation n, Map<String, RolePreset> allroles, Map<String, RoleInfo> map) { // go through all members of relation for (RelationMember member: n.getMembers()) { String role = member.getRole(); // error reporting done inside checkMemberExpressionAndType(allroles.get(role), member, n); } // verify role counts based on whole role sets for (RolePreset rp: allroles.values()) { for (Role r: rp.roles) { String keyname = r.key; if (keyname.isEmpty()) { keyname = tr("<empty>"); } checkRoleCounts(n, r, keyname, map.get(r.key)); } } // verify unwanted members for (String key : map.keySet()) { if (!allroles.containsKey(key) && !"network".equals(n.get("type")) && !"bicycle".equals(n.get("route"))) { String templates = allroles.keySet().stream().collect(Collectors.joining("/")); if (!key.isEmpty()) { errors.add(TestError.builder(this, Severity.WARNING, ROLE_UNKNOWN) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' unknown in templates ''{1}''"), key, templates) .primitives(n) .build()); } else { errors.add(TestError.builder(this, Severity.WARNING, ROLE_EMPTY) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Empty role type found when expecting one of ''{0}''"), templates) .primitives(n) .build()); } } } } private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) { long count = (ri == null) ? 0 : ri.total; long vc = r.getValidCount(count); if (count != vc) { if (count == 0) { errors.add(TestError.builder(this, Severity.WARNING, ROLE_MISSING) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' missing"), keyname) .primitives(n) .build()); } else if (vc > count) { errors.add(TestError.builder(this, Severity.WARNING, LOW_COUNT) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too low ({1})"), keyname, count) .primitives(n) .build()); } else { errors.add(TestError.builder(this, Severity.WARNING, HIGH_COUNT) .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too high ({1})"), keyname, count) .primitives(n) .build()); } } } @Override public Command fixError(TestError testError) { if (isFixable(testError) && !testError.getPrimitives().iterator().next().isDeleted()) { return new DeleteCommand(testError.getPrimitives()); } return null; } @Override public boolean isFixable(TestError testError) { Collection<? extends OsmPrimitive> primitives = testError.getPrimitives(); return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew(); } }