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}