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}