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

Last change on this file since 11466 was 11466, checked in by bastiK, 7 years ago

fixed #14252 - do not translate relation roles; add apostrophes

  • Property svn:eol-style set to native
File size: 14.7 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.util.Collection;
8import java.util.EnumSet;
9import java.util.HashMap;
10import java.util.LinkedList;
11import java.util.List;
12import java.util.Map;
13import java.util.stream.Collectors;
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.OsmPrimitiveType;
19import org.openstreetmap.josm.data.osm.Relation;
20import org.openstreetmap.josm.data.osm.RelationMember;
21import org.openstreetmap.josm.data.validation.Severity;
22import org.openstreetmap.josm.data.validation.Test;
23import org.openstreetmap.josm.data.validation.TestError;
24import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
25import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
26import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
27import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
28import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
29import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
30import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
31import org.openstreetmap.josm.tools.Utils;
32
33/**
34 * Check for wrong relations.
35 * @since 3669
36 */
37public class RelationChecker extends Test {
38
39 // CHECKSTYLE.OFF: SingleSpaceSeparator
40 /** Role {0} unknown in templates {1} */
41 public static final int ROLE_UNKNOWN = 1701;
42 /** Empty role type found when expecting one of {0} */
43 public static final int ROLE_EMPTY = 1702;
44 /** Role member does not match expression {0} in template {1} */
45 public static final int WRONG_TYPE = 1703;
46 /** Number of {0} roles too high ({1}) */
47 public static final int HIGH_COUNT = 1704;
48 /** Number of {0} roles too low ({1}) */
49 public static final int LOW_COUNT = 1705;
50 /** Role {0} missing */
51 public static final int ROLE_MISSING = 1706;
52 /** Relation type is unknown */
53 public static final int RELATION_UNKNOWN = 1707;
54 /** Relation is empty */
55 public static final int RELATION_EMPTY = 1708;
56 // CHECKSTYLE.ON: SingleSpaceSeparator
57
58 /**
59 * Error message used to group errors related to role problems.
60 * @since 6731
61 */
62 public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem");
63
64 /**
65 * Constructor
66 */
67 public RelationChecker() {
68 super(tr("Relation checker"),
69 tr("Checks for errors in relations."));
70 }
71
72 @Override
73 public void initialize() {
74 initializePresets();
75 }
76
77 private static final Collection<TaggingPreset> relationpresets = new LinkedList<>();
78
79 /**
80 * Reads the presets data.
81 */
82 public static synchronized void initializePresets() {
83 if (!relationpresets.isEmpty()) {
84 // the presets have already been initialized
85 return;
86 }
87 for (TaggingPreset p : TaggingPresets.getTaggingPresets()) {
88 for (TaggingPresetItem i : p.data) {
89 if (i instanceof Roles) {
90 relationpresets.add(p);
91 break;
92 }
93 }
94 }
95 }
96
97 private static class RolePreset {
98 private final List<Role> roles;
99 private final String name;
100
101 RolePreset(List<Role> roles, String name) {
102 this.roles = roles;
103 this.name = name;
104 }
105 }
106
107 private static class RoleInfo {
108 private int total;
109 }
110
111 @Override
112 public void visit(Relation n) {
113 Map<String, RolePreset> allroles = buildAllRoles(n);
114 if (allroles.isEmpty() && n.hasTag("type", "route")
115 && n.hasTag("route", "train", "subway", "monorail", "tram", "bus", "trolleybus", "aerialway", "ferry")) {
116 errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN)
117 .message(tr("Route scheme is unspecified. Add {0} ({1}=public_transport; {2}=legacy)", "public_transport:version", "2", "1"))
118 .primitives(n)
119 .build());
120 } else if (allroles.isEmpty()) {
121 errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN)
122 .message(tr("Relation type is unknown"))
123 .primitives(n)
124 .build());
125 }
126
127 Map<String, RoleInfo> map = buildRoleInfoMap(n);
128 if (map.isEmpty()) {
129 errors.add(TestError.builder(this, Severity.ERROR, RELATION_EMPTY)
130 .message(tr("Relation is empty"))
131 .primitives(n)
132 .build());
133 } else if (!allroles.isEmpty()) {
134 checkRoles(n, allroles, map);
135 }
136 }
137
138 private static Map<String, RoleInfo> buildRoleInfoMap(Relation n) {
139 Map<String, RoleInfo> map = new HashMap<>();
140 for (RelationMember m : n.getMembers()) {
141 String role = m.getRole();
142 RoleInfo ri = map.get(role);
143 if (ri == null) {
144 ri = new RoleInfo();
145 map.put(role, ri);
146 }
147 ri.total++;
148 }
149 return map;
150 }
151
152 // return Roles grouped by key
153 private static Map<String, RolePreset> buildAllRoles(Relation n) {
154 Map<String, RolePreset> allroles = new HashMap<>();
155
156 for (TaggingPreset p : relationpresets) {
157 final boolean matches = TaggingPresetItem.matches(Utils.filteredCollection(p.data, KeyedItem.class), n.getKeys());
158 final Roles r = Utils.find(p.data, Roles.class);
159 if (matches && r != null) {
160 for (Role role: r.roles) {
161 String key = role.key;
162 List<Role> roleGroup;
163 if (allroles.containsKey(key)) {
164 roleGroup = allroles.get(key).roles;
165 } else {
166 roleGroup = new LinkedList<>();
167 allroles.put(key, new RolePreset(roleGroup, p.name));
168 }
169 roleGroup.add(role);
170 }
171 }
172 }
173 return allroles;
174 }
175
176 private boolean checkMemberType(Role r, RelationMember member) {
177 if (r.types != null) {
178 switch (member.getDisplayType()) {
179 case NODE:
180 return r.types.contains(TaggingPresetType.NODE);
181 case CLOSEDWAY:
182 return r.types.contains(TaggingPresetType.CLOSEDWAY);
183 case WAY:
184 return r.types.contains(TaggingPresetType.WAY);
185 case MULTIPOLYGON:
186 return r.types.contains(TaggingPresetType.MULTIPOLYGON);
187 case RELATION:
188 return r.types.contains(TaggingPresetType.RELATION);
189 default: // not matching type
190 return false;
191 }
192 } else {
193 // if no types specified, then test is passed
194 return true;
195 }
196 }
197
198 /**
199 * get all role definition for specified key and check, if some definition matches
200 *
201 * @param rolePreset containing preset for role of the member
202 * @param member to be verified
203 * @param n relation to be verified
204 * @return <tt>true</tt> if member passed any of definition within preset
205 *
206 */
207 private boolean checkMemberExpressionAndType(RolePreset rolePreset, RelationMember member, Relation n) {
208 if (rolePreset == null || rolePreset.roles == null) {
209 // no restrictions on role types
210 return true;
211 }
212 TestError possibleMatchError = null;
213 // iterate through all of the role definition within preset
214 // and look for any matching definition
215 for (Role r: rolePreset.roles) {
216 if (checkMemberType(r, member)) {
217 // member type accepted by role definition
218 if (r.memberExpression == null) {
219 // no member expression - so all requirements met
220 return true;
221 } else {
222 // verify if preset accepts such member
223 OsmPrimitive primitive = member.getMember();
224 if (!primitive.isUsable()) {
225 // if member is not usable (i.e. not present in working set)
226 // we can't verify expression - so we just skip it
227 return true;
228 } else {
229 // verify expression
230 if (r.memberExpression.match(primitive)) {
231 return true;
232 } else {
233 // possible match error
234 // we still need to iterate further, as we might have
235 // different present, for which memberExpression will match
236 // but stash the error in case no better reason will be found later
237 possibleMatchError = TestError.builder(this, Severity.WARNING, WRONG_TYPE)
238 .message(ROLE_VERIF_PROBLEM_MSG,
239 marktr("Role member does not match expression ''{0}'' in template {1}"),
240 r.memberExpression, rolePreset.name)
241 .primitives(member.getMember().isUsable() ? member.getMember() : n)
242 .build();
243 }
244 }
245 }
246 } else if (OsmPrimitiveType.RELATION.equals(member.getType()) && !member.getMember().isUsable()
247 && r.types.contains(TaggingPresetType.MULTIPOLYGON)) {
248 // if relation is incomplete we cannot verify if it's a multipolygon - so we just skip it
249 return true;
250 }
251 }
252
253 if (possibleMatchError != null) {
254 // if any error found, then assume that member type was correct
255 // and complain about not matching the memberExpression
256 // (the only failure, that we could gather)
257 errors.add(possibleMatchError);
258 } else {
259 // no errors found till now. So member at least failed at matching the type
260 // it could also fail at memberExpression, but we can't guess at which
261
262 // prepare Set of all accepted types in template
263 Collection<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
264 for (Role r: rolePreset.roles) {
265 types.addAll(r.types);
266 }
267
268 // convert in localization friendly way to string of accepted types
269 String typesStr = types.stream().map(x -> tr(x.getName())).collect(Collectors.joining("/"));
270
271 errors.add(TestError.builder(this, Severity.WARNING, WRONG_TYPE)
272 .message(ROLE_VERIF_PROBLEM_MSG,
273 marktr("Role member type ''{0}'' does not match accepted list of ''{1}'' in template {2}"),
274 member.getType(), typesStr, rolePreset.name)
275 .primitives(member.getMember().isUsable() ? member.getMember() : n)
276 .build());
277 }
278 return false;
279 }
280
281 /**
282 *
283 * @param n relation to validate
284 * @param allroles contains presets for specified relation
285 * @param map contains statistics of occurances of specified role types in relation
286 */
287 private void checkRoles(Relation n, Map<String, RolePreset> allroles, Map<String, RoleInfo> map) {
288 // go through all members of relation
289 for (RelationMember member: n.getMembers()) {
290 String role = member.getRole();
291
292 // error reporting done inside
293 checkMemberExpressionAndType(allroles.get(role), member, n);
294 }
295
296 // verify role counts based on whole role sets
297 for (RolePreset rp: allroles.values()) {
298 for (Role r: rp.roles) {
299 String keyname = r.key;
300 if (keyname.isEmpty()) {
301 keyname = tr("<empty>");
302 }
303 checkRoleCounts(n, r, keyname, map.get(r.key));
304 }
305 }
306 // verify unwanted members
307 for (String key : map.keySet()) {
308 if (!allroles.containsKey(key)) {
309 String templates = allroles.keySet().stream().collect(Collectors.joining("/"));
310
311 if (!key.isEmpty()) {
312
313 errors.add(TestError.builder(this, Severity.WARNING, ROLE_UNKNOWN)
314 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' unknown in templates ''{1}''"), key, templates)
315 .primitives(n)
316 .build());
317 } else {
318 errors.add(TestError.builder(this, Severity.WARNING, ROLE_EMPTY)
319 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Empty role type found when expecting one of ''{0}''"), templates)
320 .primitives(n)
321 .build());
322 }
323 }
324 }
325 }
326
327 private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) {
328 long count = (ri == null) ? 0 : ri.total;
329 long vc = r.getValidCount(count);
330 if (count != vc) {
331 if (count == 0) {
332 errors.add(TestError.builder(this, Severity.WARNING, ROLE_MISSING)
333 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' missing"), keyname)
334 .primitives(n)
335 .build());
336 } else if (vc > count) {
337 errors.add(TestError.builder(this, Severity.WARNING, LOW_COUNT)
338 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too low ({1})"), keyname, count)
339 .primitives(n)
340 .build());
341 } else {
342 errors.add(TestError.builder(this, Severity.WARNING, HIGH_COUNT)
343 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too high ({1})"), keyname, count)
344 .primitives(n)
345 .build());
346 }
347 }
348 }
349
350 @Override
351 public Command fixError(TestError testError) {
352 if (isFixable(testError) && !testError.getPrimitives().iterator().next().isDeleted()) {
353 return new DeleteCommand(testError.getPrimitives());
354 }
355 return null;
356 }
357
358 @Override
359 public boolean isFixable(TestError testError) {
360 Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
361 return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew();
362 }
363}
Note: See TracBrowser for help on using the repository browser.