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