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, marktr("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, marktr("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, marktr("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 }