source: josm/trunk/src/org/openstreetmap/josm/gui/dialogs/ValidatorDialog.java@ 10491

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

fix #13019 - Make commands trigger an implicit layer redraw (patch by michael2402) - gsoc-core

  • Property svn:eol-style set to native
File size: 23.6 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.gui.dialogs;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.awt.event.MouseEvent;
9import java.io.IOException;
10import java.lang.reflect.InvocationTargetException;
11import java.util.ArrayList;
12import java.util.Collection;
13import java.util.Enumeration;
14import java.util.HashSet;
15import java.util.LinkedList;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.AbstractAction;
20import javax.swing.JComponent;
21import javax.swing.JOptionPane;
22import javax.swing.JPopupMenu;
23import javax.swing.SwingUtilities;
24import javax.swing.event.TreeSelectionEvent;
25import javax.swing.event.TreeSelectionListener;
26import javax.swing.tree.DefaultMutableTreeNode;
27import javax.swing.tree.TreeNode;
28import javax.swing.tree.TreePath;
29
30import org.openstreetmap.josm.Main;
31import org.openstreetmap.josm.actions.AbstractSelectAction;
32import org.openstreetmap.josm.actions.AutoScaleAction;
33import org.openstreetmap.josm.actions.relation.EditRelationAction;
34import org.openstreetmap.josm.command.Command;
35import org.openstreetmap.josm.data.SelectionChangedListener;
36import org.openstreetmap.josm.data.osm.DataSet;
37import org.openstreetmap.josm.data.osm.Node;
38import org.openstreetmap.josm.data.osm.OsmPrimitive;
39import org.openstreetmap.josm.data.osm.WaySegment;
40import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
41import org.openstreetmap.josm.data.validation.OsmValidator;
42import org.openstreetmap.josm.data.validation.TestError;
43import org.openstreetmap.josm.data.validation.ValidatorVisitor;
44import org.openstreetmap.josm.gui.PleaseWaitRunnable;
45import org.openstreetmap.josm.gui.PopupMenuHandler;
46import org.openstreetmap.josm.gui.SideButton;
47import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
48import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeEvent;
49import org.openstreetmap.josm.gui.layer.MainLayerManager.ActiveLayerChangeListener;
50import org.openstreetmap.josm.gui.layer.OsmDataLayer;
51import org.openstreetmap.josm.gui.preferences.validator.ValidatorPreference;
52import org.openstreetmap.josm.gui.progress.ProgressMonitor;
53import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher;
54import org.openstreetmap.josm.io.OsmTransferException;
55import org.openstreetmap.josm.tools.ImageProvider;
56import org.openstreetmap.josm.tools.InputMapUtils;
57import org.openstreetmap.josm.tools.Shortcut;
58import org.xml.sax.SAXException;
59
60/**
61 * A small tool dialog for displaying the current errors. The selection manager
62 * respects clicks into the selection list. Ctrl-click will remove entries from
63 * the list while single click will make the clicked entry the only selection.
64 *
65 * @author frsantos
66 */
67public class ValidatorDialog extends ToggleDialog implements SelectionChangedListener, ActiveLayerChangeListener {
68
69 /** The display tree */
70 public ValidatorTreePanel tree;
71
72 /** The fix button */
73 private final SideButton fixButton;
74 /** The ignore button */
75 private final SideButton ignoreButton;
76 /** The select button */
77 private final SideButton selectButton;
78 /** The lookup button */
79 private final SideButton lookupButton;
80
81 private final JPopupMenu popupMenu = new JPopupMenu();
82 private final transient PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu);
83
84 /** Last selected element */
85 private DefaultMutableTreeNode lastSelectedNode;
86
87 private transient OsmDataLayer linkedLayer;
88
89 /**
90 * Constructor
91 */
92 public ValidatorDialog() {
93 super(tr("Validation Results"), "validator", tr("Open the validation window."),
94 Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation results")),
95 KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150, false, ValidatorPreference.class);
96
97 popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("problem"));
98 popupMenuHandler.addAction(new EditRelationAction());
99
100 tree = new ValidatorTreePanel();
101 tree.addMouseListener(new MouseEventHandler());
102 addTreeSelectionListener(new SelectionWatch());
103 InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
104
105 List<SideButton> buttons = new LinkedList<>();
106
107 selectButton = new SideButton(new AbstractSelectAction() {
108 @Override
109 public void actionPerformed(ActionEvent e) {
110 setSelectedItems();
111 }
112 });
113 InputMapUtils.addEnterAction(tree, selectButton.getAction());
114
115 selectButton.setEnabled(false);
116 buttons.add(selectButton);
117
118 lookupButton = new SideButton(new AbstractAction() {
119 {
120 putValue(NAME, tr("Lookup"));
121 putValue(SHORT_DESCRIPTION, tr("Looks up the selected primitives in the error list."));
122 new ImageProvider("dialogs", "search").getResource().attachImageIcon(this, true);
123 }
124
125 @Override
126 public void actionPerformed(ActionEvent e) {
127 final DataSet ds = Main.getLayerManager().getEditDataSet();
128 if (ds == null) {
129 return;
130 }
131 tree.selectRelatedErrors(ds.getSelected());
132 }
133 });
134
135 buttons.add(lookupButton);
136
137 buttons.add(new SideButton(Main.main.validator.validateAction));
138
139 fixButton = new SideButton(new AbstractAction() {
140 {
141 putValue(NAME, tr("Fix"));
142 putValue(SHORT_DESCRIPTION, tr("Fix the selected issue."));
143 new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true);
144 }
145 @Override
146 public void actionPerformed(ActionEvent e) {
147 fixErrors();
148 }
149 });
150 fixButton.setEnabled(false);
151 buttons.add(fixButton);
152
153 if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
154 ignoreButton = new SideButton(new AbstractAction() {
155 {
156 putValue(NAME, tr("Ignore"));
157 putValue(SHORT_DESCRIPTION, tr("Ignore the selected issue next time."));
158 new ImageProvider("dialogs", "fix").getResource().attachImageIcon(this, true);
159 }
160 @Override
161 public void actionPerformed(ActionEvent e) {
162 ignoreErrors();
163 }
164 });
165 ignoreButton.setEnabled(false);
166 buttons.add(ignoreButton);
167 } else {
168 ignoreButton = null;
169 }
170 createLayout(tree, true, buttons);
171 }
172
173 @Override
174 public void showNotify() {
175 DataSet.addSelectionListener(this);
176 DataSet ds = Main.getLayerManager().getEditDataSet();
177 if (ds != null) {
178 updateSelection(ds.getAllSelected());
179 }
180 Main.getLayerManager().addAndFireActiveLayerChangeListener(this);
181 }
182
183 @Override
184 public void hideNotify() {
185 Main.getLayerManager().removeActiveLayerChangeListener(this);
186 DataSet.removeSelectionListener(this);
187 }
188
189 @Override
190 public void setVisible(boolean v) {
191 if (tree != null) {
192 tree.setVisible(v);
193 }
194 super.setVisible(v);
195 Main.map.repaint();
196 }
197
198 /**
199 * Fix selected errors
200 */
201 @SuppressWarnings("unchecked")
202 private void fixErrors() {
203 TreePath[] selectionPaths = tree.getSelectionPaths();
204 if (selectionPaths == null)
205 return;
206
207 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
208
209 List<TestError> errorsToFix = new LinkedList<>();
210 for (TreePath path : selectionPaths) {
211 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
212 if (node == null) {
213 continue;
214 }
215
216 Enumeration<TreeNode> children = node.breadthFirstEnumeration();
217 while (children.hasMoreElements()) {
218 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
219 if (processedNodes.contains(childNode)) {
220 continue;
221 }
222
223 processedNodes.add(childNode);
224 Object nodeInfo = childNode.getUserObject();
225 if (nodeInfo instanceof TestError) {
226 errorsToFix.add((TestError) nodeInfo);
227 }
228 }
229 }
230
231 // run fix task asynchronously
232 //
233 FixTask fixTask = new FixTask(errorsToFix);
234 Main.worker.submit(fixTask);
235 }
236
237 /**
238 * Set selected errors to ignore state
239 */
240 @SuppressWarnings("unchecked")
241 private void ignoreErrors() {
242 int asked = JOptionPane.DEFAULT_OPTION;
243 boolean changed = false;
244 TreePath[] selectionPaths = tree.getSelectionPaths();
245 if (selectionPaths == null)
246 return;
247
248 Set<DefaultMutableTreeNode> processedNodes = new HashSet<>();
249 for (TreePath path : selectionPaths) {
250 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
251 if (node == null) {
252 continue;
253 }
254
255 Object mainNodeInfo = node.getUserObject();
256 if (!(mainNodeInfo instanceof TestError)) {
257 Set<String> state = new HashSet<>();
258 // ask if the whole set should be ignored
259 if (asked == JOptionPane.DEFAULT_OPTION) {
260 String[] a = new String[] {tr("Whole group"), tr("Single elements"), tr("Nothing")};
261 asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
262 tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
263 a, a[1]);
264 }
265 if (asked == JOptionPane.YES_NO_OPTION) {
266 Enumeration<TreeNode> children = node.breadthFirstEnumeration();
267 while (children.hasMoreElements()) {
268 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
269 if (processedNodes.contains(childNode)) {
270 continue;
271 }
272
273 processedNodes.add(childNode);
274 Object nodeInfo = childNode.getUserObject();
275 if (nodeInfo instanceof TestError) {
276 TestError err = (TestError) nodeInfo;
277 err.setIgnored(true);
278 changed = true;
279 state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
280 }
281 }
282 for (String s : state) {
283 OsmValidator.addIgnoredError(s);
284 }
285 continue;
286 } else if (asked == JOptionPane.CANCEL_OPTION || asked == JOptionPane.CLOSED_OPTION) {
287 continue;
288 }
289 }
290
291 Enumeration<TreeNode> children = node.breadthFirstEnumeration();
292 while (children.hasMoreElements()) {
293 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
294 if (processedNodes.contains(childNode)) {
295 continue;
296 }
297
298 processedNodes.add(childNode);
299 Object nodeInfo = childNode.getUserObject();
300 if (nodeInfo instanceof TestError) {
301 TestError error = (TestError) nodeInfo;
302 String state = error.getIgnoreState();
303 if (state != null) {
304 OsmValidator.addIgnoredError(state);
305 }
306 changed = true;
307 error.setIgnored(true);
308 }
309 }
310 }
311 if (changed) {
312 tree.resetErrors();
313 OsmValidator.saveIgnoredErrors();
314 Main.map.repaint();
315 }
316 }
317
318 /**
319 * Sets the selection of the map to the current selected items.
320 */
321 @SuppressWarnings("unchecked")
322 private void setSelectedItems() {
323 if (tree == null)
324 return;
325
326 Collection<OsmPrimitive> sel = new HashSet<>(40);
327
328 TreePath[] selectedPaths = tree.getSelectionPaths();
329 if (selectedPaths == null)
330 return;
331
332 for (TreePath path : selectedPaths) {
333 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
334 Enumeration<TreeNode> children = node.breadthFirstEnumeration();
335 while (children.hasMoreElements()) {
336 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
337 Object nodeInfo = childNode.getUserObject();
338 if (nodeInfo instanceof TestError) {
339 TestError error = (TestError) nodeInfo;
340 sel.addAll(error.getSelectablePrimitives());
341 }
342 }
343 }
344 DataSet ds = Main.getLayerManager().getEditDataSet();
345 if (ds != null) {
346 ds.setSelected(sel);
347 }
348 }
349
350 /**
351 * Checks for fixes in selected element and, if needed, adds to the sel
352 * parameter all selected elements
353 *
354 * @param sel
355 * The collection where to add all selected elements
356 * @param addSelected
357 * if true, add all selected elements to collection
358 * @return whether the selected elements has any fix
359 */
360 @SuppressWarnings("unchecked")
361 private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
362 boolean hasFixes = false;
363
364 DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
365 if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
366 Enumeration<TreeNode> children = lastSelectedNode.breadthFirstEnumeration();
367 while (children.hasMoreElements()) {
368 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
369 Object nodeInfo = childNode.getUserObject();
370 if (nodeInfo instanceof TestError) {
371 TestError error = (TestError) nodeInfo;
372 error.setSelected(false);
373 }
374 }
375 }
376
377 lastSelectedNode = node;
378 if (node == null)
379 return hasFixes;
380
381 Enumeration<TreeNode> children = node.breadthFirstEnumeration();
382 while (children.hasMoreElements()) {
383 DefaultMutableTreeNode childNode = (DefaultMutableTreeNode) children.nextElement();
384 Object nodeInfo = childNode.getUserObject();
385 if (nodeInfo instanceof TestError) {
386 TestError error = (TestError) nodeInfo;
387 error.setSelected(true);
388
389 hasFixes = hasFixes || error.isFixable();
390 if (addSelected) {
391 sel.addAll(error.getSelectablePrimitives());
392 }
393 }
394 }
395 selectButton.setEnabled(true);
396 if (ignoreButton != null) {
397 ignoreButton.setEnabled(true);
398 }
399
400 return hasFixes;
401 }
402
403 @Override
404 public void activeOrEditLayerChanged(ActiveLayerChangeEvent e) {
405 OsmDataLayer editLayer = e.getSource().getEditLayer();
406 if (editLayer == null) {
407 tree.setErrorList(new ArrayList<TestError>());
408 } else {
409 tree.setErrorList(editLayer.validationErrors);
410 }
411 }
412
413 /**
414 * Add a tree selection listener to the validator tree.
415 * @param listener the TreeSelectionListener
416 * @since 5958
417 */
418 public void addTreeSelectionListener(TreeSelectionListener listener) {
419 tree.addTreeSelectionListener(listener);
420 }
421
422 /**
423 * Remove the given tree selection listener from the validator tree.
424 * @param listener the TreeSelectionListener
425 * @since 5958
426 */
427 public void removeTreeSelectionListener(TreeSelectionListener listener) {
428 tree.removeTreeSelectionListener(listener);
429 }
430
431 /**
432 * Replies the popup menu handler.
433 * @return The popup menu handler
434 * @since 5958
435 */
436 public PopupMenuHandler getPopupMenuHandler() {
437 return popupMenuHandler;
438 }
439
440 /**
441 * Replies the currently selected error, or {@code null}.
442 * @return The selected error, if any.
443 * @since 5958
444 */
445 public TestError getSelectedError() {
446 Object comp = tree.getLastSelectedPathComponent();
447 if (comp instanceof DefaultMutableTreeNode) {
448 Object object = ((DefaultMutableTreeNode) comp).getUserObject();
449 if (object instanceof TestError) {
450 return (TestError) object;
451 }
452 }
453 return null;
454 }
455
456 /**
457 * Watches for double clicks and launches the popup menu.
458 */
459 class MouseEventHandler extends PopupMenuLauncher {
460
461 MouseEventHandler() {
462 super(popupMenu);
463 }
464
465 @Override
466 public void mouseClicked(MouseEvent e) {
467 fixButton.setEnabled(false);
468 if (ignoreButton != null) {
469 ignoreButton.setEnabled(false);
470 }
471 selectButton.setEnabled(false);
472
473 boolean isDblClick = isDoubleClick(e);
474
475 Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
476
477 boolean hasFixes = setSelection(sel, isDblClick);
478 fixButton.setEnabled(hasFixes);
479
480 if (isDblClick) {
481 Main.getLayerManager().getEditDataSet().setSelected(sel);
482 if (Main.pref.getBoolean("validator.autozoom", false)) {
483 AutoScaleAction.zoomTo(sel);
484 }
485 }
486 }
487
488 @Override public void launch(MouseEvent e) {
489 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
490 if (selPath == null)
491 return;
492 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
493 if (!(node.getUserObject() instanceof TestError))
494 return;
495 super.launch(e);
496 }
497
498 }
499
500 /**
501 * Watches for tree selection.
502 */
503 public class SelectionWatch implements TreeSelectionListener {
504 @Override
505 public void valueChanged(TreeSelectionEvent e) {
506 fixButton.setEnabled(false);
507 if (ignoreButton != null) {
508 ignoreButton.setEnabled(false);
509 }
510 selectButton.setEnabled(false);
511
512 Collection<OsmPrimitive> sel = new HashSet<>();
513 boolean hasFixes = setSelection(sel, true);
514 fixButton.setEnabled(hasFixes);
515 popupMenuHandler.setPrimitives(sel);
516 if (Main.map != null) {
517 Main.map.repaint();
518 }
519 }
520 }
521
522 public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
523 @Override
524 public void visit(OsmPrimitive p) {
525 if (p.isUsable()) {
526 p.accept(this);
527 }
528 }
529
530 @Override
531 public void visit(WaySegment ws) {
532 if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
533 return;
534 visit(ws.way.getNodes().get(ws.lowerIndex));
535 visit(ws.way.getNodes().get(ws.lowerIndex + 1));
536 }
537
538 @Override
539 public void visit(List<Node> nodes) {
540 for (Node n: nodes) {
541 visit(n);
542 }
543 }
544
545 @Override
546 public void visit(TestError error) {
547 if (error != null) {
548 error.visitHighlighted(this);
549 }
550 }
551 }
552
553 public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
554 if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false))
555 return;
556 if (newSelection.isEmpty()) {
557 tree.setFilter(null);
558 }
559 tree.setFilter(new HashSet<>(newSelection));
560 }
561
562 @Override
563 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
564 updateSelection(newSelection);
565 }
566
567 /**
568 * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
569 *
570 *
571 */
572 class FixTask extends PleaseWaitRunnable {
573 private final Collection<TestError> testErrors;
574 private boolean canceled;
575
576 FixTask(Collection<TestError> testErrors) {
577 super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
578 this.testErrors = testErrors == null ? new ArrayList<TestError>() : testErrors;
579 }
580
581 @Override
582 protected void cancel() {
583 this.canceled = true;
584 }
585
586 @Override
587 protected void finish() {
588 // do nothing
589 }
590
591 protected void fixError(TestError error) throws InterruptedException, InvocationTargetException {
592 if (error.isFixable()) {
593 final Command fixCommand = error.getFix();
594 if (fixCommand != null) {
595 SwingUtilities.invokeAndWait(new Runnable() {
596 @Override
597 public void run() {
598 Main.main.undoRedo.addNoRedraw(fixCommand);
599 }
600 });
601 }
602 // It is wanted to ignore an error if it said fixable, even if fixCommand was null
603 // This is to fix #5764 and #5773:
604 // a delete command, for example, may be null if all concerned primitives have already been deleted
605 error.setIgnored(true);
606 }
607 }
608
609 @Override
610 protected void realRun() throws SAXException, IOException, OsmTransferException {
611 ProgressMonitor monitor = getProgressMonitor();
612 try {
613 monitor.setTicksCount(testErrors.size());
614 final DataSet ds = Main.getLayerManager().getEditDataSet();
615 int i = 0;
616 SwingUtilities.invokeAndWait(new Runnable() {
617 @Override
618 public void run() {
619 ds.beginUpdate();
620 }
621 });
622 try {
623 for (TestError error: testErrors) {
624 i++;
625 monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(), error.getMessage()));
626 if (this.canceled)
627 return;
628 fixError(error);
629 monitor.worked(1);
630 }
631 } finally {
632 SwingUtilities.invokeAndWait(new Runnable() {
633 @Override
634 public void run() {
635 ds.endUpdate();
636 }
637 });
638 }
639 monitor.subTask(tr("Updating map ..."));
640 SwingUtilities.invokeAndWait(new Runnable() {
641 @Override
642 public void run() {
643 Main.main.undoRedo.afterAdd();
644 Main.map.repaint();
645 tree.resetErrors();
646 ds.fireSelectionChanged();
647 }
648 });
649 } catch (InterruptedException | InvocationTargetException e) {
650 // FIXME: signature of realRun should have a generic checked exception we
651 // could throw here
652 throw new RuntimeException(e);
653 } finally {
654 monitor.finishTask();
655 }
656 }
657 }
658}
Note: See TracBrowser for help on using the repository browser.