001    // License: GPL. See LICENSE file for details.
002    package org.openstreetmap.josm.gui.dialogs.validator;
003    
004    import java.awt.event.MouseEvent;
005    import java.util.ArrayList;
006    import java.util.Collections;
007    import java.util.Enumeration;
008    import java.util.HashMap;
009    import java.util.HashSet;
010    import java.util.LinkedHashSet;
011    import java.util.List;
012    import java.util.Map;
013    import java.util.Set;
014    import java.util.Map.Entry;
015    
016    import javax.swing.JTree;
017    import javax.swing.ToolTipManager;
018    import javax.swing.tree.DefaultMutableTreeNode;
019    import javax.swing.tree.DefaultTreeModel;
020    import javax.swing.tree.TreePath;
021    import javax.swing.tree.TreeSelectionModel;
022    
023    import org.openstreetmap.josm.data.osm.OsmPrimitive;
024    import org.openstreetmap.josm.data.validation.Severity;
025    import org.openstreetmap.josm.data.validation.TestError;
026    import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
027    import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
028    import org.openstreetmap.josm.tools.MultiMap;
029    import org.openstreetmap.josm.Main;
030    
031    /**
032     * A panel that displays the error tree. The selection manager
033     * respects clicks into the selection list. Ctrl-click will remove entries from
034     * the list while single click will make the clicked entry the only selection.
035     *
036     * @author frsantos
037     */
038    public class ValidatorTreePanel extends JTree {
039        /** Serializable ID */
040        private static final long serialVersionUID = 2952292777351992696L;
041    
042        /**
043         * The validation data.
044         */
045        protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
046    
047        /** The list of errors shown in the tree */
048        private List<TestError> errors = new ArrayList<TestError>();
049    
050        /**
051         * If {@link #filter} is not <code>null</code> only errors are displayed
052         * that refer to one of the primitives in the filter.
053         */
054        private Set<OsmPrimitive> filter = null;
055    
056        private int updateCount;
057    
058        /**
059         * Constructor
060         * @param errors The list of errors
061         */
062        public ValidatorTreePanel(List<TestError> errors) {
063            ToolTipManager.sharedInstance().registerComponent(this);
064            this.setModel(valTreeModel);
065            this.setRootVisible(false);
066            this.setShowsRootHandles(true);
067            this.expandRow(0);
068            this.setVisibleRowCount(8);
069            this.setCellRenderer(new ValidatorTreeRenderer());
070            this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
071            setErrorList(errors);
072        }
073    
074        @Override
075        public String getToolTipText(MouseEvent e) {
076            String res = null;
077            TreePath path = getPathForLocation(e.getX(), e.getY());
078            if (path != null) {
079                DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
080                Object nodeInfo = node.getUserObject();
081    
082                if (nodeInfo instanceof TestError) {
083                    TestError error = (TestError) nodeInfo;
084                    MultipleNameVisitor v = new MultipleNameVisitor();
085                    v.visit(error.getPrimitives());
086                    res = "<html>" + v.getText() + "<br>" + error.getMessage();
087                    String d = error.getDescription();
088                    if (d != null)
089                        res += "<br>" + d;
090                    res += "</html>";
091                } else {
092                    res = node.toString();
093                }
094            }
095            return res;
096        }
097    
098        /** Constructor */
099        public ValidatorTreePanel() {
100            this(null);
101        }
102    
103        @Override
104        public void setVisible(boolean v) {
105            if (v) {
106                buildTree();
107            } else {
108                valTreeModel.setRoot(new DefaultMutableTreeNode());
109            }
110            super.setVisible(v);
111        }
112    
113        /**
114         * Builds the errors tree
115         */
116        public void buildTree() {
117            updateCount++;
118            DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
119    
120            if (errors == null || errors.isEmpty()) {
121                valTreeModel.setRoot(rootNode);
122                return;
123            }
124    
125            // Remember the currently expanded rows
126            Set<Object> oldSelectedRows = new HashSet<Object>();
127            Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
128            if (expanded != null) {
129                while (expanded.hasMoreElements()) {
130                    TreePath path = expanded.nextElement();
131                    DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
132                    Object userObject = node.getUserObject();
133                    if (userObject instanceof Severity) {
134                        oldSelectedRows.add(userObject);
135                    } else if (userObject instanceof String) {
136                        String msg = (String) userObject;
137                        msg = msg.substring(0, msg.lastIndexOf(" ("));
138                        oldSelectedRows.add(msg);
139                    }
140                }
141            }
142    
143            Map<Severity, MultiMap<String, TestError>> errorTree = new HashMap<Severity, MultiMap<String, TestError>>();
144            Map<Severity, HashMap<String, MultiMap<String, TestError>>> errorTreeDeep = new HashMap<Severity, HashMap<String, MultiMap<String, TestError>>>();
145            for (Severity s : Severity.values()) {
146                errorTree.put(s, new MultiMap<String, TestError>(20));
147                errorTreeDeep.put(s, new HashMap<String, MultiMap<String, TestError>>());
148            }
149    
150            boolean other = Main.pref.getBoolean(ValidatorPreference.PREF_OTHER, false);
151            for (TestError e : errors) {
152                if (e.getIgnored()) {
153                    continue;
154                }
155                Severity s = e.getSeverity();
156                if(!other && s == Severity.OTHER) {
157                    continue;
158                }
159                String d = e.getDescription();
160                String m = e.getMessage();
161                if (filter != null) {
162                    boolean found = false;
163                    for (OsmPrimitive p : e.getPrimitives()) {
164                        if (filter.contains(p)) {
165                            found = true;
166                            break;
167                        }
168                    }
169                    if (!found) {
170                        continue;
171                    }
172                }
173                if (d != null) {
174                    MultiMap<String, TestError> b = errorTreeDeep.get(s).get(m);
175                    if (b == null) {
176                        b = new MultiMap<String, TestError>(20);
177                        errorTreeDeep.get(s).put(m, b);
178                    }
179                    b.put(d, e);
180                } else {
181                    errorTree.get(s).put(m, e);
182                }
183            }
184    
185            List<TreePath> expandedPaths = new ArrayList<TreePath>();
186            for (Severity s : Severity.values()) {
187                MultiMap<String, TestError> severityErrors = errorTree.get(s);
188                Map<String, MultiMap<String, TestError>> severityErrorsDeep = errorTreeDeep.get(s);
189                if (severityErrors.isEmpty() && severityErrorsDeep.isEmpty()) {
190                    continue;
191                }
192    
193                // Severity node
194                DefaultMutableTreeNode severityNode = new DefaultMutableTreeNode(s);
195                rootNode.add(severityNode);
196    
197                if (oldSelectedRows.contains(s)) {
198                    expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode }));
199                }
200    
201                for (Entry<String, LinkedHashSet<TestError>> msgErrors : severityErrors.entrySet()) {
202                    // Message node
203                    Set<TestError> errs = msgErrors.getValue();
204                    String msg = msgErrors.getKey() + " (" + errs.size() + ")";
205                    DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
206                    severityNode.add(messageNode);
207    
208                    if (oldSelectedRows.contains(msgErrors.getKey())) {
209                        expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode }));
210                    }
211    
212                    for (TestError error : errs) {
213                        // Error node
214                        DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
215                        messageNode.add(errorNode);
216                    }
217                }
218                for (Entry<String, MultiMap<String, TestError>> bag : severityErrorsDeep.entrySet()) {
219                    // Group node
220                    MultiMap<String, TestError> errorlist = bag.getValue();
221                    DefaultMutableTreeNode groupNode = null;
222                    if (errorlist.size() > 1) {
223                        String nmsg = bag.getKey() + " (" + errorlist.size() + ")";
224                        groupNode = new DefaultMutableTreeNode(nmsg);
225                        severityNode.add(groupNode);
226                        if (oldSelectedRows.contains(bag.getKey())) {
227                            expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode }));
228                        }
229                    }
230    
231                    for (Entry<String, LinkedHashSet<TestError>> msgErrors : errorlist.entrySet()) {
232                        // Message node
233                        Set<TestError> errs = msgErrors.getValue();
234                        String msg;
235                        if (groupNode != null) {
236                            msg = msgErrors.getKey() + " (" + errs.size() + ")";
237                        } else {
238                            msg = msgErrors.getKey() + " - " + bag.getKey() + " (" + errs.size() + ")";
239                        }
240                        DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
241                        if (groupNode != null) {
242                            groupNode.add(messageNode);
243                        } else {
244                            severityNode.add(messageNode);
245                        }
246    
247                        if (oldSelectedRows.contains(msgErrors.getKey())) {
248                            if (groupNode != null) {
249                                expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode,
250                                        messageNode }));
251                            } else {
252                                expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode }));
253                            }
254                        }
255    
256                        for (TestError error : errs) {
257                            // Error node
258                            DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error);
259                            messageNode.add(errorNode);
260                        }
261                    }
262                }
263            }
264    
265            valTreeModel.setRoot(rootNode);
266            for (TreePath path : expandedPaths) {
267                this.expandPath(path);
268            }
269        }
270    
271        /**
272         * Sets the errors list used by a data layer
273         * @param errors The error list that is used by a data layer
274         */
275        public void setErrorList(List<TestError> errors) {
276            this.errors = errors;
277            if (isVisible()) {
278                buildTree();
279            }
280        }
281    
282        /**
283         * Clears the current error list and adds these errors to it
284         * @param errors The validation errors
285         */
286        public void setErrors(List<TestError> newerrors) {
287            if (errors == null)
288                return;
289            errors.clear();
290            for (TestError error : newerrors) {
291                if (!error.getIgnored()) {
292                    errors.add(error);
293                }
294            }
295            if (isVisible()) {
296                buildTree();
297            }
298        }
299    
300        /**
301         * Returns the errors of the tree
302         * @return  the errors of the tree
303         */
304        public List<TestError> getErrors() {
305            return errors != null ? errors : Collections.<TestError> emptyList();
306        }
307    
308        public Set<OsmPrimitive> getFilter() {
309            return filter;
310        }
311    
312        public void setFilter(Set<OsmPrimitive> filter) {
313            if (filter != null && filter.isEmpty()) {
314                this.filter = null;
315            } else {
316                this.filter = filter;
317            }
318            if (isVisible()) {
319                buildTree();
320            }
321        }
322    
323        /**
324         * Updates the current errors list
325         * @param errors The validation errors
326         */
327        public void resetErrors() {
328            List<TestError> e = new ArrayList<TestError>(errors);
329            setErrors(e);
330        }
331    
332        /**
333         * Expands all tree
334         */
335        @SuppressWarnings("unchecked")
336        public void expandAll() {
337            DefaultMutableTreeNode root = getRoot();
338    
339            int row = 0;
340            Enumeration<DefaultMutableTreeNode> children = root.breadthFirstEnumeration();
341            while (children.hasMoreElements()) {
342                children.nextElement();
343                expandRow(row++);
344            }
345        }
346    
347        /**
348         * Returns the root node model.
349         * @return The root node model
350         */
351        public DefaultMutableTreeNode getRoot() {
352            return (DefaultMutableTreeNode) valTreeModel.getRoot();
353        }
354    
355        public int getUpdateCount() {
356            return updateCount;
357        }
358    }