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