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