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