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