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