001// License: GPL. See LICENSE file for details. 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.Collections; 010import java.util.Enumeration; 011import java.util.HashMap; 012import java.util.HashSet; 013import java.util.List; 014import java.util.Map; 015import java.util.Map.Entry; 016import java.util.Set; 017 018import javax.swing.JTree; 019import javax.swing.ToolTipManager; 020import javax.swing.tree.DefaultMutableTreeNode; 021import javax.swing.tree.DefaultTreeModel; 022import javax.swing.tree.TreePath; 023import javax.swing.tree.TreeSelectionModel; 024 025import org.openstreetmap.josm.Main; 026import org.openstreetmap.josm.data.osm.DataSet; 027import org.openstreetmap.josm.data.osm.OsmPrimitive; 028import org.openstreetmap.josm.data.validation.Severity; 029import org.openstreetmap.josm.data.validation.TestError; 030import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor; 031import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference; 032import org.openstreetmap.josm.gui.util.GuiHelper; 033import org.openstreetmap.josm.tools.Destroyable; 034import org.openstreetmap.josm.tools.MultiMap; 035 036/** 037 * A panel that displays the error tree. The selection manager 038 * respects clicks into the selection list. Ctrl-click will remove entries from 039 * the list while single click will make the clicked entry the only selection. 040 * 041 * @author frsantos 042 */ 043public class ValidatorTreePanel extends JTree implements Destroyable { 044 045 private static final class GroupTreeNode extends DefaultMutableTreeNode { 046 047 public GroupTreeNode(Object userObject) { 048 super(userObject); 049 } 050 051 @Override 052 public String toString() { 053 return tr("{0} ({1})", super.toString(), getLeafCount()); 054 } 055 } 056 057 /** 058 * The validation data. 059 */ 060 protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode()); 061 062 /** The list of errors shown in the tree */ 063 private List<TestError> errors = new ArrayList<>(); 064 065 /** 066 * If {@link #filter} is not <code>null</code> only errors are displayed 067 * that refer to one of the primitives in the filter. 068 */ 069 private Set<OsmPrimitive> filter = null; 070 071 /** a counter to check if tree has been rebuild */ 072 private int updateCount; 073 074 /** 075 * Constructor 076 * @param errors The list of errors 077 */ 078 public ValidatorTreePanel(List<TestError> errors) { 079 ToolTipManager.sharedInstance().registerComponent(this); 080 this.setModel(valTreeModel); 081 this.setRootVisible(false); 082 this.setShowsRootHandles(true); 083 this.expandRow(0); 084 this.setVisibleRowCount(8); 085 this.setCellRenderer(new ValidatorTreeRenderer()); 086 this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); 087 setErrorList(errors); 088 for (KeyListener keyListener : getKeyListeners()) { 089 // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands 090 if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) { 091 removeKeyListener(keyListener); 092 } 093 } 094 } 095 096 @Override 097 public String getToolTipText(MouseEvent e) { 098 String res = null; 099 TreePath path = getPathForLocation(e.getX(), e.getY()); 100 if (path != null) { 101 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 102 Object nodeInfo = node.getUserObject(); 103 104 if (nodeInfo instanceof TestError) { 105 TestError error = (TestError) nodeInfo; 106 MultipleNameVisitor v = new MultipleNameVisitor(); 107 v.visit(error.getPrimitives()); 108 res = "<html>" + v.getText() + "<br>" + error.getMessage(); 109 String d = error.getDescription(); 110 if (d != null) 111 res += "<br>" + d; 112 res += "</html>"; 113 } else { 114 res = node.toString(); 115 } 116 } 117 return res; 118 } 119 120 /** Constructor */ 121 public ValidatorTreePanel() { 122 this(null); 123 } 124 125 @Override 126 public void setVisible(boolean v) { 127 if (v) { 128 buildTree(); 129 } else { 130 valTreeModel.setRoot(new DefaultMutableTreeNode()); 131 } 132 super.setVisible(v); 133 } 134 135 /** 136 * Builds the errors tree 137 */ 138 public void buildTree() { 139 updateCount++; 140 final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode(); 141 142 if (errors == null || errors.isEmpty()) { 143 GuiHelper.runInEDTAndWait(new Runnable() { 144 @Override 145 public void run() { 146 valTreeModel.setRoot(rootNode); 147 } 148 }); 149 return; 150 } 151 // Sort validation errors - #8517 152 Collections.sort(errors); 153 154 // Remember the currently expanded rows 155 Set<Object> oldSelectedRows = new HashSet<>(); 156 Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot())); 157 if (expanded != null) { 158 while (expanded.hasMoreElements()) { 159 TreePath path = expanded.nextElement(); 160 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); 161 Object userObject = node.getUserObject(); 162 if (userObject instanceof Severity) { 163 oldSelectedRows.add(userObject); 164 } else if (userObject instanceof String) { 165 String msg = (String) userObject; 166 int index = msg.lastIndexOf(" ("); 167 if (index > 0) { 168 msg = msg.substring(0, index); 169 } 170 oldSelectedRows.add(msg); 171 } 172 } 173 } 174 175 Map<Severity, MultiMap<String, TestError>> errorTree = new HashMap<>(); 176 Map<Severity, HashMap<String, MultiMap<String, TestError>>> errorTreeDeep = new HashMap<>(); 177 for (Severity s : Severity.values()) { 178 errorTree.put(s, new MultiMap<String, TestError>(20)); 179 errorTreeDeep.put(s, new HashMap<String, MultiMap<String, TestError>>()); 180 } 181 182 final Boolean other = ValidatorPreference.PREF_OTHER.get(); 183 for (TestError e : errors) { 184 if (e.getIgnored()) { 185 continue; 186 } 187 Severity s = e.getSeverity(); 188 if(!other && s == Severity.OTHER) { 189 continue; 190 } 191 String d = e.getDescription(); 192 String m = e.getMessage(); 193 if (filter != null) { 194 boolean found = false; 195 for (OsmPrimitive p : e.getPrimitives()) { 196 if (filter.contains(p)) { 197 found = true; 198 break; 199 } 200 } 201 if (!found) { 202 continue; 203 } 204 } 205 if (d != null) { 206 MultiMap<String, TestError> b = errorTreeDeep.get(s).get(m); 207 if (b == null) { 208 b = new MultiMap<>(20); 209 errorTreeDeep.get(s).put(m, b); 210 } 211 b.put(d, e); 212 } else { 213 errorTree.get(s).put(m, e); 214 } 215 } 216 217 List<TreePath> expandedPaths = new ArrayList<>(); 218 for (Severity s : Severity.values()) { 219 MultiMap<String, TestError> severityErrors = errorTree.get(s); 220 Map<String, MultiMap<String, TestError>> severityErrorsDeep = errorTreeDeep.get(s); 221 if (severityErrors.isEmpty() && severityErrorsDeep.isEmpty()) { 222 continue; 223 } 224 225 // Severity node 226 DefaultMutableTreeNode severityNode = new GroupTreeNode(s); 227 rootNode.add(severityNode); 228 229 if (oldSelectedRows.contains(s)) { 230 expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode })); 231 } 232 233 for (Entry<String, Set<TestError>> msgErrors : severityErrors.entrySet()) { 234 // Message node 235 Set<TestError> errs = msgErrors.getValue(); 236 String msg = tr("{0} ({1})", msgErrors.getKey(), errs.size()); 237 DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg); 238 severityNode.add(messageNode); 239 240 if (oldSelectedRows.contains(msgErrors.getKey())) { 241 expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode })); 242 } 243 244 for (TestError error : errs) { 245 // Error node 246 DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error); 247 messageNode.add(errorNode); 248 } 249 } 250 for (Entry<String, MultiMap<String, TestError>> bag : severityErrorsDeep.entrySet()) { 251 // Group node 252 MultiMap<String, TestError> errorlist = bag.getValue(); 253 DefaultMutableTreeNode groupNode = null; 254 if (errorlist.size() > 1) { 255 groupNode = new GroupTreeNode(bag.getKey()); 256 severityNode.add(groupNode); 257 if (oldSelectedRows.contains(bag.getKey())) { 258 expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode })); 259 } 260 } 261 262 for (Entry<String, Set<TestError>> msgErrors : errorlist.entrySet()) { 263 // Message node 264 Set<TestError> errs = msgErrors.getValue(); 265 String msg; 266 if (groupNode != null) { 267 msg = tr("{0} ({1})", msgErrors.getKey(), errs.size()); 268 } else { 269 msg = tr("{0} - {1} ({2})", msgErrors.getKey(), bag.getKey(), errs.size()); 270 } 271 DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg); 272 if (groupNode != null) { 273 groupNode.add(messageNode); 274 } else { 275 severityNode.add(messageNode); 276 } 277 278 if (oldSelectedRows.contains(msgErrors.getKey())) { 279 if (groupNode != null) { 280 expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, groupNode, 281 messageNode })); 282 } else { 283 expandedPaths.add(new TreePath(new Object[] { rootNode, severityNode, messageNode })); 284 } 285 } 286 287 for (TestError error : errs) { 288 // Error node 289 DefaultMutableTreeNode errorNode = new DefaultMutableTreeNode(error); 290 messageNode.add(errorNode); 291 } 292 } 293 } 294 } 295 296 valTreeModel.setRoot(rootNode); 297 for (TreePath path : expandedPaths) { 298 this.expandPath(path); 299 } 300 } 301 302 /** 303 * Sets the errors list used by a data layer 304 * @param errors The error list that is used by a data layer 305 */ 306 public final void setErrorList(List<TestError> errors) { 307 this.errors = errors; 308 if (isVisible()) { 309 buildTree(); 310 } 311 } 312 313 /** 314 * Clears the current error list and adds these errors to it 315 * @param newerrors The validation errors 316 */ 317 public void setErrors(List<TestError> newerrors) { 318 if (errors == null) 319 return; 320 clearErrors(); 321 DataSet ds = Main.main.getCurrentDataSet(); 322 for (TestError error : newerrors) { 323 if (!error.getIgnored()) { 324 errors.add(error); 325 if (ds != null) { 326 ds.addDataSetListener(error); 327 } 328 } 329 } 330 if (isVisible()) { 331 buildTree(); 332 } 333 } 334 335 /** 336 * Returns the errors of the tree 337 * @return the errors of the tree 338 */ 339 public List<TestError> getErrors() { 340 return errors != null ? errors : Collections.<TestError> emptyList(); 341 } 342 343 /** 344 * Returns the filter list 345 * @return the list of primitives used for filtering 346 */ 347 public Set<OsmPrimitive> getFilter() { 348 return filter; 349 } 350 351 /** 352 * Set the filter list to a set of primitives 353 * @param filter the list of primitives used for filtering 354 */ 355 public void setFilter(Set<OsmPrimitive> filter) { 356 if (filter != null && filter.isEmpty()) { 357 this.filter = null; 358 } else { 359 this.filter = filter; 360 } 361 if (isVisible()) { 362 buildTree(); 363 } 364 } 365 366 /** 367 * Updates the current errors list 368 */ 369 public void resetErrors() { 370 List<TestError> e = new ArrayList<>(errors); 371 setErrors(e); 372 } 373 374 /** 375 * Expands complete tree 376 */ 377 @SuppressWarnings("unchecked") 378 public void expandAll() { 379 DefaultMutableTreeNode root = getRoot(); 380 381 int row = 0; 382 Enumeration<DefaultMutableTreeNode> children = root.breadthFirstEnumeration(); 383 while (children.hasMoreElements()) { 384 children.nextElement(); 385 expandRow(row++); 386 } 387 } 388 389 /** 390 * Returns the root node model. 391 * @return The root node model 392 */ 393 public DefaultMutableTreeNode getRoot() { 394 return (DefaultMutableTreeNode) valTreeModel.getRoot(); 395 } 396 397 /** 398 * Returns a value to check if tree has been rebuild 399 * @return the current counter 400 */ 401 public int getUpdateCount() { 402 return updateCount; 403 } 404 405 private void clearErrors() { 406 if (errors != null) { 407 DataSet ds = Main.main.getCurrentDataSet(); 408 if (ds != null) { 409 for (TestError e : errors) { 410 ds.removeDataSetListener(e); 411 } 412 } 413 errors.clear(); 414 } 415 } 416 417 @Override 418 public void destroy() { 419 clearErrors(); 420 } 421}