001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.pair; 003 004import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_MERGED; 005import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.MY_WITH_THEIR; 006import static org.openstreetmap.josm.gui.conflict.pair.ComparePairType.THEIR_WITH_MERGED; 007import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MERGED_ENTRIES; 008import static org.openstreetmap.josm.gui.conflict.pair.ListRole.MY_ENTRIES; 009import static org.openstreetmap.josm.gui.conflict.pair.ListRole.THEIR_ENTRIES; 010import static org.openstreetmap.josm.tools.I18n.tr; 011 012import java.beans.PropertyChangeEvent; 013import java.beans.PropertyChangeListener; 014import java.util.ArrayList; 015import java.util.EnumMap; 016import java.util.HashSet; 017import java.util.List; 018import java.util.Map; 019import java.util.Set; 020 021import javax.swing.AbstractListModel; 022import javax.swing.ComboBoxModel; 023import javax.swing.DefaultListSelectionModel; 024import javax.swing.JOptionPane; 025import javax.swing.JTable; 026import javax.swing.ListSelectionModel; 027import javax.swing.table.DefaultTableModel; 028import javax.swing.table.TableModel; 029 030import org.openstreetmap.josm.Main; 031import org.openstreetmap.josm.command.conflict.ConflictResolveCommand; 032import org.openstreetmap.josm.data.conflict.Conflict; 033import org.openstreetmap.josm.data.osm.DataSet; 034import org.openstreetmap.josm.data.osm.OsmPrimitive; 035import org.openstreetmap.josm.data.osm.PrimitiveId; 036import org.openstreetmap.josm.data.osm.RelationMember; 037import org.openstreetmap.josm.gui.HelpAwareOptionPane; 038import org.openstreetmap.josm.gui.help.HelpUtil; 039import org.openstreetmap.josm.gui.util.ChangeNotifier; 040import org.openstreetmap.josm.gui.widgets.OsmPrimitivesTableModel; 041import org.openstreetmap.josm.tools.CheckParameterUtil; 042import org.openstreetmap.josm.tools.Logging; 043import org.openstreetmap.josm.tools.Utils; 044 045/** 046 * ListMergeModel is a model for interactively comparing and merging two list of entries 047 * of type T. It maintains three lists of entries of type T: 048 * <ol> 049 * <li>the list of <em>my</em> entries</li> 050 * <li>the list of <em>their</em> entries</li> 051 * <li>the list of <em>merged</em> entries</li> 052 * </ol> 053 * 054 * A ListMergeModel is a factory for three {@link TableModel}s and three {@link ListSelectionModel}s: 055 * <ol> 056 * <li>the table model and the list selection for for a {@link JTable} which shows my entries. 057 * See {@link #getMyTableModel()} and {@link AbstractListMergeModel#getMySelectionModel()}</li> 058 * <li>dito for their entries and merged entries</li> 059 * </ol> 060 * 061 * A ListMergeModel can be ''frozen''. If it's frozen, it doesn't accept additional merge 062 * decisions. {@link PropertyChangeListener}s can register for property value changes of 063 * {@link #FROZEN_PROP}. 064 * 065 * ListMergeModel is an abstract class. Three methods have to be implemented by subclasses: 066 * <ul> 067 * <li>{@link AbstractListMergeModel#cloneEntryForMergedList} - clones an entry of type T</li> 068 * <li>{@link AbstractListMergeModel#isEqualEntry} - checks whether two entries are equals </li> 069 * <li>{@link AbstractListMergeModel#setValueAt(DefaultTableModel, Object, int, int)} - handles values edited in 070 * a JTable, dispatched from {@link TableModel#setValueAt(Object, int, int)} </li> 071 * </ul> 072 * A ListMergeModel is used in combination with a {@link AbstractListMerger}. 073 * 074 * @param <T> the type of the list entries 075 * @param <C> the type of conflict resolution command 076 * @see AbstractListMerger 077 * @see PairTable For the table displaying this model 078 */ 079public abstract class AbstractListMergeModel<T extends PrimitiveId, C extends ConflictResolveCommand> extends ChangeNotifier { 080 /** 081 * The property name to listen for frozen changes. 082 * @see #setFrozen(boolean) 083 * @see #isFrozen() 084 */ 085 public static final String FROZEN_PROP = AbstractListMergeModel.class.getName() + ".frozen"; 086 087 private static final int MAX_DELETED_PRIMITIVE_IN_DIALOG = 5; 088 089 protected Map<ListRole, ArrayList<T>> entries; 090 091 protected EntriesTableModel myEntriesTableModel; 092 protected EntriesTableModel theirEntriesTableModel; 093 protected EntriesTableModel mergedEntriesTableModel; 094 095 protected EntriesSelectionModel myEntriesSelectionModel; 096 protected EntriesSelectionModel theirEntriesSelectionModel; 097 protected EntriesSelectionModel mergedEntriesSelectionModel; 098 099 private final Set<PropertyChangeListener> listeners; 100 private boolean isFrozen; 101 private final ComparePairListModel comparePairListModel; 102 103 private DataSet myDataset; 104 private Map<PrimitiveId, PrimitiveId> mergedMap; 105 106 /** 107 * Creates a clone of an entry of type T suitable to be included in the 108 * list of merged entries 109 * 110 * @param entry the entry 111 * @return the cloned entry 112 */ 113 protected abstract T cloneEntryForMergedList(T entry); 114 115 /** 116 * checks whether two entries are equal. This is not necessarily the same as 117 * e1.equals(e2). 118 * 119 * @param e1 the first entry 120 * @param e2 the second entry 121 * @return true, if the entries are equal, false otherwise. 122 */ 123 public abstract boolean isEqualEntry(T e1, T e2); 124 125 /** 126 * Handles method dispatches from {@link TableModel#setValueAt(Object, int, int)}. 127 * 128 * @param model the table model 129 * @param value the value to be set 130 * @param row the row index 131 * @param col the column index 132 * 133 * @see TableModel#setValueAt(Object, int, int) 134 */ 135 protected abstract void setValueAt(DefaultTableModel model, Object value, int row, int col); 136 137 /** 138 * Replies primitive from my dataset referenced by entry 139 * @param entry entry 140 * @return Primitive from my dataset referenced by entry 141 */ 142 public OsmPrimitive getMyPrimitive(T entry) { 143 return getMyPrimitiveById(entry); 144 } 145 146 public final OsmPrimitive getMyPrimitiveById(PrimitiveId entry) { 147 OsmPrimitive result = myDataset.getPrimitiveById(entry); 148 if (result == null && mergedMap != null) { 149 PrimitiveId id = mergedMap.get(entry); 150 if (id == null && entry instanceof OsmPrimitive) { 151 id = mergedMap.get(((OsmPrimitive) entry).getPrimitiveId()); 152 } 153 if (id != null) { 154 result = myDataset.getPrimitiveById(id); 155 } 156 } 157 return result; 158 } 159 160 protected void buildMyEntriesTableModel() { 161 myEntriesTableModel = new EntriesTableModel(MY_ENTRIES); 162 } 163 164 protected void buildTheirEntriesTableModel() { 165 theirEntriesTableModel = new EntriesTableModel(THEIR_ENTRIES); 166 } 167 168 protected void buildMergedEntriesTableModel() { 169 mergedEntriesTableModel = new EntriesTableModel(MERGED_ENTRIES); 170 } 171 172 protected List<T> getMergedEntries() { 173 return entries.get(MERGED_ENTRIES); 174 } 175 176 protected List<T> getMyEntries() { 177 return entries.get(MY_ENTRIES); 178 } 179 180 protected List<T> getTheirEntries() { 181 return entries.get(THEIR_ENTRIES); 182 } 183 184 public int getMyEntriesSize() { 185 return getMyEntries().size(); 186 } 187 188 public int getMergedEntriesSize() { 189 return getMergedEntries().size(); 190 } 191 192 public int getTheirEntriesSize() { 193 return getTheirEntries().size(); 194 } 195 196 /** 197 * Constructs a new {@code ListMergeModel}. 198 */ 199 public AbstractListMergeModel() { 200 entries = new EnumMap<>(ListRole.class); 201 for (ListRole role : ListRole.values()) { 202 entries.put(role, new ArrayList<T>()); 203 } 204 205 buildMyEntriesTableModel(); 206 buildTheirEntriesTableModel(); 207 buildMergedEntriesTableModel(); 208 209 myEntriesSelectionModel = new EntriesSelectionModel(entries.get(MY_ENTRIES)); 210 theirEntriesSelectionModel = new EntriesSelectionModel(entries.get(THEIR_ENTRIES)); 211 mergedEntriesSelectionModel = new EntriesSelectionModel(entries.get(MERGED_ENTRIES)); 212 213 listeners = new HashSet<>(); 214 comparePairListModel = new ComparePairListModel(); 215 216 setFrozen(true); 217 } 218 219 public void addPropertyChangeListener(PropertyChangeListener listener) { 220 synchronized (listeners) { 221 if (listener != null && !listeners.contains(listener)) { 222 listeners.add(listener); 223 } 224 } 225 } 226 227 public void removePropertyChangeListener(PropertyChangeListener listener) { 228 synchronized (listeners) { 229 if (listener != null && listeners.contains(listener)) { 230 listeners.remove(listener); 231 } 232 } 233 } 234 235 protected void fireFrozenChanged(boolean oldValue, boolean newValue) { 236 synchronized (listeners) { 237 PropertyChangeEvent evt = new PropertyChangeEvent(this, FROZEN_PROP, oldValue, newValue); 238 listeners.forEach(listener -> listener.propertyChange(evt)); 239 } 240 } 241 242 /** 243 * Sets the frozen status for this model. 244 * @param isFrozen <code>true</code> if it should be frozen. 245 */ 246 public final void setFrozen(boolean isFrozen) { 247 boolean oldValue = this.isFrozen; 248 this.isFrozen = isFrozen; 249 fireFrozenChanged(oldValue, this.isFrozen); 250 } 251 252 /** 253 * Check if the model is frozen. 254 * @return The current frozen state. 255 */ 256 public final boolean isFrozen() { 257 return isFrozen; 258 } 259 260 public OsmPrimitivesTableModel getMyTableModel() { 261 return myEntriesTableModel; 262 } 263 264 public OsmPrimitivesTableModel getTheirTableModel() { 265 return theirEntriesTableModel; 266 } 267 268 public OsmPrimitivesTableModel getMergedTableModel() { 269 return mergedEntriesTableModel; 270 } 271 272 public EntriesSelectionModel getMySelectionModel() { 273 return myEntriesSelectionModel; 274 } 275 276 public EntriesSelectionModel getTheirSelectionModel() { 277 return theirEntriesSelectionModel; 278 } 279 280 public EntriesSelectionModel getMergedSelectionModel() { 281 return mergedEntriesSelectionModel; 282 } 283 284 protected void fireModelDataChanged() { 285 myEntriesTableModel.fireTableDataChanged(); 286 theirEntriesTableModel.fireTableDataChanged(); 287 mergedEntriesTableModel.fireTableDataChanged(); 288 fireStateChanged(); 289 } 290 291 protected void copyToTop(ListRole role, int... rows) { 292 copy(role, rows, 0); 293 mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1); 294 } 295 296 /** 297 * Copies the nodes given by indices in rows from the list of my nodes to the 298 * list of merged nodes. Inserts the nodes at the top of the list of merged 299 * nodes. 300 * 301 * @param rows the indices 302 */ 303 public void copyMyToTop(int... rows) { 304 copyToTop(MY_ENTRIES, rows); 305 } 306 307 /** 308 * Copies the nodes given by indices in rows from the list of their nodes to the 309 * list of merged nodes. Inserts the nodes at the top of the list of merged 310 * nodes. 311 * 312 * @param rows the indices 313 */ 314 public void copyTheirToTop(int... rows) { 315 copyToTop(THEIR_ENTRIES, rows); 316 } 317 318 /** 319 * Copies the nodes given by indices in rows from the list of nodes in source to the 320 * list of merged nodes. Inserts the nodes at the end of the list of merged 321 * nodes. 322 * 323 * @param source the list of nodes to copy from 324 * @param rows the indices 325 */ 326 327 public void copyToEnd(ListRole source, int... rows) { 328 copy(source, rows, getMergedEntriesSize()); 329 mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1); 330 331 } 332 333 /** 334 * Copies the nodes given by indices in rows from the list of my nodes to the 335 * list of merged nodes. Inserts the nodes at the end of the list of merged 336 * nodes. 337 * 338 * @param rows the indices 339 */ 340 public void copyMyToEnd(int... rows) { 341 copyToEnd(MY_ENTRIES, rows); 342 } 343 344 /** 345 * Copies the nodes given by indices in rows from the list of their nodes to the 346 * list of merged nodes. Inserts the nodes at the end of the list of merged 347 * nodes. 348 * 349 * @param rows the indices 350 */ 351 public void copyTheirToEnd(int... rows) { 352 copyToEnd(THEIR_ENTRIES, rows); 353 } 354 355 public void clearMerged() { 356 getMergedEntries().clear(); 357 fireModelDataChanged(); 358 } 359 360 protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) { 361 CheckParameterUtil.ensureParameterNotNull(my, "my"); 362 CheckParameterUtil.ensureParameterNotNull(their, "their"); 363 this.myDataset = my.getDataSet(); 364 this.mergedMap = mergedMap; 365 getMergedEntries().clear(); 366 getMyEntries().clear(); 367 getTheirEntries().clear(); 368 } 369 370 protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) { 371 List<String> items = new ArrayList<>(); 372 for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) { 373 items.add(deletedIds.get(i).toString()); 374 } 375 if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) { 376 items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG)); 377 } 378 StringBuilder sb = new StringBuilder(); 379 sb.append("<html>") 380 .append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:")) 381 .append(Utils.joinAsHtmlUnorderedList(items)) 382 .append("</html>"); 383 HelpAwareOptionPane.showOptionDialog( 384 Main.parent, 385 sb.toString(), 386 tr("Merging deleted objects failed"), 387 JOptionPane.WARNING_MESSAGE, 388 HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed") 389 ); 390 } 391 392 private void copy(ListRole sourceRole, int[] rows, int position) { 393 if (position < 0 || position > getMergedEntriesSize()) 394 throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position); 395 List<T> newItems = new ArrayList<>(rows.length); 396 List<T> source = entries.get(sourceRole); 397 List<PrimitiveId> deletedIds = new ArrayList<>(); 398 for (int row: rows) { 399 T entry = source.get(row); 400 OsmPrimitive primitive = getMyPrimitive(entry); 401 if (!primitive.isDeleted()) { 402 T clone = cloneEntryForMergedList(entry); 403 newItems.add(clone); 404 } else { 405 deletedIds.add(primitive.getPrimitiveId()); 406 } 407 } 408 getMergedEntries().addAll(position, newItems); 409 fireModelDataChanged(); 410 if (!deletedIds.isEmpty()) { 411 alertCopyFailedForDeletedPrimitives(deletedIds); 412 } 413 } 414 415 /** 416 * Copies over all values from the given side to the merged table.. 417 * @param source The source side to copy from. 418 */ 419 public void copyAll(ListRole source) { 420 getMergedEntries().clear(); 421 422 int[] rows = new int[entries.get(source).size()]; 423 for (int i = 0; i < rows.length; i++) { 424 rows[i] = i; 425 } 426 copy(source, rows, 0); 427 } 428 429 /** 430 * Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the 431 * list of merged nodes. Inserts the nodes before row given by current. 432 * 433 * @param source the list of nodes to copy from 434 * @param rows the indices 435 * @param current the row index before which the nodes are inserted 436 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 437 */ 438 protected void copyBeforeCurrent(ListRole source, int[] rows, int current) { 439 copy(source, rows, current); 440 mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1); 441 } 442 443 /** 444 * Copies the nodes given by indices in rows from the list of my nodes to the 445 * list of merged nodes. Inserts the nodes before row given by current. 446 * 447 * @param rows the indices 448 * @param current the row index before which the nodes are inserted 449 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 450 */ 451 public void copyMyBeforeCurrent(int[] rows, int current) { 452 copyBeforeCurrent(MY_ENTRIES, rows, current); 453 } 454 455 /** 456 * Copies the nodes given by indices in rows from the list of their nodes to the 457 * list of merged nodes. Inserts the nodes before row given by current. 458 * 459 * @param rows the indices 460 * @param current the row index before which the nodes are inserted 461 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 462 */ 463 public void copyTheirBeforeCurrent(int[] rows, int current) { 464 copyBeforeCurrent(THEIR_ENTRIES, rows, current); 465 } 466 467 /** 468 * Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the 469 * list of merged nodes. Inserts the nodes after the row given by current. 470 * 471 * @param source the list of nodes to copy from 472 * @param rows the indices 473 * @param current the row index after which the nodes are inserted 474 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 475 */ 476 protected void copyAfterCurrent(ListRole source, int[] rows, int current) { 477 copy(source, rows, current + 1); 478 mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1); 479 fireStateChanged(); 480 } 481 482 /** 483 * Copies the nodes given by indices in rows from the list of my nodes to the 484 * list of merged nodes. Inserts the nodes after the row given by current. 485 * 486 * @param rows the indices 487 * @param current the row index after which the nodes are inserted 488 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 489 */ 490 public void copyMyAfterCurrent(int[] rows, int current) { 491 copyAfterCurrent(MY_ENTRIES, rows, current); 492 } 493 494 /** 495 * Copies the nodes given by indices in rows from the list of my nodes to the 496 * list of merged nodes. Inserts the nodes after the row given by current. 497 * 498 * @param rows the indices 499 * @param current the row index after which the nodes are inserted 500 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 501 */ 502 public void copyTheirAfterCurrent(int[] rows, int current) { 503 copyAfterCurrent(THEIR_ENTRIES, rows, current); 504 } 505 506 /** 507 * Moves the nodes given by indices in rows up by one position in the list 508 * of merged nodes. 509 * 510 * @param rows the indices 511 * 512 */ 513 public void moveUpMerged(int... rows) { 514 if (rows == null || rows.length == 0) 515 return; 516 if (rows[0] == 0) 517 // can't move up 518 return; 519 List<T> mergedEntries = getMergedEntries(); 520 for (int row: rows) { 521 T n = mergedEntries.get(row); 522 mergedEntries.remove(row); 523 mergedEntries.add(row -1, n); 524 } 525 fireModelDataChanged(); 526 mergedEntriesSelectionModel.setValueIsAdjusting(true); 527 mergedEntriesSelectionModel.clearSelection(); 528 for (int row: rows) { 529 mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1); 530 } 531 mergedEntriesSelectionModel.setValueIsAdjusting(false); 532 } 533 534 /** 535 * Moves the nodes given by indices in rows down by one position in the list 536 * of merged nodes. 537 * 538 * @param rows the indices 539 */ 540 public void moveDownMerged(int... rows) { 541 if (rows == null || rows.length == 0) 542 return; 543 List<T> mergedEntries = getMergedEntries(); 544 if (rows[rows.length -1] == mergedEntries.size() -1) 545 // can't move down 546 return; 547 for (int i = rows.length-1; i >= 0; i--) { 548 int row = rows[i]; 549 T n = mergedEntries.get(row); 550 mergedEntries.remove(row); 551 mergedEntries.add(row +1, n); 552 } 553 fireModelDataChanged(); 554 mergedEntriesSelectionModel.setValueIsAdjusting(true); 555 mergedEntriesSelectionModel.clearSelection(); 556 for (int row: rows) { 557 mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1); 558 } 559 mergedEntriesSelectionModel.setValueIsAdjusting(false); 560 } 561 562 /** 563 * Removes the nodes given by indices in rows from the list 564 * of merged nodes. 565 * 566 * @param rows the indices 567 */ 568 public void removeMerged(int... rows) { 569 if (rows == null || rows.length == 0) 570 return; 571 572 List<T> mergedEntries = getMergedEntries(); 573 574 for (int i = rows.length-1; i >= 0; i--) { 575 mergedEntries.remove(rows[i]); 576 } 577 fireModelDataChanged(); 578 mergedEntriesSelectionModel.clearSelection(); 579 } 580 581 /** 582 * Replies true if the list of my entries and the list of their 583 * entries are equal 584 * 585 * @return true, if the lists are equal; false otherwise 586 */ 587 protected boolean myAndTheirEntriesEqual() { 588 if (getMyEntriesSize() != getTheirEntriesSize()) 589 return false; 590 for (int i = 0; i < getMyEntriesSize(); i++) { 591 if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i))) 592 return false; 593 } 594 return true; 595 } 596 597 /** 598 * This an adapter between a {@link JTable} and one of the three entry lists 599 * in the role {@link ListRole} managed by the {@link AbstractListMergeModel}. 600 * 601 * From the point of view of the {@link JTable} it is a {@link TableModel}. 602 * 603 * @see AbstractListMergeModel#getMyTableModel() 604 * @see AbstractListMergeModel#getTheirTableModel() 605 * @see AbstractListMergeModel#getMergedTableModel() 606 */ 607 public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel { 608 private final ListRole role; 609 610 /** 611 * 612 * @param role the role 613 */ 614 public EntriesTableModel(ListRole role) { 615 this.role = role; 616 } 617 618 @Override 619 public int getRowCount() { 620 int count = Math.max(getMyEntries().size(), getMergedEntries().size()); 621 return Math.max(count, getTheirEntries().size()); 622 } 623 624 @Override 625 public Object getValueAt(int row, int column) { 626 if (row < entries.get(role).size()) 627 return entries.get(role).get(row); 628 return null; 629 } 630 631 @Override 632 public boolean isCellEditable(int row, int column) { 633 return false; 634 } 635 636 @Override 637 public void setValueAt(Object value, int row, int col) { 638 AbstractListMergeModel.this.setValueAt(this, value, row, col); 639 } 640 641 /** 642 * Returns the list merge model. 643 * @return the list merge model 644 */ 645 public AbstractListMergeModel<T, C> getListMergeModel() { 646 return AbstractListMergeModel.this; 647 } 648 649 /** 650 * replies true if the {@link ListRole} of this {@link EntriesTableModel} 651 * participates in the current {@link ComparePairType} 652 * 653 * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel} 654 * participates in the current {@link ComparePairType} 655 * 656 * @see AbstractListMergeModel.ComparePairListModel#getSelectedComparePair() 657 */ 658 public boolean isParticipatingInCurrentComparePair() { 659 return getComparePairListModel() 660 .getSelectedComparePair() 661 .isParticipatingIn(role); 662 } 663 664 /** 665 * replies true if the entry at <code>row</code> is equal to the entry at the 666 * same position in the opposite list of the current {@link ComparePairType}. 667 * 668 * @param row the row number 669 * @return true if the entry at <code>row</code> is equal to the entry at the 670 * same position in the opposite list of the current {@link ComparePairType} 671 * @throws IllegalStateException if this model is not participating in the 672 * current {@link ComparePairType} 673 * @see ComparePairType#getOppositeRole(ListRole) 674 * @see #getRole() 675 * @see #getOppositeEntries() 676 */ 677 public boolean isSamePositionInOppositeList(int row) { 678 if (!isParticipatingInCurrentComparePair()) 679 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 680 if (row >= getEntries().size()) return false; 681 if (row >= getOppositeEntries().size()) return false; 682 683 T e1 = getEntries().get(row); 684 T e2 = getOppositeEntries().get(row); 685 return isEqualEntry(e1, e2); 686 } 687 688 /** 689 * replies true if the entry at the current position is present in the opposite list 690 * of the current {@link ComparePairType}. 691 * 692 * @param row the current row 693 * @return true if the entry at the current position is present in the opposite list 694 * of the current {@link ComparePairType}. 695 * @throws IllegalStateException if this model is not participating in the 696 * current {@link ComparePairType} 697 * @see ComparePairType#getOppositeRole(ListRole) 698 * @see #getRole() 699 * @see #getOppositeEntries() 700 */ 701 public boolean isIncludedInOppositeList(int row) { 702 if (!isParticipatingInCurrentComparePair()) 703 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 704 705 if (row >= getEntries().size()) return false; 706 T e1 = getEntries().get(row); 707 return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2)); 708 } 709 710 protected List<T> getEntries() { 711 return entries.get(role); 712 } 713 714 /** 715 * replies the opposite list of entries with respect to the current {@link ComparePairType} 716 * 717 * @return the opposite list of entries 718 */ 719 protected List<T> getOppositeEntries() { 720 ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role); 721 return entries.get(opposite); 722 } 723 724 /** 725 * Get the role of the table. 726 * @return The role. 727 */ 728 public ListRole getRole() { 729 return role; 730 } 731 732 @Override 733 public OsmPrimitive getReferredPrimitive(int idx) { 734 Object value = getValueAt(idx, 1); 735 if (value instanceof OsmPrimitive) { 736 return (OsmPrimitive) value; 737 } else if (value instanceof RelationMember) { 738 return ((RelationMember) value).getMember(); 739 } else { 740 Logging.error("Unknown object type: "+value); 741 return null; 742 } 743 } 744 } 745 746 /** 747 * This is the selection model to be used in a {@link JTable} which displays 748 * an entry list managed by {@link AbstractListMergeModel}. 749 * 750 * The model ensures that only rows displaying an entry in the entry list 751 * can be selected. "Empty" rows can't be selected. 752 * 753 * @see AbstractListMergeModel#getMySelectionModel() 754 * @see AbstractListMergeModel#getMergedSelectionModel() 755 * @see AbstractListMergeModel#getTheirSelectionModel() 756 * 757 */ 758 protected class EntriesSelectionModel extends DefaultListSelectionModel { 759 private final transient List<T> entries; 760 761 public EntriesSelectionModel(List<T> nodes) { 762 this.entries = nodes; 763 } 764 765 @Override 766 public void addSelectionInterval(int index0, int index1) { 767 if (entries.isEmpty()) return; 768 if (index0 > entries.size() - 1) return; 769 index0 = Math.min(entries.size()-1, index0); 770 index1 = Math.min(entries.size()-1, index1); 771 super.addSelectionInterval(index0, index1); 772 } 773 774 @Override 775 public void insertIndexInterval(int index, int length, boolean before) { 776 if (entries.isEmpty()) return; 777 if (before) { 778 int newindex = Math.min(entries.size()-1, index); 779 if (newindex < index - length) return; 780 length = length - (index - newindex); 781 super.insertIndexInterval(newindex, length, before); 782 } else { 783 if (index > entries.size() -1) return; 784 length = Math.min(entries.size()-1 - index, length); 785 super.insertIndexInterval(index, length, before); 786 } 787 } 788 789 @Override 790 public void moveLeadSelectionIndex(int leadIndex) { 791 if (entries.isEmpty()) return; 792 leadIndex = Math.max(0, leadIndex); 793 leadIndex = Math.min(entries.size() - 1, leadIndex); 794 super.moveLeadSelectionIndex(leadIndex); 795 } 796 797 @Override 798 public void removeIndexInterval(int index0, int index1) { 799 if (entries.isEmpty()) return; 800 index0 = Math.max(0, index0); 801 index0 = Math.min(entries.size() - 1, index0); 802 803 index1 = Math.max(0, index1); 804 index1 = Math.min(entries.size() - 1, index1); 805 super.removeIndexInterval(index0, index1); 806 } 807 808 @Override 809 public void removeSelectionInterval(int index0, int index1) { 810 if (entries.isEmpty()) return; 811 index0 = Math.max(0, index0); 812 index0 = Math.min(entries.size() - 1, index0); 813 814 index1 = Math.max(0, index1); 815 index1 = Math.min(entries.size() - 1, index1); 816 super.removeSelectionInterval(index0, index1); 817 } 818 819 @Override 820 public void setAnchorSelectionIndex(int anchorIndex) { 821 if (entries.isEmpty()) return; 822 anchorIndex = Math.min(entries.size() - 1, anchorIndex); 823 super.setAnchorSelectionIndex(anchorIndex); 824 } 825 826 @Override 827 public void setLeadSelectionIndex(int leadIndex) { 828 if (entries.isEmpty()) return; 829 leadIndex = Math.min(entries.size() - 1, leadIndex); 830 super.setLeadSelectionIndex(leadIndex); 831 } 832 833 @Override 834 public void setSelectionInterval(int index0, int index1) { 835 if (entries.isEmpty()) return; 836 index0 = Math.max(0, index0); 837 index0 = Math.min(entries.size() - 1, index0); 838 839 index1 = Math.max(0, index1); 840 index1 = Math.min(entries.size() - 1, index1); 841 842 super.setSelectionInterval(index0, index1); 843 } 844 } 845 846 public ComparePairListModel getComparePairListModel() { 847 return this.comparePairListModel; 848 } 849 850 public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> { 851 852 private int selectedIdx; 853 private final List<ComparePairType> compareModes; 854 855 /** 856 * Constructs a new {@code ComparePairListModel}. 857 */ 858 public ComparePairListModel() { 859 this.compareModes = new ArrayList<>(); 860 compareModes.add(MY_WITH_THEIR); 861 compareModes.add(MY_WITH_MERGED); 862 compareModes.add(THEIR_WITH_MERGED); 863 selectedIdx = 0; 864 } 865 866 @Override 867 public ComparePairType getElementAt(int index) { 868 if (index < compareModes.size()) 869 return compareModes.get(index); 870 throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index)); 871 } 872 873 @Override 874 public int getSize() { 875 return compareModes.size(); 876 } 877 878 @Override 879 public Object getSelectedItem() { 880 return compareModes.get(selectedIdx); 881 } 882 883 @Override 884 public void setSelectedItem(Object anItem) { 885 int i = compareModes.indexOf(anItem); 886 if (i < 0) 887 throw new IllegalStateException(tr("Item {0} not found in list.", anItem)); 888 selectedIdx = i; 889 fireModelDataChanged(); 890 } 891 892 public ComparePairType getSelectedComparePair() { 893 return compareModes.get(selectedIdx); 894 } 895 } 896 897 /** 898 * Builds the command to resolve conflicts in the list. 899 * 900 * @param conflict the conflict data set 901 * @return the command 902 * @throws IllegalStateException if the merge is not yet frozen 903 */ 904 public abstract C buildResolveCommand(Conflict<? extends OsmPrimitive> conflict); 905}