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.command.conflict.ConflictResolveCommand; 031import org.openstreetmap.josm.data.conflict.Conflict; 032import org.openstreetmap.josm.data.osm.DataSet; 033import org.openstreetmap.josm.data.osm.OsmPrimitive; 034import org.openstreetmap.josm.data.osm.PrimitiveId; 035import org.openstreetmap.josm.data.osm.RelationMember; 036import org.openstreetmap.josm.gui.HelpAwareOptionPane; 037import org.openstreetmap.josm.gui.MainApplication; 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 MainApplication.getMainFrame(), 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 != null) { 402 if (!primitive.isDeleted()) { 403 T clone = cloneEntryForMergedList(entry); 404 newItems.add(clone); 405 } else { 406 deletedIds.add(primitive.getPrimitiveId()); 407 } 408 } 409 } 410 getMergedEntries().addAll(position, newItems); 411 fireModelDataChanged(); 412 if (!deletedIds.isEmpty()) { 413 alertCopyFailedForDeletedPrimitives(deletedIds); 414 } 415 } 416 417 /** 418 * Copies over all values from the given side to the merged table.. 419 * @param source The source side to copy from. 420 */ 421 public void copyAll(ListRole source) { 422 getMergedEntries().clear(); 423 424 int[] rows = new int[entries.get(source).size()]; 425 for (int i = 0; i < rows.length; i++) { 426 rows[i] = i; 427 } 428 copy(source, rows, 0); 429 } 430 431 /** 432 * Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the 433 * list of merged nodes. Inserts the nodes before row given by current. 434 * 435 * @param source the list of nodes to copy from 436 * @param rows the indices 437 * @param current the row index before which the nodes are inserted 438 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 439 */ 440 protected void copyBeforeCurrent(ListRole source, int[] rows, int current) { 441 copy(source, rows, current); 442 mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1); 443 } 444 445 /** 446 * Copies the nodes given by indices in rows from the list of my nodes to the 447 * list of merged nodes. Inserts the nodes before row given by current. 448 * 449 * @param rows the indices 450 * @param current the row index before which the nodes are inserted 451 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 452 */ 453 public void copyMyBeforeCurrent(int[] rows, int current) { 454 copyBeforeCurrent(MY_ENTRIES, rows, current); 455 } 456 457 /** 458 * Copies the nodes given by indices in rows from the list of their nodes to the 459 * list of merged nodes. Inserts the nodes before row given by current. 460 * 461 * @param rows the indices 462 * @param current the row index before which the nodes are inserted 463 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 464 */ 465 public void copyTheirBeforeCurrent(int[] rows, int current) { 466 copyBeforeCurrent(THEIR_ENTRIES, rows, current); 467 } 468 469 /** 470 * Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the 471 * list of merged nodes. Inserts the nodes after the row given by current. 472 * 473 * @param source the list of nodes to copy from 474 * @param rows the indices 475 * @param current the row index after which the nodes are inserted 476 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 477 */ 478 protected void copyAfterCurrent(ListRole source, int[] rows, int current) { 479 copy(source, rows, current + 1); 480 mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1); 481 fireStateChanged(); 482 } 483 484 /** 485 * Copies the nodes given by indices in rows from the list of my nodes to the 486 * list of merged nodes. Inserts the nodes after the row given by current. 487 * 488 * @param rows the indices 489 * @param current the row index after which the nodes are inserted 490 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 491 */ 492 public void copyMyAfterCurrent(int[] rows, int current) { 493 copyAfterCurrent(MY_ENTRIES, rows, current); 494 } 495 496 /** 497 * Copies the nodes given by indices in rows from the list of my nodes to the 498 * list of merged nodes. Inserts the nodes after the row given by current. 499 * 500 * @param rows the indices 501 * @param current the row index after which the nodes are inserted 502 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 503 */ 504 public void copyTheirAfterCurrent(int[] rows, int current) { 505 copyAfterCurrent(THEIR_ENTRIES, rows, current); 506 } 507 508 /** 509 * Moves the nodes given by indices in rows up by one position in the list 510 * of merged nodes. 511 * 512 * @param rows the indices 513 * 514 */ 515 public void moveUpMerged(int... rows) { 516 if (rows == null || rows.length == 0) 517 return; 518 if (rows[0] == 0) 519 // can't move up 520 return; 521 List<T> mergedEntries = getMergedEntries(); 522 for (int row: rows) { 523 T n = mergedEntries.get(row); 524 mergedEntries.remove(row); 525 mergedEntries.add(row -1, n); 526 } 527 fireModelDataChanged(); 528 mergedEntriesSelectionModel.setValueIsAdjusting(true); 529 mergedEntriesSelectionModel.clearSelection(); 530 for (int row: rows) { 531 mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1); 532 } 533 mergedEntriesSelectionModel.setValueIsAdjusting(false); 534 } 535 536 /** 537 * Moves the nodes given by indices in rows down by one position in the list 538 * of merged nodes. 539 * 540 * @param rows the indices 541 */ 542 public void moveDownMerged(int... rows) { 543 if (rows == null || rows.length == 0) 544 return; 545 List<T> mergedEntries = getMergedEntries(); 546 if (rows[rows.length -1] == mergedEntries.size() -1) 547 // can't move down 548 return; 549 for (int i = rows.length-1; i >= 0; i--) { 550 int row = rows[i]; 551 T n = mergedEntries.get(row); 552 mergedEntries.remove(row); 553 mergedEntries.add(row +1, n); 554 } 555 fireModelDataChanged(); 556 mergedEntriesSelectionModel.setValueIsAdjusting(true); 557 mergedEntriesSelectionModel.clearSelection(); 558 for (int row: rows) { 559 mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1); 560 } 561 mergedEntriesSelectionModel.setValueIsAdjusting(false); 562 } 563 564 /** 565 * Removes the nodes given by indices in rows from the list 566 * of merged nodes. 567 * 568 * @param rows the indices 569 */ 570 public void removeMerged(int... rows) { 571 if (rows == null || rows.length == 0) 572 return; 573 574 List<T> mergedEntries = getMergedEntries(); 575 576 for (int i = rows.length-1; i >= 0; i--) { 577 mergedEntries.remove(rows[i]); 578 } 579 fireModelDataChanged(); 580 mergedEntriesSelectionModel.clearSelection(); 581 } 582 583 /** 584 * Replies true if the list of my entries and the list of their 585 * entries are equal 586 * 587 * @return true, if the lists are equal; false otherwise 588 */ 589 protected boolean myAndTheirEntriesEqual() { 590 if (getMyEntriesSize() != getTheirEntriesSize()) 591 return false; 592 for (int i = 0; i < getMyEntriesSize(); i++) { 593 if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i))) 594 return false; 595 } 596 return true; 597 } 598 599 /** 600 * This an adapter between a {@link JTable} and one of the three entry lists 601 * in the role {@link ListRole} managed by the {@link AbstractListMergeModel}. 602 * 603 * From the point of view of the {@link JTable} it is a {@link TableModel}. 604 * 605 * @see AbstractListMergeModel#getMyTableModel() 606 * @see AbstractListMergeModel#getTheirTableModel() 607 * @see AbstractListMergeModel#getMergedTableModel() 608 */ 609 public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel { 610 private final ListRole role; 611 612 /** 613 * 614 * @param role the role 615 */ 616 public EntriesTableModel(ListRole role) { 617 this.role = role; 618 } 619 620 @Override 621 public int getRowCount() { 622 int count = Math.max(getMyEntries().size(), getMergedEntries().size()); 623 return Math.max(count, getTheirEntries().size()); 624 } 625 626 @Override 627 public Object getValueAt(int row, int column) { 628 if (row < entries.get(role).size()) 629 return entries.get(role).get(row); 630 return null; 631 } 632 633 @Override 634 public boolean isCellEditable(int row, int column) { 635 return false; 636 } 637 638 @Override 639 public void setValueAt(Object value, int row, int col) { 640 AbstractListMergeModel.this.setValueAt(this, value, row, col); 641 } 642 643 /** 644 * Returns the list merge model. 645 * @return the list merge model 646 */ 647 public AbstractListMergeModel<T, C> getListMergeModel() { 648 return AbstractListMergeModel.this; 649 } 650 651 /** 652 * replies true if the {@link ListRole} of this {@link EntriesTableModel} 653 * participates in the current {@link ComparePairType} 654 * 655 * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel} 656 * participates in the current {@link ComparePairType} 657 * 658 * @see AbstractListMergeModel.ComparePairListModel#getSelectedComparePair() 659 */ 660 public boolean isParticipatingInCurrentComparePair() { 661 return getComparePairListModel() 662 .getSelectedComparePair() 663 .isParticipatingIn(role); 664 } 665 666 /** 667 * replies true if the entry at <code>row</code> is equal to the entry at the 668 * same position in the opposite list of the current {@link ComparePairType}. 669 * 670 * @param row the row number 671 * @return true if the entry at <code>row</code> is equal to the entry at the 672 * same position in the opposite list of the current {@link ComparePairType} 673 * @throws IllegalStateException if this model is not participating in the 674 * current {@link ComparePairType} 675 * @see ComparePairType#getOppositeRole(ListRole) 676 * @see #getRole() 677 * @see #getOppositeEntries() 678 */ 679 public boolean isSamePositionInOppositeList(int row) { 680 if (!isParticipatingInCurrentComparePair()) 681 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 682 if (row >= getEntries().size()) return false; 683 if (row >= getOppositeEntries().size()) return false; 684 685 T e1 = getEntries().get(row); 686 T e2 = getOppositeEntries().get(row); 687 return isEqualEntry(e1, e2); 688 } 689 690 /** 691 * replies true if the entry at the current position is present in the opposite list 692 * of the current {@link ComparePairType}. 693 * 694 * @param row the current row 695 * @return true if the entry at the current position is present in the opposite list 696 * of the current {@link ComparePairType}. 697 * @throws IllegalStateException if this model is not participating in the 698 * current {@link ComparePairType} 699 * @see ComparePairType#getOppositeRole(ListRole) 700 * @see #getRole() 701 * @see #getOppositeEntries() 702 */ 703 public boolean isIncludedInOppositeList(int row) { 704 if (!isParticipatingInCurrentComparePair()) 705 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 706 707 if (row >= getEntries().size()) return false; 708 T e1 = getEntries().get(row); 709 return getOppositeEntries().stream().anyMatch(e2 -> isEqualEntry(e1, e2)); 710 } 711 712 protected List<T> getEntries() { 713 return entries.get(role); 714 } 715 716 /** 717 * replies the opposite list of entries with respect to the current {@link ComparePairType} 718 * 719 * @return the opposite list of entries 720 */ 721 protected List<T> getOppositeEntries() { 722 ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role); 723 return entries.get(opposite); 724 } 725 726 /** 727 * Get the role of the table. 728 * @return The role. 729 */ 730 public ListRole getRole() { 731 return role; 732 } 733 734 @Override 735 public OsmPrimitive getReferredPrimitive(int idx) { 736 Object value = getValueAt(idx, 1); 737 if (value instanceof OsmPrimitive) { 738 return (OsmPrimitive) value; 739 } else if (value instanceof RelationMember) { 740 return ((RelationMember) value).getMember(); 741 } else { 742 Logging.error("Unknown object type: "+value); 743 return null; 744 } 745 } 746 } 747 748 /** 749 * This is the selection model to be used in a {@link JTable} which displays 750 * an entry list managed by {@link AbstractListMergeModel}. 751 * 752 * The model ensures that only rows displaying an entry in the entry list 753 * can be selected. "Empty" rows can't be selected. 754 * 755 * @see AbstractListMergeModel#getMySelectionModel() 756 * @see AbstractListMergeModel#getMergedSelectionModel() 757 * @see AbstractListMergeModel#getTheirSelectionModel() 758 * 759 */ 760 protected class EntriesSelectionModel extends DefaultListSelectionModel { 761 private final transient List<T> entries; 762 763 public EntriesSelectionModel(List<T> nodes) { 764 this.entries = nodes; 765 } 766 767 @Override 768 public void addSelectionInterval(int index0, int index1) { 769 if (entries.isEmpty()) return; 770 if (index0 > entries.size() - 1) return; 771 index0 = Math.min(entries.size()-1, index0); 772 index1 = Math.min(entries.size()-1, index1); 773 super.addSelectionInterval(index0, index1); 774 } 775 776 @Override 777 public void insertIndexInterval(int index, int length, boolean before) { 778 if (entries.isEmpty()) return; 779 if (before) { 780 int newindex = Math.min(entries.size()-1, index); 781 if (newindex < index - length) return; 782 length = length - (index - newindex); 783 super.insertIndexInterval(newindex, length, before); 784 } else { 785 if (index > entries.size() -1) return; 786 length = Math.min(entries.size()-1 - index, length); 787 super.insertIndexInterval(index, length, before); 788 } 789 } 790 791 @Override 792 public void moveLeadSelectionIndex(int leadIndex) { 793 if (entries.isEmpty()) return; 794 leadIndex = Math.max(0, leadIndex); 795 leadIndex = Math.min(entries.size() - 1, leadIndex); 796 super.moveLeadSelectionIndex(leadIndex); 797 } 798 799 @Override 800 public void removeIndexInterval(int index0, int index1) { 801 if (entries.isEmpty()) return; 802 index0 = Math.max(0, index0); 803 index0 = Math.min(entries.size() - 1, index0); 804 805 index1 = Math.max(0, index1); 806 index1 = Math.min(entries.size() - 1, index1); 807 super.removeIndexInterval(index0, index1); 808 } 809 810 @Override 811 public void removeSelectionInterval(int index0, int index1) { 812 if (entries.isEmpty()) return; 813 index0 = Math.max(0, index0); 814 index0 = Math.min(entries.size() - 1, index0); 815 816 index1 = Math.max(0, index1); 817 index1 = Math.min(entries.size() - 1, index1); 818 super.removeSelectionInterval(index0, index1); 819 } 820 821 @Override 822 public void setAnchorSelectionIndex(int anchorIndex) { 823 if (entries.isEmpty()) return; 824 anchorIndex = Math.min(entries.size() - 1, anchorIndex); 825 super.setAnchorSelectionIndex(anchorIndex); 826 } 827 828 @Override 829 public void setLeadSelectionIndex(int leadIndex) { 830 if (entries.isEmpty()) return; 831 leadIndex = Math.min(entries.size() - 1, leadIndex); 832 super.setLeadSelectionIndex(leadIndex); 833 } 834 835 @Override 836 public void setSelectionInterval(int index0, int index1) { 837 if (entries.isEmpty()) return; 838 index0 = Math.max(0, index0); 839 index0 = Math.min(entries.size() - 1, index0); 840 841 index1 = Math.max(0, index1); 842 index1 = Math.min(entries.size() - 1, index1); 843 844 super.setSelectionInterval(index0, index1); 845 } 846 } 847 848 public ComparePairListModel getComparePairListModel() { 849 return this.comparePairListModel; 850 } 851 852 public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> { 853 854 private int selectedIdx; 855 private final List<ComparePairType> compareModes; 856 857 /** 858 * Constructs a new {@code ComparePairListModel}. 859 */ 860 public ComparePairListModel() { 861 this.compareModes = new ArrayList<>(); 862 compareModes.add(MY_WITH_THEIR); 863 compareModes.add(MY_WITH_MERGED); 864 compareModes.add(THEIR_WITH_MERGED); 865 selectedIdx = 0; 866 } 867 868 @Override 869 public ComparePairType getElementAt(int index) { 870 if (index < compareModes.size()) 871 return compareModes.get(index); 872 throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index)); 873 } 874 875 @Override 876 public int getSize() { 877 return compareModes.size(); 878 } 879 880 @Override 881 public Object getSelectedItem() { 882 return compareModes.get(selectedIdx); 883 } 884 885 @Override 886 public void setSelectedItem(Object anItem) { 887 int i = compareModes.indexOf(anItem); 888 if (i < 0) 889 throw new IllegalStateException(tr("Item {0} not found in list.", anItem)); 890 selectedIdx = i; 891 fireModelDataChanged(); 892 } 893 894 public ComparePairType getSelectedComparePair() { 895 return compareModes.get(selectedIdx); 896 } 897 } 898 899 /** 900 * Builds the command to resolve conflicts in the list. 901 * 902 * @param conflict the conflict data set 903 * @return the command 904 * @throws IllegalStateException if the merge is not yet frozen 905 */ 906 public abstract C buildResolveCommand(Conflict<? extends OsmPrimitive> conflict); 907}