001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.command; 003 004import static org.openstreetmap.josm.tools.I18n.marktr; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.util.Collection; 009import java.util.Collections; 010import java.util.HashMap; 011import java.util.LinkedList; 012import java.util.List; 013import java.util.Map; 014import java.util.Map.Entry; 015import java.util.NoSuchElementException; 016import java.util.Objects; 017import java.util.stream.Collectors; 018 019import javax.swing.Icon; 020 021import org.openstreetmap.josm.data.osm.DataSet; 022import org.openstreetmap.josm.data.osm.DefaultNameFormatter; 023import org.openstreetmap.josm.data.osm.OsmPrimitive; 024import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 025import org.openstreetmap.josm.data.osm.Tagged; 026import org.openstreetmap.josm.tools.I18n; 027import org.openstreetmap.josm.tools.ImageProvider; 028import org.openstreetmap.josm.tools.Utils; 029 030/** 031 * Command that manipulate the key/value structure of several objects. Manages deletion, 032 * adding and modify of values and keys. 033 * 034 * @author imi 035 * @since 24 036 */ 037public class ChangePropertyCommand extends Command { 038 039 static final class OsmPseudoCommand implements PseudoCommand { 040 private final OsmPrimitive osm; 041 042 OsmPseudoCommand(OsmPrimitive osm) { 043 this.osm = osm; 044 } 045 046 @Override 047 public String getDescriptionText() { 048 return osm.getDisplayName(DefaultNameFormatter.getInstance()); 049 } 050 051 @Override 052 public Icon getDescriptionIcon() { 053 return ImageProvider.get(osm.getDisplayType()); 054 } 055 056 @Override 057 public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 058 return Collections.singleton(osm); 059 } 060 } 061 062 /** 063 * All primitives that are affected with this command. 064 */ 065 private final List<OsmPrimitive> objects = new LinkedList<>(); 066 067 /** 068 * Key and value pairs. If value is <code>null</code>, delete all key references with the given 069 * key. Otherwise, change the tags of all objects to the given value or create keys of 070 * those objects that do not have the key yet. 071 */ 072 private final Map<String, String> tags; 073 074 /** 075 * Creates a command to change multiple tags of multiple objects 076 * 077 * @param ds The target data set. Must not be {@code null} 078 * @param objects the objects to modify. Must not be empty 079 * @param tags the tags to set. Caller must make sure that the tas are not changed once the command was executed. 080 * @since 12726 081 */ 082 public ChangePropertyCommand(DataSet ds, Collection<? extends OsmPrimitive> objects, Map<String, String> tags) { 083 super(ds); 084 this.tags = tags; 085 init(objects); 086 } 087 088 /** 089 * Creates a command to change multiple tags of multiple objects 090 * 091 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set 092 * @param tags the tags to set. Caller must make sure that the tas are not changed once the command was executed. 093 * @throws NullPointerException if objects is null or contain null item 094 * @throws NoSuchElementException if objects is empty 095 */ 096 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, Map<String, String> tags) { 097 this(objects.iterator().next().getDataSet(), objects, tags); 098 } 099 100 /** 101 * Creates a command to change one tag of multiple objects 102 * 103 * @param objects the objects to modify. Must not be empty, and objects must belong to a data set 104 * @param key the key of the tag to set 105 * @param value the value of the key to set 106 * @throws NullPointerException if objects is null or contain null item 107 * @throws NoSuchElementException if objects is empty 108 */ 109 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, String key, String value) { 110 super(objects.iterator().next().getDataSet()); 111 this.tags = Collections.singletonMap(key, value); 112 init(objects); 113 } 114 115 /** 116 * Creates a command to change one tag of one object 117 * 118 * @param object the object to modify. Must belong to a data set 119 * @param key the key of the tag to set 120 * @param value the value of the key to set 121 * @throws NullPointerException if object is null 122 */ 123 public ChangePropertyCommand(OsmPrimitive object, String key, String value) { 124 this(Collections.singleton(object), key, value); 125 } 126 127 /** 128 * Initialize the instance by finding what objects will be modified 129 * 130 * @param objects the objects to (possibly) modify 131 */ 132 private void init(Collection<? extends OsmPrimitive> objects) { 133 // determine what objects will be modified 134 for (OsmPrimitive osm : objects) { 135 boolean modified = false; 136 137 // loop over all tags 138 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 139 String oldVal = osm.get(tag.getKey()); 140 String newVal = tag.getValue(); 141 142 if (Utils.isEmpty(newVal)) { 143 if (oldVal != null) { 144 // new value is null and tag exists (will delete tag) 145 modified = true; 146 break; 147 } 148 } else if (oldVal == null || !newVal.equals(oldVal)) { 149 // new value is not null and is different from current value 150 modified = true; 151 break; 152 } 153 } 154 if (modified) 155 this.objects.add(osm); 156 } 157 } 158 159 @Override 160 public boolean executeCommand() { 161 if (objects.isEmpty()) 162 return true; 163 final DataSet dataSet = objects.get(0).getDataSet(); 164 if (dataSet != null) { 165 dataSet.beginUpdate(); 166 } 167 try { 168 super.executeCommand(); // save old 169 170 for (OsmPrimitive osm : objects) { 171 // loop over all tags 172 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 173 String oldVal = osm.get(tag.getKey()); 174 String newVal = tag.getValue(); 175 176 if (Utils.isEmpty(newVal)) { 177 if (oldVal != null) 178 osm.remove(tag.getKey()); 179 } else if (oldVal == null || !newVal.equals(oldVal)) 180 osm.put(tag.getKey(), newVal); 181 } 182 // init() only keeps modified primitives. Therefore the modified 183 // bit can be set without further checks. 184 osm.setModified(true); 185 } 186 return true; 187 } finally { 188 if (dataSet != null) { 189 dataSet.endUpdate(); 190 } 191 } 192 } 193 194 @Override 195 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 196 modified.addAll(objects); 197 } 198 199 @Override 200 public String getDescriptionText() { 201 @I18n.QuirkyPluralString 202 final String text; 203 if (objects.size() == 1 && tags.size() == 1) { 204 OsmPrimitive primitive = objects.get(0); 205 String msg; 206 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 207 if (Utils.isEmpty(entry.getValue())) { 208 switch(OsmPrimitiveType.from(primitive)) { 209 case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break; 210 case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break; 211 case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break; 212 default: throw new AssertionError(); 213 } 214 text = tr(msg, entry.getKey(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 215 } else { 216 switch(OsmPrimitiveType.from(primitive)) { 217 case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break; 218 case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break; 219 case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break; 220 default: throw new AssertionError(); 221 } 222 text = tr(msg, entry.getKey(), entry.getValue(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 223 } 224 } else if (objects.size() > 1 && tags.size() == 1) { 225 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 226 if (Utils.isEmpty(entry.getValue())) { 227 /* I18n: plural form for objects, but value < 2 not possible! */ 228 text = trn("Remove \"{0}\" for {1} object", "Remove \"{0}\" for {1} objects", objects.size(), entry.getKey(), objects.size()); 229 } else { 230 /* I18n: plural form for objects, but value < 2 not possible! */ 231 text = trn("Set {0}={1} for {2} object", "Set {0}={1} for {2} objects", 232 objects.size(), entry.getKey(), entry.getValue(), objects.size()); 233 } 234 } else { 235 boolean allNull = this.tags.entrySet().stream() 236 .allMatch(tag -> Utils.isEmpty(tag.getValue())); 237 238 if (allNull) { 239 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 240 text = trn("Deleted {0} tags for {1} object", "Deleted {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 241 } else { 242 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 243 text = trn("Set {0} tags for {1} object", "Set {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 244 } 245 } 246 return text; 247 } 248 249 @Override 250 public Icon getDescriptionIcon() { 251 return ImageProvider.get("dialogs", "propertiesdialog", ImageProvider.ImageSizes.SMALLICON); 252 } 253 254 @Override 255 public Collection<PseudoCommand> getChildren() { 256 if (objects.size() == 1) 257 return null; 258 return objects.stream().map(OsmPseudoCommand::new).collect(Collectors.toList()); 259 } 260 261 /** 262 * Returns the number of objects that will effectively be modified, before the command is executed. 263 * @return the number of objects that will effectively be modified (can be 0) 264 * @see Command#getParticipatingPrimitives() 265 * @since 8945 266 */ 267 public final int getObjectsNumber() { 268 return objects.size(); 269 } 270 271 /** 272 * Returns the tags to set (key/value pairs). 273 * @return the tags to set (key/value pairs) 274 */ 275 public Map<String, String> getTags() { 276 return Collections.unmodifiableMap(tags); 277 } 278 279 @Override 280 public int hashCode() { 281 return Objects.hash(super.hashCode(), objects, tags); 282 } 283 284 @Override 285 public boolean equals(Object obj) { 286 if (this == obj) return true; 287 if (obj == null || getClass() != obj.getClass()) return false; 288 if (!super.equals(obj)) return false; 289 ChangePropertyCommand that = (ChangePropertyCommand) obj; 290 return Objects.equals(objects, that.objects) && 291 Objects.equals(tags, that.tags); 292 } 293 294 /** 295 * Calculate the {@link ChangePropertyCommand} that is needed to change the tags in source to be equal to those in target. 296 * @param source the source primitive 297 * @param target the target primitive 298 * @return null if no changes are needed, else a {@link ChangePropertyCommand} 299 * @since 17357 300 */ 301 public static Command build(OsmPrimitive source, Tagged target) { 302 Map<String, String> changedTags = new HashMap<>(); 303 // find tags which have to be changed or removed 304 for (Entry<String, String> tag : source.getKeys().entrySet()) { 305 String key = tag.getKey(); 306 String val = target.get(key); 307 if (!tag.getValue().equals(val)) 308 changedTags.put(key, val); // null or a different value 309 } 310 // find tags which exist only in target, they have to be added 311 for (Entry<String, String> tag : target.getKeys().entrySet()) { 312 String key = tag.getKey(); 313 if (!source.hasTag(key)) 314 changedTags.put(key, tag.getValue()); 315 } 316 if (changedTags.isEmpty()) 317 return null; 318 if (changedTags.size() == 1) { 319 Entry<String, String> tag = changedTags.entrySet().iterator().next(); 320 return new ChangePropertyCommand(Collections.singleton(source), tag.getKey(), tag.getValue()); 321 } 322 return new ChangePropertyCommand(Collections.singleton(source), new HashMap<>(changedTags)); 323 } 324}