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 TagCollection tags; 025 private List<String> displayedKeys; 026 private Set<String> keysWithConflicts; 027 private Map<String, MultiValueResolutionDecision> decisions; 028 private int numConflicts; 029 private PropertyChangeSupport support; 030 private boolean showTagsWithConflictsOnly = false; 031 private boolean showTagsWithMultiValuesOnly = false; 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 thrown 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 } 181 } 182 GuiHelper.runInEDTAndWait(new Runnable() { 183 @Override public void run() { 184 fireTableDataChanged(); 185 } 186 }); 187 refreshNumConflicts(); 188 } 189 190 /** 191 * Replies true if each {@link MultiValueResolutionDecision} is decided. 192 * 193 * @return true if each {@link MultiValueResolutionDecision} is decided; false otherwise 194 */ 195 public boolean isResolvedCompletely() { 196 return numConflicts == 0 && keysWithConflicts != null && keysWithConflicts.isEmpty(); 197 } 198 199 public int getNumConflicts() { 200 return numConflicts; 201 } 202 203 public int getNumDecisions() { 204 return decisions == null ? 0 : decisions.size(); 205 } 206 207 //TODO Should this method work with all decisions or only with displayed decisions? For MergeNodes it should be 208 //all decisions, but this method is also used on other places, so I've made new method just for MergeNodes 209 public TagCollection getResolution() { 210 TagCollection tc = new TagCollection(); 211 for (String key: displayedKeys) { 212 tc.add(decisions.get(key).getResolution()); 213 } 214 return tc; 215 } 216 217 public TagCollection getAllResolutions() { 218 TagCollection tc = new TagCollection(); 219 for (MultiValueResolutionDecision value: decisions.values()) { 220 tc.add(value.getResolution()); 221 } 222 return tc; 223 } 224 225 /** 226 * Returns the conflict resolution decision at the given row. 227 * @param row The table row 228 * @return the conflict resolution decision at the given row. 229 */ 230 public MultiValueResolutionDecision getDecision(int row) { 231 return decisions.get(getKey(row)); 232 } 233 234 /** 235 * Sets whether all tags or only tags with conflicts are displayed 236 * 237 * @param showTagsWithConflictsOnly if true, only tags with conflicts are displayed 238 */ 239 public void setShowTagsWithConflictsOnly(boolean showTagsWithConflictsOnly) { 240 this.showTagsWithConflictsOnly = showTagsWithConflictsOnly; 241 rebuild(); 242 } 243 244 /** 245 * Sets whether all conflicts or only conflicts with multiple values are displayed 246 * 247 * @param showTagsWithMultiValuesOnly if true, only tags with multiple values are displayed 248 */ 249 public void setShowTagsWithMultiValuesOnly(boolean showTagsWithMultiValuesOnly) { 250 this.showTagsWithMultiValuesOnly = showTagsWithMultiValuesOnly; 251 rebuild(); 252 } 253 254 /** 255 * Prepare the default decisions for the current model 256 * 257 */ 258 public void prepareDefaultTagDecisions() { 259 for (MultiValueResolutionDecision decision: decisions.values()) { 260 List<String> values = decision.getValues(); 261 values.remove(""); 262 if (values.size() == 1) { 263 // TODO: Do not suggest to keep the single value in order to avoid long highways to become tunnels+bridges+... (only if both primitives are tagged) 264 decision.keepOne(values.get(0)); 265 } else { 266 // Do not suggest to keep all values in order to reduce the wrong usage of semicolon values, see #9104! 267 } 268 } 269 rebuild(); 270 } 271 272 /** 273 * Returns the set of keys in conflict. 274 * @return the set of keys in conflict. 275 * @since 6616 276 */ 277 public final Set<String> getKeysWithConflicts() { 278 return new HashSet<>(keysWithConflicts); 279 } 280}