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

Last change on this file since 13637 was 13637, checked in by Don-vip, 6 years ago

minor fixes

  • 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.LinkedHashMap;
11import java.util.LinkedList;
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 RoleInfo {
98 private int total;
99 }
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}
Note: See TracBrowser for help on using the repository browser.