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.AbstractMap; 009import java.util.ArrayList; 010import java.util.Arrays; 011import java.util.Collection; 012import java.util.Collections; 013import java.util.HashMap; 014import java.util.LinkedList; 015import java.util.List; 016import java.util.Map; 017 018import javax.swing.Icon; 019 020import org.openstreetmap.josm.Main; 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.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 */ 032public class ChangePropertyCommand extends Command { 033 /** 034 * All primitives that are affected with this command. 035 */ 036 private final List<OsmPrimitive> objects; 037 /** 038 * Key and value pairs. If value is <code>null</code>, delete all key references with the given 039 * key. Otherwise, change the tags of all objects to the given value or create keys of 040 * those objects that do not have the key yet. 041 */ 042 private final AbstractMap<String, String> tags; 043 044 /** 045 * Creates a command to change multiple tags of multiple objects 046 * 047 * @param objects the objects to modify 048 * @param tags the tags to set 049 */ 050 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, AbstractMap<String, String> tags) { 051 this.objects = new LinkedList<>(); 052 this.tags = tags; 053 init(objects); 054 } 055 056 /** 057 * Creates a command to change one tag of multiple objects 058 * 059 * @param objects the objects to modify 060 * @param key the key of the tag to set 061 * @param value the value of the key to set 062 */ 063 public ChangePropertyCommand(Collection<? extends OsmPrimitive> objects, String key, String value) { 064 this.objects = new LinkedList<>(); 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 } 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 public boolean executeCommand() { 111 Main.main.getCurrentDataSet().beginUpdate(); 112 try { 113 super.executeCommand(); // save old 114 115 for (OsmPrimitive osm : objects) { 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 osm.remove(tag.getKey()); 124 } 125 else if (oldVal == null || !newVal.equals(oldVal)) 126 osm.put(tag.getKey(), newVal); 127 } 128 // init() only keeps modified primitives. Therefore the modified 129 // bit can be set without further checks. 130 osm.setModified(true); 131 } 132 return true; 133 } 134 finally { 135 Main.main.getCurrentDataSet().endUpdate(); 136 } 137 } 138 139 @Override public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) { 140 modified.addAll(objects); 141 } 142 143 @Override 144 public String getDescriptionText() { 145 String text; 146 if (objects.size() == 1 && tags.size() == 1) { 147 OsmPrimitive primitive = objects.iterator().next(); 148 String msg = ""; 149 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 150 if (entry.getValue() == null) { 151 switch(OsmPrimitiveType.from(primitive)) { 152 case NODE: msg = marktr("Remove \"{0}\" for node ''{1}''"); break; 153 case WAY: msg = marktr("Remove \"{0}\" for way ''{1}''"); break; 154 case RELATION: msg = marktr("Remove \"{0}\" for relation ''{1}''"); break; 155 } 156 text = tr(msg, entry.getKey(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 157 } else { 158 switch(OsmPrimitiveType.from(primitive)) { 159 case NODE: msg = marktr("Set {0}={1} for node ''{2}''"); break; 160 case WAY: msg = marktr("Set {0}={1} for way ''{2}''"); break; 161 case RELATION: msg = marktr("Set {0}={1} for relation ''{2}''"); break; 162 } 163 text = tr(msg, entry.getKey(), entry.getValue(), primitive.getDisplayName(DefaultNameFormatter.getInstance())); 164 } 165 } else if (objects.size() > 1 && tags.size() == 1) { 166 Map.Entry<String, String> entry = tags.entrySet().iterator().next(); 167 if (entry.getValue() == null) { 168 /* I18n: plural form for objects, but value < 2 not possible! */ 169 text = trn("Remove \"{0}\" for {1} object", "Remove \"{0}\" for {1} objects", objects.size(), entry.getKey(), objects.size()); 170 } else { 171 /* I18n: plural form for objects, but value < 2 not possible! */ 172 text = trn("Set {0}={1} for {2} object", "Set {0}={1} for {2} objects", objects.size(), entry.getKey(), entry.getValue(), objects.size()); 173 } 174 } 175 else { 176 boolean allnull = true; 177 for (Map.Entry<String, String> tag : this.tags.entrySet()) { 178 if (tag.getValue() != null) { 179 allnull = false; 180 break; 181 } 182 } 183 184 if (allnull) { 185 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 186 text = trn("Deleted {0} tags for {1} object", "Deleted {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 187 } else { 188 /* I18n: plural form detected for objects only (but value < 2 not possible!), try to do your best for tags */ 189 text = trn("Set {0} tags for {1} object", "Set {0} tags for {1} objects", objects.size(), tags.size(), objects.size()); 190 } 191 } 192 return text; 193 } 194 195 @Override 196 public Icon getDescriptionIcon() { 197 return ImageProvider.get("data", "key"); 198 } 199 200 @Override public Collection<PseudoCommand> getChildren() { 201 if (objects.size() == 1) 202 return null; 203 List<PseudoCommand> children = new ArrayList<>(); 204 for (final OsmPrimitive osm : objects) { 205 children.add(new PseudoCommand() { 206 @Override public String getDescriptionText() { 207 return osm.getDisplayName(DefaultNameFormatter.getInstance()); 208 } 209 210 @Override public Icon getDescriptionIcon() { 211 return ImageProvider.get(osm.getDisplayType()); 212 } 213 214 @Override public Collection<? extends OsmPrimitive> getParticipatingPrimitives() { 215 return Collections.singleton(osm); 216 } 217 218 }); 219 } 220 return children; 221 } 222 223 /** 224 * Returns the tags to set (key/value pairs). 225 * @return the tags to set (key/value pairs) 226 */ 227 public Map<String, String> getTags() { 228 return Collections.unmodifiableMap(tags); 229 } 230}