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

Last change on this file since 11989 was 11989, checked in by stoecker, 7 years ago

add support for type=building relation and support variable role names using regexp (level_0, level_1, ...)

  • Property svn:eol-style set to native
File size: 14.2 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 RoleInfo {
98 private int total;
99 }
100
101 @Override
102 public void visit(Relation n) {
103 List<Role> 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 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 static List<Role> buildAllRoles(Relation n) {
144 List<Role> allroles = new LinkedList<>();
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 allroles.addAll(r.roles);
151 }
152 }
153 return allroles;
154 }
155
156 private boolean checkMemberType(Role r, RelationMember member) {
157 if (r.types != null) {
158 switch (member.getDisplayType()) {
159 case NODE:
160 return r.types.contains(TaggingPresetType.NODE);
161 case CLOSEDWAY:
162 return r.types.contains(TaggingPresetType.CLOSEDWAY);
163 case WAY:
164 return r.types.contains(TaggingPresetType.WAY);
165 case MULTIPOLYGON:
166 return r.types.contains(TaggingPresetType.MULTIPOLYGON);
167 case RELATION:
168 return r.types.contains(TaggingPresetType.RELATION);
169 default: // not matching type
170 return false;
171 }
172 } else {
173 // if no types specified, then test is passed
174 return true;
175 }
176 }
177
178 /**
179 * get all role definition for specified key and check, if some definition matches
180 *
181 * @param allroles containing list of possible role presets of the member
182 * @param member to be verified
183 * @param n relation to be verified
184 * @return <tt>true</tt> if member passed any of definition within preset
185 *
186 */
187 private boolean checkMemberExpressionAndType(List<Role> allroles, RelationMember member, Relation n) {
188 String role = member.getRole();
189 String name = null;
190 // Set of all accepted types in template
191 Collection<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
192 TestError possibleMatchError = null;
193 // iterate through all of the role definition within preset
194 // and look for any matching definition
195 for (Role r: allroles) {
196 if (!r.isRole(role)) {
197 continue;
198 }
199 name = r.key;
200 types.addAll(r.types);
201 if (checkMemberType(r, member)) {
202 // member type accepted by role definition
203 if (r.memberExpression == null) {
204 // no member expression - so all requirements met
205 return true;
206 } else {
207 // verify if preset accepts such member
208 OsmPrimitive primitive = member.getMember();
209 if (!primitive.isUsable()) {
210 // if member is not usable (i.e. not present in working set)
211 // we can't verify expression - so we just skip it
212 return true;
213 } else {
214 // verify expression
215 if (r.memberExpression.match(primitive)) {
216 return true;
217 } else {
218 // possible match error
219 // we still need to iterate further, as we might have
220 // different present, for which memberExpression will match
221 // but stash the error in case no better reason will be found later
222 possibleMatchError = TestError.builder(this, Severity.WARNING, WRONG_TYPE)
223 .message(ROLE_VERIF_PROBLEM_MSG,
224 marktr("Role of relation member does not match expression ''{0}'' in template {1}"),
225 r.memberExpression, name)
226 .primitives(member.getMember().isUsable() ? member.getMember() : n)
227 .build();
228 }
229 }
230 }
231 } else if (OsmPrimitiveType.RELATION.equals(member.getType()) && !member.getMember().isUsable()
232 && r.types.contains(TaggingPresetType.MULTIPOLYGON)) {
233 // if relation is incomplete we cannot verify if it's a multipolygon - so we just skip it
234 return true;
235 }
236 }
237
238 if (name == null) {
239 return true;
240 }
241 else if (possibleMatchError != null) {
242 // if any error found, then assume that member type was correct
243 // and complain about not matching the memberExpression
244 // (the only failure, that we could gather)
245 errors.add(possibleMatchError);
246 } else {
247 // no errors found till now. So member at least failed at matching the type
248 // it could also fail at memberExpression, but we can't guess at which
249
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 return false;
261 }
262
263 /**
264 *
265 * @param n relation to validate
266 * @param allroles contains presets for specified relation
267 * @param map contains statistics of occurances of specified role types in relation
268 */
269 private void checkRoles(Relation n, List<Role> allroles, Map<String, RoleInfo> map) {
270 // go through all members of relation
271 for (RelationMember member: n.getMembers()) {
272 // error reporting done inside
273 checkMemberExpressionAndType(allroles, member, n);
274 }
275
276 // verify role counts based on whole role sets
277 for (Role r: allroles) {
278 String keyname = r.key;
279 if (keyname.isEmpty()) {
280 keyname = tr("<empty>");
281 }
282 checkRoleCounts(n, r, keyname, map.get(r.key));
283 }
284 if("network".equals(n.get("type")) && !"bicycle".equals(n.get("route"))) {
285 return;
286 }
287 // verify unwanted members
288 for (String key : map.keySet()) {
289 boolean found = false;
290 for (Role r: allroles) {
291 if (r.isRole(key)) {
292 found = true;
293 break;
294 }
295 }
296
297 if (!found) {
298 String templates = allroles.stream().map(r -> r.key).collect(Collectors.joining("/"));
299
300 if (!key.isEmpty()) {
301 errors.add(TestError.builder(this, Severity.WARNING, ROLE_UNKNOWN)
302 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' unknown in templates ''{1}''"), key, templates)
303 .primitives(n)
304 .build());
305 } else {
306 errors.add(TestError.builder(this, Severity.WARNING, ROLE_EMPTY)
307 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Empty role type found when expecting one of ''{0}''"), templates)
308 .primitives(n)
309 .build());
310 }
311 }
312 }
313 }
314
315 private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) {
316 long count = (ri == null) ? 0 : ri.total;
317 long vc = r.getValidCount(count);
318 if (count != vc) {
319 if (count == 0) {
320 errors.add(TestError.builder(this, Severity.WARNING, ROLE_MISSING)
321 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' missing"), keyname)
322 .primitives(n)
323 .build());
324 } else if (vc > count) {
325 errors.add(TestError.builder(this, Severity.WARNING, LOW_COUNT)
326 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too low ({1})"), keyname, count)
327 .primitives(n)
328 .build());
329 } else {
330 errors.add(TestError.builder(this, Severity.WARNING, HIGH_COUNT)
331 .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too high ({1})"), keyname, count)
332 .primitives(n)
333 .build());
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.