source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/validator/ValidatorTreePanel.java@ 14973

Last change on this file since 14973 was 14973, checked in by GerdP, 5 years ago

simplify code, improve performance: Remove obsolete method sortErrors()

The sorting was introduced to fix #8517 but is no longer needed since errors are reordered in OsmValidator.getErrorsBySeverityMessageDescription() when the tree is created. The obsolete sort takes much longer and doesn't improve anything.

  • Property svn:eol-style set to native
File size: 21.1 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs.validator;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.KeyListener;
7import java.awt.event.MouseEvent;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.Enumeration;
11import java.util.HashSet;
12import java.util.List;
13import java.util.Map;
14import java.util.Map.Entry;
15import java.util.Set;
16import java.util.function.Consumer;
17import java.util.function.Predicate;
18
19import javax.swing.JTree;
20import javax.swing.ToolTipManager;
21import javax.swing.tree.DefaultMutableTreeNode;
22import javax.swing.tree.DefaultTreeModel;
23import javax.swing.tree.TreeNode;
24import javax.swing.tree.TreePath;
25import javax.swing.tree.TreeSelectionModel;
26
27import org.openstreetmap.josm.data.osm.OsmPrimitive;
28import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
29import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
30import org.openstreetmap.josm.data.osm.event.DataSetListener;
31import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
32import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
33import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
34import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
35import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
36import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
37import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
38import org.openstreetmap.josm.data.preferences.sources.ValidatorPrefHelper;
39import org.openstreetmap.josm.data.validation.OsmValidator;
40import org.openstreetmap.josm.data.validation.Severity;
41import org.openstreetmap.josm.data.validation.TestError;
42import org.openstreetmap.josm.gui.util.GuiHelper;
43import org.openstreetmap.josm.tools.Destroyable;
44import org.openstreetmap.josm.tools.ListenerList;
45
46/**
47 * A panel that displays the error tree. The selection manager
48 * respects clicks into the selection list. Ctrl-click will remove entries from
49 * the list while single click will make the clicked entry the only selection.
50 *
51 * @author frsantos
52 */
53public class ValidatorTreePanel extends JTree implements Destroyable, DataSetListener {
54
55 private static final class GroupTreeNode extends DefaultMutableTreeNode {
56
57 GroupTreeNode(Object userObject) {
58 super(userObject);
59 }
60
61 @Override
62 public String toString() {
63 return tr("{0} ({1})", super.toString(), getLeafCount());
64 }
65 }
66
67 /**
68 * The validation data.
69 */
70 protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
71
72 /** The list of errors shown in the tree, normally identical to field validationErrors in current edit layer*/
73 private transient List<TestError> errors;
74
75 /**
76 * If {@link #filter} is not <code>null</code> only errors are displayed
77 * that refer to one of the primitives in the filter.
78 */
79 private transient Set<? extends OsmPrimitive> filter;
80
81 private final transient ListenerList<Runnable> invalidationListeners = ListenerList.create();
82
83 /** if true, buildTree() does nothing */
84 private boolean resetScheduled;
85
86 /**
87 * Constructor
88 * @param errors The list of errors
89 */
90 public ValidatorTreePanel(List<TestError> errors) {
91 setErrorList(errors);
92 ToolTipManager.sharedInstance().registerComponent(this);
93 this.setModel(valTreeModel);
94 this.setRootVisible(false);
95 this.setShowsRootHandles(true);
96 this.expandRow(0);
97 this.setVisibleRowCount(8);
98 this.setCellRenderer(new ValidatorTreeRenderer());
99 this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
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 res = "<html>" + error.getNameVisitor().getText() + "<br>" + error.getMessage();
120 String d = error.getDescription();
121 if (d != null)
122 res += "<br>" + d;
123 res += "</html>";
124 } else {
125 res = node.toString();
126 }
127 }
128 return res;
129 }
130
131 /** Constructor */
132 public ValidatorTreePanel() {
133 this(null);
134 }
135
136 @Override
137 public void setVisible(boolean v) {
138 if (v) {
139 buildTree();
140 } else {
141 valTreeModel.setRoot(new DefaultMutableTreeNode());
142 }
143 super.setVisible(v);
144 invalidationListeners.fireEvent(Runnable::run);
145 }
146
147 /**
148 * Builds the errors tree
149 */
150 public void buildTree() {
151 buildTree(true);
152 }
153
154 /**
155 * Builds the errors tree
156 * @param expandAgain if true, try to expand the same rows as before
157 */
158 public void buildTree(boolean expandAgain) {
159 if (resetScheduled)
160 return;
161 final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
162
163 if (errors == null || errors.isEmpty()) {
164 GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode));
165 return;
166 }
167
168 // Remember first selected tree row
169 TreePath selPath = getSelectionPath();
170 int selRow = selPath == null ? -1 : getRowForPath(selPath);
171
172 // Remember the currently expanded rows
173 Set<Object> oldExpandedRows = new HashSet<>();
174 if (expandAgain) {
175 Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
176 if (expanded != null) {
177 while (expanded.hasMoreElements()) {
178 TreePath path = expanded.nextElement();
179 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
180 Object userObject = node.getUserObject();
181 if (userObject instanceof Severity) {
182 oldExpandedRows.add(userObject);
183 } else if (userObject instanceof String) {
184 String msg = removeSize((String) userObject);
185 oldExpandedRows.add(msg);
186 }
187 }
188 }
189 }
190
191 Predicate<TestError> filterToUse = e -> !e.isIgnored();
192 if (!ValidatorPrefHelper.PREF_OTHER.get()) {
193 filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
194 }
195 if (filter != null) {
196 filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
197 }
198 Map<Severity, Map<String, Map<String, List<TestError>>>> errorsBySeverityMessageDescription
199 = OsmValidator.getErrorsBySeverityMessageDescription(errors, filterToUse);
200
201 final List<TreePath> expandedPaths = new ArrayList<>();
202 for (Entry<Severity, Map<String, Map<String, List<TestError>>>> entry: errorsBySeverityMessageDescription.entrySet()) {
203 Severity severity = entry.getKey();
204 Map<String, Map<String, List<TestError>>> errorsByMessageDescription = entry.getValue();
205
206 // Severity node
207 final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity);
208 rootNode.add(severityNode);
209
210 if (oldExpandedRows.contains(severity)) {
211 expandedPaths.add(new TreePath(severityNode.getPath()));
212 }
213
214 final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get("");
215 if (errorsWithEmptyMessageByDescription != null) {
216 errorsWithEmptyMessageByDescription.forEach((description, noDescriptionErrors) -> {
217 final String msg = addSize(description, noDescriptionErrors);
218 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
219 severityNode.add(messageNode);
220
221 if (oldExpandedRows.contains(description)) {
222 expandedPaths.add(new TreePath(messageNode.getPath()));
223 }
224 // add the matching errors to the current node
225 noDescriptionErrors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
226 });
227 }
228
229 errorsByMessageDescription.forEach((message, errorsByDescription) -> {
230 if (message.isEmpty()) {
231 return;
232 }
233 // Group node
234 final DefaultMutableTreeNode groupNode;
235 if (errorsByDescription.size() > 1) {
236 groupNode = new GroupTreeNode(message);
237 severityNode.add(groupNode);
238 if (oldExpandedRows.contains(message)) {
239 expandedPaths.add(new TreePath(groupNode.getPath()));
240 }
241 } else {
242 groupNode = null;
243 }
244
245 errorsByDescription.forEach((description, errorsWithDescription) -> {
246 // Message node
247 final String searchMsg;
248 if (groupNode != null) {
249 searchMsg = description;
250 } else if (description == null || description.isEmpty()) {
251 searchMsg = message;
252 } else {
253 searchMsg = message + " - " + description;
254 }
255 final String msg = addSize(searchMsg, errorsWithDescription);
256
257 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
258 DefaultMutableTreeNode currNode = groupNode != null ? groupNode : severityNode;
259 currNode.add(messageNode);
260 if (oldExpandedRows.contains(searchMsg)) {
261 expandedPaths.add(new TreePath(messageNode.getPath()));
262 }
263
264 // add the matching errors to the current node
265 errorsWithDescription.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
266 });
267 });
268 }
269
270 valTreeModel.setRoot(rootNode);
271 for (TreePath path : expandedPaths) {
272 this.expandPath(path);
273 }
274
275 if (selPath != null) {
276 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent();
277 Object userObject = node.getUserObject();
278 if (userObject instanceof TestError && ((TestError) userObject).isIgnored()) {
279 // don't try to find ignored error
280 selPath = null;
281 }
282 }
283 if (selPath != null) {
284 // try to reselect previously selected row. May not work if tree structure changed too much.
285 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getLastPathComponent();
286 Object searchObject = node.getUserObject();
287 String msg = null;
288 if (searchObject instanceof String) {
289 msg = removeSize((String) searchObject);
290 }
291 String searchString = msg;
292 visitTreeNodes(getRoot(), n -> {
293 boolean found = false;
294 final Object userInfo = n.getUserObject();
295 if (searchObject instanceof TestError && userInfo instanceof TestError) {
296 TestError e1 = (TestError) searchObject;
297 TestError e2 = (TestError) userInfo;
298 found |= e1.getCode() == e2.getCode() && e1.getMessage().equals(e2.getMessage())
299 && e1.getPrimitives().size() == e2.getPrimitives().size()
300 && e1.getPrimitives().containsAll(e2.getPrimitives());
301 } else if (searchObject instanceof String && userInfo instanceof String) {
302 found |= ((String) userInfo).startsWith(searchString);
303 } else if (searchObject instanceof Severity) {
304 found |= searchObject.equals(userInfo);
305 }
306
307 if (found) {
308 TreePath path = new TreePath(n.getPath());
309 setSelectionPath(path);
310 scrollPathToVisible(path);
311 }
312 });
313 }
314 if (selRow >= 0 && selRow < getRowCount() && getSelectionCount() == 0) {
315 // fall back: if we cannot find the previously selected entry, select the row by position
316 setSelectionRow(selRow);
317 scrollRowToVisible(selRow);
318 }
319
320 invalidationListeners.fireEvent(Runnable::run);
321 }
322
323 private static String addSize(String msg, Collection<?> coll) {
324 return msg + " (" + coll.size() + ")";
325 }
326
327 private static String removeSize(String msg) {
328 int index = msg.lastIndexOf(" (");
329 return index > 0 ? msg.substring(0, index) : msg;
330 }
331
332 /**
333 * Add a new invalidation listener
334 * @param listener The listener
335 */
336 public void addInvalidationListener(Runnable listener) {
337 invalidationListeners.addListener(listener);
338 }
339
340 /**
341 * Remove an invalidation listener
342 * @param listener The listener
343 * @since 10880
344 */
345 public void removeInvalidationListener(Runnable listener) {
346 invalidationListeners.removeListener(listener);
347 }
348
349 /**
350 * Sets the errors list used by a data layer
351 * @param errors The error list that is used by a data layer
352 */
353 public final void setErrorList(List<TestError> errors) {
354 if (errors != null && errors == this.errors)
355 return;
356 this.errors = errors != null ? errors : new ArrayList<>();
357 if (isVisible()) {
358 //TODO: If list is changed because another layer was activated it would be good to store/restore
359 // the expanded / selected paths.
360 clearSelection();
361 buildTree(false);
362 }
363 }
364
365 /**
366 * Clears the current error list and adds these errors to it
367 * @param newerrors The validation errors
368 */
369 public void setErrors(List<TestError> newerrors) {
370 errors.clear();
371 for (TestError error : newerrors) {
372 if (!error.isIgnored()) {
373 errors.add(error);
374 }
375 }
376 if (isVisible()) {
377 buildTree();
378 }
379 }
380
381 /**
382 * Returns the errors of the tree
383 * @return the errors of the tree
384 */
385 public List<TestError> getErrors() {
386 return errors;
387 }
388
389 /**
390 * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
391 * returns a primitive present in {@code primitives}.
392 * @param primitives collection of primitives
393 */
394 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
395 final List<TreePath> paths = new ArrayList<>();
396 walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
397 clearSelection();
398 setSelectionPaths(paths.toArray(new TreePath[0]));
399 // make sure that first path is visible
400 if (!paths.isEmpty()) {
401 scrollPathToVisible(paths.get(0));
402 }
403 }
404
405 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
406 final int count = getModel().getChildCount(p.getLastPathComponent());
407 for (int i = 0; i < count; i++) {
408 final Object child = getModel().getChild(p.getLastPathComponent(), i);
409 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
410 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
411 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
412 if (error.getPrimitives().stream().anyMatch(isRelevant)) {
413 paths.add(p.pathByAddingChild(child));
414 }
415 } else {
416 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
417 }
418 }
419 }
420
421 /**
422 * Returns the filter list
423 * @return the list of primitives used for filtering
424 */
425 public Set<? extends OsmPrimitive> getFilter() {
426 return filter;
427 }
428
429 /**
430 * Set the filter list to a set of primitives
431 * @param filter the list of primitives used for filtering
432 */
433 public void setFilter(Set<? extends OsmPrimitive> filter) {
434 if (filter != null && filter.isEmpty()) {
435 this.filter = null;
436 } else {
437 this.filter = filter;
438 }
439 if (isVisible()) {
440 buildTree();
441 }
442 }
443
444 /**
445 * Updates the current errors list
446 */
447 public void resetErrors() {
448 resetScheduled = false;
449 filterRemovedPrimitives();
450 setErrors(new ArrayList<>(errors));
451 }
452
453 /**
454 * Expands complete tree
455 */
456 public void expandAll() {
457 visitTreeNodes(getRoot(), x -> expandPath(new TreePath(x.getPath())));
458 }
459
460 /**
461 * Returns the root node model.
462 * @return The root node model
463 */
464 public DefaultMutableTreeNode getRoot() {
465 return (DefaultMutableTreeNode) valTreeModel.getRoot();
466 }
467
468 @Override
469 public void destroy() {
470 DatasetEventManager.getInstance().removeDatasetListener(this);
471 ToolTipManager.sharedInstance().unregisterComponent(this);
472 errors.clear();
473 }
474
475 /**
476 * Visitor call for all tree nodes children of root, in breadth-first order.
477 * @param root Root node
478 * @param visitor Visitor
479 * @since 13940
480 */
481 public static void visitTreeNodes(DefaultMutableTreeNode root, Consumer<DefaultMutableTreeNode> visitor) {
482 @SuppressWarnings("unchecked")
483 Enumeration<TreeNode> errorMessages = root.breadthFirstEnumeration();
484 while (errorMessages.hasMoreElements()) {
485 visitor.accept(((DefaultMutableTreeNode) errorMessages.nextElement()));
486 }
487 }
488
489 /**
490 * Visitor call for all {@link TestError} nodes children of root, in breadth-first order.
491 * @param root Root node
492 * @param visitor Visitor
493 * @since 13940
494 */
495 public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor) {
496 visitTestErrors(root, visitor, null);
497 }
498
499 /**
500 * Visitor call for all {@link TestError} nodes children of root, in breadth-first order.
501 * @param root Root node
502 * @param visitor Visitor
503 * @param processedNodes Set of already visited nodes (optional)
504 * @since 13940
505 */
506 public static void visitTestErrors(DefaultMutableTreeNode root, Consumer<TestError> visitor,
507 Set<DefaultMutableTreeNode> processedNodes) {
508 visitTreeNodes(root, n -> {
509 if (processedNodes == null || !processedNodes.contains(n)) {
510 if (processedNodes != null) {
511 processedNodes.add(n);
512 }
513 Object o = n.getUserObject();
514 if (o instanceof TestError) {
515 visitor.accept((TestError) o);
516 }
517 }
518 });
519 }
520
521 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
522 // Remove purged primitives (fix #8639)
523 if (filterRemovedPrimitives()) {
524 buildTree();
525 }
526 }
527
528 @Override public void primitivesAdded(PrimitivesAddedEvent event) {
529 // Do nothing
530 }
531
532 @Override public void tagsChanged(TagsChangedEvent event) {
533 // Do nothing
534 }
535
536 @Override public void nodeMoved(NodeMovedEvent event) {
537 // Do nothing
538 }
539
540 @Override public void wayNodesChanged(WayNodesChangedEvent event) {
541 // Do nothing
542 }
543
544 @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
545 // Do nothing
546 }
547
548 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
549 // Do nothing
550 }
551
552 @Override public void dataChanged(DataChangedEvent event) {
553 if (filterRemovedPrimitives()) {
554 buildTree();
555 }
556 }
557
558 /**
559 * Can be called to suppress execution of buildTree() while doing multiple updates. Caller must
560 * call resetErrors() to end this state.
561 * @since 14849
562 */
563 public void setResetScheduled() {
564 resetScheduled = true;
565 }
566
567 /**
568 * Remove errors which refer to removed or purged primitives.
569 * @return true if error list was changed
570 */
571 private boolean filterRemovedPrimitives() {
572 return errors.removeIf(
573 error -> error.getPrimitives().stream().anyMatch(p -> p.isDeleted() || p.getDataSet() == null));
574 }
575
576}
Note: See TracBrowser for help on using the repository browser.