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