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 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<>(); 044 listeners = new ArrayList<>(); 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<>(); 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 rows the array of row indices 158 * @param decision the decision 159 */ 160 public void decide(int [] rows, MergeDecisionType decision) { 161 if (rows == null || rows.length == 0) 162 return; 163 for (int row : rows) { 164 rememberDecision(row, decision); 165 } 166 fireTableDataChanged(); 167 refreshNumUndecidedTags(); 168 } 169 170 @Override 171 public int getRowCount() { 172 return tagMergeItems == null ? 0 : tagMergeItems.size(); 173 } 174 175 @Override 176 public Object getValueAt(int row, int column) { 177 // return the tagMergeItem for both columns. The cell 178 // renderer will dispatch on the column index and get 179 // the key or the value from the TagMergeItem 180 // 181 return tagMergeItems.get(row); 182 } 183 184 @Override 185 public boolean isCellEditable(int row, int column) { 186 return false; 187 } 188 189 public TagConflictResolveCommand buildResolveCommand(Conflict<? extends OsmPrimitive> conflict) { 190 return new TagConflictResolveCommand(conflict, tagMergeItems); 191 } 192 193 public boolean isResolvedCompletely() { 194 for (TagMergeItem item: tagMergeItems) { 195 if (item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) 196 return false; 197 } 198 return true; 199 } 200 201 public int getNumResolvedConflicts() { 202 int n = 0; 203 for (TagMergeItem item: tagMergeItems) { 204 if (!item.getMergeDecision().equals(MergeDecisionType.UNDECIDED)) { 205 n++; 206 } 207 } 208 return n; 209 210 } 211 212 public int getFirstUndecided(int startIndex) { 213 for (int i=startIndex; i<tagMergeItems.size(); i++) { 214 if (tagMergeItems.get(i).getMergeDecision() == MergeDecisionType.UNDECIDED) 215 return i; 216 } 217 return -1; 218 } 219}