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 || entry.getValue().isEmpty()) {
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 || entry.getValue().isEmpty()) {
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 && !tag.getValue().isEmpty()) {
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        return children;
231    }
232
233    /**
234     * Returns the number of objects that will effectively be modified, before the command is executed.
235     * @return the number of objects that will effectively be modified (can be 0)
236     * @see Command#getParticipatingPrimitives()
237     * @since 8945
238     */
239    public final int getObjectsNumber() {
240        return objects.size();
241    }
242
243    /**
244     * Returns the tags to set (key/value pairs).
245     * @return the tags to set (key/value pairs)
246     */
247    public Map<String, String> getTags() {
248        return Collections.unmodifiableMap(tags);
249    }
250
251    @Override
252    public int hashCode() {
253        return Objects.hash(super.hashCode(), objects, tags);
254    }
255
256    @Override
257    public boolean equals(Object obj) {
258        if (this == obj) return true;
259        if (obj == null || getClass() != obj.getClass()) return false;
260        if (!super.equals(obj)) return false;
261        ChangePropertyCommand that = (ChangePropertyCommand) obj;
262        return Objects.equals(objects, that.objects) &&
263                Objects.equals(tags, that.tags);
264    }
265}