001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.tags; 003 004import java.beans.PropertyChangeListener; 005import java.beans.PropertyChangeSupport; 006import java.util.ArrayList; 007import java.util.Collections; 008import java.util.Comparator; 009import java.util.HashMap; 010import java.util.HashSet; 011import java.util.List; 012import java.util.Map; 013import java.util.Set; 014 015import javax.swing.table.DefaultTableModel; 016 017import org.openstreetmap.josm.data.osm.TagCollection; 018import org.openstreetmap.josm.gui.util.GuiHelper; 019import org.openstreetmap.josm.tools.CheckParameterUtil; 020 021public class TagConflictResolverModel extends DefaultTableModel { 022 public static final String NUM_CONFLICTS_PROP = TagConflictResolverModel.class.getName() + ".numConflicts"; 023 024 private transient TagCollection tags; 025 private List<String> displayedKeys; 026 private Set<String> keysWithConflicts; 027 private transient Map<String, MultiValueResolutionDecision> decisions; 028 private int numConflicts; 029 private PropertyChangeSupport support; 030 private boolean showTagsWithConflictsOnly; 031 private boolean showTagsWithMultiValuesOnly; 032 033 /** 034 * Constructs a new {@code TagConflictResolverModel}. 035 */ 036 public TagConflictResolverModel() { 037 numConflicts = 0; 038 support = new PropertyChangeSupport(this); 039 } 040 041 public void addPropertyChangeListener(PropertyChangeListener listener) { 042 support.addPropertyChangeListener(listener); 043 } 044 045 public void removePropertyChangeListener(PropertyChangeListener listener) { 046 support.removePropertyChangeListener(listener); 047 } 048 049 protected void setNumConflicts(int numConflicts) { 050 int oldValue = this.numConflicts; 051 this.numConflicts = numConflicts; 052 if (oldValue != this.numConflicts) { 053 support.firePropertyChange(NUM_CONFLICTS_PROP, oldValue, this.numConflicts); 054 } 055 } 056 057 protected void refreshNumConflicts() { 058 int count = 0; 059 for (MultiValueResolutionDecision d : decisions.values()) { 060 if (!d.isDecided()) { 061 count++; 062 } 063 } 064 setNumConflicts(count); 065 } 066 067 protected void sort() { 068 Collections.sort( 069 displayedKeys, 070 new Comparator<String>() { 071 @Override 072 public int compare(String key1, String key2) { 073 if (decisions.get(key1).isDecided() && !decisions.get(key2).isDecided()) 074 return 1; 075 else if (!decisions.get(key1).isDecided() && decisions.get(key2).isDecided()) 076 return -1; 077 return key1.compareTo(key2); 078 } 079 } 080 ); 081 } 082 083 /** 084 * initializes the model from the current tags 085 * 086 */ 087 public void rebuild() { 088 if (tags == null) return; 089 for (String key: tags.getKeys()) { 090 MultiValueResolutionDecision decision = new MultiValueResolutionDecision(tags.getTagsFor(key)); 091 if (decisions.get(key) == null) { 092 decisions.put(key, decision); 093 } 094 } 095 displayedKeys.clear(); 096 Set<String> keys = tags.getKeys(); 097 if (showTagsWithConflictsOnly) { 098 keys.retainAll(keysWithConflicts); 099 if (showTagsWithMultiValuesOnly) { 100 Set<String> keysWithMultiValues = new HashSet<>(); 101 for (String key: keys) { 102 if (decisions.get(key).canKeepAll()) { 103 keysWithMultiValues.add(key); 104 } 105 } 106 keys.retainAll(keysWithMultiValues); 107 } 108 for (String key: tags.getKeys()) { 109 if (!decisions.get(key).isDecided() && !keys.contains(key)) { 110 keys.add(key); 111 } 112 } 113 } 114 displayedKeys.addAll(keys); 115 refreshNumConflicts(); 116 sort(); 117 GuiHelper.runInEDTAndWait(new Runnable() { 118 @Override public void run() { 119 fireTableDataChanged(); 120 } 121 }); 122 } 123 124 /** 125 * Populates the model with the tags for which conflicts are to be resolved. 126 * 127 * @param tags the tag collection with the tags. Must not be null. 128 * @param keysWithConflicts the set of tag keys with conflicts 129 * @throws IllegalArgumentException if tags is null 130 */ 131 public void populate(TagCollection tags, Set<String> keysWithConflicts) { 132 CheckParameterUtil.ensureParameterNotNull(tags, "tags"); 133 this.tags = tags; 134 displayedKeys = new ArrayList<>(); 135 this.keysWithConflicts = keysWithConflicts == null ? new HashSet<String>() : keysWithConflicts; 136 decisions = new HashMap<>(); 137 rebuild(); 138 } 139 140 /** 141 * Returns the OSM key at the given row. 142 * @param row The table row 143 * @return the OSM key at the given row. 144 * @since 6616 145 */ 146 public final String getKey(int row) { 147 return displayedKeys.get(row); 148 } 149 150 @Override 151 public int getRowCount() { 152 if (displayedKeys == null) return 0; 153 return displayedKeys.size(); 154 } 155 156 @Override 157 public Object getValueAt(int row, int column) { 158 return getDecision(row); 159 } 160 161 @Override 162 public boolean isCellEditable(int row, int column) { 163 return column == 2; 164 } 165 166 @Override 167 public void setValueAt(Object value, int row, int column) { 168 MultiValueResolutionDecision decision = getDecision(row); 169 if (value instanceof String) { 170 decision.keepOne((String) value); 171 } else if (value instanceof MultiValueDecisionType) { 172 MultiValueDecisionType type = (MultiValueDecisionType) value; 173 switch(type) { 174 case KEEP_NONE: 175 decision.keepNone(); 176 break; 177 case KEEP_ALL: 178 decision.keepAll(); 179 break; 180 case SUM_ALL_NUMERIC: 181 decision.sumAllNumeric(); 182 break; 183 } 184 } 185 GuiHelper.runInEDTAndWait(new Runnable() { 186 @Override public void run() { 187 fireTableDataChanged(); 188 } 189 }); 190 refreshNumConflicts(); 191 } 192 193 /** 194 * Replies true if each {@link MultiValueResolutionDecision} is decided. 195 * 196 * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise 197 */ 198 public boolean isResolvedCompletely() { 199 return numConflicts == 0 && keysWithConflicts != null && keysWithConflicts.isEmpty(); 200 } 201 202 public int getNumConflicts() { 203 return numConflicts; 204 } 205 206 public int getNumDecisions() { 207 return decisions == null ? 0 : decisions.size(); 208 } 209 210 //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be 211 //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes 212 public TagCollection getResolution() { 213 TagCollection tc = new TagCollection(); 214 for (String key: displayedKeys) { 215 tc.add(decisions.get(key).getResolution()); 216 } 217 return tc; 218 } 219 220 public TagCollection getAllResolutions() { 221 TagCollection tc = new TagCollection(); 222 for (MultiValueResolutionDecision value: decisions.values()) { 223 tc.add(value.getResolution()); 224 } 225 return tc; 226 } 227 228 /** 229 * Returns the conflict resolution decision at the given row. 230 * @param row The table row 231 * @return the conflict resolution decision at the given row. 232 */ 233 public MultiValueResolutionDecision getDecision(int row) { 234 return decisions.get(getKey(row)); 235 } 236 237 /** 238 * Sets whether all tags or only tags with conflicts are displayed 239 * 240 * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed 241 */ 242 public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) { 243 this.showTagsWithConflictsOnly = showTagsWithConflictsOnly; 244 rebuild(); 245 } 246 247 /** 248 * Sets whether all conflicts or only conflicts with multiple values are displayed 249 * 250 * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed 251 */ 252 public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) { 253 this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly; 254 rebuild(); 255 } 256 257 /** 258 * Prepare the default decisions for the current model 259 * 260 */ 261 public void prepareDefaultTagDecisions() { 262 for (MultiValueResolutionDecision decision: decisions.values()) { 263 List<String> values = decision.getValues(); 264 values.remove(""); 265 if (values.size() == 1) { 266 // TODO: Do not suggest to keep the single value in order to avoid long highways to become tunnels+bridges+... 267 // (only if both primitives are tagged) 268 decision.keepOne(values.get(0)); 269 } 270 // else: Do not suggest to keep all values in order to reduce the wrong usage of semicolon values, see #9104! 271 } 272 rebuild(); 273 } 274 275 /** 276 * Returns the set of keys in conflict. 277 * @return the set of keys in conflict. 278 * @since 6616 279 */ 280 public final Set<String> getKeysWithConflicts() { 281 return new HashSet<>(keysWithConflicts); 282 } 283}