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}