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

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

see #15182 - deprecate Main.getLayerManager(). Replacement: gui.MainApplication.getLayerManager()

  • 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.data.osm.DataSet;
30import org.openstreetmap.josm.data.osm.OsmPrimitive;
31import org.openstreetmap.josm.data.osm.event.AbstractDatasetChangedEvent;
32import org.openstreetmap.josm.data.osm.event.DataChangedEvent;
33import org.openstreetmap.josm.data.osm.event.DataSetListener;
34import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
35import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
36import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
37import org.openstreetmap.josm.data.osm.event.PrimitivesRemovedEvent;
38import org.openstreetmap.josm.data.osm.event.RelationMembersChangedEvent;
39import org.openstreetmap.josm.data.osm.event.TagsChangedEvent;
40import org.openstreetmap.josm.data.osm.event.WayNodesChangedEvent;
41import org.openstreetmap.josm.data.validation.Severity;
42import org.openstreetmap.josm.data.validation.TestError;
43import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
44import org.openstreetmap.josm.gui.MainApplication;
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().stream().anyMatch(isRelevant)) {
357 paths.add(p.pathByAddingChild(child));
358 }
359 } else {
360 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
361 }
362 }
363 }
364
365 /**
366 * Returns the filter list
367 * @return the list of primitives used for filtering
368 */
369 public Set<? extends OsmPrimitive> getFilter() {
370 return filter;
371 }
372
373 /**
374 * Set the filter list to a set of primitives
375 * @param filter the list of primitives used for filtering
376 */
377 public void setFilter(Set<? extends OsmPrimitive> filter) {
378 if (filter != null && filter.isEmpty()) {
379 this.filter = null;
380 } else {
381 this.filter = filter;
382 }
383 if (isVisible()) {
384 buildTree();
385 }
386 }
387
388 /**
389 * Updates the current errors list
390 */
391 public void resetErrors() {
392 List<TestError> e = new ArrayList<>(errors);
393 setErrors(e);
394 }
395
396 /**
397 * Expands complete tree
398 */
399 @SuppressWarnings("unchecked")
400 public void expandAll() {
401 DefaultMutableTreeNode root = getRoot();
402
403 int row = 0;
404 Enumeration<TreeNode> children = root.breadthFirstEnumeration();
405 while (children.hasMoreElements()) {
406 children.nextElement();
407 expandRow(row++);
408 }
409 }
410
411 /**
412 * Returns the root node model.
413 * @return The root node model
414 */
415 public DefaultMutableTreeNode getRoot() {
416 return (DefaultMutableTreeNode) valTreeModel.getRoot();
417 }
418
419 private void clearErrors() {
420 if (errors != null) {
421 errors.clear();
422 }
423 }
424
425 @Override
426 public void destroy() {
427 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
428 if (ds != null) {
429 ds.removeDataSetListener(this);
430 }
431 clearErrors();
432 }
433
434 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
435 // Remove purged primitives (fix #8639)
436 if (errors != null) {
437 final Set<? extends OsmPrimitive> deletedPrimitives = new HashSet<>(event.getPrimitives());
438 errors.removeIf(error -> error.getPrimitives().stream().anyMatch(deletedPrimitives::contains));
439 }
440 }
441
442 @Override public void primitivesAdded(PrimitivesAddedEvent event) {
443 // Do nothing
444 }
445
446 @Override public void tagsChanged(TagsChangedEvent event) {
447 // Do nothing
448 }
449
450 @Override public void nodeMoved(NodeMovedEvent event) {
451 // Do nothing
452 }
453
454 @Override public void wayNodesChanged(WayNodesChangedEvent event) {
455 // Do nothing
456 }
457
458 @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
459 // Do nothing
460 }
461
462 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
463 // Do nothing
464 }
465
466 @Override public void dataChanged(DataChangedEvent event) {
467 // Do nothing
468 }
469}
Note: See TracBrowser for help on using the repository browser.