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 */ 352 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) { 353 final Collection<TreePath> paths = new ArrayList<>(); 354 walkAndSelectRelatedErrors(new TreePath(getRoot()), Predicates.inCollection(new HashSet<>(primitives)), paths); 355 getSelectionModel().clearSelection(); 356 for (TreePath path : paths) { 357 expandPath(path); 358 getSelectionModel().addSelectionPath(path); 359 } 360 } 361 362 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) { 363 final int count = getModel().getChildCount(p.getLastPathComponent()); 364 for (int i = 0; i < count; i++) { 365 final Object child = getModel().getChild(p.getLastPathComponent(), i); 366 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode 367 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) { 368 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject(); 369 if (error.getPrimitives() != null) { 370 if (Utils.exists(error.getPrimitives(), isRelevant)) { 371 paths.add(p.pathByAddingChild(child)); 372 } 373 } 374 } else { 375 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths); 376 } 377 } 378 } 379 380 /** 381 * Returns the filter list 382 * @return the list of primitives used for filtering 383 */ 384 public Set<? extends OsmPrimitive> getFilter() { 385 return filter; 386 } 387 388 /** 389 * Set the filter list to a set of primitives 390 * @param filter the list of primitives used for filtering 391 */ 392 public void setFilter(Set<? extends OsmPrimitive> filter) { 393 if (filter != null && filter.isEmpty()) { 394 this.filter = null; 395 } else { 396 this.filter = filter; 397 } 398 if (isVisible()) { 399 buildTree(); 400 } 401 } 402 403 /** 404 * Updates the current errors list 405 */ 406 public void resetErrors() { 407 List<TestError> e = new ArrayList<>(errors); 408 setErrors(e); 409 } 410 411 /** 412 * Expands complete tree 413 */ 414 @SuppressWarnings("unchecked") 415 public void expandAll() { 416 DefaultMutableTreeNode root = getRoot(); 417 418 int row = 0; 419 Enumeration<TreeNode> children = root.breadthFirstEnumeration(); 420 while (children.hasMoreElements()) { 421 children.nextElement(); 422 expandRow(row++); 423 } 424 } 425 426 /** 427 * Returns the root node model. 428 * @return The root node model 429 */ 430 public DefaultMutableTreeNode getRoot() { 431 return (DefaultMutableTreeNode) valTreeModel.getRoot(); 432 } 433 434 /** 435 * Returns a value to check if tree has been rebuild 436 * @return the current counter 437 */ 438 public int getUpdateCount() { 439 return updateCount; 440 } 441 442 private void clearErrors() { 443 if (errors != null) { 444 DataSet ds = Main.main.getCurrentDataSet(); 445 if (ds != null) { 446 for (TestError e : errors) { 447 ds.removeDataSetListener(e); 448 } 449 } 450 errors.clear(); 451 } 452 } 453 454 @Override 455 public void destroy() { 456 clearErrors(); 457 } 458}