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