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