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