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.Arrays;
009import java.util.Collection;
010import java.util.HashMap;
011import java.util.HashSet;
012import java.util.LinkedList;
013import java.util.Set;
014
015import org.openstreetmap.josm.command.Command;
016import org.openstreetmap.josm.command.DeleteCommand;
017import org.openstreetmap.josm.data.osm.Node;
018import org.openstreetmap.josm.data.osm.OsmPrimitive;
019import org.openstreetmap.josm.data.osm.Relation;
020import org.openstreetmap.josm.data.osm.RelationMember;
021import org.openstreetmap.josm.data.osm.Way;
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.preferences.map.TaggingPresetPreference;
026import org.openstreetmap.josm.gui.tagging.TaggingPreset;
027import org.openstreetmap.josm.gui.tagging.TaggingPresetItem;
028import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Role;
029import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Key;
030import org.openstreetmap.josm.gui.tagging.TaggingPresetItems.Roles;
031import org.openstreetmap.josm.gui.tagging.TaggingPresetType;
032
033/**
034 * Check for wrong relations
035 *
036 */
037public class RelationChecker extends Test {
038
039    protected static final int ROLE_UNKNOWN      = 1701;
040    protected static final int ROLE_EMPTY        = 1702;
041    protected static final int WRONG_TYPE        = 1703;
042    protected static final int HIGH_COUNT        = 1704;
043    protected static final int LOW_COUNT         = 1705;
044    protected static final int ROLE_MISSING      = 1706;
045    protected static final int RELATION_UNKNOWN  = 1707;
046    protected static final int RELATION_EMPTY    = 1708;
047
048    /**
049     * Constructor
050     */
051    public RelationChecker() {
052        super(tr("Relation checker"),
053                tr("This plugin checks for errors in relations."));
054    }
055
056    @Override
057    public void initialize() {
058        initializePresets();
059    }
060
061    private static Collection<TaggingPreset> relationpresets = new LinkedList<TaggingPreset>();
062
063    /**
064     * Reads the presets data.
065     *
066     */
067    public void initializePresets() {
068        Collection<TaggingPreset> presets = TaggingPresetPreference.taggingPresets;
069        if (presets != null) {
070            for (TaggingPreset p : presets) {
071                for (TaggingPresetItem i : p.data) {
072                    if (i instanceof Roles) {
073                        relationpresets.add(p);
074                        break;
075                    }
076                }
077            }
078        }
079    }
080
081    private static class RoleInfo {
082        private int total = 0;
083        private Collection<Node> nodes = new LinkedList<Node>();
084        private Collection<Way> ways = new LinkedList<Way>();
085        private Collection<Way> openways = new LinkedList<Way>();
086        private Collection<Relation> relations = new LinkedList<Relation>();
087    }
088
089    @SuppressWarnings("unchecked")
090    @Override
091    public void visit(Relation n) {
092        LinkedList<Role> allroles = new LinkedList<Role>();
093        for (TaggingPreset p : relationpresets) {
094            boolean matches = true;
095            Roles r = null;
096            for (TaggingPresetItem i : p.data) {
097                if (i instanceof Key) {
098                    Key k = (Key) i;
099                    if (!k.value.equals(n.get(k.key))) {
100                        matches = false;
101                        break;
102                    }
103                } else if (i instanceof Roles) {
104                    r = (Roles) i;
105                }
106            }
107            if (matches && r != null) {
108                allroles.addAll(r.roles);
109            }
110        }
111        if (allroles.isEmpty()) {
112            errors.add( new TestError(this, Severity.WARNING, tr("Relation type is unknown"),
113                    RELATION_UNKNOWN, n) );
114        } else {
115            HashMap<String,RoleInfo> map = new HashMap<String, RoleInfo>();
116            for (RelationMember m : n.getMembers()) {
117                String s = "";
118                if (m.hasRole()) {
119                    s = m.getRole();
120                }
121                RoleInfo ri = map.get(s);
122                if (ri == null) {
123                    ri = new RoleInfo();
124                }
125                ri.total++;
126                if (m.isRelation()) {
127                    ri.relations.add(m.getRelation());
128                } else if(m.isWay()) {
129                    ri.ways.add(m.getWay());
130                    if (!m.getWay().isClosed()) {
131                        ri.openways.add(m.getWay());
132                    }
133                }
134                else if (m.isNode()) {
135                    ri.nodes.add(m.getNode());
136                }
137                map.put(s, ri);
138            }
139            if(map.isEmpty()) {
140                errors.add( new TestError(this, Severity.ERROR, tr("Relation is empty"),
141                        RELATION_EMPTY, n) );
142            } else {
143                LinkedList<String> done = new LinkedList<String>();
144                String errorMessage = tr("Role verification problem");
145                for (Role r : allroles) {
146                    done.add(r.key);
147                    String keyname = r.key;
148                    if ("".equals(keyname)) {
149                        keyname = tr("<empty>");
150                    }
151                    RoleInfo ri = map.get(r.key);
152                    long count = (ri == null) ? 0 : ri.total;
153                    long vc = r.getValidCount(count);
154                    if (count != vc) {
155                        if (count == 0) {
156                            String s = marktr("Role {0} missing");
157                            errors.add(new TestError(this, Severity.WARNING, errorMessage,
158                                    tr(s, keyname), MessageFormat.format(s, keyname), ROLE_MISSING, n));
159                        }
160                        else if (vc > count) {
161                            String s = marktr("Number of {0} roles too low ({1})");
162                            errors.add(new TestError(this, Severity.WARNING, errorMessage,
163                                    tr(s, keyname, count), MessageFormat.format(s, keyname, count), LOW_COUNT, n));
164                        } else {
165                            String s = marktr("Number of {0} roles too high ({1})");
166                            errors.add(new TestError(this, Severity.WARNING, errorMessage,
167                                    tr(s, keyname, count), MessageFormat.format(s, keyname, count), HIGH_COUNT, n));
168                        }
169                    }
170                    if (ri != null) {
171                        Set<OsmPrimitive> wrongTypes = new HashSet<OsmPrimitive>();
172                        if (r.types != null) {
173                            if (!r.types.contains(TaggingPresetType.WAY)) {
174                                wrongTypes.addAll(r.types.contains(TaggingPresetType.CLOSEDWAY) ? ri.openways : ri.ways);
175                            }
176                            if (!r.types.contains(TaggingPresetType.NODE)) {
177                                wrongTypes.addAll(ri.nodes);
178                            }
179                            if (!r.types.contains(TaggingPresetType.RELATION)) {
180                                wrongTypes.addAll(ri.relations);
181                            }
182                        }
183                        if (r.memberExpression != null) {
184                            for (Collection<OsmPrimitive> c : Arrays.asList(new Collection[]{ri.nodes, ri.ways, ri.relations})) {
185                                for (OsmPrimitive p : c) {
186                                    if (p.isUsable() && !r.memberExpression.match(p)) {
187                                        wrongTypes.add(p);
188                                    }
189                                }
190                            }
191                        }
192                        if (!wrongTypes.isEmpty()) {
193                            String s = marktr("Member for role {0} of wrong type");
194                            LinkedList<OsmPrimitive> highlight = new LinkedList<OsmPrimitive>(wrongTypes);
195                            highlight.addFirst(n);
196                            errors.add(new TestError(this, Severity.WARNING, errorMessage,
197                                    tr(s, keyname), MessageFormat.format(s, keyname), WRONG_TYPE,
198                                    highlight, wrongTypes));
199                        }
200                    }
201                }
202                for (String key : map.keySet()) {
203                    if (!done.contains(key)) {
204                        if (key.length() > 0) {
205                            String s = marktr("Role {0} unknown");
206                            errors.add(new TestError(this, Severity.WARNING, errorMessage,
207                                    tr(s, key), MessageFormat.format(s, key), ROLE_UNKNOWN, n));
208                        } else {
209                            String s = marktr("Empty role found");
210                            errors.add(new TestError(this, Severity.WARNING, errorMessage,
211                                    tr(s), s, ROLE_EMPTY, n));
212                        }
213                    }
214                }
215            }
216        }
217    }
218
219    @Override
220    public Command fixError(TestError testError) {
221        if (isFixable(testError)) {
222            return new DeleteCommand(testError.getPrimitives());
223        }
224        return null;
225    }
226
227    @Override
228    public boolean isFixable(TestError testError) {
229        Collection<? extends OsmPrimitive> primitives = testError.getPrimitives();
230        return testError.getCode() == RELATION_EMPTY && !primitives.isEmpty() && primitives.iterator().next().isNew();
231    }
232}