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}