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