001 // License: GPL. See LICENSE file for details.
002 package org.openstreetmap.josm.gui.dialogs;
003
004 import static org.openstreetmap.josm.tools.I18n.marktr;
005 import static org.openstreetmap.josm.tools.I18n.tr;
006
007 import java.awt.event.ActionEvent;
008 import java.awt.event.ActionListener;
009 import java.awt.event.KeyEvent;
010 import java.awt.event.MouseAdapter;
011 import java.awt.event.MouseEvent;
012 import java.io.IOException;
013 import java.lang.reflect.InvocationTargetException;
014 import java.util.ArrayList;
015 import java.util.Collection;
016 import java.util.Enumeration;
017 import java.util.HashSet;
018 import java.util.LinkedList;
019 import java.util.List;
020 import java.util.Set;
021
022 import javax.swing.AbstractAction;
023 import javax.swing.JComponent;
024 import javax.swing.JMenuItem;
025 import javax.swing.JOptionPane;
026 import javax.swing.JPopupMenu;
027 import javax.swing.SwingUtilities;
028 import javax.swing.event.TreeSelectionEvent;
029 import javax.swing.event.TreeSelectionListener;
030 import javax.swing.tree.DefaultMutableTreeNode;
031 import javax.swing.tree.TreePath;
032
033 import org.openstreetmap.josm.Main;
034 import org.openstreetmap.josm.actions.AutoScaleAction;
035 import org.openstreetmap.josm.command.Command;
036 import org.openstreetmap.josm.data.SelectionChangedListener;
037 import org.openstreetmap.josm.data.osm.DataSet;
038 import org.openstreetmap.josm.data.osm.Node;
039 import org.openstreetmap.josm.data.osm.OsmPrimitive;
040 import org.openstreetmap.josm.data.osm.WaySegment;
041 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
042 import org.openstreetmap.josm.data.validation.OsmValidator;
043 import org.openstreetmap.josm.data.validation.TestError;
044 import org.openstreetmap.josm.data.validation.ValidatorVisitor;
045 import org.openstreetmap.josm.gui.MapView;
046 import org.openstreetmap.josm.gui.MapView.LayerChangeListener;
047 import org.openstreetmap.josm.gui.PleaseWaitRunnable;
048 import org.openstreetmap.josm.gui.SideButton;
049 import org.openstreetmap.josm.gui.dialogs.validator.ValidatorTreePanel;
050 import org.openstreetmap.josm.gui.layer.Layer;
051 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
052 import org.openstreetmap.josm.gui.preferences.ValidatorPreference;
053 import org.openstreetmap.josm.gui.progress.ProgressMonitor;
054 import org.openstreetmap.josm.io.OsmTransferException;
055 import org.openstreetmap.josm.tools.ImageProvider;
056 import org.openstreetmap.josm.tools.InputMapUtils;
057 import org.openstreetmap.josm.tools.Shortcut;
058 import org.xml.sax.SAXException;
059
060 /**
061 * A small tool dialog for displaying the current errors. The selection manager
062 * respects clicks into the selection list. Ctrl-click will remove entries from
063 * the list while single click will make the clicked entry the only selection.
064 *
065 * @author frsantos
066 */
067 public class ValidatorDialog extends ToggleDialog implements SelectionChangedListener, LayerChangeListener {
068 /** Serializable ID */
069 private static final long serialVersionUID = 2952292777351992696L;
070
071 /** The display tree */
072 public ValidatorTreePanel tree;
073
074 /** The fix button */
075 private SideButton fixButton;
076 /** The ignore button */
077 private SideButton ignoreButton;
078 /** The select button */
079 private SideButton selectButton;
080
081 private JPopupMenu popupMenu;
082 private TestError popupMenuError = null;
083
084 /** Last selected element */
085 private DefaultMutableTreeNode lastSelectedNode = null;
086
087 private OsmDataLayer linkedLayer;
088
089 /**
090 * Constructor
091 */
092 public ValidatorDialog() {
093 super(tr("Validation Results"), "validator", tr("Open the validation window."),
094 Shortcut.registerShortcut("subwindow:validator", tr("Toggle: {0}", tr("Validation results")),
095 KeyEvent.VK_V, Shortcut.ALT_SHIFT), 150);
096
097 popupMenu = new JPopupMenu();
098
099 JMenuItem zoomTo = new JMenuItem(tr("Zoom to problem"));
100 zoomTo.addActionListener(new ActionListener() {
101 @Override
102 public void actionPerformed(ActionEvent e) {
103 zoomToProblem();
104 }
105 });
106 popupMenu.add(zoomTo);
107
108 tree = new ValidatorTreePanel();
109 tree.addMouseListener(new ClickWatch());
110 tree.addTreeSelectionListener(new SelectionWatch());
111 InputMapUtils.unassignCtrlShiftUpDown(tree, JComponent.WHEN_FOCUSED);
112
113 List<SideButton> buttons = new LinkedList<SideButton>();
114
115 selectButton = new SideButton(new AbstractAction() {
116 {
117 putValue(NAME, tr("Select"));
118 putValue(SHORT_DESCRIPTION, tr("Set the selected elements on the map to the selected items in the list above."));
119 putValue(SMALL_ICON, ImageProvider.get("dialogs","select"));
120 }
121 @Override
122 public void actionPerformed(ActionEvent e) {
123 setSelectedItems();
124 }
125 });
126 InputMapUtils.addEnterAction(tree, selectButton.getAction());
127
128 selectButton.setEnabled(false);
129 buttons.add(selectButton);
130
131 buttons.add(new SideButton(Main.main.validator.validateAction));
132
133 fixButton = new SideButton(new AbstractAction() {
134 {
135 putValue(NAME, tr("Fix"));
136 putValue(SHORT_DESCRIPTION, tr("Fix the selected issue."));
137 putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
138 }
139 @Override
140 public void actionPerformed(ActionEvent e) {
141 fixErrors(e);
142 }
143 });
144 fixButton.setEnabled(false);
145 buttons.add(fixButton);
146
147 if (Main.pref.getBoolean(ValidatorPreference.PREF_USE_IGNORE, true)) {
148 ignoreButton = new SideButton(new AbstractAction() {
149 {
150 putValue(NAME, tr("Ignore"));
151 putValue(SHORT_DESCRIPTION, tr("Ignore the selected issue next time."));
152 putValue(SMALL_ICON, ImageProvider.get("dialogs","fix"));
153 }
154 @Override
155 public void actionPerformed(ActionEvent e) {
156 ignoreErrors(e);
157 }
158 });
159 ignoreButton.setEnabled(false);
160 buttons.add(ignoreButton);
161 } else {
162 ignoreButton = null;
163 }
164 createLayout(tree, true, buttons);
165 }
166
167 @Override
168 public void showNotify() {
169 DataSet.addSelectionListener(this);
170 DataSet ds = Main.main.getCurrentDataSet();
171 if (ds != null) {
172 updateSelection(ds.getAllSelected());
173 }
174 MapView.addLayerChangeListener(this);
175 Layer activeLayer = Main.map.mapView.getActiveLayer();
176 if (activeLayer != null) {
177 activeLayerChange(null, activeLayer);
178 }
179 }
180
181 @Override
182 public void hideNotify() {
183 MapView.removeLayerChangeListener(this);
184 DataSet.removeSelectionListener(this);
185 }
186
187 @Override
188 public void setVisible(boolean v) {
189 if (tree != null) {
190 tree.setVisible(v);
191 }
192 super.setVisible(v);
193 Main.map.repaint();
194 }
195
196 /**
197 * Fix selected errors
198 *
199 * @param e
200 */
201 @SuppressWarnings("unchecked")
202 private void fixErrors(ActionEvent e) {
203 TreePath[] selectionPaths = tree.getSelectionPaths();
204 if (selectionPaths == null)
205 return;
206
207 Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
208
209 LinkedList<TestError> errorsToFix = new LinkedList<TestError>();
210 for (TreePath path : selectionPaths) {
211 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
212 if (node == null) {
213 continue;
214 }
215
216 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
217 while (children.hasMoreElements()) {
218 DefaultMutableTreeNode childNode = 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 * @param e
241 */
242 @SuppressWarnings("unchecked")
243 private void ignoreErrors(ActionEvent e) {
244 int asked = JOptionPane.DEFAULT_OPTION;
245 boolean changed = false;
246 TreePath[] selectionPaths = tree.getSelectionPaths();
247 if (selectionPaths == null)
248 return;
249
250 Set<DefaultMutableTreeNode> processedNodes = new HashSet<DefaultMutableTreeNode>();
251 for (TreePath path : selectionPaths) {
252 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
253 if (node == null) {
254 continue;
255 }
256
257 Object mainNodeInfo = node.getUserObject();
258 if (!(mainNodeInfo instanceof TestError)) {
259 Set<String> state = new HashSet<String>();
260 // ask if the whole set should be ignored
261 if (asked == JOptionPane.DEFAULT_OPTION) {
262 String[] a = new String[] { tr("Whole group"), tr("Single elements"), tr("Nothing") };
263 asked = JOptionPane.showOptionDialog(Main.parent, tr("Ignore whole group or individual elements?"),
264 tr("Ignoring elements"), JOptionPane.YES_NO_CANCEL_OPTION, JOptionPane.WARNING_MESSAGE, null,
265 a, a[1]);
266 }
267 if (asked == JOptionPane.YES_NO_OPTION) {
268 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
269 while (children.hasMoreElements()) {
270 DefaultMutableTreeNode childNode = children.nextElement();
271 if (processedNodes.contains(childNode)) {
272 continue;
273 }
274
275 processedNodes.add(childNode);
276 Object nodeInfo = childNode.getUserObject();
277 if (nodeInfo instanceof TestError) {
278 TestError err = (TestError) nodeInfo;
279 err.setIgnored(true);
280 changed = true;
281 state.add(node.getDepth() == 1 ? err.getIgnoreSubGroup() : err.getIgnoreGroup());
282 }
283 }
284 for (String s : state) {
285 OsmValidator.addIgnoredError(s);
286 }
287 continue;
288 } else if (asked == JOptionPane.CANCEL_OPTION) {
289 continue;
290 }
291 }
292
293 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
294 while (children.hasMoreElements()) {
295 DefaultMutableTreeNode childNode = children.nextElement();
296 if (processedNodes.contains(childNode)) {
297 continue;
298 }
299
300 processedNodes.add(childNode);
301 Object nodeInfo = childNode.getUserObject();
302 if (nodeInfo instanceof TestError) {
303 TestError error = (TestError) nodeInfo;
304 String state = error.getIgnoreState();
305 if (state != null) {
306 OsmValidator.addIgnoredError(state);
307 }
308 changed = true;
309 error.setIgnored(true);
310 }
311 }
312 }
313 if (changed) {
314 tree.resetErrors();
315 OsmValidator.saveIgnoredErrors();
316 Main.map.repaint();
317 }
318 }
319
320 private void showPopupMenu(MouseEvent e) {
321 if (!e.isPopupTrigger())
322 return;
323 popupMenuError = null;
324 TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
325 if (selPath == null)
326 return;
327 DefaultMutableTreeNode node = (DefaultMutableTreeNode) selPath.getPathComponent(selPath.getPathCount() - 1);
328 if (!(node.getUserObject() instanceof TestError))
329 return;
330 popupMenuError = (TestError) node.getUserObject();
331 popupMenu.show(e.getComponent(), e.getX(), e.getY());
332 }
333
334 private void zoomToProblem() {
335 if (popupMenuError == null)
336 return;
337 ValidatorBoundingXYVisitor bbox = new ValidatorBoundingXYVisitor();
338 popupMenuError.visitHighlighted(bbox);
339 if (bbox.getBounds() == null)
340 return;
341 bbox.enlargeBoundingBox(Main.pref.getDouble("validator.zoom-enlarge-bbox", 0.0002));
342 Main.map.mapView.recalculateCenterScale(bbox);
343 }
344
345 /**
346 * Sets the selection of the map to the current selected items.
347 */
348 @SuppressWarnings("unchecked")
349 private void setSelectedItems() {
350 if (tree == null)
351 return;
352
353 Collection<OsmPrimitive> sel = new HashSet<OsmPrimitive>(40);
354
355 TreePath[] selectedPaths = tree.getSelectionPaths();
356 if (selectedPaths == null)
357 return;
358
359 for (TreePath path : selectedPaths) {
360 DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
361 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
362 while (children.hasMoreElements()) {
363 DefaultMutableTreeNode childNode = children.nextElement();
364 Object nodeInfo = childNode.getUserObject();
365 if (nodeInfo instanceof TestError) {
366 TestError error = (TestError) nodeInfo;
367 sel.addAll(error.getSelectablePrimitives());
368 }
369 }
370 }
371 DataSet ds = Main.main.getCurrentDataSet();
372 if (ds != null) {
373 ds.setSelected(sel);
374 }
375 }
376
377 /**
378 * Checks for fixes in selected element and, if needed, adds to the sel
379 * parameter all selected elements
380 *
381 * @param sel
382 * The collection where to add all selected elements
383 * @param addSelected
384 * if true, add all selected elements to collection
385 * @return whether the selected elements has any fix
386 */
387 @SuppressWarnings("unchecked")
388 private boolean setSelection(Collection<OsmPrimitive> sel, boolean addSelected) {
389 boolean hasFixes = false;
390
391 DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
392 if (lastSelectedNode != null && !lastSelectedNode.equals(node)) {
393 Enumeration<DefaultMutableTreeNode> children = lastSelectedNode.breadthFirstEnumeration();
394 while (children.hasMoreElements()) {
395 DefaultMutableTreeNode childNode = children.nextElement();
396 Object nodeInfo = childNode.getUserObject();
397 if (nodeInfo instanceof TestError) {
398 TestError error = (TestError) nodeInfo;
399 error.setSelected(false);
400 }
401 }
402 }
403
404 lastSelectedNode = node;
405 if (node == null)
406 return hasFixes;
407
408 Enumeration<DefaultMutableTreeNode> children = node.breadthFirstEnumeration();
409 while (children.hasMoreElements()) {
410 DefaultMutableTreeNode childNode = children.nextElement();
411 Object nodeInfo = childNode.getUserObject();
412 if (nodeInfo instanceof TestError) {
413 TestError error = (TestError) nodeInfo;
414 error.setSelected(true);
415
416 hasFixes = hasFixes || error.isFixable();
417 if (addSelected) {
418 // sel.addAll(error.getPrimitives()); // was selecting already deleted primitives! see #6640
419 sel.addAll(error.getSelectablePrimitives());
420 }
421 }
422 }
423 selectButton.setEnabled(true);
424 if (ignoreButton != null) {
425 ignoreButton.setEnabled(true);
426 }
427
428 return hasFixes;
429 }
430
431 @Override
432 public void activeLayerChange(Layer oldLayer, Layer newLayer) {
433 if (newLayer instanceof OsmDataLayer) {
434 linkedLayer = (OsmDataLayer)newLayer;
435 tree.setErrorList(linkedLayer.validationErrors);
436 }
437 }
438
439 @Override
440 public void layerAdded(Layer newLayer) {}
441
442 @Override
443 public void layerRemoved(Layer oldLayer) {
444 if (oldLayer == linkedLayer) {
445 tree.setErrorList(new ArrayList<TestError>());
446 }
447 }
448
449 /**
450 * Watches for clicks.
451 */
452 public class ClickWatch extends MouseAdapter {
453 @Override
454 public void mouseClicked(MouseEvent e) {
455 fixButton.setEnabled(false);
456 if (ignoreButton != null) {
457 ignoreButton.setEnabled(false);
458 }
459 selectButton.setEnabled(false);
460
461 boolean isDblClick = e.getClickCount() > 1;
462
463 Collection<OsmPrimitive> sel = isDblClick ? new HashSet<OsmPrimitive>(40) : null;
464
465 boolean hasFixes = setSelection(sel, isDblClick);
466 fixButton.setEnabled(hasFixes);
467
468 if (isDblClick) {
469 Main.main.getCurrentDataSet().setSelected(sel);
470 if(Main.pref.getBoolean("validator.autozoom", false)) {
471 AutoScaleAction.zoomTo(sel);
472 }
473 }
474 }
475
476 @Override
477 public void mousePressed(MouseEvent e) {
478 showPopupMenu(e);
479 }
480
481 @Override
482 public void mouseReleased(MouseEvent e) {
483 showPopupMenu(e);
484 }
485
486 }
487
488 /**
489 * Watches for tree selection.
490 */
491 public class SelectionWatch implements TreeSelectionListener {
492 @Override
493 public void valueChanged(TreeSelectionEvent e) {
494 fixButton.setEnabled(false);
495 if (ignoreButton != null) {
496 ignoreButton.setEnabled(false);
497 }
498 selectButton.setEnabled(false);
499
500 boolean hasFixes = setSelection(null, false);
501 fixButton.setEnabled(hasFixes);
502 Main.map.repaint();
503 }
504 }
505
506 public static class ValidatorBoundingXYVisitor extends BoundingXYVisitor implements ValidatorVisitor {
507 @Override
508 public void visit(OsmPrimitive p) {
509 if (p.isUsable()) {
510 p.visit(this);
511 }
512 }
513
514 @Override
515 public void visit(WaySegment ws) {
516 if (ws.lowerIndex < 0 || ws.lowerIndex + 1 >= ws.way.getNodesCount())
517 return;
518 visit(ws.way.getNodes().get(ws.lowerIndex));
519 visit(ws.way.getNodes().get(ws.lowerIndex + 1));
520 }
521
522 @Override
523 public void visit(List<Node> nodes) {
524 for (Node n: nodes) {
525 visit(n);
526 }
527 }
528 }
529
530 public void updateSelection(Collection<? extends OsmPrimitive> newSelection) {
531 if (!Main.pref.getBoolean(ValidatorPreference.PREF_FILTER_BY_SELECTION, false))
532 return;
533 if (newSelection.isEmpty()) {
534 tree.setFilter(null);
535 }
536 HashSet<OsmPrimitive> filter = new HashSet<OsmPrimitive>(newSelection);
537 tree.setFilter(filter);
538 }
539
540 @Override
541 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) {
542 updateSelection(newSelection);
543 }
544
545 /**
546 * Task for fixing a collection of {@link TestError}s. Can be run asynchronously.
547 *
548 *
549 */
550 class FixTask extends PleaseWaitRunnable {
551 private Collection<TestError> testErrors;
552 private boolean canceled;
553
554 public FixTask(Collection<TestError> testErrors) {
555 super(tr("Fixing errors ..."), false /* don't ignore exceptions */);
556 this.testErrors = testErrors == null ? new ArrayList<TestError> (): testErrors;
557 }
558
559 @Override
560 protected void cancel() {
561 this.canceled = true;
562 }
563
564 @Override
565 protected void finish() {
566 // do nothing
567 }
568
569 @Override
570 protected void realRun() throws SAXException, IOException,
571 OsmTransferException {
572 ProgressMonitor monitor = getProgressMonitor();
573 try {
574 monitor.setTicksCount(testErrors.size());
575 int i=0;
576 for (TestError error: testErrors) {
577 i++;
578 monitor.subTask(tr("Fixing ({0}/{1}): ''{2}''", i, testErrors.size(),error.getMessage()));
579 if (this.canceled)
580 return;
581 if (error.isFixable()) {
582 final Command fixCommand = error.getFix();
583 if (fixCommand != null) {
584 SwingUtilities.invokeAndWait(new Runnable() {
585 @Override
586 public void run() {
587 Main.main.undoRedo.addNoRedraw(fixCommand);
588 }
589 });
590 }
591 // It is wanted to ignore an error if it said fixable, even if fixCommand was null
592 // This is to fix #5764 and #5773: a delete command, for example, may be null if all concerned primitives have already been deleted
593 error.setIgnored(true);
594 }
595 monitor.worked(1);
596 }
597 monitor.subTask(tr("Updating map ..."));
598 SwingUtilities.invokeAndWait(new Runnable() {
599 @Override
600 public void run() {
601 Main.main.undoRedo.afterAdd();
602 Main.map.repaint();
603 tree.resetErrors();
604 Main.main.getCurrentDataSet().fireSelectionChanged();
605 }
606 });
607 } catch(InterruptedException e) {
608 // FIXME: signature of realRun should have a generic checked exception we
609 // could throw here
610 throw new RuntimeException(e);
611 } catch(InvocationTargetException e) {
612 throw new RuntimeException(e);
613 } finally {
614 monitor.finishTask();
615 }
616 }
617 }
618 }