001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.pair.tags; 003 004import java.beans.PropertyChangeEvent; 005import java.beans.PropertyChangeListener; 006import java.util.ArrayList; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Set; 010import java.util.stream.Collectors; 011import java.util.stream.IntStream; 012import java.util.stream.Stream; 013 014import javax.swing.table.DefaultTableModel; 015 016import org.openstreetmap.josm.command.conflict.TagConflictResolveCommand; 017import org.openstreetmap.josm.data.conflict.Conflict; 018import org.openstreetmap.josm.data.osm.OsmPrimitive; 019import org.openstreetmap.josm.gui.conflict.pair.MergeDecisionType; 020 021/** 022 * This is the {@link javax.swing.table.TableModel} used in the tables of the {@link TagMerger}. 023 * 024 * The model can {@link #populate(OsmPrimitive, OsmPrimitive)} itself from the conflicts 025 * in the tag sets of two {@link OsmPrimitive}s. Internally, it keeps a list of {@link TagMergeItem}s. 026 * 027 * {@link #decide(int, MergeDecisionType)} and {@link #decide(int[], MergeDecisionType)} can be used 028 * to remember a merge decision for a specific row in the model. 029 * 030 * The model notifies {@link PropertyChangeListener}s about updates of the number of 031 * undecided tags (see {@link #PROP_NUM_UNDECIDED_TAGS}). 032 * 033 */ 034public class TagMergeModel extends DefaultTableModel { 035 public static final String PROP_NUM_UNDECIDED_TAGS = TagMergeModel.class.getName() + ".numUndecidedTags"; 036 037 /** the list of tag merge items */ 038 private final transient List<TagMergeItem> tagMergeItems; 039 040 /** the property change listeners */ 041 private final transient Set<PropertyChangeListener> listeners; 042 043 private int numUndecidedTags; 044 045 /** 046 * Constructs a new {@code TagMergeModel}. 047 */ 048 public TagMergeModel() { 049 tagMergeItems = new ArrayList<>(); 050 listeners = new HashSet<>(); 051 } 052 053 public void addPropertyChangeListener(PropertyChangeListener listener) { 054 synchronized (listeners) { 055 if (listener == null) return; 056 if (listeners.contains(listener)) return; 057 listeners.add(listener); 058 } 059 } 060 061 public void removePropertyChangeListener(PropertyChangeListener listener) { 062 synchronized (listeners) { 063 if (listener == null) return; 064 if (!listeners.contains(listener)) return; 065 listeners.remove(listener); 066 } 067 } 068 069 /** 070 * notifies {@link PropertyChangeListener}s about an update of {@link TagMergeModel#PROP_NUM_UNDECIDED_TAGS} 071 072 * @param oldValue the old value 073 * @param newValue the new value 074 */ 075 protected void fireNumUndecidedTagsChanged(int oldValue, int newValue) { 076 PropertyChangeEvent evt = new PropertyChangeEvent(this, PROP_NUM_UNDECIDED_TAGS, oldValue, newValue); 077 synchronized (listeners) { 078 for (PropertyChangeListener l : listeners) { 079 l.propertyChange(evt); 080 } 081 } 082 } 083 084 /** 085 * refreshes the number of undecided tag conflicts after an update in the list of 086 * {@link TagMergeItem}s. Notifies {@link PropertyChangeListener} if necessary. 087 * 088 */ 089 protected void refreshNumUndecidedTags() { 090 int oldValue = numUndecidedTags; 091 numUndecidedTags = getNumUnresolvedConflicts(); 092 fireNumUndecidedTagsChanged(oldValue, numUndecidedTags); 093 } 094 095 /** 096 * Populate the model with conflicts between the tag sets of the two 097 * {@link OsmPrimitive} <code>my</code> and <code>their</code>. 098 * 099 * @param my my primitive (i.e. the primitive from the local dataset) 100 * @param their their primitive (i.e. the primitive from the server dataset) 101 * 102 */ 103 public void populate(OsmPrimitive my, OsmPrimitive their) { 104 tagMergeItems.clear(); 105 Set<String> keys = Stream.concat(my.keys(), their.keys()).collect(Collectors.toSet()); 106 for (String key : keys) { 107 String myValue = my.get(key); 108 String theirValue = their.get(key); 109 if (myValue == null || theirValue == null || !myValue.equals(theirValue)) { 110 tagMergeItems.add( 111 new TagMergeItem(key, my, their) 112 ); 113 } 114 } 115 fireTableDataChanged(); 116 refreshNumUndecidedTags(); 117 } 118 119 /** 120 * add a {@link TagMergeItem} to the model 121 * 122 * @param item the item 123 */ 124 public void addItem(TagMergeItem item) { 125 if (item != null) { 126 tagMergeItems.add(item); 127 fireTableDataChanged(); 128 refreshNumUndecidedTags(); 129 } 130 } 131 132 protected void rememberDecision(int row, MergeDecisionType decision) { 133 TagMergeItem item = tagMergeItems.get(row); 134 item.decide(decision); 135 } 136 137 /** 138 * set the merge decision of the {@link TagMergeItem} in row <code>row</code> 139 * to <code>decision</code>. 140 * 141 * @param row the row 142 * @param decision the decision 143 */ 144 public void decide(int row, MergeDecisionType decision) { 145 rememberDecision(row, decision); 146 fireTableRowsUpdated(row, row); 147 refreshNumUndecidedTags(); 148 } 149 150 /** 151 * set the merge decision of all {@link TagMergeItem} given by indices in <code>rows</code> 152 * to <code>decision</code>. 153 * 154 * @param rows the array of row indices 155 * @param decision the decision 156 */ 157 public void decide(int[] rows, MergeDecisionType decision) { 158 if (rows == null || rows.length == 0) 159 return; 160 for (int row : rows) { 161 rememberDecision(row, decision); 162 } 163 fireTableDataChanged(); 164 refreshNumUndecidedTags(); 165 } 166 167 @Override 168 public int getRowCount() { 169 return tagMergeItems == null ? 0 : tagMergeItems.size(); 170 } 171 172 @Override 173 public Object getValueAt(int row, int column) { 174 // return the tagMergeItem for both columns. The cell 175 // renderer will dispatch on the column index and get 176 // the key or the value from the TagMergeItem 177 // 178 return tagMergeItems.get(row); 179 } 180 181 @Override 182 public boolean isCellEditable(int row, int column) { 183 return false; 184 } 185 186 public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) { 187 return new TagConflictResolveCommand(conflict, new ArrayList<>(tagMergeItems)); 188 } 189 190 public boolean isResolvedCompletely() { 191 return tagMergeItems.stream() 192 .noneMatch(item -> item.getMergeDecision() == MergeDecisionType.UNDECIDED); 193 } 194 195 public void decideRemaining(MergeDecisionType decision) { 196 for (TagMergeItem item: tagMergeItems) { 197 if (item.getMergeDecision() == MergeDecisionType.UNDECIDED) 198 item.decide(decision); 199 } 200 } 201 202 public int getNumResolvedConflicts() { 203 return (int) tagMergeItems.stream() 204 .filter(item -> item.getMergeDecision() != MergeDecisionType.UNDECIDED) 205 .count(); 206 } 207 208 public int getNumUnresolvedConflicts() { 209 return (int) tagMergeItems.stream() 210 .filter(item -> item.getMergeDecision() == MergeDecisionType.UNDECIDED) 211 .count(); 212 } 213 214 public int getFirstUndecided(int startIndex) { 215 return IntStream.range(startIndex, tagMergeItems.size()) 216 .filter(i -> tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED) 217 .findFirst().orElse(-1); 218 } 219}