001// License: GPL. See LICENSE file for details. 002package org.openstreetmap.josm.gui.dialogs; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007import static org.openstreetmap.josm.tools.I18n.trn; 008 009import java.awt.Color; 010import java.awt.Graphics; 011import java.awt.Point; 012import java.awt.event.ActionEvent; 013import java.awt.event.KeyEvent; 014import java.awt.event.MouseEvent; 015import java.util.Arrays; 016import java.util.Collection; 017import java.util.HashSet; 018import java.util.LinkedList; 019import java.util.Set; 020import java.util.concurrent.CopyOnWriteArrayList; 021 022import javax.swing.AbstractAction; 023import javax.swing.JList; 024import javax.swing.JOptionPane; 025import javax.swing.JPopupMenu; 026import javax.swing.ListModel; 027import javax.swing.ListSelectionModel; 028import javax.swing.event.ListDataEvent; 029import javax.swing.event.ListDataListener; 030import javax.swing.event.ListSelectionEvent; 031import javax.swing.event.ListSelectionListener; 032 033import org.openstreetmap.josm.Main; 034import org.openstreetmap.josm.data.SelectionChangedListener; 035import org.openstreetmap.josm.data.conflict.Conflict; 036import org.openstreetmap.josm.data.conflict.ConflictCollection; 037import org.openstreetmap.josm.data.conflict.IConflictListener; 038import org.openstreetmap.josm.data.osm.DataSet; 039import org.openstreetmap.josm.data.osm.Node; 040import org.openstreetmap.josm.data.osm.OsmPrimitive; 041import org.openstreetmap.josm.data.osm.Relation; 042import org.openstreetmap.josm.data.osm.RelationMember; 043import org.openstreetmap.josm.data.osm.Way; 044import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; 045import org.openstreetmap.josm.data.osm.visitor.Visitor; 046import org.openstreetmap.josm.gui.HelpAwareOptionPane; 047import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 048import org.openstreetmap.josm.gui.MapView; 049import org.openstreetmap.josm.gui.NavigatableComponent; 050import org.openstreetmap.josm.gui.OsmPrimitivRenderer; 051import org.openstreetmap.josm.gui.PopupMenuHandler; 052import org.openstreetmap.josm.gui.SideButton; 053import org.openstreetmap.josm.gui.layer.OsmDataLayer; 054import org.openstreetmap.josm.gui.util.GuiHelper; 055import org.openstreetmap.josm.gui.widgets.PopupMenuLauncher; 056import org.openstreetmap.josm.tools.ImageProvider; 057import org.openstreetmap.josm.tools.Shortcut; 058 059/** 060 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle 061 * dialog on the right of the main frame. 062 * 063 */ 064public final class ConflictDialog extends ToggleDialog implements MapView.EditLayerChangeListener, IConflictListener, SelectionChangedListener{ 065 066 /** 067 * Replies the color used to paint conflicts. 068 * 069 * @return the color used to paint conflicts 070 * @since 1221 071 * @see #paintConflicts 072 */ 073 public static Color getColor() { 074 return Main.pref.getColor(marktr("conflict"), Color.gray); 075 } 076 077 /** the collection of conflicts displayed by this conflict dialog */ 078 private ConflictCollection conflicts; 079 080 /** the model for the list of conflicts */ 081 private ConflictListModel model; 082 /** the list widget for the list of conflicts */ 083 private JList<OsmPrimitive> lstConflicts; 084 085 private final JPopupMenu popupMenu = new JPopupMenu(); 086 private final PopupMenuHandler popupMenuHandler = new PopupMenuHandler(popupMenu); 087 088 private ResolveAction actResolve; 089 private SelectAction actSelect; 090 091 /** 092 * builds the GUI 093 */ 094 protected void build() { 095 model = new ConflictListModel(); 096 097 lstConflicts = new JList<>(model); 098 lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 099 lstConflicts.setCellRenderer(new OsmPrimitivRenderer()); 100 lstConflicts.addMouseListener(new MouseEventHandler()); 101 addListSelectionListener(new ListSelectionListener(){ 102 @Override 103 public void valueChanged(ListSelectionEvent e) { 104 Main.map.mapView.repaint(); 105 } 106 }); 107 108 SideButton btnResolve = new SideButton(actResolve = new ResolveAction()); 109 addListSelectionListener(actResolve); 110 111 SideButton btnSelect = new SideButton(actSelect = new SelectAction()); 112 addListSelectionListener(actSelect); 113 114 createLayout(lstConflicts, true, Arrays.asList(new SideButton[] { 115 btnResolve, btnSelect 116 })); 117 118 popupMenuHandler.addAction(Main.main.menu.autoScaleActions.get("conflict")); 119 } 120 121 /** 122 * constructor 123 */ 124 public ConflictDialog() { 125 super(tr("Conflict"), "conflict", tr("Resolve conflicts."), 126 Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")), 127 KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100); 128 129 build(); 130 refreshView(); 131 } 132 133 @Override 134 public void showNotify() { 135 DataSet.addSelectionListener(this); 136 MapView.addEditLayerChangeListener(this, true); 137 refreshView(); 138 } 139 140 @Override 141 public void hideNotify() { 142 MapView.removeEditLayerChangeListener(this); 143 DataSet.removeSelectionListener(this); 144 } 145 146 /** 147 * Add a list selection listener to the conflicts list. 148 * @param listener the ListSelectionListener 149 * @since 5958 150 */ 151 public void addListSelectionListener(ListSelectionListener listener) { 152 lstConflicts.getSelectionModel().addListSelectionListener(listener); 153 } 154 155 /** 156 * Remove the given list selection listener from the conflicts list. 157 * @param listener the ListSelectionListener 158 * @since 5958 159 */ 160 public void removeListSelectionListener(ListSelectionListener listener) { 161 lstConflicts.getSelectionModel().removeListSelectionListener(listener); 162 } 163 164 /** 165 * Replies the popup menu handler. 166 * @return The popup menu handler 167 * @since 5958 168 */ 169 public PopupMenuHandler getPopupMenuHandler() { 170 return popupMenuHandler; 171 } 172 173 /** 174 * Launches a conflict resolution dialog for the first selected conflict 175 * 176 */ 177 private final void resolve() { 178 if (conflicts == null || model.getSize() == 0) return; 179 180 int index = lstConflicts.getSelectedIndex(); 181 if (index < 0) { 182 index = 0; 183 } 184 185 Conflict<? extends OsmPrimitive> c = conflicts.get(index); 186 ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent); 187 dialog.getConflictResolver().populate(c); 188 dialog.setVisible(true); 189 190 lstConflicts.setSelectedIndex(index); 191 192 Main.map.mapView.repaint(); 193 } 194 195 /** 196 * refreshes the view of this dialog 197 */ 198 public final void refreshView() { 199 OsmDataLayer editLayer = Main.main.getEditLayer(); 200 conflicts = (editLayer == null ? new ConflictCollection() : editLayer.getConflicts()); 201 GuiHelper.runInEDT(new Runnable() { 202 @Override 203 public void run() { 204 model.fireContentChanged(); 205 updateTitle(); 206 } 207 }); 208 } 209 210 private void updateTitle() { 211 int conflictsCount = conflicts.size(); 212 if (conflictsCount > 0) { 213 setTitle(trn("Conflict: {0} unresolved", "Conflicts: {0} unresolved", conflictsCount, conflictsCount) + 214 " ("+tr("Rel.:{0} / Ways:{1} / Nodes:{2}", 215 conflicts.getRelationConflicts().size(), 216 conflicts.getWayConflicts().size(), 217 conflicts.getNodeConflicts().size())+")"); 218 } else { 219 setTitle(tr("Conflict")); 220 } 221 } 222 223 /** 224 * Paints all conflicts that can be expressed on the main window. 225 * 226 * @param g The {@code Graphics} used to paint 227 * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes 228 * @since 86 229 */ 230 public void paintConflicts(final Graphics g, final NavigatableComponent nc) { 231 Color preferencesColor = getColor(); 232 if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black))) 233 return; 234 g.setColor(preferencesColor); 235 Visitor conflictPainter = new AbstractVisitor() { 236 // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938) 237 private final Set<Relation> visited = new HashSet<>(); 238 @Override 239 public void visit(Node n) { 240 Point p = nc.getPoint(n); 241 g.drawRect(p.x-1, p.y-1, 2, 2); 242 } 243 public void visit(Node n1, Node n2) { 244 Point p1 = nc.getPoint(n1); 245 Point p2 = nc.getPoint(n2); 246 g.drawLine(p1.x, p1.y, p2.x, p2.y); 247 } 248 @Override 249 public void visit(Way w) { 250 Node lastN = null; 251 for (Node n : w.getNodes()) { 252 if (lastN == null) { 253 lastN = n; 254 continue; 255 } 256 visit(lastN, n); 257 lastN = n; 258 } 259 } 260 @Override 261 public void visit(Relation e) { 262 if (!visited.contains(e)) { 263 visited.add(e); 264 try { 265 for (RelationMember em : e.getMembers()) { 266 em.getMember().accept(this); 267 } 268 } finally { 269 visited.remove(e); 270 } 271 } 272 } 273 }; 274 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) { 275 if (conflicts == null || !conflicts.hasConflictForMy(o)) { 276 continue; 277 } 278 conflicts.getConflictForMy(o).getTheir().accept(conflictPainter); 279 } 280 } 281 282 @Override 283 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 284 if (oldLayer != null) { 285 oldLayer.getConflicts().removeConflictListener(this); 286 } 287 if (newLayer != null) { 288 newLayer.getConflicts().addConflictListener(this); 289 } 290 refreshView(); 291 } 292 293 294 /** 295 * replies the conflict collection currently held by this dialog; may be null 296 * 297 * @return the conflict collection currently held by this dialog; may be null 298 */ 299 public ConflictCollection getConflicts() { 300 return conflicts; 301 } 302 303 /** 304 * returns the first selected item of the conflicts list 305 * 306 * @return Conflict 307 */ 308 public Conflict<? extends OsmPrimitive> getSelectedConflict() { 309 if (conflicts == null || model.getSize() == 0) return null; 310 311 int index = lstConflicts.getSelectedIndex(); 312 if (index < 0) return null; 313 314 return conflicts.get(index); 315 } 316 317 @Override 318 public void onConflictsAdded(ConflictCollection conflicts) { 319 refreshView(); 320 } 321 322 @Override 323 public void onConflictsRemoved(ConflictCollection conflicts) { 324 Main.info("1 conflict has been resolved."); 325 refreshView(); 326 } 327 328 @Override 329 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 330 lstConflicts.clearSelection(); 331 for (OsmPrimitive osm : newSelection) { 332 if (conflicts != null && conflicts.hasConflictForMy(osm)) { 333 int pos = model.indexOf(osm); 334 if (pos >= 0) { 335 lstConflicts.addSelectionInterval(pos, pos); 336 } 337 } 338 } 339 } 340 341 @Override 342 public String helpTopic() { 343 return ht("/Dialog/ConflictList"); 344 } 345 346 class MouseEventHandler extends PopupMenuLauncher { 347 public MouseEventHandler() { 348 super(popupMenu); 349 } 350 @Override public void mouseClicked(MouseEvent e) { 351 if (isDoubleClick(e)) { 352 resolve(); 353 } 354 } 355 } 356 357 /** 358 * The {@link ListModel} for conflicts 359 * 360 */ 361 class ConflictListModel implements ListModel<OsmPrimitive> { 362 363 private CopyOnWriteArrayList<ListDataListener> listeners; 364 365 public ConflictListModel() { 366 listeners = new CopyOnWriteArrayList<>(); 367 } 368 369 @Override 370 public void addListDataListener(ListDataListener l) { 371 if (l != null) { 372 listeners.addIfAbsent(l); 373 } 374 } 375 376 @Override 377 public void removeListDataListener(ListDataListener l) { 378 listeners.remove(l); 379 } 380 381 protected void fireContentChanged() { 382 ListDataEvent evt = new ListDataEvent( 383 this, 384 ListDataEvent.CONTENTS_CHANGED, 385 0, 386 getSize() 387 ); 388 for (ListDataListener listener : listeners) { 389 listener.contentsChanged(evt); 390 } 391 } 392 393 @Override 394 public OsmPrimitive getElementAt(int index) { 395 if (index < 0) return null; 396 if (index >= getSize()) return null; 397 return conflicts.get(index).getMy(); 398 } 399 400 @Override 401 public int getSize() { 402 if (conflicts == null) return 0; 403 return conflicts.size(); 404 } 405 406 public int indexOf(OsmPrimitive my) { 407 if (conflicts == null) return -1; 408 for (int i=0; i < conflicts.size();i++) { 409 if (conflicts.get(i).isMatchingMy(my)) 410 return i; 411 } 412 return -1; 413 } 414 415 public OsmPrimitive get(int idx) { 416 if (conflicts == null) return null; 417 return conflicts.get(idx).getMy(); 418 } 419 } 420 421 class ResolveAction extends AbstractAction implements ListSelectionListener { 422 public ResolveAction() { 423 putValue(NAME, tr("Resolve")); 424 putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above.")); 425 putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict")); 426 putValue("help", ht("/Dialog/ConflictList#ResolveAction")); 427 } 428 429 @Override 430 public void actionPerformed(ActionEvent e) { 431 resolve(); 432 } 433 434 @Override 435 public void valueChanged(ListSelectionEvent e) { 436 ListSelectionModel model = (ListSelectionModel)e.getSource(); 437 boolean enabled = model.getMinSelectionIndex() >= 0 438 && model.getMaxSelectionIndex() >= model.getMinSelectionIndex(); 439 setEnabled(enabled); 440 } 441 } 442 443 class SelectAction extends AbstractAction implements ListSelectionListener { 444 public SelectAction() { 445 putValue(NAME, tr("Select")); 446 putValue(SHORT_DESCRIPTION, tr("Set the selected elements on the map to the selected items in the list above.")); 447 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 448 putValue("help", ht("/Dialog/ConflictList#SelectAction")); 449 } 450 451 @Override 452 public void actionPerformed(ActionEvent e) { 453 Collection<OsmPrimitive> sel = new LinkedList<>(); 454 for (OsmPrimitive o : lstConflicts.getSelectedValuesList()) { 455 sel.add(o); 456 } 457 DataSet ds = Main.main.getCurrentDataSet(); 458 if (ds != null) { // Can't see how it is possible but it happened in #7942 459 ds.setSelected(sel); 460 } 461 } 462 463 @Override 464 public void valueChanged(ListSelectionEvent e) { 465 ListSelectionModel model = (ListSelectionModel)e.getSource(); 466 boolean enabled = model.getMinSelectionIndex() >= 0 467 && model.getMaxSelectionIndex() >= model.getMinSelectionIndex(); 468 setEnabled(enabled); 469 } 470 } 471 472 /** 473 * Warns the user about the number of detected conflicts 474 * 475 * @param numNewConflicts the number of detected conflicts 476 * @since 5775 477 */ 478 public void warnNumNewConflicts(int numNewConflicts) { 479 if (numNewConflicts == 0) return; 480 481 String msg1 = trn( 482 "There was {0} conflict detected.", 483 "There were {0} conflicts detected.", 484 numNewConflicts, 485 numNewConflicts 486 ); 487 488 final StringBuilder sb = new StringBuilder(); 489 sb.append("<html>").append(msg1).append("</html>"); 490 if (numNewConflicts > 0) { 491 final ButtonSpec[] options = new ButtonSpec[] { 492 new ButtonSpec( 493 tr("OK"), 494 ImageProvider.get("ok"), 495 tr("Click to close this dialog and continue editing"), 496 null /* no specific help */ 497 ) 498 }; 499 GuiHelper.runInEDT(new Runnable() { 500 @Override 501 public void run() { 502 HelpAwareOptionPane.showOptionDialog( 503 Main.parent, 504 sb.toString(), 505 tr("Conflicts detected"), 506 JOptionPane.WARNING_MESSAGE, 507 null, /* no icon */ 508 options, 509 options[0], 510 ht("/Concepts/Conflict#WarningAboutDetectedConflicts") 511 ); 512 unfurlDialog(); 513 Main.map.repaint(); 514 } 515 }); 516 } 517 } 518}