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.equals(item.getMergeDecision())) { 090 newValue++; 091 } 092 } 093 int oldValue = numUndecidedTags; 094 numUndecidedTags = newValue; 095 fireNumUndecidedTagsChanged(oldValue, numUndecidedTags); 096 097 } 098 099 /** 100 * Populate the model with conflicts between the tag sets of the two 101 * {@link OsmPrimitive} <code>my</code> and <code>their</code>. 102 * 103 * @param my my primitive (i.e. the primitive from the local dataset) 104 * @param their their primitive (i.e. the primitive from the server dataset) 105 * 106 */ 107 public void populate(OsmPrimitive my, OsmPrimitive their) { 108 tagMergeItems.clear(); 109 Set<String> keys = new HashSet<>(); 110 keys.addAll(my.keySet()); 111 keys.addAll(their.keySet()); 112 for (String key : keys) { 113 String myValue = my.get(key); 114 String theirValue = their.get(key); 115 if (myValue == null || theirValue == null || !myValue.equals(theirValue)) { 116 tagMergeItems.add( 117 new TagMergeItem(key, my, their) 118 ); 119 } 120 } 121 fireTableDataChanged(); 122 refreshNumUndecidedTags(); 123 } 124 125 /** 126 * add a {@link TagMergeItem} to the model 127 * 128 * @param item the item 129 */ 130 public void addItem(TagMergeItem item) { 131 if (item != null) { 132 tagMergeItems.add(item); 133 fireTableDataChanged(); 134 refreshNumUndecidedTags(); 135 } 136 } 137 138 protected void rememberDecision(int row, MergeDecisionType decision) { 139 TagMergeItem item = tagMergeItems.get(row); 140 item.decide(decision); 141 } 142 143 /** 144 * set the merge decision of the {@link TagMergeItem} in row <code>row</code> 145 * to <code>decision</code>. 146 * 147 * @param row the row 148 * @param decision the decision 149 */ 150 public void decide(int row, MergeDecisionType decision) { 151 rememberDecision(row, decision); 152 fireTableRowsUpdated(row, row); 153 refreshNumUndecidedTags(); 154 } 155 156 /** 157 * set the merge decision of all {@link TagMergeItem} given by indices in <code>rows</code> 158 * to <code>decision</code>. 159 * 160 * @param rows the array of row indices 161 * @param decision the decision 162 */ 163 public void decide(int[] rows, MergeDecisionType decision) { 164 if (rows == null || rows.length == 0) 165 return; 166 for (int row : rows) { 167 rememberDecision(row, decision); 168 } 169 fireTableDataChanged(); 170 refreshNumUndecidedTags(); 171 } 172 173 @Override 174 public int getRowCount() { 175 return tagMergeItems == null ? 0 : tagMergeItems.size(); 176 } 177 178 @Override 179 public Object getValueAt(int row, int column) { 180 // return the tagMergeItem for both columns. The cell 181 // renderer will dispatch on the column index and get 182 // the key or the value from the TagMergeItem 183 // 184 return tagMergeItems.get(row); 185 } 186 187 @Override 188 public boolean isCellEditable(int row, int column) { 189 return false; 190 } 191 192 public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) { 193 return new TagConflictResolveCommand(conflict, tagMergeItems); 194 } 195 196 public boolean isResolvedCompletely() { 197 for (TagMergeItem item: tagMergeItems) { 198 if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) 199 return false; 200 } 201 return true; 202 } 203 204 public int getNumResolvedConflicts() { 205 int n = 0; 206 for (TagMergeItem item: tagMergeItems) { 207 if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) { 208 n++; 209 } 210 } 211 return n; 212 213 } 214 215 public int getFirstUndecided(int startIndex) { 216 for (int i = startIndex; i < tagMergeItems.size(); i++) { 217 if (tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED) 218 return i; 219 } 220 return -1; 221 } 222}