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