// License: GPL. See LICENSE file for details. package org.openstreetmap.josm.gui.dialogs.validator; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import javax.swing.JTree; import javax.swing.ToolTipManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.osm.DataSet; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.validation.Severity; import org.openstreetmap.josm.data.validation.TestError; import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor; import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.tools.Destroyable; import org.openstreetmap.josm.tools.MultiMap; /** * A panel that displays the error tree. The selection manager * respects clicks into the selection list. Ctrl-click will remove entries from * the list while single click will make the clicked entry the only selection. * * @author frsantos */ public class ValidatorTreePanel extends JTree implements Destroyable { private static final class GroupTreeNode extends DefaultMutableTreeNode { public GroupTreeNode(Object userObject) { super(userObject); } @Override public String toString() { return tr("{0} ({1})", super.toString(), getLeafCount()); } } /** * The validation data. */ protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); /** The list of errors shown in the tree */ private transient List errors = new ArrayList<>(); /** * If {@link #filter} is not null only errors are displayed * that refer to one of the primitives in the filter. */ private transient Set filter = null; /** a counter to check if tree has been rebuild */ private int updateCount; /** * Constructor * @param errors The list of errors */ public ValidatorTreePanel(List errors) { ToolTipManager.sharedInstance().registerComponent(this); this.setModel(valTreeModel); this.setRootVisible(false); this.setShowsRootHandles(true); this.expandRow(0); this.setVisibleRowCount(8); this.setCellRenderer(new ValidatorTreeRenderer()); this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); setErrorList(errors); for (KeyListener keyListener : getKeyListeners()) { // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) { removeKeyListener(keyListener); } } } @Override public String getToolTipText(MouseEvent e) { String res = null; TreePath path = getPathForLocation(e.getX(), e.getY()); if (path != null) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); Object nodeInfo = node.getUserObject(); if (nodeInfo instanceof TestError) { TestError error = (TestError) nodeInfo; MultipleNameVisitor v = new MultipleNameVisitor(); v.visit(error.getPrimitives()); res = "" + v.getText() + "
" + error.getMessage(); String d = error.getDescription(); if (d != null) res += "
" + d; res += ""; } else { res = node.toString(); } } return res; } /** Constructor */ public ValidatorTreePanel() { this(null); } @Override public void setVisible(boolean v) { if (v) { buildTree(); } else { valTreeModel.setRoot(new DefaultMutableTreeNode()); } super.setVisible(v); } /** * Builds the errors tree */ public void buildTree() { updateCount++; final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); if (errors == null || errors.isEmpty()) { GuiHelper.runInEDTAndWait(new Runnable() { @Override public void run() { valTreeModel.setRoot(rootNode); } }); return; } // Sort validation errors - #8517 Collections.sort(errors); // Remember the currently expanded rows Set oldSelectedRows = new HashSet<>(); Enumeration expanded = getExpandedDescendants(new TreePath(getRoot())); if (expanded != null) { while (expanded.hasMoreElements()) { TreePath path = expanded.nextElement(); DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); Object userObject = node.getUserObject(); if (userObject instanceof Severity) { oldSelectedRows.add(userObject); } else if (userObject instanceof String) { String msg = (String) userObject; int index = msg.lastIndexOf(" ("); if (index > 0) { msg = msg.substring(0, index); } oldSelectedRows.add(msg); } } } Map> errorTree = new HashMap<>(); Map>> errorTreeDeep = new HashMap<>(); for (Severity s : Severity.values()) { errorTree.put(s, new MultiMap(20)); errorTreeDeep.put(s, new HashMap>()); } final Boolean other = ValidatorPreference.PREF_OTHER.get(); for (TestError e : errors) { if (e.getIgnored()) { continue; } Severity s = e.getSeverity(); if(!other && s == Severity.OTHER) { continue; } String d = e.getDescription(); String m = e.getMessage(); if (filter != null) { boolean found = false; for (OsmPrimitive p : e.getPrimitives()) { if (filter.contains(p)) { found = true; break; } } if (!found) { continue; } } if (d != null) { MultiMap b = errorTreeDeep.get(s).get(m); if (b == null) { b = new MultiMap<>(20); errorTreeDeep.get(s).put(m, b); } b.put(d, e); } else { errorTree.get(s).put(m, e); } } List expandedPaths = new ArrayList<>(); for (Severity s : Severity.values()) { MultiMap severityErrors = errorTree.get(s); Map> severityErrorsDeep = errorTreeDeep.get(s); if (severityErrors.isEmpty() && severityErrorsDeep.isEmpty()) { continue; } // Severity node DefaultMutableTreeNode severityNode = new GroupTreeNode(s); rootNode.add(severityNode); if (oldSelectedRows.contains(s)) { expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode })); } for (Entry> msgErrors : severityErrors.entrySet()) { // Message node Set errs = msgErrors.getValue(); String msg = tr("{0} ({1})", msgErrors.getKey(), errs.size()); DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg); severityNode.add(messageNode); if (oldSelectedRows.contains(msgErrors.getKey())) { expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode })); } for (TestError error : errs) { // Error node DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error); messageNode.add(errorNode); } } for (Entry> bag : severityErrorsDeep.entrySet()) { // Group node MultiMap errorlist = bag.getValue(); DefaultMutableTreeNode groupNode = null; if (errorlist.size() > 1) { groupNode = new GroupTreeNode(bag.getKey()); severityNode.add(groupNode); if (oldSelectedRows.contains(bag.getKey())) { expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode })); } } for (Entry> msgErrors : errorlist.entrySet()) { // Message node Set errs = msgErrors.getValue(); String msg; if (groupNode != null) { msg = tr("{0} ({1})", msgErrors.getKey(), errs.size()); } else { msg = tr("{0} - {1} ({2})", msgErrors.getKey(), bag.getKey(), errs.size()); } DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg); if (groupNode != null) { groupNode.add(messageNode); } else { severityNode.add(messageNode); } if (oldSelectedRows.contains(msgErrors.getKey())) { if (groupNode != null) { expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode, messageNode })); } else { expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode })); } } for (TestError error : errs) { // Error node DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error); messageNode.add(errorNode); } } } } valTreeModel.setRoot(rootNode); for (TreePath path : expandedPaths) { this.expandPath(path); } } /** * Sets the errors list used by a data layer * @param errors The error list that is used by a data layer */ public final void setErrorList(List errors) { this.errors = errors; if (isVisible()) { buildTree(); } } /** * Clears the current error list and adds these errors to it * @param newerrors The validation errors */ public void setErrors(List newerrors) { if (errors == null) return; clearErrors(); DataSet ds = Main.main.getCurrentDataSet(); for (TestError error : newerrors) { if (!error.getIgnored()) { errors.add(error); if (ds != null) { ds.addDataSetListener(error); } } } if (isVisible()) { buildTree(); } } /** * Returns the errors of the tree * @return the errors of the tree */ public List getErrors() { return errors != null ? errors : Collections. emptyList(); } /** * Returns the filter list * @return the list of primitives used for filtering */ public Set getFilter() { return filter; } /** * Set the filter list to a set of primitives * @param filter the list of primitives used for filtering */ public void setFilter(Set filter) { if (filter != null && filter.isEmpty()) { this.filter = null; } else { this.filter = filter; } if (isVisible()) { buildTree(); } } /** * Updates the current errors list */ public void resetErrors() { List e = new ArrayList<>(errors); setErrors(e); } /** * Expands complete tree */ @SuppressWarnings("unchecked") public void expandAll() { DefaultMutableTreeNode root = getRoot(); int row = 0; Enumeration children = root.breadthFirstEnumeration(); while (children.hasMoreElements()) { children.nextElement(); expandRow(row++); } } /** * Returns the root node model. * @return The root node model */ public DefaultMutableTreeNode getRoot() { return (DefaultMutableTreeNode) valTreeModel.getRoot(); } /** * Returns a value to check if tree has been rebuild * @return the current counter */ public int getUpdateCount() { return updateCount; } private void clearErrors() { if (errors != null) { DataSet ds = Main.main.getCurrentDataSet(); if (ds != null) { for (TestError e : errors) { ds.removeDataSetListener(e); } } errors.clear(); } } @Override public void destroy() { clearErrors(); } }