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.Observable; 020import java.util.Set; 021 022import javax.swing.AbstractListModel; 023import javax.swing.ComboBoxModel; 024import javax.swing.DefaultListSelectionModel; 025import javax.swing.JOptionPane; 026import javax.swing.JTable; 027import javax.swing.ListSelectionModel; 028import javax.swing.table.DefaultTableModel; 029import javax.swing.table.TableModel; 030 031import org.openstreetmap.josm.Main; 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.help.HelpUtil; 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 Observable { 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 setChanged(); 273 notifyObservers(); 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 notifyObservers(); 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 notifyObservers(); 508 mergedEntriesSelectionModel.clearSelection(); 509 for (int row: rows) { 510 mergedEntriesSelectionModel.addSelectionInterval(row-1, row-1); 511 } 512 } 513 514 /** 515 * Moves the nodes given by indices in rows down by one position in the list 516 * of merged nodes. 517 * 518 * @param rows the indices 519 */ 520 public void moveDownMerged(int[] rows) { 521 if (rows == null || rows.length == 0) 522 return; 523 List<T> mergedEntries = getMergedEntries(); 524 if (rows[rows.length -1] == mergedEntries.size() -1) 525 // can't move down 526 return; 527 for (int i = rows.length-1; i >= 0; i--) { 528 int row = rows[i]; 529 T n = mergedEntries.get(row); 530 mergedEntries.remove(row); 531 mergedEntries.add(row +1, n); 532 } 533 fireModelDataChanged(); 534 notifyObservers(); 535 mergedEntriesSelectionModel.clearSelection(); 536 for (int row: rows) { 537 mergedEntriesSelectionModel.addSelectionInterval(row+1, row+1); 538 } 539 } 540 541 /** 542 * Removes the nodes given by indices in rows from the list 543 * of merged nodes. 544 * 545 * @param rows the indices 546 */ 547 public void removeMerged(int[] rows) { 548 if (rows == null || rows.length == 0) 549 return; 550 551 List<T> mergedEntries = getMergedEntries(); 552 553 for (int i = rows.length-1; i >= 0; i--) { 554 mergedEntries.remove(rows[i]); 555 } 556 fireModelDataChanged(); 557 notifyObservers(); 558 mergedEntriesSelectionModel.clearSelection(); 559 } 560 561 /** 562 * Replies true if the list of my entries and the list of their 563 * entries are equal 564 * 565 * @return true, if the lists are equal; false otherwise 566 */ 567 protected boolean myAndTheirEntriesEqual() { 568 569 if (getMyEntriesSize() != getTheirEntriesSize()) 570 return false; 571 for (int i = 0; i < getMyEntriesSize(); i++) { 572 if (!isEqualEntry(getMyEntries().get(i), getTheirEntries().get(i))) 573 return false; 574 } 575 return true; 576 } 577 578 /** 579 * This an adapter between a {@link JTable} and one of the three entry lists 580 * in the role {@link ListRole} managed by the {@link ListMergeModel}. 581 * 582 * From the point of view of the {@link JTable} it is a {@link TableModel}. 583 * 584 * @see ListMergeModel#getMyTableModel() 585 * @see ListMergeModel#getTheirTableModel() 586 * @see ListMergeModel#getMergedTableModel() 587 */ 588 public class EntriesTableModel extends DefaultTableModel implements OsmPrimitivesTableModel { 589 private final ListRole role; 590 591 /** 592 * 593 * @param role the role 594 */ 595 public EntriesTableModel(ListRole role) { 596 this.role = role; 597 } 598 599 @Override 600 public int getRowCount() { 601 int count = Math.max(getMyEntries().size(), getMergedEntries().size()); 602 return Math.max(count, getTheirEntries().size()); 603 } 604 605 @Override 606 public Object getValueAt(int row, int column) { 607 if (row < entries.get(role).size()) 608 return entries.get(role).get(row); 609 return null; 610 } 611 612 @Override 613 public boolean isCellEditable(int row, int column) { 614 return false; 615 } 616 617 @Override 618 public void setValueAt(Object value, int row, int col) { 619 ListMergeModel.this.setValueAt(this, value, row, col); 620 } 621 622 public ListMergeModel<T> getListMergeModel() { 623 return ListMergeModel.this; 624 } 625 626 /** 627 * replies true if the {@link ListRole} of this {@link EntriesTableModel} 628 * participates in the current {@link ComparePairType} 629 * 630 * @return true, if the if the {@link ListRole} of this {@link EntriesTableModel} 631 * participates in the current {@link ComparePairType} 632 * 633 * @see ListMergeModel.ComparePairListModel#getSelectedComparePair() 634 */ 635 public boolean isParticipatingInCurrentComparePair() { 636 return getComparePairListModel() 637 .getSelectedComparePair() 638 .isParticipatingIn(role); 639 } 640 641 /** 642 * replies 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 * 645 * @param row the row number 646 * @return true if the entry at <code>row</code> is equal to the entry at the 647 * same position in the opposite list of the current {@link ComparePairType} 648 * @throws IllegalStateException if this model is not participating in the 649 * current {@link ComparePairType} 650 * @see ComparePairType#getOppositeRole(ListRole) 651 * @see #getRole() 652 * @see #getOppositeEntries() 653 */ 654 public boolean isSamePositionInOppositeList(int row) { 655 if (!isParticipatingInCurrentComparePair()) 656 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 657 if (row >= getEntries().size()) return false; 658 if (row >= getOppositeEntries().size()) return false; 659 660 T e1 = getEntries().get(row); 661 T e2 = getOppositeEntries().get(row); 662 return isEqualEntry(e1, e2); 663 } 664 665 /** 666 * replies true if the entry at the current position is present in the opposite list 667 * of the current {@link ComparePairType}. 668 * 669 * @param row the current row 670 * @return true if the entry at the current position is present in the opposite list 671 * of the current {@link ComparePairType}. 672 * @throws IllegalStateException if this model is not participating in the 673 * current {@link ComparePairType} 674 * @see ComparePairType#getOppositeRole(ListRole) 675 * @see #getRole() 676 * @see #getOppositeEntries() 677 */ 678 public boolean isIncludedInOppositeList(int row) { 679 if (!isParticipatingInCurrentComparePair()) 680 throw new IllegalStateException(tr("List in role {0} is currently not participating in a compare pair.", role.toString())); 681 682 if (row >= getEntries().size()) return false; 683 T e1 = getEntries().get(row); 684 for (T e2: getOppositeEntries()) { 685 if (isEqualEntry(e1, e2)) return true; 686 } 687 return false; 688 } 689 690 protected List<T> getEntries() { 691 return entries.get(role); 692 } 693 694 /** 695 * replies the opposite list of entries with respect to the current {@link ComparePairType} 696 * 697 * @return the opposite list of entries 698 */ 699 protected List<T> getOppositeEntries() { 700 ListRole opposite = getComparePairListModel().getSelectedComparePair().getOppositeRole(role); 701 return entries.get(opposite); 702 } 703 704 public ListRole getRole() { 705 return role; 706 } 707 708 @Override 709 public OsmPrimitive getReferredPrimitive(int idx) { 710 Object value = getValueAt(idx, 1); 711 if (value instanceof OsmPrimitive) { 712 return (OsmPrimitive) value; 713 } else if (value instanceof RelationMember) { 714 return ((RelationMember) value).getMember(); 715 } else { 716 Main.error("Unknown object type: "+value); 717 return null; 718 } 719 } 720 } 721 722 /** 723 * This is the selection model to be used in a {@link JTable} which displays 724 * an entry list managed by {@link ListMergeModel}. 725 * 726 * The model ensures that only rows displaying an entry in the entry list 727 * can be selected. "Empty" rows can't be selected. 728 * 729 * @see ListMergeModel#getMySelectionModel() 730 * @see ListMergeModel#getMergedSelectionModel() 731 * @see ListMergeModel#getTheirSelectionModel() 732 * 733 */ 734 protected class EntriesSelectionModel extends DefaultListSelectionModel { 735 private final transient List<T> entries; 736 737 public EntriesSelectionModel(List<T> nodes) { 738 this.entries = nodes; 739 } 740 741 @Override 742 public void addSelectionInterval(int index0, int index1) { 743 if (entries.isEmpty()) return; 744 if (index0 > entries.size() - 1) return; 745 index0 = Math.min(entries.size()-1, index0); 746 index1 = Math.min(entries.size()-1, index1); 747 super.addSelectionInterval(index0, index1); 748 } 749 750 @Override 751 public void insertIndexInterval(int index, int length, boolean before) { 752 if (entries.isEmpty()) return; 753 if (before) { 754 int newindex = Math.min(entries.size()-1, index); 755 if (newindex < index - length) return; 756 length = length - (index - newindex); 757 super.insertIndexInterval(newindex, length, before); 758 } else { 759 if (index > entries.size() -1) return; 760 length = Math.min(entries.size()-1 - index, length); 761 super.insertIndexInterval(index, length, before); 762 } 763 } 764 765 @Override 766 public void moveLeadSelectionIndex(int leadIndex) { 767 if (entries.isEmpty()) return; 768 leadIndex = Math.max(0, leadIndex); 769 leadIndex = Math.min(entries.size() - 1, leadIndex); 770 super.moveLeadSelectionIndex(leadIndex); 771 } 772 773 @Override 774 public void removeIndexInterval(int index0, int index1) { 775 if (entries.isEmpty()) return; 776 index0 = Math.max(0, index0); 777 index0 = Math.min(entries.size() - 1, index0); 778 779 index1 = Math.max(0, index1); 780 index1 = Math.min(entries.size() - 1, index1); 781 super.removeIndexInterval(index0, index1); 782 } 783 784 @Override 785 public void removeSelectionInterval(int index0, int index1) { 786 if (entries.isEmpty()) return; 787 index0 = Math.max(0, index0); 788 index0 = Math.min(entries.size() - 1, index0); 789 790 index1 = Math.max(0, index1); 791 index1 = Math.min(entries.size() - 1, index1); 792 super.removeSelectionInterval(index0, index1); 793 } 794 795 @Override 796 public void setAnchorSelectionIndex(int anchorIndex) { 797 if (entries.isEmpty()) return; 798 anchorIndex = Math.min(entries.size() - 1, anchorIndex); 799 super.setAnchorSelectionIndex(anchorIndex); 800 } 801 802 @Override 803 public void setLeadSelectionIndex(int leadIndex) { 804 if (entries.isEmpty()) return; 805 leadIndex = Math.min(entries.size() - 1, leadIndex); 806 super.setLeadSelectionIndex(leadIndex); 807 } 808 809 @Override 810 public void setSelectionInterval(int index0, int index1) { 811 if (entries.isEmpty()) return; 812 index0 = Math.max(0, index0); 813 index0 = Math.min(entries.size() - 1, index0); 814 815 index1 = Math.max(0, index1); 816 index1 = Math.min(entries.size() - 1, index1); 817 818 super.setSelectionInterval(index0, index1); 819 } 820 } 821 822 public ComparePairListModel getComparePairListModel() { 823 return this.comparePairListModel; 824 } 825 826 public class ComparePairListModel extends AbstractListModel<ComparePairType> implements ComboBoxModel<ComparePairType> { 827 828 private int selectedIdx; 829 private final List<ComparePairType> compareModes; 830 831 /** 832 * Constructs a new {@code ComparePairListModel}. 833 */ 834 public ComparePairListModel() { 835 this.compareModes = new ArrayList<>(); 836 compareModes.add(MY_WITH_THEIR); 837 compareModes.add(MY_WITH_MERGED); 838 compareModes.add(THEIR_WITH_MERGED); 839 selectedIdx = 0; 840 } 841 842 @Override 843 public ComparePairType getElementAt(int index) { 844 if (index < compareModes.size()) 845 return compareModes.get(index); 846 throw new IllegalArgumentException(tr("Unexpected value of parameter ''index''. Got {0}.", index)); 847 } 848 849 @Override 850 public int getSize() { 851 return compareModes.size(); 852 } 853 854 @Override 855 public Object getSelectedItem() { 856 return compareModes.get(selectedIdx); 857 } 858 859 @Override 860 public void setSelectedItem(Object anItem) { 861 int i = compareModes.indexOf(anItem); 862 if (i < 0) 863 throw new IllegalStateException(tr("Item {0} not found in list.", anItem)); 864 selectedIdx = i; 865 fireModelDataChanged(); 866 } 867 868 public ComparePairType getSelectedComparePair() { 869 return compareModes.get(selectedIdx); 870 } 871 } 872}