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

Last change on this file since 18208 was 18208, checked in by Don-vip, 3 years ago

global use of Utils.isEmpty/isBlank

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