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