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