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