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