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