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.Enumeration; 012import java.util.HashSet; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016import java.util.function.Consumer; 017import java.util.function.Predicate; 018 019import javax.swing.JTree; 020import javax.swing.ToolTipManager; 021import javax.swing.tree.DefaultMutableTreeNode; 022import javax.swing.tree.DefaultTreeModel; 023import javax.swing.tree.TreeNode; 024import javax.swing.tree.TreePath; 025import javax.swing.tree.TreeSelectionModel; 026 027import org.openstreetmap.josm.data.osm.OsmPrimitive; 028import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent; 029import org.openstreetmap.josm.data.osm.event.DataChangedEvent; 030import org.openstreetmap.josm.data.osm.event.DataSetListener; 031import org.openstreetmap.josm.data.osm.event.DatasetEventManager; 032import org.openstreetmap.josm.data.osm.event.NodeMovedEvent; 033import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent; 034import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent; 035import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent; 036import org.openstreetmap.josm.data.osm.event.TagsChangedEvent; 037import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent; 038import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper; 039import org.openstreetmap.josm.data.validation.OsmValidator; 040import org.openstreetmap.josm.data.validation.Severity; 041import org.openstreetmap.josm.data.validation.TestError; 042import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor; 043import org.openstreetmap.josm.gui.util.GuiHelper; 044import org.openstreetmap.josm.tools.Destroyable; 045import org.openstreetmap.josm.tools.ListenerList; 046 047/** 048 * A panel that displays the error tree. The selection manager 049 * respects clicks into the selection list. Ctrl-click will remove entries from 050 * the list while single click will make the clicked entry the only selection. 051 * 052 * @author frsantos 053 */ 054public class ValidatorTreePanel extends JTree implements Destroyable, DataSetListener { 055 056 private static final class GroupTreeNode extends DefaultMutableTreeNode { 057 058 GroupTreeNode(Object userObject) { 059 super(userObject); 060 } 061 062 @Override 063 public String toString() { 064 return tr("{0} ({1})", super.toString(), getLeafCount()); 065 } 066 } 067 068 /** 069 * The validation data. 070 */ 071 protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 072 073 /** The list of errors shown in the tree */ 074 private transient List<TestError> errors = new ArrayList<>(); 075 076 /** 077 * If {@link #filter} is not <code>null</code> only errors are displayed 078 * that refer to one of the primitives in the filter. 079 */ 080 private transient Set<? extends OsmPrimitive> filter; 081 082 private final ListenerList<Runnable> invalidationListeners = ListenerList.create(); 083 084 /** 085 * Constructor 086 * @param errors The list of errors 087 */ 088 public ValidatorTreePanel(List<TestError> errors) { 089 ToolTipManager.sharedInstance().registerComponent(this); 090 this.setModel(valTreeModel); 091 this.setRootVisible(false); 092 this.setShowsRootHandles(true); 093 this.expandRow(0); 094 this.setVisibleRowCount(8); 095 this.setCellRenderer(new ValidatorTreeRenderer()); 096 this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); 097 setErrorList(errors); 098 for (KeyListener keyListener : getKeyListeners()) { 099 // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands 100 if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) { 101 removeKeyListener(keyListener); 102 } 103 } 104 DatasetEventManager.getInstance().addDatasetListener(this, DatasetEventManager.FireMode.IN_EDT); 105 } 106 107 @Override 108 public String getToolTipText(MouseEvent e) { 109 String res = null; 110 TreePath path = getPathForLocation(e.getX(), e.getY()); 111 if (path != null) { 112 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 113 Object nodeInfo = node.getUserObject(); 114 115 if (nodeInfo instanceof TestError) { 116 TestError error = (TestError) nodeInfo; 117 MultipleNameVisitor v = new MultipleNameVisitor(); 118 v.visit(error.getPrimitives()); 119 res = "<html>" + v.getText() + "<br>" + error.getMessage(); 120 String d = error.getDescription(); 121 if (d != null) 122 res += "<br>" + d; 123 res += "</html>"; 124 } else { 125 res = node.toString(); 126 } 127 } 128 return res; 129 } 130 131 /** Constructor */ 132 public ValidatorTreePanel() { 133 this(null); 134 } 135 136 @Override 137 public void setVisible(boolean v) { 138 if (v) { 139 buildTree(); 140 } else { 141 valTreeModel.setRoot(new DefaultMutableTreeNode()); 142 } 143 super.setVisible(v); 144 invalidationListeners.fireEvent(Runnable::run); 145 } 146 147 /** 148 * Builds the errors tree 149 */ 150 public void buildTree() { 151 final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); 152 153 if (errors == null || errors.isEmpty()) { 154 GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode)); 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 Predicate<TestError> filterToUse = e -> !e.isIgnored(); 182 if (!ValidatorPrefHelper.PREF_OTHER.get()) { 183 filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER); 184 } 185 if (filter != null) { 186 filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains)); 187 } 188 Map<Severity, Map<String, Map<String, List<TestError>>>> errorsBySeverityMessageDescription 189 = OsmValidator.getErrorsBySeverityMessageDescription(errors, filterToUse); 190 191 final List<TreePath> expandedPaths = new ArrayList<>(); 192 errorsBySeverityMessageDescription.forEach((severity, errorsByMessageDescription) -> { 193 // Severity node 194 final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity); 195 rootNode.add(severityNode); 196 197 if (oldSelectedRows.contains(severity)) { 198 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode})); 199 } 200 201 final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get(""); 202 if (errorsWithEmptyMessageByDescription != null) { 203 errorsWithEmptyMessageByDescription.forEach((description, errors) -> { 204 final String msg = tr("{0} ({1})", description, errors.size()); 205 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg); 206 severityNode.add(messageNode); 207 208 if (oldSelectedRows.contains(description)) { 209 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode})); 210 } 211 212 errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add); 213 }); 214 } 215 216 errorsByMessageDescription.forEach((message, errorsByDescription) -> { 217 if (message.isEmpty()) { 218 return; 219 } 220 // Group node 221 final DefaultMutableTreeNode groupNode; 222 if (errorsByDescription.size() > 1) { 223 groupNode = new GroupTreeNode(message); 224 severityNode.add(groupNode); 225 if (oldSelectedRows.contains(message)) { 226 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode})); 227 } 228 } else { 229 groupNode = null; 230 } 231 232 errorsByDescription.forEach((description, errors) -> { 233 boolean emptyDescription = description == null || description.isEmpty(); 234 // Message node 235 final String msg; 236 if (groupNode != null) { 237 msg = tr("{0} ({1})", description, errors.size()); 238 } else if (emptyDescription) { 239 msg = tr("{0} ({1})", message, errors.size()); 240 } else { 241 msg = tr("{0} - {1} ({2})", message, description, errors.size()); 242 } 243 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg); 244 if (groupNode != null) { 245 groupNode.add(messageNode); 246 } else { 247 severityNode.add(messageNode); 248 } 249 250 if (oldSelectedRows.contains(description) || (emptyDescription && oldSelectedRows.contains(message))) { 251 if (groupNode != null) { 252 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode})); 253 } else { 254 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode})); 255 } 256 } 257 258 errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add); 259 }); 260 }); 261 }); 262 263 valTreeModel.setRoot(rootNode); 264 for (TreePath path : expandedPaths) { 265 this.expandPath(path); 266 } 267 268 invalidationListeners.fireEvent(Runnable::run); 269 } 270 271 /** 272 * Add a new invalidation listener 273 * @param listener The listener 274 */ 275 public void addInvalidationListener(Runnable listener) { 276 invalidationListeners.addListener(listener); 277 } 278 279 /** 280 * Remove an invalidation listener 281 * @param listener The listener 282 * @since 10880 283 */ 284 public void removeInvalidationListener(Runnable listener) { 285 invalidationListeners.removeListener(listener); 286 } 287 288 /** 289 * Sets the errors list used by a data layer 290 * @param errors The error list that is used by a data layer 291 */ 292 public final void setErrorList(List<TestError> errors) { 293 this.errors = errors; 294 if (isVisible()) { 295 buildTree(); 296 } 297 } 298 299 /** 300 * Clears the current error list and adds these errors to it 301 * @param newerrors The validation errors 302 */ 303 public void setErrors(List<TestError> newerrors) { 304 if (errors == null) 305 return; 306 clearErrors(); 307 for (TestError error : newerrors) { 308 if (!error.isIgnored()) { 309 errors.add(error); 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 * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()} 327 * returns a primitive present in {@code primitives}. 328 * @param primitives collection of primitives 329 */ 330 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) { 331 final Collection<TreePath> paths = new ArrayList<>(); 332 walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths); 333 getSelectionModel().clearSelection(); 334 for (TreePath path : paths) { 335 expandPath(path); 336 getSelectionModel().addSelectionPath(path); 337 } 338 } 339 340 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) { 341 final int count = getModel().getChildCount(p.getLastPathComponent()); 342 for (int i = 0; i < count; i++) { 343 final Object child = getModel().getChild(p.getLastPathComponent(), i); 344 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode 345 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) { 346 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject(); 347 if (error.getPrimitives().stream().anyMatch(isRelevant)) { 348 paths.add(p.pathByAddingChild(child)); 349 } 350 } else { 351 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths); 352 } 353 } 354 } 355 356 /** 357 * Returns the filter list 358 * @return the list of primitives used for filtering 359 */ 360 public Set<? extends OsmPrimitive> getFilter() { 361 return filter; 362 } 363 364 /** 365 * Set the filter list to a set of primitives 366 * @param filter the list of primitives used for filtering 367 */ 368 public void setFilter(Set<? extends OsmPrimitive> filter) { 369 if (filter != null && filter.isEmpty()) { 370 this.filter = null; 371 } else { 372 this.filter = filter; 373 } 374 if (isVisible()) { 375 buildTree(); 376 } 377 } 378 379 /** 380 * Updates the current errors list 381 */ 382 public void resetErrors() { 383 setErrors(new ArrayList<>(errors)); 384 } 385 386 /** 387 * Expands complete tree 388 */ 389 public void expandAll() { 390 visitTreeNodes(getRoot(), x -> expandPath(new TreePath(x.getPath()))); 391 } 392 393 /** 394 * Returns the root node model. 395 * @return The root node model 396 */ 397 public DefaultMutableTreeNode getRoot() { 398 return (DefaultMutableTreeNode) valTreeModel.getRoot(); 399 } 400 401 private void clearErrors() { 402 if (errors != null) { 403 errors.clear(); 404 } 405 } 406 407 @Override 408 public void destroy() { 409 DatasetEventManager.getInstance().removeDatasetListener(this); 410 ToolTipManager.sharedInstance().unregisterComponent(this); 411 clearErrors(); 412 } 413 414 /** 415 * Visitor call for all tree nodes children of root, in breadth-first order. 416 * @param root Root node 417 * @param visitor Visitor 418 * @since 13940 419 */ 420 public static void visitTreeNodes(DefaultMutableTreeNode root, Consumer<DefaultMutableTreeNode> visitor) { 421 @SuppressWarnings("unchecked") 422 Enumeration<TreeNode> errorMessages = root.breadthFirstEnumeration(); 423 while (errorMessages.hasMoreElements()) { 424 visitor.accept(((DefaultMutableTreeNode) errorMessages.nextElement())); 425 } 426 } 427 428 /** 429 * Visitor call for all {@link TestError} nodes children of root, in breadth-first order. 430 * @param root Root node 431 * @param visitor Visitor 432 * @since 13940 433 */ 434 public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor) { 435 visitTestErrors(root, visitor, null); 436 } 437 438 /** 439 * Visitor call for all {@link TestError} nodes children of root, in breadth-first order. 440 * @param root Root node 441 * @param visitor Visitor 442 * @param processedNodes Set of already visited nodes (optional) 443 * @since 13940 444 */ 445 public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor, 446 Set<DefaultMutableTreeNode> processedNodes) { 447 visitTreeNodes(root, n -> { 448 if (processedNodes == null || !processedNodes.contains(n)) { 449 if (processedNodes != null) { 450 processedNodes.add(n); 451 } 452 Object o = n.getUserObject(); 453 if (o instanceof TestError) { 454 visitor.accept((TestError) o); 455 } 456 } 457 }); 458 } 459 460 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) { 461 // Remove purged primitives (fix #8639) 462 if (errors != null) { 463 final Set<? extends OsmPrimitive> deletedPrimitives = new HashSet<>(event.getPrimitives()); 464 errors.removeIf(error -> error.getPrimitives().stream().anyMatch(deletedPrimitives::contains)); 465 } 466 } 467 468 @Override public void primitivesAdded(PrimitivesAddedEvent event) { 469 // Do nothing 470 } 471 472 @Override public void tagsChanged(TagsChangedEvent event) { 473 // Do nothing 474 } 475 476 @Override public void nodeMoved(NodeMovedEvent event) { 477 // Do nothing 478 } 479 480 @Override public void wayNodesChanged(WayNodesChangedEvent event) { 481 // Do nothing 482 } 483 484 @Override public void relationMembersChanged(RelationMembersChangedEvent event) { 485 // Do nothing 486 } 487 488 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) { 489 // Do nothing 490 } 491 492 @Override public void dataChanged(DataChangedEvent event) { 493 // Do nothing 494 } 495}