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