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

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

fix #13429 - Clean validator tree and use listener to find changes (patch by michael2402) - gsoc-core

  • Property svn:eol-style set to native
File size: 14.9 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.Map.Entry;
17import java.util.Set;
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.validation.Severity;
33import org.openstreetmap.josm.data.validation.TestError;
34import org.openstreetmap.josm.data.validation.util.MultipleNameVisitor;
35import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
36import org.openstreetmap.josm.gui.util.GuiHelper;
37import org.openstreetmap.josm.tools.Destroyable;
38import org.openstreetmap.josm.tools.ListenerList;
39
40/**
41 * A panel that displays the error tree. The selection manager
42 * respects clicks into the selection list. Ctrl-click will remove entries from
43 * the list while single click will make the clicked entry the only selection.
44 *
45 * @author frsantos
46 */
47public class ValidatorTreePanel extends JTree implements Destroyable {
48
49 private static final class GroupTreeNode extends DefaultMutableTreeNode {
50
51 GroupTreeNode(Object userObject) {
52 super(userObject);
53 }
54
55 @Override
56 public String toString() {
57 return tr("{0} ({1})", super.toString(), getLeafCount());
58 }
59 }
60
61 /**
62 * The validation data.
63 */
64 protected DefaultTreeModel valTreeModel = new DefaultTreeModel(new DefaultMutableTreeNode());
65
66 /** The list of errors shown in the tree */
67 private transient List<TestError> errors = new ArrayList<>();
68
69 /**
70 * If {@link #filter} is not <code>null</code> only errors are displayed
71 * that refer to one of the primitives in the filter.
72 */
73 private transient Set<? extends OsmPrimitive> filter;
74
75 private final ListenerList<Runnable> invalidationListeners = ListenerList.create();
76
77 /**
78 * Constructor
79 * @param errors The list of errors
80 */
81 public ValidatorTreePanel(List<TestError> errors) {
82 ToolTipManager.sharedInstance().registerComponent(this);
83 this.setModel(valTreeModel);
84 this.setRootVisible(false);
85 this.setShowsRootHandles(true);
86 this.expandRow(0);
87 this.setVisibleRowCount(8);
88 this.setCellRenderer(new ValidatorTreeRenderer());
89 this.getSelectionModel().setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
90 setErrorList(errors);
91 for (KeyListener keyListener : getKeyListeners()) {
92 // Fix #3596 - Remove default keyListener to avoid conflicts with JOSM commands
93 if ("javax.swing.plaf.basic.BasicTreeUI$Handler".equals(keyListener.getClass().getName())) {
94 removeKeyListener(keyListener);
95 }
96 }
97 }
98
99 @Override
100 public String getToolTipText(MouseEvent e) {
101 String res = null;
102 TreePath path = getPathForLocation(e.getX(), e.getY());
103 if (path != null) {
104 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
105 Object nodeInfo = node.getUserObject();
106
107 if (nodeInfo instanceof TestError) {
108 TestError error = (TestError) nodeInfo;
109 MultipleNameVisitor v = new MultipleNameVisitor();
110 v.visit(error.getPrimitives());
111 res = "<html>" + v.getText() + "<br>" + error.getMessage();
112 String d = error.getDescription();
113 if (d != null)
114 res += "<br>" + d;
115 res += "</html>";
116 } else {
117 res = node.toString();
118 }
119 }
120 return res;
121 }
122
123 /** Constructor */
124 public ValidatorTreePanel() {
125 this(null);
126 }
127
128 @Override
129 public void setVisible(boolean v) {
130 if (v) {
131 buildTree();
132 } else {
133 valTreeModel.setRoot(new DefaultMutableTreeNode());
134 }
135 super.setVisible(v);
136 invalidationListeners.fireEvent(Runnable::run);
137 }
138
139 /**
140 * Builds the errors tree
141 */
142 public void buildTree() {
143 final DefaultMutableTreeNode rootNode = new DefaultMutableTreeNode();
144
145 if (errors == null || errors.isEmpty()) {
146 GuiHelper.runInEDTAndWait(() -> valTreeModel.setRoot(rootNode));
147 return;
148 }
149 // Sort validation errors - #8517
150 Collections.sort(errors);
151
152 // Remember the currently expanded rows
153 Set<Object> oldSelectedRows = new HashSet<>();
154 Enumeration<TreePath> expanded = getExpandedDescendants(new TreePath(getRoot()));
155 if (expanded != null) {
156 while (expanded.hasMoreElements()) {
157 TreePath path = expanded.nextElement();
158 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
159 Object userObject = node.getUserObject();
160 if (userObject instanceof Severity) {
161 oldSelectedRows.add(userObject);
162 } else if (userObject instanceof String) {
163 String msg = (String) userObject;
164 int index = msg.lastIndexOf(" (");
165 if (index > 0) {
166 msg = msg.substring(0, index);
167 }
168 oldSelectedRows.add(msg);
169 }
170 }
171 }
172
173 Predicate<TestError> filterToUse = e -> !e.isIgnored();
174 if (!ValidatorPreference.PREF_OTHER.get()) {
175 filterToUse = filterToUse.and(e -> e.getSeverity() != Severity.OTHER);
176 }
177 if (filter != null) {
178 filterToUse = filterToUse.and(e -> e.getPrimitives().stream().anyMatch(filter::contains));
179 }
180 Map<Severity, Map<String, Map<String, List<TestError>>>> errorTreeDeep
181 = errors.stream().filter(filterToUse).collect(
182 Collectors.groupingBy(e -> e.getSeverity(), () -> new EnumMap<>(Severity.class),
183 Collectors.groupingBy(e -> e.getDescription() == null ? "" : e.getDescription(),
184 Collectors.groupingBy(e -> e.getMessage()))));
185
186 List<TreePath> expandedPaths = new ArrayList<>();
187 errorTreeDeep.forEach((s, severityErrorsDeep) -> {
188 // Severity node
189 DefaultMutableTreeNode severityNode = new GroupTreeNode(s);
190 rootNode.add(severityNode);
191
192 if (oldSelectedRows.contains(s)) {
193 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode}));
194 }
195
196 Map<String, List<TestError>> severityErrors = severityErrorsDeep.get("");
197 if (severityErrors != null) {
198 for (Entry<String, List<TestError>> msgErrors : severityErrors.entrySet()) {
199 // Message node
200 List<TestError> errs = msgErrors.getValue();
201 String msg = tr("{0} ({1})", msgErrors.getKey(), errs.size());
202 DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
203 severityNode.add(messageNode);
204
205 if (oldSelectedRows.contains(msgErrors.getKey())) {
206 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
207 }
208
209 errs.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
210 }
211 }
212
213 severityErrorsDeep.forEach((description, errorlist) -> {
214 if (description.isEmpty()) {
215 return;
216 }
217 // Group node
218 DefaultMutableTreeNode groupNode;
219 if (errorlist.size() > 1) {
220 groupNode = new GroupTreeNode(description);
221 severityNode.add(groupNode);
222 if (oldSelectedRows.contains(description)) {
223 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode}));
224 }
225 } else {
226 groupNode = null;
227 }
228
229 errorlist.forEach((message, errs) -> {
230 // Message node
231 String msg;
232 if (groupNode != null) {
233 msg = tr("{0} ({1})", message, errs.size());
234 } else {
235 msg = tr("{0} - {1} ({2})", message, description, errs.size());
236 }
237 DefaultMutableTreeNode messageNode = new DefaultMutableTreeNode(msg);
238 if (groupNode != null) {
239 groupNode.add(messageNode);
240 } else {
241 severityNode.add(messageNode);
242 }
243
244 if (oldSelectedRows.contains(message)) {
245 if (groupNode != null) {
246 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, groupNode, messageNode}));
247 } else {
248 expandedPaths.add(new TreePath(new Object[] {rootNode, severityNode, messageNode}));
249 }
250 }
251
252 errs.stream().map(DefaultMutableTreeNode::new).forEach(messageNode::add);
253 });
254 });
255 });
256
257 valTreeModel.setRoot(rootNode);
258 for (TreePath path : expandedPaths) {
259 this.expandPath(path);
260 }
261
262 invalidationListeners.fireEvent(Runnable::run);
263 }
264
265 /**
266 * Add a new invalidation listener
267 * @param listener The listener
268 */
269 public void addInvalidationListener(Runnable listener) {
270 invalidationListeners.addListener(listener);
271 }
272
273 /**
274 * Remove an invalidation listener
275 * @param listener The listener
276 * @since 10880
277 */
278 public void removeInvalidationListener(Runnable listener) {
279 invalidationListeners.removeListener(listener);
280 }
281
282 /**
283 * Sets the errors list used by a data layer
284 * @param errors The error list that is used by a data layer
285 */
286 public final void setErrorList(List<TestError> errors) {
287 this.errors = errors;
288 if (isVisible()) {
289 buildTree();
290 }
291 }
292
293 /**
294 * Clears the current error list and adds these errors to it
295 * @param newerrors The validation errors
296 */
297 public void setErrors(List<TestError> newerrors) {
298 if (errors == null)
299 return;
300 clearErrors();
301 DataSet ds = Main.getLayerManager().getEditDataSet();
302 for (TestError error : newerrors) {
303 if (!error.isIgnored()) {
304 errors.add(error);
305 if (ds != null) {
306 ds.addDataSetListener(error);
307 }
308 }
309 }
310 if (isVisible()) {
311 buildTree();
312 }
313 }
314
315 /**
316 * Returns the errors of the tree
317 * @return the errors of the tree
318 */
319 public List<TestError> getErrors() {
320 return errors != null ? errors : Collections.<TestError>emptyList();
321 }
322
323 /**
324 * Selects all errors related to the specified {@code primitives}, i.e. where {@link TestError#getPrimitives()}
325 * returns a primitive present in {@code primitives}.
326 * @param primitives collection of primitives
327 */
328 public void selectRelatedErrors(final Collection<OsmPrimitive> primitives) {
329 final Collection<TreePath> paths = new ArrayList<>();
330 walkAndSelectRelatedErrors(new TreePath(getRoot()), new HashSet<>(primitives)::contains, paths);
331 getSelectionModel().clearSelection();
332 for (TreePath path : paths) {
333 expandPath(path);
334 getSelectionModel().addSelectionPath(path);
335 }
336 }
337
338 private void walkAndSelectRelatedErrors(final TreePath p, final Predicate<OsmPrimitive> isRelevant, final Collection<TreePath> paths) {
339 final int count = getModel().getChildCount(p.getLastPathComponent());
340 for (int i = 0; i < count; i++) {
341 final Object child = getModel().getChild(p.getLastPathComponent(), i);
342 if (getModel().isLeaf(child) && child instanceof DefaultMutableTreeNode
343 && ((DefaultMutableTreeNode) child).getUserObject() instanceof TestError) {
344 final TestError error = (TestError) ((DefaultMutableTreeNode) child).getUserObject();
345 if (error.getPrimitives() != null) {
346 if (error.getPrimitives().stream().anyMatch(isRelevant)) {
347 paths.add(p.pathByAddingChild(child));
348 }
349 }
350 } else {
351 walkAndSelectRelatedErrors(p.pathByAddingChild(child), isRelevant, paths);
352 }
353 }
354 }
355
356 /**
357 * Returns the filter list
358 * @return the list of primitives used for filtering
359 */
360 public Set<? extends OsmPrimitive> getFilter() {
361 return filter;
362 }
363
364 /**
365 * Set the filter list to a set of primitives
366 * @param filter the list of primitives used for filtering
367 */
368 public void setFilter(Set<? extends OsmPrimitive> filter) {
369 if (filter != null && filter.isEmpty()) {
370 this.filter = null;
371 } else {
372 this.filter = filter;
373 }
374 if (isVisible()) {
375 buildTree();
376 }
377 }
378
379 /**
380 * Updates the current errors list
381 */
382 public void resetErrors() {
383 List<TestError> e = new ArrayList<>(errors);
384 setErrors(e);
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 DataSet ds = Main.getLayerManager().getEditDataSet();
413 if (ds != null) {
414 for (TestError e : errors) {
415 ds.removeDataSetListener(e);
416 }
417 }
418 errors.clear();
419 }
420 }
421
422 @Override
423 public void destroy() {
424 clearErrors();
425 }
426}
Note: See TracBrowser for help on using the repository browser.