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.ArrayList; 009import java.util.Collection; 010import java.util.HashMap; 011import java.util.HashSet; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Set; 015 016import org.openstreetmap.josm.command.Command; 017import org.openstreetmap.josm.command.DeleteCommand; 018import org.openstreetmap.josm.data.osm.Node; 019import org.openstreetmap.josm.data.osm.OsmPrimitive; 020import org.openstreetmap.josm.data.osm.Relation; 021import org.openstreetmap.josm.data.osm.RelationMember; 022import org.openstreetmap.josm.data.osm.Way; 023import org.openstreetmap.josm.data.validation.Severity; 024import org.openstreetmap.josm.data.validation.Test; 025import org.openstreetmap.josm.data.validation.TestError; 026import org.openstreetmap.josm.gui.tagging.TaggingPreset; 027import org.openstreetmap.josm.gui.tagging.TaggingPresetItem; 028import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Key; 029import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role; 030import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles; 031import org.openstreetmap.josm.gui.tagging.TaggingPresetType; 032import org.openstreetmap.josm.gui.tagging.TaggingPresets; 033 034/** 035 * Check for wrong relations. 036 * @since 3669 037 */ 038public class RelationChecker extends Test { 039 040 protected static final int ROLE_UNKNOWN = 1701; 041 protected static final int ROLE_EMPTY = 1702; 042 protected static final int WRONG_TYPE = 1703; 043 protected static final int HIGH_COUNT = 1704; 044 protected static final int LOW_COUNT = 1705; 045 protected static final int ROLE_MISSING = 1706; 046 protected static final int RELATION_UNKNOWN = 1707; 047 protected static final int RELATION_EMPTY = 1708; 048 049 /** 050 * Error message used to group errors related to role problems. 051 * @since 6731 052 */ 053 public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem"); 054 055 /** 056 * Constructor 057 */ 058 public RelationChecker() { 059 super(tr("Relation checker"), 060 tr("Checks for errors in relations.")); 061 } 062 063 @Override 064 public void initialize() { 065 initializePresets(); 066 } 067 068 private static Collection<TaggingPreset> relationpresets = new LinkedList<>(); 069 070 /** 071 * Reads the presets data. 072 */ 073 public static synchronized void initializePresets() { 074 if (!relationpresets.isEmpty()) { 075 // the presets have already been initialized 076 return; 077 } 078 for (TaggingPreset p : TaggingPresets.getTaggingPresets()) { 079 for (TaggingPresetItem i : p.data) { 080 if (i instanceof Roles) { 081 relationpresets.add(p); 082 break; 083 } 084 } 085 } 086 } 087 088 private static class RoleInfo { 089 private int total = 0; 090 private Collection<Node> nodes = new LinkedList<>(); 091 private Collection<Way> ways = new LinkedList<>(); 092 private Collection<Way> openways = new LinkedList<>(); 093 private Collection<Relation> relations = new LinkedList<>(); 094 } 095 096 @Override 097 public void visit(Relation n) { 098 LinkedList<Role> allroles = buildAllRoles(n); 099 if (allroles.isEmpty() && n.hasTag("type", "route") 100 && n.hasTag("route", "train", "subway", "monorail", "tram", "bus", "trolleybus", "aerialway", "ferry")) { 101 errors.add(new TestError(this, Severity.WARNING, 102 tr("Route scheme (public_transport or legacy) is unspecified. Add {0}", "public_transport:version"), 103 RELATION_UNKNOWN, n)); 104 } else if (allroles.isEmpty()) { 105 errors.add(new TestError(this, Severity.WARNING, tr("Relation type is unknown"), RELATION_UNKNOWN, n)); 106 } else { 107 HashMap<String, RoleInfo> map = buildRoleInfoMap(n); 108 if (map.isEmpty()) { 109 errors.add(new TestError(this, Severity.ERROR, tr("Relation is empty"), RELATION_EMPTY, n)); 110 } else { 111 checkRoles(n, allroles, map); 112 } 113 } 114 } 115 116 private HashMap<String, RoleInfo> buildRoleInfoMap(Relation n) { 117 HashMap<String,RoleInfo> map = new HashMap<>(); 118 for (RelationMember m : n.getMembers()) { 119 String role = m.getRole(); 120 RoleInfo ri = map.get(role); 121 if (ri == null) { 122 ri = new RoleInfo(); 123 } 124 ri.total++; 125 if (m.isRelation()) { 126 ri.relations.add(m.getRelation()); 127 } else if(m.isWay()) { 128 ri.ways.add(m.getWay()); 129 if (!m.getWay().isClosed()) { 130 ri.openways.add(m.getWay()); 131 } 132 } 133 else if (m.isNode()) { 134 ri.nodes.add(m.getNode()); 135 } 136 map.put(role, ri); 137 } 138 return map; 139 } 140 141 private LinkedList<Role> buildAllRoles(Relation n) { 142 LinkedList<Role> allroles = new LinkedList<>(); 143 for (TaggingPreset p : relationpresets) { 144 boolean matches = true; 145 Roles r = null; 146 for (TaggingPresetItem i : p.data) { 147 if (i instanceof Key) { 148 Key k = (Key) i; 149 if (!k.value.equals(n.get(k.key))) { 150 matches = false; 151 break; 152 } 153 } else if (i instanceof Roles) { 154 r = (Roles) i; 155 } 156 } 157 if (matches && r != null) { 158 allroles.addAll(r.roles); 159 } 160 } 161 return allroles; 162 } 163 164 private void checkRoles(Relation n, LinkedList<Role> allroles, HashMap<String, RoleInfo> map) { 165 List<String> done = new LinkedList<>(); 166 // Remove empty roles if several exist (like in route=hiking, see #9844) 167 List<Role> emptyRoles = new LinkedList<>(); 168 for (Role r : allroles) { 169 if ("".equals(r.key)) { 170 emptyRoles.add(r); 171 } 172 } 173 if (emptyRoles.size() > 1) { 174 allroles.removeAll(emptyRoles); 175 } 176 for (Role r : allroles) { 177 done.add(r.key); 178 String keyname = r.key; 179 if ("".equals(keyname)) { 180 keyname = tr("<empty>"); 181 } 182 RoleInfo ri = map.get(r.key); 183 checkRoleCounts(n, r, keyname, ri); 184 if (ri != null) { 185 if (r.types != null) { 186 checkRoleTypes(n, r, keyname, ri); 187 } 188 if (r.memberExpression != null) { 189 checkRoleMemberExpressions(n, r, keyname, ri); 190 } 191 } 192 } 193 for (String key : map.keySet()) { 194 if (!done.contains(key)) { 195 if (key.length() > 0) { 196 String s = marktr("Role {0} unknown"); 197 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 198 tr(s, key), MessageFormat.format(s, key), ROLE_UNKNOWN, n)); 199 } else { 200 String s = marktr("Empty role found"); 201 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 202 tr(s), s, ROLE_EMPTY, n)); 203 } 204 } 205 } 206 } 207 208 private void checkRoleMemberExpressions(Relation n, Role r, String keyname, RoleInfo ri) { 209 Set<OsmPrimitive> notMatching = new HashSet<>(); 210 Collection<OsmPrimitive> allPrimitives = new ArrayList<>(); 211 allPrimitives.addAll(ri.nodes); 212 allPrimitives.addAll(ri.ways); 213 allPrimitives.addAll(ri.relations); 214 for (OsmPrimitive p : allPrimitives) { 215 if (p.isUsable() && !r.memberExpression.match(p)) { 216 notMatching.add(p); 217 } 218 } 219 if (!notMatching.isEmpty()) { 220 String s = marktr("Member for role ''{0}'' does not match ''{1}''"); 221 LinkedList<OsmPrimitive> highlight = new LinkedList<>(notMatching); 222 highlight.addFirst(n); 223 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 224 tr(s, keyname, r.memberExpression), MessageFormat.format(s, keyname, r.memberExpression), WRONG_TYPE, 225 highlight, notMatching)); 226 } 227 } 228 229 private void checkRoleTypes(Relation n, Role r, String keyname, RoleInfo ri) { 230 Set<OsmPrimitive> wrongTypes = new HashSet<>(); 231 if (!r.types.contains(TaggingPresetType.WAY)) { 232 wrongTypes.addAll(r.types.contains(TaggingPresetType.CLOSEDWAY) ? ri.openways : ri.ways); 233 } 234 if (!r.types.contains(TaggingPresetType.NODE)) { 235 wrongTypes.addAll(ri.nodes); 236 } 237 if (!r.types.contains(TaggingPresetType.RELATION)) { 238 wrongTypes.addAll(ri.relations); 239 } 240 if (!wrongTypes.isEmpty()) { 241 String s = marktr("Member for role {0} of wrong type"); 242 LinkedList<OsmPrimitive> highlight = new LinkedList<>(wrongTypes); 243 highlight.addFirst(n); 244 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 245 tr(s, keyname), MessageFormat.format(s, keyname), WRONG_TYPE, 246 highlight, wrongTypes)); 247 } 248 } 249 250 private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) { 251 long count = (ri == null) ? 0 : ri.total; 252 long vc = r.getValidCount(count); 253 if (count != vc) { 254 if (count == 0) { 255 String s = marktr("Role {0} missing"); 256 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 257 tr(s, keyname), MessageFormat.format(s, keyname), ROLE_MISSING, n)); 258 } 259 else if (vc > count) { 260 String s = marktr("Number of {0} roles too low ({1})"); 261 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 262 tr(s, keyname, count), MessageFormat.format(s, keyname, count), LOW_COUNT, n)); 263 } else { 264 String s = marktr("Number of {0} roles too high ({1})"); 265 errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG, 266 tr(s, keyname, count), MessageFormat.format(s, keyname, count), HIGH_COUNT, n)); 267 } 268 } 269 } 270 271 @Override 272 public Command fixError(TestError testError) { 273 if (isFixable(testError)) { 274 return new DeleteCommand(testError.getPrimitives()); 275 } 276 return null; 277 } 278 279 @Override 280 public boolean isFixable(TestError testError) { 281 Collection<? extends OsmPrimitive> primitives = testError.getPrimitives(); 282 return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew(); 283 } 284}