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