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