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