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