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

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

fix #14776 - NPE

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