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 for (PropertyChangeListener listener: listeners) { 229 listener.propertyChange(evt); 230 } 231 } 232 } 233 234 public final void setFrozen(boolean isFrozen) { 235 boolean oldValue = this.isFrozen; 236 this.isFrozen = isFrozen; 237 fireFrozenChanged(oldValue, this.isFrozen); 238 } 239 240 public final boolean isFrozen() { 241 return isFrozen; 242 } 243 244 public OsmPrimitivesTableModel getMyTableModel() { 245 return myEntriesTableModel; 246 } 247 248 public OsmPrimitivesTableModel getTheirTableModel() { 249 return theirEntriesTableModel; 250 } 251 252 public OsmPrimitivesTableModel getMergedTableModel() { 253 return mergedEntriesTableModel; 254 } 255 256 public EntriesSelectionModel getMySelectionModel() { 257 return myEntriesSelectionModel; 258 } 259 260 public EntriesSelectionModel getTheirSelectionModel() { 261 return theirEntriesSelectionModel; 262 } 263 264 public EntriesSelectionModel getMergedSelectionModel() { 265 return mergedEntriesSelectionModel; 266 } 267 268 protected void fireModelDataChanged() { 269 myEntriesTableModel.fireTableDataChanged(); 270 theirEntriesTableModel.fireTableDataChanged(); 271 mergedEntriesTableModel.fireTableDataChanged(); 272 fireStateChanged(); 273 } 274 275 protected void copyToTop(ListRole role, int[] rows) { 276 copy(role, rows, 0); 277 mergedEntriesSelectionModel.setSelectionInterval(0, rows.length -1); 278 } 279 280 /** 281 * Copies the nodes given by indices in rows from the list of my nodes to the 282 * list of merged nodes. Inserts the nodes at the top of the list of merged 283 * nodes. 284 * 285 * @param rows the indices 286 */ 287 public void copyMyToTop(int[] rows) { 288 copyToTop(MY_ENTRIES, rows); 289 } 290 291 /** 292 * Copies the nodes given by indices in rows from the list of their nodes to the 293 * list of merged nodes. Inserts the nodes at the top of the list of merged 294 * nodes. 295 * 296 * @param rows the indices 297 */ 298 public void copyTheirToTop(int[] rows) { 299 copyToTop(THEIR_ENTRIES, rows); 300 } 301 302 /** 303 * Copies the nodes given by indices in rows from the list of nodes in source to the 304 * list of merged nodes. Inserts the nodes at the end of the list of merged 305 * nodes. 306 * 307 * @param source the list of nodes to copy from 308 * @param rows the indices 309 */ 310 311 public void copyToEnd(ListRole source, int[] rows) { 312 copy(source, rows, getMergedEntriesSize()); 313 mergedEntriesSelectionModel.setSelectionInterval(getMergedEntriesSize()-rows.length, getMergedEntriesSize() -1); 314 315 } 316 317 /** 318 * Copies the nodes given by indices in rows from the list of my nodes to the 319 * list of merged nodes. Inserts the nodes at the end of the list of merged 320 * nodes. 321 * 322 * @param rows the indices 323 */ 324 public void copyMyToEnd(int[] rows) { 325 copyToEnd(MY_ENTRIES, rows); 326 } 327 328 /** 329 * Copies the nodes given by indices in rows from the list of their nodes to the 330 * list of merged nodes. Inserts the nodes at the end of the list of merged 331 * nodes. 332 * 333 * @param rows the indices 334 */ 335 public void copyTheirToEnd(int[] rows) { 336 copyToEnd(THEIR_ENTRIES, rows); 337 } 338 339 public void clearMerged() { 340 getMergedEntries().clear(); 341 fireModelDataChanged(); 342 } 343 344 protected final void initPopulate(OsmPrimitive my, OsmPrimitive their, Map<PrimitiveId, PrimitiveId> mergedMap) { 345 CheckParameterUtil.ensureParameterNotNull(my, "my"); 346 CheckParameterUtil.ensureParameterNotNull(their, "their"); 347 this.myDataset = my.getDataSet(); 348 this.mergedMap = mergedMap; 349 getMergedEntries().clear(); 350 getMyEntries().clear(); 351 getTheirEntries().clear(); 352 } 353 354 protected void alertCopyFailedForDeletedPrimitives(List<PrimitiveId> deletedIds) { 355 List<String> items = new ArrayList<>(); 356 for (int i = 0; i < Math.min(MAX_DELETED_PRIMITIVE_IN_DIALOG, deletedIds.size()); i++) { 357 items.add(deletedIds.get(i).toString()); 358 } 359 if (deletedIds.size() > MAX_DELETED_PRIMITIVE_IN_DIALOG) { 360 items.add(tr("{0} more...", deletedIds.size() - MAX_DELETED_PRIMITIVE_IN_DIALOG)); 361 } 362 StringBuilder sb = new StringBuilder(); 363 sb.append("<html>") 364 .append(tr("The following objects could not be copied to the target object<br>because they are deleted in the target dataset:")) 365 .append(Utils.joinAsHtmlUnorderedList(items)) 366 .append("</html>"); 367 HelpAwareOptionPane.showOptionDialog( 368 Main.parent, 369 sb.toString(), 370 tr("Merging deleted objects failed"), 371 JOptionPane.WARNING_MESSAGE, 372 HelpUtil.ht("/Dialog/Conflict#MergingDeletedPrimitivesFailed") 373 ); 374 } 375 376 private void copy(ListRole sourceRole, int[] rows, int position) { 377 if (position < 0 || position > getMergedEntriesSize()) 378 throw new IllegalArgumentException("Position must be between 0 and "+getMergedEntriesSize()+" but is "+position); 379 List<T> newItems = new ArrayList<>(rows.length); 380 List<T> source = entries.get(sourceRole); 381 List<PrimitiveId> deletedIds = new ArrayList<>(); 382 for (int row: rows) { 383 T entry = source.get(row); 384 OsmPrimitive primitive = getMyPrimitive(entry); 385 if (!primitive.isDeleted()) { 386 T clone = cloneEntryForMergedList(entry); 387 newItems.add(clone); 388 } else { 389 deletedIds.add(primitive.getPrimitiveId()); 390 } 391 } 392 getMergedEntries().addAll(position, newItems); 393 fireModelDataChanged(); 394 if (!deletedIds.isEmpty()) { 395 alertCopyFailedForDeletedPrimitives(deletedIds); 396 } 397 } 398 399 public void copyAll(ListRole source) { 400 getMergedEntries().clear(); 401 402 int[] rows = new int[entries.get(source).size()]; 403 for (int i = 0; i < rows.length; i++) { 404 rows[i] = i; 405 } 406 copy(source, rows, 0); 407 } 408 409 /** 410 * Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the 411 * list of merged nodes. Inserts the nodes before row given by current. 412 * 413 * @param source the list of nodes to copy from 414 * @param rows the indices 415 * @param current the row index before which the nodes are inserted 416 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 417 */ 418 protected void copyBeforeCurrent(ListRole source, int[] rows, int current) { 419 copy(source, rows, current); 420 mergedEntriesSelectionModel.setSelectionInterval(current, current + rows.length-1); 421 } 422 423 /** 424 * Copies the nodes given by indices in rows from the list of my nodes to the 425 * list of merged nodes. Inserts the nodes before row given by current. 426 * 427 * @param rows the indices 428 * @param current the row index before which the nodes are inserted 429 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 430 */ 431 public void copyMyBeforeCurrent(int[] rows, int current) { 432 copyBeforeCurrent(MY_ENTRIES, rows, current); 433 } 434 435 /** 436 * Copies the nodes given by indices in rows from the list of their nodes to the 437 * list of merged nodes. Inserts the nodes before row given by current. 438 * 439 * @param rows the indices 440 * @param current the row index before which the nodes are inserted 441 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 442 */ 443 public void copyTheirBeforeCurrent(int[] rows, int current) { 444 copyBeforeCurrent(THEIR_ENTRIES, rows, current); 445 } 446 447 /** 448 * Copies the nodes given by indices in rows from the list of nodes <code>source</code> to the 449 * list of merged nodes. Inserts the nodes after the row given by current. 450 * 451 * @param source the list of nodes to copy from 452 * @param rows the indices 453 * @param current the row index after which the nodes are inserted 454 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 455 */ 456 protected void copyAfterCurrent(ListRole source, int[] rows, int current) { 457 copy(source, rows, current + 1); 458 mergedEntriesSelectionModel.setSelectionInterval(current+1, current + rows.length-1); 459 fireStateChanged(); 460 } 461 462 /** 463 * Copies the nodes given by indices in rows from the list of my nodes to the 464 * list of merged nodes. Inserts the nodes after the row given by current. 465 * 466 * @param rows the indices 467 * @param current the row index after which the nodes are inserted 468 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 469 */ 470 public void copyMyAfterCurrent(int[] rows, int current) { 471 copyAfterCurrent(MY_ENTRIES, rows, current); 472 } 473 474 /** 475 * Copies the nodes given by indices in rows from the list of my nodes to the 476 * list of merged nodes. Inserts the nodes after the row given by current. 477 * 478 * @param rows the indices 479 * @param current the row index after which the nodes are inserted 480 * @throws IllegalArgumentException if current < 0 or >= #nodes in list of merged nodes 481 */ 482 public void copyTheirAfterCurrent(int[] rows, int current) { 483 copyAfterCurrent(THEIR_ENTRIES, rows, current); 484 } 485 486 /** 487 * Moves the nodes given by indices in rows up by one position in the list 488 * of merged nodes. 489 * 490 * @param rows the indices 491 * 492 */ 493 public void moveUpMerged(int[] rows) { 494 if (rows == null || rows.length == 0) 495 return; 496 if (rows[0] == 0) 497 // can't move up 498 return; 499 List<T> mergedEntries = getMergedEntries(); 500 for (int row: rows) { 501 T n = mergedEntries.get(row); 502 mergedEntries.remove(row); 503 mergedEntries.add(row -1, n); 504 } 505 fireModelDataChanged(); 506 mergedEntriesSelectionModel.clearSelection(); 507 for (int row: rows) { 508 mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1); 509 } 510 } 511 512 /** 513 * Moves the nodes given by indices in rows down by one position in the list 514 * of merged nodes. 515 * 516 * @param rows the indices 517 */ 518 public void moveDownMerged(int[] rows) { 519 if (rows == null || rows.length == 0) 520 return; 521 List<T> mergedEntries = getMergedEntries(); 522 if (rows[rows.length -1] == mergedEntries.size() -1) 523 // can't move down 524 return; 525 for (int i = rows.length-1; i >= 0; i--) { 526 int row = rows[i]; 527 T n = mergedEntries.get(row); 528 mergedEntries.remove(row); 529 mergedEntries.add(row +1, n); 530 } 531 fireModelDataChanged(); 532 mergedEntriesSelectionModel.clearSelection(); 533 for (int row: rows) { 534 mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1); 535 } 536 } 537 538 /** 539 * Removes the nodes given by indices in rows from the list 540 * of merged nodes. 541 * 542 * @param rows the indices 543 */ 544 public void removeMerged(int[] rows) { 545 if (rows == null || rows.length == 0) 546 return; 547 548 List<T> mergedEntries = getMergedEntries(); 549 550 for (int i = rows.length-1; i >= 0; i--) { 551 mergedEntries.remove(rows[i]); 552 } 553 fireModelDataChanged(); 554 mergedEntriesSelectionModel.clearSelection(); 555 } 556 557 /** 558 * Replies true if the list of my entries and the list of their 559 * entries are equal 560 * 561 * @return true, if the lists are equal; false otherwise 562 */ 563 protected boolean myAndTheirEntriesEqual() { 564 565 if (getMyEntriesSize() != getTheirEntriesSize()) 566 return false; 567 for (int i = 0; i < getMyEntriesSize(); i++) { 568 if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i))) 569 return false; 570 } 571 return true; 572 } 573 574 /** 575 * This an adapter between a {@link JTable} and one of the three entry lists 576 * in the role {@link ListRole} managed by the {@link ListMergeModel}. 577 * 578 * From the point of view of the {@link JTable} it is a {@link TableModel}. 579 * 580 * @see ListMergeModel#getMyTableModel() 581 * @see ListMergeModel#getTheirTableModel() 582 * @see ListMergeModel#getMergedTableModel() 583 */ 584 public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel { 585 private final ListRole role; 586 587 /** 588 * 589 * @param role the role 590 */ 591 public EntriesTableModel(ListRole role) { 592 this.role = role; 593 } 594 595 @Override 596 public int getRowCount() { 597 int count = Math.max(getMyEntries().size(), getMergedEntries().size()); 598 return Math.max(count, getTheirEntries().size()); 599 } 600 601 @Override 602 public Object getValueAt(int row, int column) { 603 if (row < entries.get(role).size()) 604 return entries.get(role).get(row); 605 return null; 606 } 607 608 @Override 609 public boolean isCellEditable(int row, int column) { 610 return false; 611 } 612 613 @Override 614 public void setValueAt(Object value, int row, int col) { 615 ListMergeModel.this.setValueAt(this, value, row, col); 616 } 617 618 public ListMergeModel<T> getListMergeModel() { 619 return ListMergeModel.this; 620 } 621 622 /** 623 * replies true if the {@link ListRole} of this {@link EntriesTableModel} 624 * participates in the current {@link ComparePairType} 625 * 626 * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel} 627 * participates in the current {@link ComparePairType} 628 * 629 * @see ListMergeModel.ComparePairListModel#getSelectedComparePair() 630 */ 631 public boolean isParticipatingInCurrentComparePair() { 632 return getComparePairListModel() 633 .getSelectedComparePair() 634 .isParticipatingIn(role); 635 } 636 637 /** 638 * replies true if the entry at <code>row</code> is equal to the entry at the 639 * same position in the opposite list of the current {@link ComparePairType}. 640 * 641 * @param row the row number 642 * @return true if the entry at <code>row</code> is equal to the entry at the 643 * same position in the opposite list of the current {@link ComparePairType} 644 * @throws IllegalStateException if this model is not participating in the 645 * current {@link ComparePairType} 646 * @see ComparePairType#getOppositeRole(ListRole) 647 * @see #getRole() 648 * @see #getOppositeEntries() 649 */ 650 public boolean isSamePositionInOppositeList(int row) { 651 if (!isParticipatingInCurrentComparePair()) 652 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 653 if (row >= getEntries().size()) return false; 654 if (row >= getOppositeEntries().size()) return false; 655 656 T e1 = getEntries().get(row); 657 T e2 = getOppositeEntries().get(row); 658 return isEqualEntry(e1, e2); 659 } 660 661 /** 662 * replies true if the entry at the current position is present in the opposite list 663 * of the current {@link ComparePairType}. 664 * 665 * @param row the current row 666 * @return true if the entry at the current position is present in the opposite list 667 * of the current {@link ComparePairType}. 668 * @throws IllegalStateException if this model is not participating in the 669 * current {@link ComparePairType} 670 * @see ComparePairType#getOppositeRole(ListRole) 671 * @see #getRole() 672 * @see #getOppositeEntries() 673 */ 674 public boolean isIncludedInOppositeList(int row) { 675 if (!isParticipatingInCurrentComparePair()) 676 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 677 678 if (row >= getEntries().size()) return false; 679 T e1 = getEntries().get(row); 680 for (T e2: getOppositeEntries()) { 681 if (isEqualEntry(e1, e2)) return true; 682 } 683 return false; 684 } 685 686 protected List<T> getEntries() { 687 return entries.get(role); 688 } 689 690 /** 691 * replies the opposite list of entries with respect to the current {@link ComparePairType} 692 * 693 * @return the opposite list of entries 694 */ 695 protected List<T> getOppositeEntries() { 696 ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role); 697 return entries.get(opposite); 698 } 699 700 public ListRole getRole() { 701 return role; 702 } 703 704 @Override 705 public OsmPrimitive getReferredPrimitive(int idx) { 706 Object value = getValueAt(idx, 1); 707 if (value instanceof OsmPrimitive) { 708 return (OsmPrimitive) value; 709 } else if (value instanceof RelationMember) { 710 return ((RelationMember) value).getMember(); 711 } else { 712 Main.error("Unknown object type: "+value); 713 return null; 714 } 715 } 716 } 717 718 /** 719 * This is the selection model to be used in a {@link JTable} which displays 720 * an entry list managed by {@link ListMergeModel}. 721 * 722 * The model ensures that only rows displaying an entry in the entry list 723 * can be selected. "Empty" rows can't be selected. 724 * 725 * @see ListMergeModel#getMySelectionModel() 726 * @see ListMergeModel#getMergedSelectionModel() 727 * @see ListMergeModel#getTheirSelectionModel() 728 * 729 */ 730 protected class EntriesSelectionModel extends DefaultListSelectionModel { 731 private final transient List<T> entries; 732 733 public EntriesSelectionModel(List<T> nodes) { 734 this.entries = nodes; 735 } 736 737 @Override 738 public void addSelectionInterval(int index0, int index1) { 739 if (entries.isEmpty()) return; 740 if (index0 > entries.size() - 1) return; 741 index0 = Math.min(entries.size()-1, index0); 742 index1 = Math.min(entries.size()-1, index1); 743 super.addSelectionInterval(index0, index1); 744 } 745 746 @Override 747 public void insertIndexInterval(int index, int length, boolean before) { 748 if (entries.isEmpty()) return; 749 if (before) { 750 int newindex = Math.min(entries.size()-1, index); 751 if (newindex < index - length) return; 752 length = length - (index - newindex); 753 super.insertIndexInterval(newindex, length, before); 754 } else { 755 if (index > entries.size() -1) return; 756 length = Math.min(entries.size()-1 - index, length); 757 super.insertIndexInterval(index, length, before); 758 } 759 } 760 761 @Override 762 public void moveLeadSelectionIndex(int leadIndex) { 763 if (entries.isEmpty()) return; 764 leadIndex = Math.max(0, leadIndex); 765 leadIndex = Math.min(entries.size() - 1, leadIndex); 766 super.moveLeadSelectionIndex(leadIndex); 767 } 768 769 @Override 770 public void removeIndexInterval(int index0, int index1) { 771 if (entries.isEmpty()) return; 772 index0 = Math.max(0, index0); 773 index0 = Math.min(entries.size() - 1, index0); 774 775 index1 = Math.max(0, index1); 776 index1 = Math.min(entries.size() - 1, index1); 777 super.removeIndexInterval(index0, index1); 778 } 779 780 @Override 781 public void removeSelectionInterval(int index0, int index1) { 782 if (entries.isEmpty()) return; 783 index0 = Math.max(0, index0); 784 index0 = Math.min(entries.size() - 1, index0); 785 786 index1 = Math.max(0, index1); 787 index1 = Math.min(entries.size() - 1, index1); 788 super.removeSelectionInterval(index0, index1); 789 } 790 791 @Override 792 public void setAnchorSelectionIndex(int anchorIndex) { 793 if (entries.isEmpty()) return; 794 anchorIndex = Math.min(entries.size() - 1, anchorIndex); 795 super.setAnchorSelectionIndex(anchorIndex); 796 } 797 798 @Override 799 public void setLeadSelectionIndex(int leadIndex) { 800 if (entries.isEmpty()) return; 801 leadIndex = Math.min(entries.size() - 1, leadIndex); 802 super.setLeadSelectionIndex(leadIndex); 803 } 804 805 @Override 806 public void setSelectionInterval(int index0, int index1) { 807 if (entries.isEmpty()) return; 808 index0 = Math.max(0, index0); 809 index0 = Math.min(entries.size() - 1, index0); 810 811 index1 = Math.max(0, index1); 812 index1 = Math.min(entries.size() - 1, index1); 813 814 super.setSelectionInterval(index0, index1); 815 } 816 } 817 818 public ComparePairListModel getComparePairListModel() { 819 return this.comparePairListModel; 820 } 821 822 public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> { 823 824 private int selectedIdx; 825 private final List<ComparePairType> compareModes; 826 827 /** 828 * Constructs a new {@code ComparePairListModel}. 829 */ 830 public ComparePairListModel() { 831 this.compareModes = new ArrayList<>(); 832 compareModes.add(MY_WITH_THEIR); 833 compareModes.add(MY_WITH_MERGED); 834 compareModes.add(THEIR_WITH_MERGED); 835 selectedIdx = 0; 836 } 837 838 @Override 839 public ComparePairType getElementAt(int index) { 840 if (index < compareModes.size()) 841 return compareModes.get(index); 842 throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index)); 843 } 844 845 @Override 846 public int getSize() { 847 return compareModes.size(); 848 } 849 850 @Override 851 public Object getSelectedItem() { 852 return compareModes.get(selectedIdx); 853 } 854 855 @Override 856 public void setSelectedItem(Object anItem) { 857 int i = compareModes.indexOf(anItem); 858 if (i < 0) 859 throw new IllegalStateException(tr("Item {0} not found in list.", anItem)); 860 selectedIdx = i; 861 fireModelDataChanged(); 862 } 863 864 public ComparePairType getSelectedComparePair() { 865 return compareModes.get(selectedIdx); 866 } 867 } 868}