001 // License: GPL. See LICENSE file for details. 002 package org.openstreetmap.josm.gui.dialogs; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.marktr; 006 import static org.openstreetmap.josm.tools.I18n.tr; 007 008 import java.awt.Color; 009 import java.awt.Graphics; 010 import java.awt.Point; 011 import java.awt.event.ActionEvent; 012 import java.awt.event.KeyEvent; 013 import java.awt.event.MouseAdapter; 014 import java.awt.event.MouseEvent; 015 import java.util.Arrays; 016 import java.util.Collection; 017 import java.util.HashSet; 018 import java.util.Iterator; 019 import java.util.LinkedList; 020 import java.util.Set; 021 import java.util.concurrent.CopyOnWriteArrayList; 022 023 import javax.swing.AbstractAction; 024 import javax.swing.JList; 025 import javax.swing.ListModel; 026 import javax.swing.ListSelectionModel; 027 import javax.swing.event.ListDataEvent; 028 import javax.swing.event.ListDataListener; 029 import javax.swing.event.ListSelectionEvent; 030 import javax.swing.event.ListSelectionListener; 031 032 import org.openstreetmap.josm.Main; 033 import org.openstreetmap.josm.data.SelectionChangedListener; 034 import org.openstreetmap.josm.data.conflict.Conflict; 035 import org.openstreetmap.josm.data.conflict.ConflictCollection; 036 import org.openstreetmap.josm.data.conflict.IConflictListener; 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.Relation; 041 import org.openstreetmap.josm.data.osm.RelationMember; 042 import org.openstreetmap.josm.data.osm.Way; 043 import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor; 044 import org.openstreetmap.josm.data.osm.visitor.Visitor; 045 import org.openstreetmap.josm.gui.MapView; 046 import org.openstreetmap.josm.gui.NavigatableComponent; 047 import org.openstreetmap.josm.gui.OsmPrimitivRenderer; 048 import org.openstreetmap.josm.gui.SideButton; 049 import org.openstreetmap.josm.gui.layer.OsmDataLayer; 050 import org.openstreetmap.josm.gui.util.GuiHelper; 051 import org.openstreetmap.josm.tools.ImageProvider; 052 import org.openstreetmap.josm.tools.Shortcut; 053 054 /** 055 * This dialog displays the {@link ConflictCollection} of the active {@link OsmDataLayer} in a toggle 056 * dialog on the right of the main frame. 057 * 058 */ 059 public final class ConflictDialog extends ToggleDialog implements MapView.EditLayerChangeListener, IConflictListener, SelectionChangedListener{ 060 061 /** 062 * Replies the color used to paint conflicts. 063 * 064 * @return the color used to paint conflicts 065 * @since 1221 066 * @see #paintConflicts 067 */ 068 static public Color getColor() { 069 return Main.pref.getColor(marktr("conflict"), Color.gray); 070 } 071 072 /** the collection of conflicts displayed by this conflict dialog */ 073 private ConflictCollection conflicts; 074 075 /** the model for the list of conflicts */ 076 private ConflictListModel model; 077 /** the list widget for the list of conflicts */ 078 private JList lstConflicts; 079 080 private ResolveAction actResolve; 081 private SelectAction actSelect; 082 083 /** 084 * builds the GUI 085 */ 086 protected void build() { 087 model = new ConflictListModel(); 088 089 lstConflicts = new JList(model); 090 lstConflicts.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); 091 lstConflicts.setCellRenderer(new OsmPrimitivRenderer()); 092 lstConflicts.addMouseListener(new MouseAdapter(){ 093 @Override public void mouseClicked(MouseEvent e) { 094 if (e.getClickCount() >= 2) { 095 resolve(); 096 } 097 } 098 }); 099 lstConflicts.getSelectionModel().addListSelectionListener(new ListSelectionListener(){ 100 public void valueChanged(ListSelectionEvent e) { 101 Main.map.mapView.repaint(); 102 } 103 }); 104 105 SideButton btnResolve = new SideButton(actResolve = new ResolveAction()); 106 lstConflicts.getSelectionModel().addListSelectionListener(actResolve); 107 108 SideButton btnSelect = new SideButton(actSelect = new SelectAction()); 109 lstConflicts.getSelectionModel().addListSelectionListener(actSelect); 110 111 createLayout(lstConflicts, true, Arrays.asList(new SideButton[] { 112 btnResolve, btnSelect 113 })); 114 } 115 116 /** 117 * constructor 118 */ 119 public ConflictDialog() { 120 super(tr("Conflict"), "conflict", tr("Resolve conflicts."), 121 Shortcut.registerShortcut("subwindow:conflict", tr("Toggle: {0}", tr("Conflict")), 122 KeyEvent.VK_C, Shortcut.ALT_SHIFT), 100); 123 124 build(); 125 refreshView(); 126 } 127 128 @Override 129 public void showNotify() { 130 DataSet.addSelectionListener(this); 131 MapView.addEditLayerChangeListener(this, true); 132 refreshView(); 133 } 134 135 @Override 136 public void hideNotify() { 137 MapView.removeEditLayerChangeListener(this); 138 DataSet.removeSelectionListener(this); 139 } 140 141 /** 142 * Launches a conflict resolution dialog for the first selected conflict 143 * 144 */ 145 private final void resolve() { 146 if (conflicts == null || model.getSize() == 0) return; 147 148 int index = lstConflicts.getSelectedIndex(); 149 if (index < 0) { 150 index = 0; 151 } 152 153 Conflict<? extends OsmPrimitive> c = conflicts.get(index); 154 ConflictResolutionDialog dialog = new ConflictResolutionDialog(Main.parent); 155 dialog.getConflictResolver().populate(c); 156 dialog.setVisible(true); 157 158 lstConflicts.setSelectedIndex(index); 159 160 Main.map.mapView.repaint(); 161 } 162 163 /** 164 * refreshes the view of this dialog 165 */ 166 public final void refreshView() { 167 OsmDataLayer editLayer = Main.main.getEditLayer(); 168 conflicts = (editLayer == null ? new ConflictCollection() : editLayer.getConflicts()); 169 GuiHelper.runInEDT(new Runnable() { 170 @Override 171 public void run() { 172 model.fireContentChanged(); 173 updateTitle(conflicts.size()); 174 } 175 }); 176 } 177 178 private void updateTitle(int conflictsCount) { 179 if (conflictsCount > 0) { 180 setTitle(tr("Conflicts: {0} unresolved", conflicts.size())); 181 } else { 182 setTitle(tr("Conflict")); 183 } 184 } 185 186 /** 187 * Paints all conflicts that can be expressed on the main window. 188 * 189 * @param g The {@code Graphics} used to paint 190 * @param nc The {@code NavigatableComponent} used to get screen coordinates of nodes 191 * @since 86 192 */ 193 public void paintConflicts(final Graphics g, final NavigatableComponent nc) { 194 Color preferencesColor = getColor(); 195 if (preferencesColor.equals(Main.pref.getColor(marktr("background"), Color.black))) 196 return; 197 g.setColor(preferencesColor); 198 Visitor conflictPainter = new AbstractVisitor() { 199 // Manage a stack of visited relations to avoid infinite recursion with cyclic relations (fix #7938) 200 private final Set<Relation> visited = new HashSet<Relation>(); 201 public void visit(Node n) { 202 Point p = nc.getPoint(n); 203 g.drawRect(p.x-1, p.y-1, 2, 2); 204 } 205 public void visit(Node n1, Node n2) { 206 Point p1 = nc.getPoint(n1); 207 Point p2 = nc.getPoint(n2); 208 g.drawLine(p1.x, p1.y, p2.x, p2.y); 209 } 210 public void visit(Way w) { 211 Node lastN = null; 212 for (Node n : w.getNodes()) { 213 if (lastN == null) { 214 lastN = n; 215 continue; 216 } 217 visit(lastN, n); 218 lastN = n; 219 } 220 } 221 public void visit(Relation e) { 222 if (!visited.contains(e)) { 223 visited.add(e); 224 try { 225 for (RelationMember em : e.getMembers()) { 226 em.getMember().visit(this); 227 } 228 } finally { 229 visited.remove(e); 230 } 231 } 232 } 233 }; 234 for (Object o : lstConflicts.getSelectedValues()) { 235 if (conflicts == null || !conflicts.hasConflictForMy((OsmPrimitive)o)) { 236 continue; 237 } 238 conflicts.getConflictForMy((OsmPrimitive)o).getTheir().visit(conflictPainter); 239 } 240 } 241 242 public void editLayerChanged(OsmDataLayer oldLayer, OsmDataLayer newLayer) { 243 if (oldLayer != null) { 244 oldLayer.getConflicts().removeConflictListener(this); 245 } 246 if (newLayer != null) { 247 newLayer.getConflicts().addConflictListener(this); 248 } 249 refreshView(); 250 } 251 252 253 /** 254 * replies the conflict collection currently held by this dialog; may be null 255 * 256 * @return the conflict collection currently held by this dialog; may be null 257 */ 258 public ConflictCollection getConflicts() { 259 return conflicts; 260 } 261 262 /** 263 * returns the first selected item of the conflicts list 264 * 265 * @return Conflict 266 */ 267 public Conflict<? extends OsmPrimitive> getSelectedConflict() { 268 if (conflicts == null || model.getSize() == 0) return null; 269 270 int index = lstConflicts.getSelectedIndex(); 271 if (index < 0) return null; 272 273 return conflicts.get(index); 274 } 275 276 public void onConflictsAdded(ConflictCollection conflicts) { 277 refreshView(); 278 } 279 280 public void onConflictsRemoved(ConflictCollection conflicts) { 281 System.err.println("1 conflict has been resolved."); 282 refreshView(); 283 } 284 285 public void selectionChanged(Collection<? extends OsmPrimitive> newSelection) { 286 lstConflicts.clearSelection(); 287 for (OsmPrimitive osm : newSelection) { 288 if (conflicts != null && conflicts.hasConflictForMy(osm)) { 289 int pos = model.indexOf(osm); 290 if (pos >= 0) { 291 lstConflicts.addSelectionInterval(pos, pos); 292 } 293 } 294 } 295 } 296 297 @Override 298 public String helpTopic() { 299 return ht("/Dialog/ConflictList"); 300 } 301 302 /** 303 * The {@link ListModel} for conflicts 304 * 305 */ 306 class ConflictListModel implements ListModel { 307 308 private CopyOnWriteArrayList<ListDataListener> listeners; 309 310 public ConflictListModel() { 311 listeners = new CopyOnWriteArrayList<ListDataListener>(); 312 } 313 314 public void addListDataListener(ListDataListener l) { 315 if (l != null) { 316 listeners.addIfAbsent(l); 317 } 318 } 319 320 public void removeListDataListener(ListDataListener l) { 321 listeners.remove(l); 322 } 323 324 protected void fireContentChanged() { 325 ListDataEvent evt = new ListDataEvent( 326 this, 327 ListDataEvent.CONTENTS_CHANGED, 328 0, 329 getSize() 330 ); 331 Iterator<ListDataListener> it = listeners.iterator(); 332 while(it.hasNext()) { 333 it.next().contentsChanged(evt); 334 } 335 } 336 337 public Object getElementAt(int index) { 338 if (index < 0) return null; 339 if (index >= getSize()) return null; 340 return conflicts.get(index).getMy(); 341 } 342 343 public int getSize() { 344 if (conflicts == null) return 0; 345 return conflicts.size(); 346 } 347 348 public int indexOf(OsmPrimitive my) { 349 if (conflicts == null) return -1; 350 for (int i=0; i < conflicts.size();i++) { 351 if (conflicts.get(i).isMatchingMy(my)) 352 return i; 353 } 354 return -1; 355 } 356 357 public OsmPrimitive get(int idx) { 358 if (conflicts == null) return null; 359 return conflicts.get(idx).getMy(); 360 } 361 } 362 363 class ResolveAction extends AbstractAction implements ListSelectionListener { 364 public ResolveAction() { 365 putValue(NAME, tr("Resolve")); 366 putValue(SHORT_DESCRIPTION, tr("Open a merge dialog of all selected items in the list above.")); 367 putValue(SMALL_ICON, ImageProvider.get("dialogs", "conflict")); 368 putValue("help", ht("/Dialog/ConflictList#ResolveAction")); 369 } 370 371 public void actionPerformed(ActionEvent e) { 372 resolve(); 373 } 374 375 public void valueChanged(ListSelectionEvent e) { 376 ListSelectionModel model = (ListSelectionModel)e.getSource(); 377 boolean enabled = model.getMinSelectionIndex() >= 0 378 && model.getMaxSelectionIndex() >= model.getMinSelectionIndex(); 379 setEnabled(enabled); 380 } 381 } 382 383 class SelectAction extends AbstractAction implements ListSelectionListener { 384 public SelectAction() { 385 putValue(NAME, tr("Select")); 386 putValue(SHORT_DESCRIPTION, tr("Set the selected elements on the map to the selected items in the list above.")); 387 putValue(SMALL_ICON, ImageProvider.get("dialogs", "select")); 388 putValue("help", ht("/Dialog/ConflictList#SelectAction")); 389 } 390 391 public void actionPerformed(ActionEvent e) { 392 Collection<OsmPrimitive> sel = new LinkedList<OsmPrimitive>(); 393 for (Object o : lstConflicts.getSelectedValues()) { 394 sel.add((OsmPrimitive)o); 395 } 396 DataSet ds = Main.main.getCurrentDataSet(); 397 if (ds != null) { // Can't see how it is possible but it happened in #7942 398 ds.setSelected(sel); 399 } 400 } 401 402 public void valueChanged(ListSelectionEvent e) { 403 ListSelectionModel model = (ListSelectionModel)e.getSource(); 404 boolean enabled = model.getMinSelectionIndex() >= 0 405 && model.getMaxSelectionIndex() >= model.getMinSelectionIndex(); 406 setEnabled(enabled); 407 } 408 } 409 410 }