source: josm/trunk/src/org/openstreetmap/josm/data/validation/tests/RelationChecker.java@ 9932

Last change on this file since 9932 was 9932, checked in by simon04, 8 years ago

see #8460 - fix typo from r9931, fix matching, improve conditions, add unit test

  • Property svn:eol-style set to native
File size: 14.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.data.validation.tests;
3
4import static org.openstreetmap.josm.tools.I18n.marktr;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.text.MessageFormat;
8import java.util.Collection;
9import java.util.EnumSet;
10import java.util.HashMap;
11import java.util.LinkedList;
12import java.util.List;
13import java.util.Map;
14
15import org.openstreetmap.josm.command.Command;
16import org.openstreetmap.josm.command.DeleteCommand;
17import org.openstreetmap.josm.data.osm.OsmPrimitive;
18import org.openstreetmap.josm.data.osm.Relation;
19import org.openstreetmap.josm.data.osm.RelationMember;
20import org.openstreetmap.josm.data.validation.Severity;
21import org.openstreetmap.josm.data.validation.Test;
22import org.openstreetmap.josm.data.validation.TestError;
23import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
24import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
25import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
26import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
27import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
28import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
29import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
30import org.openstreetmap.josm.tools.Utils;
31
32/**
33 * Check for wrong relations.
34 * @since 3669
35 */
36public class RelationChecker extends Test {
37
38 /** Role {0} unknown in templates {1} */
39 public static final int ROLE_UNKNOWN = 1701;
40 /** Empty role type found when expecting one of {0} */
41 public static final int ROLE_EMPTY = 1702;
42 /** Role member does not match expression {0} in template {1} */
43 public static final int WRONG_TYPE = 1703;
44 /** Number of {0} roles too high ({1}) */
45 public static final int HIGH_COUNT = 1704;
46 /** Number of {0} roles too low ({1}) */
47 public static final int LOW_COUNT = 1705;
48 /** Role {0} missing */
49 public static final int ROLE_MISSING = 1706;
50 /** Relation type is unknown */
51 public static final int RELATION_UNKNOWN = 1707;
52 /** Relation is empty */
53 public static final int RELATION_EMPTY = 1708;
54
55 /**
56 * Error message used to group errors related to role problems.
57 * @since 6731
58 */
59 public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem");
60
61 /**
62 * Constructor
63 */
64 public RelationChecker() {
65 super(tr("Relation checker"),
66 tr("Checks for errors in relations."));
67 }
68
69 @Override
70 public void initialize() {
71 initializePresets();
72 }
73
74 private static Collection<TaggingPreset> relationpresets = new LinkedList<>();
75
76 /**
77 * Reads the presets data.
78 */
79 public static synchronized void initializePresets() {
80 if (!relationpresets.isEmpty()) {
81 // the presets have already been initialized
82 return;
83 }
84 for (TaggingPreset p : TaggingPresets.getTaggingPresets()) {
85 for (TaggingPresetItem i : p.data) {
86 if (i instanceof Roles) {
87 relationpresets.add(p);
88 break;
89 }
90 }
91 }
92 }
93
94 private static class RolePreset {
95 private final List<Role> roles;
96 private final String name;
97
98 RolePreset(List<Role> roles, String name) {
99 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}
Note: See TracBrowser for help on using the repository browser.