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

Last change on this file since 11335 was 11335, checked in by simon04, 7 years ago

fix #13948 fix #14040 - UnsupportedOperationException in ValidatorTreePanel

  • Property svn:eol-style set to native
File size: 17.0 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.EnumMap;
12import java.util.Enumeration;
13import java.util.HashSet;
14import java.util.List;
15import java.util.Map;
16import java.util.Set;
17import java.util.TreeMap;
18import java.util.function.Predicate;
19import java.util.stream.Collectors;
20
21import javax.swing.JTree;
22import javax.swing.ToolTipManager;
23import javax.swing.tree.DefaultMutableTreeNode;
24import javax.swing.tree.DefaultTreeModel;
25import javax.swing.tree.TreeNode;
26import javax.swing.tree.TreePath;
27import javax.swing.tree.TreeSelectionModel;
28
29import org.openstreetmap.josm.Main;
30import org.openstreetmap.josm.data.osm.DataSet;
31import org.openstreetmap.josm.data.osm.OsmPrimitive;
32import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
33import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
34import org.openstreetmap.josm.data.osm.event.DataSetListener;
35import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
36import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
37import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
38import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
39import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
40import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
41import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
42import org.openstreetmap.josm.data.validation.Severity;
43import org.openstreetmap.josm.data.validation.TestError;
44import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
45import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
46import org.openstreetmap.josm.gui.util.GuiHelper;
47import org.openstreetmap.josm.tools.AlphanumComparator;
48import org.openstreetmap.josm.tools.Destroyable;
49import org.openstreetmap.josm.tools.ListenerList;
50
51/**
52 * A panel that displays the error tree. The selection manager
53 * respects clicks into the selection list. Ctrl-click will remove entries from
54 * the list while single click will make the clicked entry the only selection.
55 *
56 * @author frsantos
57 */
58public class ValidatorTreePanel extends JTree implements Destroyable, DataSetListener {
59
60 private static final class GroupTreeNode extends DefaultMutableTreeNode {
61
62 GroupTreeNode(Object userObject) {
63 super(userObject);
64 }
65
66 @Override
67 public String toString() {
68 return tr("{0} ({1})", super.toString(), getLeafCount());
69 }
70 }
71
72 /**
73 * The validation data.
74 */
75 protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
76
77 /** The list of errors shown in the tree */
78 private transient List<TestError> errors = new ArrayList<>();
79
80 /**
81 * If {@link #filter} is not <code>null</code> only errors are displayed
82 * that refer to one of the primitives in the filter.
83 */
84 private transient Set<? extends OsmPrimitive> filter;
85
86 private final ListenerList<Runnable> invalidationListeners = ListenerList.create();
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 MultipleNameVisitor v = new MultipleNameVisitor();
122 v.visit(error.getPrimitives());
123 res = "<html>" + v.getText() + "<br>" + error.getMessage();
124 String d = error.getDescription();
125 if (d != null)
126 res += "<br>" + d;
127 res += "</html>";
128 } else {
129 res = node.toString();
130 }
131 }
132 return res;
133 }
134
135 /** Constructor */
136 public ValidatorTreePanel() {
137 this(null);
138 }
139
140 @Override
141 public void setVisible(boolean v) {
142 if (v) {
143 buildTree();
144 } else {
145 valTreeModel.setRoot(new DefaultMutableTreeNode());
146 }
147 super.setVisible(v);
148 invalidationListeners.fireEvent(Runnable::run);
149 }
150
151 /**
152 * Builds the errors tree
153 */
154 public void buildTree() {
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 Collections.sort(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 (!ValidatorPreference.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 = errors.stream().filter(filterToUse).collect(
194 Collectors.groupingBy(TestError::getSeverity, () -> new EnumMap<>(Severity.class),
195 Collectors.groupingBy(TestError::getMessage, () -> new TreeMap<>(AlphanumComparator.getInstance()),
196 Collectors.groupingBy(e -> e.getDescription() == null ? "" : e.getDescription(),
197 () -> new TreeMap<>(AlphanumComparator.getInstance()),
198 Collectors.toList()
199 ))));
200
201 final List<TreePath> expandedPaths = new ArrayList<>();
202 errorsBySeverityMessageDescription.forEach((severity, errorsByMessageDescription) -> {
203 // Severity node
204 final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity);
205 rootNode.add(severityNode);
206
207 if (oldSelectedRows.contains(severity)) {
208 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode}));
209 }
210
211 final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get("");
212 if (errorsWithEmptyMessageByDescription != null) {
213 errorsWithEmptyMessageByDescription.forEach((description, errors) -> {
214 final String msg = tr("{0} ({1})", description, errors.size());
215 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
216 severityNode.add(messageNode);
217
218 if (oldSelectedRows.contains(description)) {
219 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
220 }
221
222 errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
223 });
224 }
225
226 errorsByMessageDescription.forEach((message, errorsByDescription) -> {
227 if (message.isEmpty()) {
228 return;
229 }
230 // Group node
231 final DefaultMutableTreeNode groupNode;
232 if (errorsByDescription.size() > 1) {
233 groupNode = new GroupTreeNode(message);
234 severityNode.add(groupNode);
235 if (oldSelectedRows.contains(message)) {
236 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode}));
237 }
238 } else {
239 groupNode = null;
240 }
241
242 errorsByDescription.forEach((description, errors) -> {
243 // Message node
244 final String msg;
245 if (groupNode != null) {
246 msg = tr("{0} ({1})", description, errors.size());
247 } else if (description == null || description.isEmpty()) {
248 msg = tr("{0} ({1})", message, errors.size());
249 } else {
250 msg = tr("{0} - {1} ({2})", message, description, errors.size());
251 }
252 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
253 if (groupNode != null) {
254 groupNode.add(messageNode);
255 } else {
256 severityNode.add(messageNode);
257 }
258
259 if (oldSelectedRows.contains(description)) {
260 if (groupNode != null) {
261 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode}));
262 } else {
263 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
264 }
265 }
266
267 errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
268 });
269 });
270 });
271
272 valTreeModel.setRoot(rootNode);
273 for (TreePath path : expandedPaths) {
274 this.expandPath(path);
275 }
276
277 invalidationListeners.fireEvent(Runnable::run);
278 }
279
280 /**
281 * Add a new invalidation listener
282 * @param listener The listener
283 */
284 public void addInvalidationListener(Runnable listener) {
285 invalidationListeners.addListener(listener);
286 }
287
288 /**
289 * Remove an invalidation listener
290 * @param listener The listener
291 * @since 10880
292 */
293 public void removeInvalidationListener(Runnable listener) {
294 invalidationListeners.removeListener(listener);
295 }
296
297 /**
298 * Sets the errors list used by a data layer
299 * @param errors The error list that is used by a data layer
300 */
301 public final void setErrorList(List<TestError> errors) {
302 this.errors = errors;
303 if (isVisible()) {
304 buildTree();
305 }
306 }
307
308 /**
309 * Clears the current error list and adds these errors to it
310 * @param newerrors The validation errors
311 */
312 public void setErrors(List<TestError> newerrors) {
313 if (errors == null)
314 return;
315 clearErrors();
316 for (TestError error : newerrors) {
317 if (!error.isIgnored()) {
318 errors.add(error);
319 }
320 }
321 if (isVisible()) {
322 buildTree();
323 }
324 }
325
326 /**
327 * Returns the errors of the tree
328 * @return the errors of the tree
329 */
330 public List<TestError> getErrors() {
331 return errors != null ? errors : Collections.<TestError>emptyList();
332 }
333
334 /**
335 * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
336 * returns a primitive present in {@code primitives}.
337 * @param primitives collection of primitives
338 */
339 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
340 final Collection<TreePath> paths = new ArrayList<>();
341 walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
342 getSelectionModel().clearSelection();
343 for (TreePath path : paths) {
344 expandPath(path);
345 getSelectionModel().addSelectionPath(path);
346 }
347 }
348
349 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
350 final int count = getModel().getChildCount(p.getLastPathComponent());
351 for (int i = 0; i < count; i++) {
352 final Object child = getModel().getChild(p.getLastPathComponent(), i);
353 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
354 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
355 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
356 if (error.getPrimitives() != null) {
357 if (error.getPrimitives().stream().anyMatch(isRelevant)) {
358 paths.add(p.pathByAddingChild(child));
359 }
360 }
361 } else {
362 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
363 }
364 }
365 }
366
367 /**
368 * Returns the filter list
369 * @return the list of primitives used for filtering
370 */
371 public Set<? extends OsmPrimitive> getFilter() {
372 return filter;
373 }
374
375 /**
376 * Set the filter list to a set of primitives
377 * @param filter the list of primitives used for filtering
378 */
379 public void setFilter(Set<? extends OsmPrimitive> filter) {
380 if (filter != null && filter.isEmpty()) {
381 this.filter = null;
382 } else {
383 this.filter = filter;
384 }
385 if (isVisible()) {
386 buildTree();
387 }
388 }
389
390 /**
391 * Updates the current errors list
392 */
393 public void resetErrors() {
394 List<TestError> e = new ArrayList<>(errors);
395 setErrors(e);
396 }
397
398 /**
399 * Expands complete tree
400 */
401 @SuppressWarnings("unchecked")
402 public void expandAll() {
403 DefaultMutableTreeNode root = getRoot();
404
405 int row = 0;
406 Enumeration<TreeNode> children = root.breadthFirstEnumeration();
407 while (children.hasMoreElements()) {
408 children.nextElement();
409 expandRow(row++);
410 }
411 }
412
413 /**
414 * Returns the root node model.
415 * @return The root node model
416 */
417 public DefaultMutableTreeNode getRoot() {
418 return (DefaultMutableTreeNode) valTreeModel.getRoot();
419 }
420
421 private void clearErrors() {
422 if (errors != null) {
423 errors.clear();
424 }
425 }
426
427 @Override
428 public void destroy() {
429 DataSet ds = Main.getLayerManager().getEditDataSet();
430 if (ds != null) {
431 ds.removeDataSetListener(this);
432 }
433 clearErrors();
434 }
435
436 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
437 // Remove purged primitives (fix #8639)
438 if (errors != null) {
439 final Set<? extends OsmPrimitive> deletedPrimitives = new HashSet<>(event.getPrimitives());
440 errors.removeIf(error -> error.getPrimitives().stream().anyMatch(deletedPrimitives::contains));
441 }
442 }
443
444 @Override public void primitivesAdded(PrimitivesAddedEvent event) {
445 // Do nothing
446 }
447
448 @Override public void tagsChanged(TagsChangedEvent event) {
449 // Do nothing
450 }
451
452 @Override public void nodeMoved(NodeMovedEvent event) {
453 // Do nothing
454 }
455
456 @Override public void wayNodesChanged(WayNodesChangedEvent event) {
457 // Do nothing
458 }
459
460 @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
461 // Do nothing
462 }
463
464 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
465 // Do nothing
466 }
467
468 @Override public void dataChanged(DataChangedEvent event) {
469 // Do nothing
470 }
471}
Note: See TracBrowser for help on using the repository browser.