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