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

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

fix #17412: Update validator tree when primitives are purged or removed and existing errors refer to those primitives

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