001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.conflict.tags; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.text.MessageFormat; 007import java.util.ArrayList; 008import java.util.Arrays; 009import java.util.Collection; 010import java.util.Collections; 011import java.util.List; 012import java.util.Optional; 013 014import org.openstreetmap.josm.command.ChangePropertyCommand; 015import org.openstreetmap.josm.command.Command; 016import org.openstreetmap.josm.data.osm.OsmPrimitive; 017import org.openstreetmap.josm.data.osm.Tag; 018import org.openstreetmap.josm.data.osm.TagCollection; 019import org.openstreetmap.josm.tools.CheckParameterUtil; 020 021/** 022 * Represents a decision for a conflict due to multiple possible value for a tag. 023 * @since 2008 024 */ 025public class MultiValueResolutionDecision { 026 027 /** the type of decision */ 028 private MultiValueDecisionType type; 029 /** the collection of tags for which a decision is needed */ 030 private final TagCollection tags; 031 /** the selected value if {@link #type} is {@link MultiValueDecisionType#KEEP_ONE} */ 032 private String value; 033 034 private static final String[] SUMMABLE_KEYS = { 035 "capacity(:.+)?", "step_count" 036 }; 037 038 /** 039 * constructor 040 */ 041 public MultiValueResolutionDecision() { 042 type = MultiValueDecisionType.UNDECIDED; 043 tags = new TagCollection(); 044 autoDecide(); 045 } 046 047 /** 048 * Creates a new decision for the tag collection <code>tags</code>. 049 * All tags must have the same key. 050 * 051 * @param tags the tags. Must not be null. 052 * @throws IllegalArgumentException if tags is null 053 * @throws IllegalArgumentException if there are more than one keys 054 * @throws IllegalArgumentException if tags is empty 055 */ 056 public MultiValueResolutionDecision(TagCollection tags) { 057 CheckParameterUtil.ensureParameterNotNull(tags, "tags"); 058 if (tags.isEmpty()) 059 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' must not be empty.", "tags")); 060 if (tags.getKeys().size() != 1) 061 throw new IllegalArgumentException(MessageFormat.format("Parameter ''{0}'' with tags for exactly one key expected. Got {1}.", 062 "tags", tags.getKeys().size())); 063 this.tags = tags; 064 autoDecide(); 065 } 066 067 /** 068 * Tries to find the best decision based on the current values. 069 */ 070 protected final void autoDecide() { 071 this.type = MultiValueDecisionType.UNDECIDED; 072 // exactly one empty value ? -> delete the tag 073 if (tags.size() == 1 && tags.getValues().contains("")) { 074 this.type = MultiValueDecisionType.KEEP_NONE; 075 076 // exactly one non empty value? -> keep this value 077 } else if (tags.size() == 1) { 078 this.type = MultiValueDecisionType.KEEP_ONE; 079 this.value = tags.getValues().iterator().next(); 080 } 081 } 082 083 /** 084 * Apply the decision to keep no value 085 */ 086 public void keepNone() { 087 this.type = MultiValueDecisionType.KEEP_NONE; 088 } 089 090 /** 091 * Apply the decision to keep all values 092 */ 093 public void keepAll() { 094 this.type = MultiValueDecisionType.KEEP_ALL; 095 } 096 097 /** 098 * Apply the decision to sum all numeric values 099 * @since 7743 100 */ 101 public void sumAllNumeric() { 102 this.type = MultiValueDecisionType.SUM_ALL_NUMERIC; 103 } 104 105 /** 106 * Apply the decision to keep exactly one value 107 * 108 * @param value the value to keep 109 * @throws IllegalArgumentException if value is null 110 * @throws IllegalStateException if value is not in the list of known values for this tag 111 */ 112 public void keepOne(String value) { 113 CheckParameterUtil.ensureParameterNotNull(value, "value"); 114 if (!tags.getValues().contains(value)) 115 throw new IllegalStateException(tr("Tag collection does not include the selected value ''{0}''.", value)); 116 this.value = value; 117 this.type = MultiValueDecisionType.KEEP_ONE; 118 } 119 120 /** 121 * sets a new value for this 122 * 123 * @param value the new value 124 */ 125 public void setNew(String value) { 126 this.value = Optional.ofNullable(value).orElse(""); 127 this.type = MultiValueDecisionType.KEEP_ONE; 128 } 129 130 /** 131 * marks this as undecided 132 * 133 */ 134 public void undecide() { 135 this.type = MultiValueDecisionType.UNDECIDED; 136 } 137 138 /** 139 * Replies the chosen value 140 * 141 * @return the chosen value 142 * @throws IllegalStateException if this resolution is not yet decided 143 */ 144 public String getChosenValue() { 145 switch(type) { 146 case UNDECIDED: throw new IllegalStateException(tr("Not decided yet")); 147 case KEEP_ONE: return value; 148 case SUM_ALL_NUMERIC: return tags.getSummedValues(getKey()); 149 case KEEP_ALL: return tags.getJoinedValues(getKey()); 150 case KEEP_NONE: 151 default: return null; 152 } 153 } 154 155 /** 156 * Replies the list of possible, non empty values 157 * 158 * @return the list of possible, non empty values 159 */ 160 public List<String> getValues() { 161 List<String> ret = new ArrayList<>(tags.getValues()); 162 ret.remove(""); 163 ret.remove(null); 164 Collections.sort(ret); 165 return ret; 166 } 167 168 /** 169 * Replies the key of the tag to be resolved by this resolution 170 * 171 * @return the key of the tag to be resolved by this resolution 172 */ 173 public String getKey() { 174 return tags.getKeys().iterator().next(); 175 } 176 177 /** 178 * Replies true if the empty value is a possible value in this resolution 179 * 180 * @return true if the empty value is a possible value in this resolution 181 */ 182 public boolean canKeepNone() { 183 return tags.getValues().contains(""); 184 } 185 186 /** 187 * Replies true, if this resolution has more than 1 possible non-empty values 188 * 189 * @return true, if this resolution has more than 1 possible non-empty values 190 */ 191 public boolean canKeepAll() { 192 return getValues().size() > 1; 193 } 194 195 /** 196 * Replies true, if summing all numeric values is a possible value in this resolution 197 * 198 * @return true, if summing all numeric values is a possible value in this resolution 199 * @since 7743 200 */ 201 public boolean canSumAllNumeric() { 202 return canKeepAll() && Arrays.stream(SUMMABLE_KEYS).anyMatch(key -> getKey().matches(key)); 203 } 204 205 /** 206 * Replies true if this resolution is decided 207 * 208 * @return true if this resolution is decided 209 */ 210 public boolean isDecided() { 211 return type != MultiValueDecisionType.UNDECIDED; 212 } 213 214 /** 215 * Replies the type of the resolution 216 * 217 * @return the type of the resolution 218 */ 219 public MultiValueDecisionType getDecisionType() { 220 return type; 221 } 222 223 /** 224 * Applies the resolution to an {@link OsmPrimitive} 225 * 226 * @param primitive the primitive 227 * @throws IllegalStateException if this resolution is not resolved yet 228 * 229 */ 230 public void applyTo(OsmPrimitive primitive) { 231 if (primitive == null) return; 232 if (!isDecided()) 233 throw new IllegalStateException(tr("Not decided yet")); 234 String key = tags.getKeys().iterator().next(); 235 if (type == MultiValueDecisionType.KEEP_NONE) { 236 primitive.remove(key); 237 } else { 238 primitive.put(key, getChosenValue()); 239 } 240 } 241 242 /** 243 * Applies this resolution to a collection of primitives 244 * 245 * @param primitives the collection of primitives 246 * @throws IllegalStateException if this resolution is not resolved yet 247 */ 248 public void applyTo(Collection<? extends OsmPrimitive> primitives) { 249 if (primitives == null) return; 250 for (OsmPrimitive primitive: primitives) { 251 if (primitive == null) { 252 continue; 253 } 254 applyTo(primitive); 255 } 256 } 257 258 /** 259 * Builds a change command for applying this resolution to a primitive 260 * 261 * @param primitive the primitive 262 * @return the change command 263 * @throws IllegalArgumentException if primitive is null 264 * @throws IllegalStateException if this resolution is not resolved yet 265 */ 266 public Command buildChangeCommand(OsmPrimitive primitive) { 267 CheckParameterUtil.ensureParameterNotNull(primitive, "primitive"); 268 if (!isDecided()) 269 throw new IllegalStateException(tr("Not decided yet")); 270 String key = tags.getKeys().iterator().next(); 271 return new ChangePropertyCommand(primitive, key, getChosenValue()); 272 } 273 274 /** 275 * Builds a change command for applying this resolution to a collection of primitives 276 * 277 * @param primitives the collection of primitives 278 * @return the change command 279 * @throws IllegalArgumentException if primitives is null 280 * @throws IllegalStateException if this resolution is not resolved yet 281 */ 282 public Command buildChangeCommand(Collection<? extends OsmPrimitive> primitives) { 283 CheckParameterUtil.ensureParameterNotNull(primitives, "primitives"); 284 if (!isDecided()) 285 throw new IllegalStateException(tr("Not decided yet")); 286 String key = tags.getKeys().iterator().next(); 287 return new ChangePropertyCommand(primitives, key, getChosenValue()); 288 } 289 290 /** 291 * Replies a tag representing the current resolution. Null, if this resolution is not resolved yet. 292 * 293 * @return a tag representing the current resolution. Null, if this resolution is not resolved yet 294 */ 295 public Tag getResolution() { 296 switch(type) { 297 case SUM_ALL_NUMERIC: return new Tag(getKey(), tags.getSummedValues(getKey())); 298 case KEEP_ALL: return new Tag(getKey(), tags.getJoinedValues(getKey())); 299 case KEEP_ONE: return new Tag(getKey(), value); 300 case KEEP_NONE: return new Tag(getKey(), ""); 301 case UNDECIDED: 302 default: return null; 303 } 304 } 305}