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

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

see #15229 - deprecate Main.parent and Main itself

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