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.ArrayList;
009import java.util.Collection;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.LinkedList;
013import java.util.List;
014import java.util.Set;
015
016import org.openstreetmap.josm.command.Command;
017import org.openstreetmap.josm.command.DeleteCommand;
018import org.openstreetmap.josm.data.osm.Node;
019import org.openstreetmap.josm.data.osm.OsmPrimitive;
020import org.openstreetmap.josm.data.osm.Relation;
021import org.openstreetmap.josm.data.osm.RelationMember;
022import org.openstreetmap.josm.data.osm.Way;
023import org.openstreetmap.josm.data.validation.Severity;
024import org.openstreetmap.josm.data.validation.Test;
025import org.openstreetmap.josm.data.validation.TestError;
026import org.openstreetmap.josm.gui.tagging.TaggingPreset;
027import org.openstreetmap.josm.gui.tagging.TaggingPresetItem;
028import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Key;
029import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role;
030import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles;
031import org.openstreetmap.josm.gui.tagging.TaggingPresetType;
032import org.openstreetmap.josm.gui.tagging.TaggingPresets;
033
034/**
035 * Check for wrong relations.
036 * @since 3669
037 */
038public class RelationChecker extends Test {
039
040    protected static final int ROLE_UNKNOWN      = 1701;
041    protected static final int ROLE_EMPTY        = 1702;
042    protected static final int WRONG_TYPE        = 1703;
043    protected static final int HIGH_COUNT        = 1704;
044    protected static final int LOW_COUNT         = 1705;
045    protected static final int ROLE_MISSING      = 1706;
046    protected static final int RELATION_UNKNOWN  = 1707;
047    protected static final int RELATION_EMPTY    = 1708;
048
049    /**
050     * Error message used to group errors related to role problems.
051     * @since 6731
052     */
053    public static final String ROLE_VERIF_PROBLEM_MSG = tr("Role verification problem");
054
055    /**
056     * Constructor
057     */
058    public RelationChecker() {
059        super(tr("Relation checker"),
060                tr("Checks for errors in relations."));
061    }
062
063    @Override
064    public void initialize() {
065        initializePresets();
066    }
067
068    private static Collection<TaggingPreset> relationpresets = new LinkedList<>();
069
070    /**
071     * Reads the presets data.
072     */
073    public static synchronized void initializePresets() {
074        if (!relationpresets.isEmpty()) {
075            // the presets have already been initialized
076            return;
077        }
078        for (TaggingPreset p : TaggingPresets.getTaggingPresets()) {
079            for (TaggingPresetItem i : p.data) {
080                if (i instanceof Roles) {
081                    relationpresets.add(p);
082                    break;
083                }
084            }
085        }
086    }
087
088    private static class RoleInfo {
089        private int total = 0;
090        private Collection<Node> nodes = new LinkedList<>();
091        private Collection<Way> ways = new LinkedList<>();
092        private Collection<Way> openways = new LinkedList<>();
093        private Collection<Relation> relations = new LinkedList<>();
094    }
095
096    @Override
097    public void visit(Relation n) {
098        LinkedList<Role> allroles = buildAllRoles(n);
099        if (allroles.isEmpty() && n.hasTag("type", "route")
100                && n.hasTag("route", "train", "subway", "monorail", "tram", "bus", "trolleybus", "aerialway", "ferry")) {
101            errors.add(new TestError(this, Severity.WARNING,
102                    tr("Route scheme is unspecified. Add {0} ({1}=public_transport; {2}=legacy)", "public_transport:version", "2", "1"),
103                    RELATION_UNKNOWN, n));
104        } else if (allroles.isEmpty()) {
105            errors.add(new TestError(this, Severity.WARNING, tr("Relation type is unknown"), RELATION_UNKNOWN, n));
106        }
107
108        HashMap<String, RoleInfo> map = buildRoleInfoMap(n);
109        if (map.isEmpty()) {
110            errors.add(new TestError(this, Severity.ERROR, tr("Relation is empty"), RELATION_EMPTY, n));
111        } else if (!allroles.isEmpty()) {
112            checkRoles(n, allroles, map);
113        }
114    }
115
116    private HashMap<String, RoleInfo> buildRoleInfoMap(Relation n) {
117        HashMap<String,RoleInfo> map = new HashMap<>();
118        for (RelationMember m : n.getMembers()) {
119            String role = m.getRole();
120            RoleInfo ri = map.get(role);
121            if (ri == null) {
122                ri = new RoleInfo();
123            }
124            ri.total++;
125            if (m.isRelation()) {
126                ri.relations.add(m.getRelation());
127            } else if(m.isWay()) {
128                ri.ways.add(m.getWay());
129                if (!m.getWay().isClosed()) {
130                    ri.openways.add(m.getWay());
131                }
132            }
133            else if (m.isNode()) {
134                ri.nodes.add(m.getNode());
135            }
136            map.put(role, ri);
137        }
138        return map;
139    }
140
141    private LinkedList<Role> buildAllRoles(Relation n) {
142        LinkedList<Role> allroles = new LinkedList<>();
143        for (TaggingPreset p : relationpresets) {
144            boolean matches = true;
145            Roles r = null;
146            for (TaggingPresetItem i : p.data) {
147                if (i instanceof Key) {
148                    Key k = (Key) i;
149                    if (!k.value.equals(n.get(k.key))) {
150                        matches = false;
151                        break;
152                    }
153                } else if (i instanceof Roles) {
154                    r = (Roles) i;
155                }
156            }
157            if (matches && r != null) {
158                allroles.addAll(r.roles);
159            }
160        }
161        return allroles;
162    }
163
164    private void checkRoles(Relation n, LinkedList<Role> allroles, HashMap<String, RoleInfo> map) {
165        List<String> done = new LinkedList<>();
166        // Remove empty roles if several exist (like in route=hiking, see #9844)
167        List<Role> emptyRoles = new LinkedList<>();
168        for (Role r : allroles) {
169            if ("".equals(r.key)) {
170                emptyRoles.add(r);
171            }
172        }
173        if (emptyRoles.size() > 1) {
174            allroles.removeAll(emptyRoles);
175        }
176        for (Role r : allroles) {
177            done.add(r.key);
178            String keyname = r.key;
179            if ("".equals(keyname)) {
180                keyname = tr("<empty>");
181            }
182            RoleInfo ri = map.get(r.key);
183            checkRoleCounts(n, r, keyname, ri);
184            if (ri != null) {
185                if (r.types != null) {
186                    checkRoleTypes(n, r, keyname, ri);
187                }
188                if (r.memberExpression != null) {
189                    checkRoleMemberExpressions(n, r, keyname, ri);
190                }
191            }
192        }
193        for (String key : map.keySet()) {
194            if (!done.contains(key)) {
195                if (key.length() > 0) {
196                    String s = marktr("Role {0} unknown");
197                    errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
198                            tr(s, key), MessageFormat.format(s, key), ROLE_UNKNOWN, n));
199                } else {
200                    String s = marktr("Empty role found");
201                    errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
202                            tr(s), s, ROLE_EMPTY, n));
203                }
204            }
205        }
206    }
207
208    private void checkRoleMemberExpressions(Relation n, Role r, String keyname, RoleInfo ri) {
209        Set<OsmPrimitive> notMatching = new HashSet<>();
210        Collection<OsmPrimitive> allPrimitives = new ArrayList<>();
211        allPrimitives.addAll(ri.nodes);
212        allPrimitives.addAll(ri.ways);
213        allPrimitives.addAll(ri.relations);
214        for (OsmPrimitive p : allPrimitives) {
215            if (p.isUsable() && !r.memberExpression.match(p)) {
216                notMatching.add(p);
217            }
218        }
219        if (!notMatching.isEmpty()) {
220            String s = marktr("Member for role ''{0}'' does not match ''{1}''");
221            LinkedList<OsmPrimitive> highlight = new LinkedList<>(notMatching);
222            highlight.addFirst(n);
223            errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
224                    tr(s, keyname, r.memberExpression), MessageFormat.format(s, keyname, r.memberExpression), WRONG_TYPE,
225                    highlight, notMatching));
226        }
227    }
228
229    private void checkRoleTypes(Relation n, Role r, String keyname, RoleInfo ri) {
230        Set<OsmPrimitive> wrongTypes = new HashSet<>();
231        if (!r.types.contains(TaggingPresetType.WAY)) {
232            wrongTypes.addAll(r.types.contains(TaggingPresetType.CLOSEDWAY) ? ri.openways : ri.ways);
233        }
234        if (!r.types.contains(TaggingPresetType.NODE)) {
235            wrongTypes.addAll(ri.nodes);
236        }
237        if (!r.types.contains(TaggingPresetType.RELATION)) {
238            wrongTypes.addAll(ri.relations);
239        }
240        if (!wrongTypes.isEmpty()) {
241            String s = marktr("Member for role {0} of wrong type");
242            LinkedList<OsmPrimitive> highlight = new LinkedList<>(wrongTypes);
243            highlight.addFirst(n);
244            errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
245                    tr(s, keyname), MessageFormat.format(s, keyname), WRONG_TYPE,
246                    highlight, wrongTypes));
247        }
248    }
249
250    private void checkRoleCounts(Relation n, Role r, String keyname, RoleInfo ri) {
251        long count = (ri == null) ? 0 : ri.total;
252        long vc = r.getValidCount(count);
253        if (count != vc) {
254            if (count == 0) {
255                String s = marktr("Role {0} missing");
256                errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
257                        tr(s, keyname), MessageFormat.format(s, keyname), ROLE_MISSING, n));
258            }
259            else if (vc > count) {
260                String s = marktr("Number of {0} roles too low ({1})");
261                errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
262                        tr(s, keyname, count), MessageFormat.format(s, keyname, count), LOW_COUNT, n));
263            } else {
264                String s = marktr("Number of {0} roles too high ({1})");
265                errors.add(new TestError(this, Severity.WARNING, ROLE_VERIF_PROBLEM_MSG,
266                        tr(s, keyname, count), MessageFormat.format(s, keyname, count), HIGH_COUNT, n));
267            }
268        }
269    }
270
271    @Override
272    public Command fixError(TestError testError) {
273        if (isFixable(testError)) {
274            return new DeleteCommand(testError.getPrimitives());
275        }
276        return null;
277    }
278
279    @Override
280    public boolean isFixable(TestError testError) {
281        Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
282        return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew();
283    }
284}