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