001// License: GPL. For details, see LICENSE file.
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.util.ArrayList;
008import java.util.Collection;
009import java.util.EnumSet;
010import java.util.HashMap;
011import java.util.LinkedHashMap;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Map;
015import java.util.stream.Collectors;
016
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.command.DeleteCommand;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
021import org.openstreetmap.josm.data.osm.Relation;
022import org.openstreetmap.josm.data.osm.RelationMember;
023import org.openstreetmap.josm.data.validation.OsmValidator;
024import org.openstreetmap.josm.data.validation.Severity;
025import org.openstreetmap.josm.data.validation.Test;
026import org.openstreetmap.josm.data.validation.TestError;
027import org.openstreetmap.josm.gui.progress.ProgressMonitor;
028import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
029import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
030import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetListener;
031import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
032import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
033import org.openstreetmap.josm.gui.tagging.presets.items.KeyedItem;
034import org.openstreetmap.josm.gui.tagging.presets.items.Roles;
035import org.openstreetmap.josm.gui.tagging.presets.items.Roles.Role;
036import org.openstreetmap.josm.tools.SubclassFilteredCollection;
037import org.openstreetmap.josm.tools.Utils;
038
039/**
040 * Check for wrong relations.
041 * @since 3669
042 */
043public class RelationChecker extends Test implements TaggingPresetListener {
044
045    // CHECKSTYLE.OFF: SingleSpaceSeparator
046    /** Role ''{0}'' is not in templates ''{1}'' */
047    public static final int ROLE_UNKNOWN     = 1701;
048    /** Empty role found when expecting one of ''{0}'' */
049    public static final int ROLE_EMPTY       = 1702;
050    /** Role of relation member does not match template expression ''{0}'' in preset {1} */
051    public static final int WRONG_ROLE       = 1708;
052    /** Number of ''{0}'' roles too high ({1}) */
053    public static final int HIGH_COUNT       = 1704;
054    /** Number of ''{0}'' roles too low ({1}) */
055    public static final int LOW_COUNT        = 1705;
056    /** Role ''{0}'' missing */
057    public static final int ROLE_MISSING     = 1706;
058    /** Relation type is unknown */
059    public static final int RELATION_UNKNOWN = 1707;
060    /** Relation is empty */
061    public static final int RELATION_EMPTY   = 1708;
062    /** Type ''{0}'' of relation member with role ''{1}'' does not match accepted types ''{2}'' in preset {3} */
063    public static final int WRONG_TYPE       = 1709;
064    // CHECKSTYLE.ON: SingleSpaceSeparator
065
066    /**
067     * Error message used to group errors related to role problems.
068     * @since 6731
069     */
070    public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem");
071    private boolean ignoreMultiPolygons;
072    private boolean ignoreTurnRestrictions;
073
074    /**
075     * Constructor
076     */
077    public RelationChecker() {
078        super(tr("Relation checker"),
079                tr("Checks for errors in relations."));
080    }
081
082    @Override
083    public void initialize() {
084        TaggingPresets.addListener(this);
085        initializePresets();
086    }
087
088    private static final Collection<TaggingPreset> relationpresets = new LinkedList<>();
089
090    /**
091     * Reads the presets data.
092     */
093    public static synchronized void initializePresets() {
094        if (!relationpresets.isEmpty()) {
095            // the presets have already been initialized
096            return;
097        }
098        for (TaggingPreset p : TaggingPresets.getTaggingPresets()) {
099            for (TaggingPresetItem i : p.data) {
100                if (i instanceof Roles) {
101                    relationpresets.add(p);
102                    break;
103                }
104            }
105        }
106    }
107
108    private static class RoleInfo {
109        private int total;
110    }
111
112    @Override
113    public void startTest(ProgressMonitor progressMonitor) {
114        super.startTest(progressMonitor);
115
116        for (Test t : OsmValidator.getEnabledTests(false)) {
117            if (t instanceof MultipolygonTest) {
118                ignoreMultiPolygons = true;
119            }
120            if (t instanceof TurnrestrictionTest) {
121                ignoreTurnRestrictions = true;
122            }
123        }
124    }
125
126    @Override
127    public void visit(Relation n) {
128        Map<String, RoleInfo> map = buildRoleInfoMap(n);
129        if (map.isEmpty()) {
130            errors.add(TestError.builder(this, Severity.ERROR, RELATION_EMPTY)
131                    .message(tr("Relation is empty"))
132                    .primitives(n)
133                    .build());
134        }
135        if (ignoreMultiPolygons && n.isMultipolygon()) {
136            // see #17010: don't report same problem twice
137            return;
138        }
139        if (ignoreTurnRestrictions && n.hasTag("type", "restriction")) {
140            // see #17561: don't report same problem twice
141            return;
142        }
143        Map<Role, String> allroles = buildAllRoles(n);
144        if (allroles.isEmpty() && n.hasTag("type", "route")
145                && n.hasTag("route", "train", "subway", "monorail", "tram", "bus", "trolleybus", "aerialway", "ferry")) {
146            errors.add(TestError.builder(this, Severity.WARNING, RELATION_UNKNOWN)
147                    .message(tr("Route scheme is unspecified. Add {0} ({1}=public_transport; {2}=legacy)", "public_transport:version", "2", "1"))
148                    .primitives(n)
149                    .build());
150        } else if (allroles.isEmpty()) {
151            errors.add(TestError.builder(this, Severity.OTHER, RELATION_UNKNOWN)
152                    .message(tr("Relation type is unknown"))
153                    .primitives(n)
154                    .build());
155        }
156
157        if (!map.isEmpty() && !allroles.isEmpty()) {
158            checkRoles(n, allroles, map);
159        }
160    }
161
162    private static Map<String, RoleInfo> buildRoleInfoMap(Relation n) {
163        Map<String, RoleInfo> map = new HashMap<>();
164        for (RelationMember m : n.getMembers()) {
165            map.computeIfAbsent(m.getRole(), k -> new RoleInfo()).total++;
166        }
167        return map;
168    }
169
170    // return Roles grouped by key
171    private static Map<Role, String> buildAllRoles(Relation n) {
172        Map<Role, String> allroles = new LinkedHashMap<>();
173
174        for (TaggingPreset p : relationpresets) {
175            final boolean matches = TaggingPresetItem.matches(Utils.filteredCollection(p.data, KeyedItem.class), n.getKeys());
176            final SubclassFilteredCollection<TaggingPresetItem, Roles> roles = Utils.filteredCollection(p.data, Roles.class);
177            if (matches && !roles.isEmpty()) {
178                for (Role role: roles.iterator().next().roles) {
179                    allroles.put(role, p.name);
180                }
181            }
182        }
183        return allroles;
184    }
185
186    private static boolean checkMemberType(Role r, RelationMember member) {
187        if (r.types != null) {
188            switch (member.getDisplayType()) {
189            case NODE:
190                return r.types.contains(TaggingPresetType.NODE);
191            case CLOSEDWAY:
192                return r.types.contains(TaggingPresetType.CLOSEDWAY);
193            case WAY:
194                return r.types.contains(TaggingPresetType.WAY);
195            case MULTIPOLYGON:
196                return r.types.contains(TaggingPresetType.MULTIPOLYGON);
197            case RELATION:
198                return r.types.contains(TaggingPresetType.RELATION);
199            default: // not matching type
200                return false;
201            }
202        } else {
203            // if no types specified, then test is passed
204            return true;
205        }
206    }
207
208    /**
209     * get all role definition for specified key and check, if some definition matches
210     *
211     * @param allroles containing list of possible role presets of the member
212     * @param member to be verified
213     * @param n relation to be verified
214     * @return <code>true</code> if member passed any of definition within preset
215     *
216     */
217    private boolean checkMemberExpressionAndType(Map<Role, String> allroles, RelationMember member, Relation n) {
218        String role = member.getRole();
219        String name = null;
220        // Set of all accepted types in preset
221        Collection<TaggingPresetType> types = EnumSet.noneOf(TaggingPresetType.class);
222        TestError possibleMatchError = null;
223        // iterate through all of the role definition within preset
224        // and look for any matching definition
225        for (Map.Entry<Role, String> e : allroles.entrySet()) {
226            Role r = e.getKey();
227            if (!r.isRole(role)) {
228                continue;
229            }
230            name = e.getValue();
231            types.addAll(r.types);
232            if (checkMemberType(r, member)) {
233                // member type accepted by role definition
234                if (r.memberExpression == null) {
235                    // no member expression - so all requirements met
236                    return true;
237                } else {
238                    // verify if preset accepts such member
239                    OsmPrimitive primitive = member.getMember();
240                    if (!primitive.isUsable()) {
241                        // if member is not usable (i.e. not present in working set)
242                        // we can't verify expression - so we just skip it
243                        return true;
244                    } else {
245                        // verify expression
246                        if (r.memberExpression.match(primitive)) {
247                            return true;
248                        } else {
249                            // possible match error
250                            // we still need to iterate further, as we might have
251                            // different preset, for which memberExpression will match
252                            // but stash the error in case no better reason will be found later
253                            possibleMatchError = TestError.builder(this, Severity.WARNING, WRONG_ROLE)
254                                    .message(ROLE_VERIF_PROBLEM_MSG,
255                                            marktr("Role of relation member does not match template expression ''{0}'' in preset {1}"),
256                                            r.memberExpression, name)
257                                    .primitives(member.getMember().isUsable() ? member.getMember() : n)
258                                    .build();
259                        }
260                    }
261                }
262            } else if (OsmPrimitiveType.RELATION == member.getType() && !member.getMember().isUsable()
263                    && r.types.contains(TaggingPresetType.MULTIPOLYGON)) {
264                // if relation is incomplete we cannot verify if it's a multipolygon - so we just skip it
265                return true;
266            }
267        }
268
269        if (name == null) {
270           return true;
271        } else if (possibleMatchError != null) {
272            // if any error found, then assume that member type was correct
273            // and complain about not matching the memberExpression
274            // (the only failure, that we could gather)
275            errors.add(possibleMatchError);
276        } else {
277            // no errors found till now. So member at least failed at matching the type
278            // it could also fail at memberExpression, but we can't guess at which
279
280            // Do not raise an error for incomplete ways for which we expect them to be closed, as we cannot know
281            boolean ignored = member.getMember().isIncomplete() && OsmPrimitiveType.WAY == member.getType()
282                    && !types.contains(TaggingPresetType.WAY) && types.contains(TaggingPresetType.CLOSEDWAY);
283            if (!ignored) {
284                // convert in localization friendly way to string of accepted types
285                String typesStr = types.stream().map(x -> tr(x.getName())).collect(Collectors.joining("/"));
286
287                errors.add(TestError.builder(this, Severity.WARNING, WRONG_TYPE)
288                        .message(ROLE_VERIF_PROBLEM_MSG,
289                            marktr("Type ''{0}'' of relation member with role ''{1}'' does not match accepted types ''{2}'' in preset {3}"),
290                            member.getType(), member.getRole(), typesStr, name)
291                        .primitives(member.getMember().isUsable() ? member.getMember() : n)
292                        .build());
293            }
294        }
295        return false;
296    }
297
298    /**
299     *
300     * @param n relation to validate
301     * @param allroles contains presets for specified relation
302     * @param map contains statistics of occurrences of specified role in relation
303     */
304    private void checkRoles(Relation n, Map<Role, String> allroles, Map<String, RoleInfo> map) {
305        // go through all members of relation
306        for (RelationMember member: n.getMembers()) {
307            // error reporting done inside
308            checkMemberExpressionAndType(allroles, member, n);
309        }
310
311        // verify role counts based on whole role sets
312        for (Role r: allroles.keySet()) {
313            String keyname = r.key;
314            if (keyname.isEmpty()) {
315                keyname = tr("<empty>");
316            }
317            checkRoleCounts(n, r, keyname, map.get(r.key));
318        }
319        if ("network".equals(n.get("type")) && !"bicycle".equals(n.get("route"))) {
320            return;
321        }
322        // verify unwanted members
323        for (String key : map.keySet()) {
324            if (allroles.keySet().stream().noneMatch(role -> role.isRole(key))) {
325                String templates = allroles.keySet().stream().map(r -> r.key).collect(Collectors.joining("/"));
326                List<OsmPrimitive> primitives = new ArrayList<>(n.findRelationMembers(key));
327                primitives.add(0, n);
328
329                if (!key.isEmpty()) {
330                    errors.add(TestError.builder(this, Severity.WARNING, ROLE_UNKNOWN)
331                            .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' is not in templates ''{1}''"), key, templates)
332                            .primitives(primitives)
333                            .build());
334                } else {
335                    errors.add(TestError.builder(this, Severity.WARNING, ROLE_EMPTY)
336                            .message(ROLE_VERIF_PROBLEM_MSG, marktr("Empty role found when expecting one of ''{0}''"), templates)
337                            .primitives(primitives)
338                            .build());
339                }
340            }
341        }
342    }
343
344    private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) {
345        long count = (ri == null) ? 0 : ri.total;
346        long vc = r.getValidCount(count);
347        if (count != vc) {
348            if (count == 0) {
349                errors.add(TestError.builder(this, Severity.WARNING, ROLE_MISSING)
350                        .message(ROLE_VERIF_PROBLEM_MSG, marktr("Role ''{0}'' missing"), keyname)
351                        .primitives(n)
352                        .build());
353            } else if (vc > count) {
354                errors.add(TestError.builder(this, Severity.WARNING, LOW_COUNT)
355                        .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too low ({1})"), keyname, count)
356                        .primitives(n)
357                        .build());
358            } else {
359                errors.add(TestError.builder(this, Severity.WARNING, HIGH_COUNT)
360                        .message(ROLE_VERIF_PROBLEM_MSG, marktr("Number of ''{0}'' roles too high ({1})"), keyname, count)
361                        .primitives(n)
362                        .build());
363            }
364        }
365    }
366
367    @Override
368    public Command fixError(TestError testError) {
369        Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
370        if (isFixable(testError) && !primitives.iterator().next().isDeleted()) {
371            return new DeleteCommand(primitives);
372        }
373        return null;
374    }
375
376    @Override
377    public boolean isFixable(TestError testError) {
378        Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
379        return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew();
380    }
381
382    @Override
383    public void taggingPresetsModified() {
384        relationpresets.clear();
385        initializePresets();
386    }
387}