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

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

fix #14665 - Validator incorrectly folds first-level categories when ignoring elements + avoid repaint of complete mapview, invalidate validator layer instead

  • Property svn:eol-style set to native
File size: 16.5 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.Predicate;
17
18import javax.swing.JTree;
19import javax.swing.ToolTipManager;
20import javax.swing.tree.DefaultMutableTreeNode;
21import javax.swing.tree.DefaultTreeModel;
22import javax.swing.tree.TreeNode;
23import javax.swing.tree.TreePath;
24import javax.swing.tree.TreeSelectionModel;
25
26import org.openstreetmap.josm.data.osm.DataSet;
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.data.validation.util.MultipleNameVisitor;
43import org.openstreetmap.josm.gui.MainApplication;
44import org.openstreetmap.josm.gui.util.GuiHelper;
45import org.openstreetmap.josm.tools.Destroyable;
46import org.openstreetmap.josm.tools.ListenerList;
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 ListenerList<Runnable> invalidationListeners = ListenerList.create();
84
85 /**
86 * Constructor
87 * @param errors The list of errors
88 */
89 public ValidatorTreePanel(List<TestError> errors) {
90 ToolTipManager.sharedInstance().registerComponent(this);
91 this.setModel(valTreeModel);
92 this.setRootVisible(false);
93 this.setShowsRootHandles(true);
94 this.expandRow(0);
95 this.setVisibleRowCount(8);
96 this.setCellRenderer(new ValidatorTreeRenderer());
97 this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
98 setErrorList(errors);
99 for (KeyListener keyListener : getKeyListeners()) {
100 // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands
101 if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) {
102 removeKeyListener(keyListener);
103 }
104 }
105 DatasetEventManager.getInstance().addDatasetListener(this, DatasetEventManager.FireMode.IN_EDT);
106 }
107
108 @Override
109 public String getToolTipText(MouseEvent e) {
110 String res = null;
111 TreePath path = getPathForLocation(e.getX(), e.getY());
112 if (path != null) {
113 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
114 Object nodeInfo = node.getUserObject();
115
116 if (nodeInfo instanceof TestError) {
117 TestError error = (TestError) nodeInfo;
118 MultipleNameVisitor v = new MultipleNameVisitor();
119 v.visit(error.getPrimitives());
120 res = "<html>" + v.getText() + "<br>" + error.getMessage();
121 String d = error.getDescription();
122 if (d != null)
123 res += "<br>" + d;
124 res += "</html>";
125 } else {
126 res = node.toString();
127 }
128 }
129 return res;
130 }
131
132 /** Constructor */
133 public ValidatorTreePanel() {
134 this(null);
135 }
136
137 @Override
138 public void setVisible(boolean v) {
139 if (v) {
140 buildTree();
141 } else {
142 valTreeModel.setRoot(new DefaultMutableTreeNode());
143 }
144 super.setVisible(v);
145 invalidationListeners.fireEvent(Runnable::run);
146 }
147
148 /**
149 * Builds the errors tree
150 */
151 public void buildTree() {
152 final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
153
154 if (errors == null || errors.isEmpty()) {
155 GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode));
156 return;
157 }
158 // Sort validation errors - #8517
159 Collections.sort(errors);
160
161 // Remember the currently expanded rows
162 Set<Object> oldSelectedRows = new HashSet<>();
163 Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
164 if (expanded != null) {
165 while (expanded.hasMoreElements()) {
166 TreePath path = expanded.nextElement();
167 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
168 Object userObject = node.getUserObject();
169 if (userObject instanceof Severity) {
170 oldSelectedRows.add(userObject);
171 } else if (userObject instanceof String) {
172 String msg = (String) userObject;
173 int index = msg.lastIndexOf(" (");
174 if (index > 0) {
175 msg = msg.substring(0, index);
176 }
177 oldSelectedRows.add(msg);
178 }
179 }
180 }
181
182 Predicate<TestError> filterToUse = e -> !e.isIgnored();
183 if (!ValidatorPrefHelper.PREF_OTHER.get()) {
184 filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
185 }
186 if (filter != null) {
187 filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
188 }
189 Map<Severity, Map<String, Map<String, List<TestError>>>> errorsBySeverityMessageDescription
190 = OsmValidator.getErrorsBySeverityMessageDescription(errors, filterToUse);
191
192 final List<TreePath> expandedPaths = new ArrayList<>();
193 errorsBySeverityMessageDescription.forEach((severity, errorsByMessageDescription) -> {
194 // Severity node
195 final DefaultMutableTreeNode severityNode = new GroupTreeNode(severity);
196 rootNode.add(severityNode);
197
198 if (oldSelectedRows.contains(severity)) {
199 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode}));
200 }
201
202 final Map<String, List<TestError>> errorsWithEmptyMessageByDescription = errorsByMessageDescription.get("");
203 if (errorsWithEmptyMessageByDescription != null) {
204 errorsWithEmptyMessageByDescription.forEach((description, errors) -> {
205 final String msg = tr("{0} ({1})", description, errors.size());
206 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
207 severityNode.add(messageNode);
208
209 if (oldSelectedRows.contains(description)) {
210 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
211 }
212
213 errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
214 });
215 }
216
217 errorsByMessageDescription.forEach((message, errorsByDescription) -> {
218 if (message.isEmpty()) {
219 return;
220 }
221 // Group node
222 final DefaultMutableTreeNode groupNode;
223 if (errorsByDescription.size() > 1) {
224 groupNode = new GroupTreeNode(message);
225 severityNode.add(groupNode);
226 if (oldSelectedRows.contains(message)) {
227 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode}));
228 }
229 } else {
230 groupNode = null;
231 }
232
233 errorsByDescription.forEach((description, errors) -> {
234 boolean emptyDescription = description == null || description.isEmpty();
235 // Message node
236 final String msg;
237 if (groupNode != null) {
238 msg = tr("{0} ({1})", description, errors.size());
239 } else if (emptyDescription) {
240 msg = tr("{0} ({1})", message, errors.size());
241 } else {
242 msg = tr("{0} - {1} ({2})", message, description, errors.size());
243 }
244 final DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
245 if (groupNode != null) {
246 groupNode.add(messageNode);
247 } else {
248 severityNode.add(messageNode);
249 }
250
251 if (oldSelectedRows.contains(description) || (emptyDescription && oldSelectedRows.contains(message))) {
252 if (groupNode != null) {
253 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode}));
254 } else {
255 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
256 }
257 }
258
259 errors.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
260 });
261 });
262 });
263
264 valTreeModel.setRoot(rootNode);
265 for (TreePath path : expandedPaths) {
266 this.expandPath(path);
267 }
268
269 invalidationListeners.fireEvent(Runnable::run);
270 }
271
272 /**
273 * Add a new invalidation listener
274 * @param listener The listener
275 */
276 public void addInvalidationListener(Runnable listener) {
277 invalidationListeners.addListener(listener);
278 }
279
280 /**
281 * Remove an invalidation listener
282 * @param listener The listener
283 * @since 10880
284 */
285 public void removeInvalidationListener(Runnable listener) {
286 invalidationListeners.removeListener(listener);
287 }
288
289 /**
290 * Sets the errors list used by a data layer
291 * @param errors The error list that is used by a data layer
292 */
293 public final void setErrorList(List<TestError> errors) {
294 this.errors = errors;
295 if (isVisible()) {
296 buildTree();
297 }
298 }
299
300 /**
301 * Clears the current error list and adds these errors to it
302 * @param newerrors The validation errors
303 */
304 public void setErrors(List<TestError> newerrors) {
305 if (errors == null)
306 return;
307 clearErrors();
308 for (TestError error : newerrors) {
309 if (!error.isIgnored()) {
310 errors.add(error);
311 }
312 }
313 if (isVisible()) {
314 buildTree();
315 }
316 }
317
318 /**
319 * Returns the errors of the tree
320 * @return the errors of the tree
321 */
322 public List<TestError> getErrors() {
323 return errors != null ? errors : Collections.<TestError>emptyList();
324 }
325
326 /**
327 * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
328 * returns a primitive present in {@code primitives}.
329 * @param primitives collection of primitives
330 */
331 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
332 final Collection<TreePath> paths = new ArrayList<>();
333 walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
334 getSelectionModel().clearSelection();
335 for (TreePath path : paths) {
336 expandPath(path);
337 getSelectionModel().addSelectionPath(path);
338 }
339 }
340
341 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
342 final int count = getModel().getChildCount(p.getLastPathComponent());
343 for (int i = 0; i < count; i++) {
344 final Object child = getModel().getChild(p.getLastPathComponent(), i);
345 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
346 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
347 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
348 if (error.getPrimitives().stream().anyMatch(isRelevant)) {
349 paths.add(p.pathByAddingChild(child));
350 }
351 } else {
352 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
353 }
354 }
355 }
356
357 /**
358 * Returns the filter list
359 * @return the list of primitives used for filtering
360 */
361 public Set<? extends OsmPrimitive> getFilter() {
362 return filter;
363 }
364
365 /**
366 * Set the filter list to a set of primitives
367 * @param filter the list of primitives used for filtering
368 */
369 public void setFilter(Set<? extends OsmPrimitive> filter) {
370 if (filter != null && filter.isEmpty()) {
371 this.filter = null;
372 } else {
373 this.filter = filter;
374 }
375 if (isVisible()) {
376 buildTree();
377 }
378 }
379
380 /**
381 * Updates the current errors list
382 */
383 public void resetErrors() {
384 setErrors(new ArrayList<>(errors));
385 }
386
387 /**
388 * Expands complete tree
389 */
390 @SuppressWarnings("unchecked")
391 public void expandAll() {
392 DefaultMutableTreeNode root = getRoot();
393
394 int row = 0;
395 Enumeration<TreeNode> children = root.breadthFirstEnumeration();
396 while (children.hasMoreElements()) {
397 children.nextElement();
398 expandRow(row++);
399 }
400 }
401
402 /**
403 * Returns the root node model.
404 * @return The root node model
405 */
406 public DefaultMutableTreeNode getRoot() {
407 return (DefaultMutableTreeNode) valTreeModel.getRoot();
408 }
409
410 private void clearErrors() {
411 if (errors != null) {
412 errors.clear();
413 }
414 }
415
416 @Override
417 public void destroy() {
418 DataSet ds = MainApplication.getLayerManager().getEditDataSet();
419 if (ds != null) {
420 ds.removeDataSetListener(this);
421 }
422 clearErrors();
423 }
424
425 @Override public void primitivesRemoved(PrimitivesRemovedEvent event) {
426 // Remove purged primitives (fix #8639)
427 if (errors != null) {
428 final Set<? extends OsmPrimitive> deletedPrimitives = new HashSet<>(event.getPrimitives());
429 errors.removeIf(error -> error.getPrimitives().stream().anyMatch(deletedPrimitives::contains));
430 }
431 }
432
433 @Override public void primitivesAdded(PrimitivesAddedEvent event) {
434 // Do nothing
435 }
436
437 @Override public void tagsChanged(TagsChangedEvent event) {
438 // Do nothing
439 }
440
441 @Override public void nodeMoved(NodeMovedEvent event) {
442 // Do nothing
443 }
444
445 @Override public void wayNodesChanged(WayNodesChangedEvent event) {
446 // Do nothing
447 }
448
449 @Override public void relationMembersChanged(RelationMembersChangedEvent event) {
450 // Do nothing
451 }
452
453 @Override public void otherDatasetChange(AbstractDatasetChangedEvent event) {
454 // Do nothing
455 }
456
457 @Override public void dataChanged(DataChangedEvent event) {
458 // Do nothing
459 }
460}
Note: See TracBrowser for help on using the repository browser.