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.ArrayList; 009import java.util.Arrays; 010import java.util.Collection; 011import java.util.Collections; 012import java.util.HashMap; 013import java.util.LinkedList; 014import java.util.List; 015import java.util.Map; 016 017import javax.swing.Icon; 018 019import org.openstreetmap.josm.data.osm.DataSet; 020import org.openstreetmap.josm.data.osm.OsmPrimitive; 021import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 022import org.openstreetmap.josm.gui.DefaultNameFormatter; 023import org.openstreetmap.josm.tools.I18n; 024import org.openstreetmap.josm.tools.ImageProvider; 025 026/** 027 * Command that manipulate the key/value structure of several objects. Manages deletion, 028 * adding and modify of values and keys. 029 * 030 * @author imi 031 * @since 24 032 */ 033public class ChangePropertyCommand extends Command { 034 /** 035 * All primitives that are affected with this command. 036 */ 037 private final List<OsmPrimitive> objects = new LinkedList<>(); 038 039 /** 040 * Key and value pairs. If value is <code>null</code>, delete all key references with the given 041 * key. Otherwise, change the tags of all objects to the given value or create keys of 042 * those objects that do not have the key yet. 043 */ 044 private final Map<String, String> tags; 045 046 /** 047 * Creates a command to change multiple tags of multiple objects 048 * 049 * @param objects the objects to modify 050 * @param tags the tags to set 051 */ 052 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, Map<String, String> tags) { 053 this.tags = tags; 054 init(objects); 055 } 056 057 /** 058 * Creates a command to change one tag of multiple objects 059 * 060 * @param objects the objects to modify 061 * @param key the key of the tag to set 062 * @param value the value of the key to set 063 */ 064 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, String key, String value) { 065 this.tags = new HashMap<>(1); 066 this.tags.put(key, value); 067 init(objects); 068 } 069 070 /** 071 * Creates a command to change one tag of one object 072 * 073 * @param object the object to modify 074 * @param key the key of the tag to set 075 * @param value the value of the key to set 076 */ 077 public ChangePropertyCommand(OsmPrimitive object, String key, String value) { 078 this(Arrays.asList(object), key, value); 079 } 080 081 /** 082 * Initialize the instance by finding what objects will be modified 083 * 084 * @param objects the objects to (possibly) modify 085 */ 086 private void init(Collection<? extends OsmPrimitive> objects) { 087 // determine what objects will be modified 088 for (OsmPrimitive osm : objects) { 089 boolean modified = false; 090 091 // loop over all tags 092 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 093 String oldVal = osm.get(tag.getKey()); 094 String newVal = tag.getValue(); 095 096 if (newVal == null || newVal.isEmpty()) { 097 if (oldVal != null) 098 // new value is null and tag exists (will delete tag) 099 modified = true; 100 } else if (oldVal == null || !newVal.equals(oldVal)) 101 // new value is not null and is different from current value 102 modified = true; 103 } 104 if (modified) 105 this.objects.add(osm); 106 } 107 } 108 109 @Override 110 public boolean executeCommand() { 111 if (objects.isEmpty()) 112 return true; 113 final DataSet dataSet = objects.get(0).getDataSet(); 114 if (dataSet != null) { 115 dataSet.beginUpdate(); 116 } 117 try { 118 super.executeCommand(); // save old 119 120 for (OsmPrimitive osm : objects) { 121 // loop over all tags 122 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 123 String oldVal = osm.get(tag.getKey()); 124 String newVal = tag.getValue(); 125 126 if (newVal == null || newVal.isEmpty()) { 127 if (oldVal != null) 128 osm.remove(tag.getKey()); 129 } else if (oldVal == null || !newVal.equals(oldVal)) 130 osm.put(tag.getKey(), newVal); 131 } 132 // init() only keeps modified primitives. Therefore the modified 133 // bit can be set without further checks. 134 osm.setModified(true); 135 } 136 return true; 137 } finally { 138 if (dataSet != null) { 139 dataSet.endUpdate(); 140 } 141 } 142 } 143 144 @Override 145 public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 146 modified.addAll(objects); 147 } 148 149 @Override 150 public String getDescriptionText() { 151 @I18n.QuirkyPluralString 152 final String text; 153 if (objects.size() == 1 && tags.size() == 1) { 154 OsmPrimitive primitive = objects.get(0); 155 String msg = ""; 156 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 157 if (entry.getValue() == null) { 158 switch(OsmPrimitiveType.from(primitive)) { 159 case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break; 160 case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break; 161 case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break; 162 } 163 text = tr(msg, entry.getKey(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 164 } else { 165 switch(OsmPrimitiveType.from(primitive)) { 166 case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break; 167 case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break; 168 case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break; 169 } 170 text = tr(msg, entry.getKey(), entry.getValue(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 171 } 172 } else if (objects.size() > 1 && tags.size() == 1) { 173 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 174 if (entry.getValue() == null) { 175 /* I18n: plural form for objects, but value < 2 not possible! */ 176 text = trn("Remove \"{0}\" for {1} object", "Remove \"{0}\" for {1} objects", objects.size(), entry.getKey(), objects.size()); 177 } else { 178 /* I18n: plural form for objects, but value < 2 not possible! */ 179 text = trn("Set {0}={1} for {2} object", "Set {0}={1} for {2} objects", 180 objects.size(), entry.getKey(), entry.getValue(), objects.size()); 181 } 182 } else { 183 boolean allnull = true; 184 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 185 if (tag.getValue() != null) { 186 allnull = false; 187 break; 188 } 189 } 190 191 if (allnull) { 192 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 193 text = trn("Deleted {0} tags for {1} object", "Deleted {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 194 } else { 195 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 196 text = trn("Set {0} tags for {1} object", "Set {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 197 } 198 } 199 return text; 200 } 201 202 @Override 203 public Icon getDescriptionIcon() { 204 return ImageProvider.get("data", "key"); 205 } 206 207 @Override 208 public Collection<PseudoCommand> getChildren() { 209 if (objects.size() == 1) 210 return null; 211 List<PseudoCommand> children = new ArrayList<>(); 212 for (final OsmPrimitive osm : objects) { 213 children.add(new PseudoCommand() { 214 @Override public String getDescriptionText() { 215 return osm.getDisplayName(DefaultNameFormatter.getInstance()); 216 } 217 218 @Override public Icon getDescriptionIcon() { 219 return ImageProvider.get(osm.getDisplayType()); 220 } 221 222 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 223 return Collections.singleton(osm); 224 } 225 226 }); 227 } 228 return children; 229 } 230 231 /** 232 * Returns the number of objects that will effectively be modified, before the command is executed. 233 * @return the number of objects that will effectively be modified (can be 0) 234 * @see Command#getParticipatingPrimitives() 235 * @since 8945 236 */ 237 public final int getObjectsNumber() { 238 return objects.size(); 239 } 240 241 /** 242 * Returns the tags to set (key/value pairs). 243 * @return the tags to set (key/value pairs) 244 */ 245 public Map<String, String> getTags() { 246 return Collections.unmodifiableMap(tags); 247 } 248 249 @Override 250 public int hashCode() { 251 final int prime = 31; 252 int result = super.hashCode(); 253 result = prime * result + ((objects == null) ? 0 : objects.hashCode()); 254 result = prime * result + ((tags == null) ? 0 : tags.hashCode()); 255 return result; 256 } 257 258 @Override 259 public boolean equals(Object obj) { 260 if (this == obj) 261 return true; 262 if (!super.equals(obj)) 263 return false; 264 if (getClass() != obj.getClass()) 265 return false; 266 ChangePropertyCommand other = (ChangePropertyCommand) obj; 267 if (objects == null) { 268 if (other.objects != null) 269 return false; 270 } else if (!objects.equals(other.objects)) 271 return false; 272 if (tags == null) { 273 if (other.tags != null) 274 return false; 275 } else if (!tags.equals(other.tags)) 276 return false; 277 return true; 278 } 279}