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