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