001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.actions;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.event.ActionEvent;
009import java.awt.event.KeyEvent;
010import java.util.ArrayList;
011import java.util.Collection;
012import java.util.HashMap;
013import java.util.List;
014import java.util.Map;
015import java.util.Map.Entry;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.command.ChangePropertyCommand;
019import org.openstreetmap.josm.command.Command;
020import org.openstreetmap.josm.command.SequenceCommand;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
023import org.openstreetmap.josm.data.osm.PrimitiveData;
024import org.openstreetmap.josm.data.osm.Tag;
025import org.openstreetmap.josm.data.osm.TagCollection;
026import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog;
027import org.openstreetmap.josm.tools.Shortcut;
028import org.openstreetmap.josm.tools.TextTagParser;
029import org.openstreetmap.josm.tools.Utils;
030
031/**
032 * Action, to paste all tags from one primitive to another.
033 *
034 * It will take the primitive from the copy-paste buffer an apply all its tags
035 * to the selected primitive(s).
036 *
037 * @author David Earl
038 */
039public final class PasteTagsAction extends JosmAction {
040
041    private static final String help = ht("/Action/PasteTags");
042
043    /**
044     * Constructs a new {@code PasteTagsAction}.
045     */
046    public PasteTagsAction() {
047        super(tr("Paste Tags"), "pastetags",
048                tr("Apply tags of contents of paste buffer to all selected items."),
049                Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")),
050                KeyEvent.VK_V, Shortcut.CTRL_SHIFT), true);
051        putValue("help", help);
052    }
053
054    public static class TagPaster {
055
056        private final Collection<PrimitiveData> source;
057        private final Collection<OsmPrimitive> target;
058        private final List<Tag> commands = new ArrayList<Tag>();
059
060        public TagPaster(Collection<PrimitiveData> source, Collection<OsmPrimitive> target) {
061            this.source = source;
062            this.target = target;
063        }
064
065        /**
066         * Replies true if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of
067         * {@link OsmPrimitive}s of exactly one type
068         */
069        protected boolean isHeteogeneousSource() {
070            int count = 0;
071            count = !getSourcePrimitivesByType(OsmPrimitiveType.NODE).isEmpty() ? count + 1 : count;
072            count = !getSourcePrimitivesByType(OsmPrimitiveType.WAY).isEmpty() ? count + 1 : count;
073            count = !getSourcePrimitivesByType(OsmPrimitiveType.RELATION).isEmpty() ? count + 1 : count;
074            return count > 1;
075        }
076
077        /**
078         * Replies all primitives of type <code>type</code> in the current selection.
079         *
080         * @param <T>
081         * @param type  the type
082         * @return all primitives of type <code>type</code> in the current selection.
083         */
084        protected <T extends PrimitiveData> Collection<? extends PrimitiveData> getSourcePrimitivesByType(OsmPrimitiveType type) {
085            return PrimitiveData.getFilteredList(source, type);
086        }
087
088        /**
089         * Replies the collection of tags for all primitives of type <code>type</code> in the current
090         * selection
091         *
092         * @param <T>
093         * @param type  the type
094         * @return the collection of tags for all primitives of type <code>type</code> in the current
095         * selection
096         */
097        protected <T extends OsmPrimitive> TagCollection getSourceTagsByType(OsmPrimitiveType type) {
098            return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
099        }
100
101        /**
102         * Replies true if there is at least one tag in the current selection for primitives of
103         * type <code>type</code>
104         *
105         * @param <T>
106         * @param type the type
107         * @return true if there is at least one tag in the current selection for primitives of
108         * type <code>type</code>
109         */
110        protected <T extends OsmPrimitive> boolean hasSourceTagsByType(OsmPrimitiveType type) {
111            return ! getSourceTagsByType(type).isEmpty();
112        }
113
114        protected void buildChangeCommand(Collection<? extends OsmPrimitive> selection, TagCollection tc) {
115            for (String key : tc.getKeys()) {
116                commands.add(new Tag(key, tc.getValues(key).iterator().next()));
117            }
118        }
119
120        protected Map<OsmPrimitiveType, Integer> getSourceStatistics() {
121            HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>();
122            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
123                if (!getSourceTagsByType(type).isEmpty()) {
124                    ret.put(type, getSourcePrimitivesByType(type).size());
125                }
126            }
127            return ret;
128        }
129
130        protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
131            HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>();
132            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
133                int count = OsmPrimitive.getFilteredList(target, type.getOsmClass()).size();
134                if (count > 0) {
135                    ret.put(type, count);
136                }
137            }
138            return ret;
139        }
140
141        /**
142         * Pastes the tags from a homogeneous source (the {@link Main#pasteBuffer}s selection consisting
143         * of one type of {@link OsmPrimitive}s only).
144         *
145         * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
146         * regardless of their type, receive the same tags.
147         */
148        protected void pasteFromHomogeneousSource() {
149            TagCollection tc = null;
150            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
151                TagCollection tc1 = getSourceTagsByType(type);
152                if (!tc1.isEmpty()) {
153                    tc = tc1;
154                }
155            }
156            if (tc == null)
157                // no tags found to paste. Abort.
158                return;
159
160            if (!tc.isApplicableToPrimitive()) {
161                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
162                dialog.populate(tc, getSourceStatistics(), getTargetStatistics());
163                dialog.setVisible(true);
164                if (dialog.isCanceled())
165                    return;
166                buildChangeCommand(target, dialog.getResolution());
167            } else {
168                // no conflicts in the source tags to resolve. Just apply the tags
169                // to the target primitives
170                //
171                buildChangeCommand(target, tc);
172            }
173        }
174
175        /**
176         * Replies true if there is at least one primitive of type <code>type</code>
177         * is in the target collection
178         *
179         * @param <T>
180         * @param type  the type to look for
181         * @return true if there is at least one primitive of type <code>type</code> in the collection
182         * <code>selection</code>
183         */
184        protected <T extends OsmPrimitive> boolean hasTargetPrimitives(Class<T> type) {
185            return !OsmPrimitive.getFilteredList(target, type).isEmpty();
186        }
187
188        /**
189         * Replies true if this a heterogeneous source can be pasted without conflict to targets
190         *
191         * @param targets the collection of target primitives
192         * @return true if this a heterogeneous source can be pasted without conflicts to targets
193         */
194        protected boolean canPasteFromHeterogeneousSourceWithoutConflict(Collection<OsmPrimitive> targets) {
195            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
196                if (hasTargetPrimitives(type.getOsmClass())) {
197                    TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type));
198                    if (!tc.isEmpty() && ! tc.isApplicableToPrimitive())
199                        return false;
200                }
201            }
202            return true;
203        }
204
205        /**
206         * Pastes the tags in the current selection of the paste buffer to a set of target
207         * primitives.
208         */
209        protected void pasteFromHeterogeneousSource() {
210            if (canPasteFromHeterogeneousSourceWithoutConflict(target)) {
211                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
212                    if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
213                        buildChangeCommand(target, getSourceTagsByType(type));
214                    }
215                }
216            } else {
217                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent);
218                dialog.populate(
219                        getSourceTagsByType(OsmPrimitiveType.NODE),
220                        getSourceTagsByType(OsmPrimitiveType.WAY),
221                        getSourceTagsByType(OsmPrimitiveType.RELATION),
222                        getSourceStatistics(),
223                        getTargetStatistics()
224                );
225                dialog.setVisible(true);
226                if (dialog.isCanceled())
227                    return;
228                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
229                    if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) {
230                        buildChangeCommand(OsmPrimitive.getFilteredList(target, type.getOsmClass()), dialog.getResolution(type));
231                    }
232                }
233            }
234        }
235
236        public List<Tag> execute() {
237            commands.clear();
238            if (isHeteogeneousSource()) {
239                pasteFromHeterogeneousSource();
240            } else {
241                pasteFromHomogeneousSource();
242            }
243            return commands;
244        }
245
246    }
247
248    @Override
249    public void actionPerformed(ActionEvent e) {
250        Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
251
252        if (selection.isEmpty())
253            return;
254
255        String buf = Utils.getClipboardContent();
256        if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) {
257            pasteTagsFromJOSMBuffer(selection);
258        } else {
259            // Paste tags from arbitrary text
260            pasteTagsFromText(selection, buf);
261        }
262    }
263
264    /** Paste tags from arbitrary text, not using JOSM buffer
265     * @return true if action was successful
266     */
267    public static boolean pasteTagsFromText(Collection<OsmPrimitive> selection, String text) {
268        Map<String, String> tags = TextTagParser.readTagsFromText(text);
269        if (tags==null || tags.isEmpty()) {
270            TextTagParser.showBadBufferMessage(help);
271            return false;
272        }
273        if (!TextTagParser.validateTags(tags)) return false;
274
275        List<Command> commands = new ArrayList<Command>(tags.size());
276        for (Entry<String, String> entry: tags.entrySet()) {
277            String v = entry.getValue();
278            commands.add(new ChangePropertyCommand(selection, entry.getKey(), "".equals(v)?null:v));
279        }
280        commitCommands(selection, commands);
281        return !commands.isEmpty();
282    }
283
284    /** Paste tags from JOSM buffer
285     * @param selection objects that will have the tags
286     * @return false if JOSM buffer was empty
287     */
288    public static boolean pasteTagsFromJOSMBuffer(Collection<OsmPrimitive> selection) {
289        List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded();
290        if (directlyAdded==null || directlyAdded.isEmpty()) return false;
291
292        PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, selection);
293        List<Command> commands = new ArrayList<Command>();
294        for (Tag tag : tagPaster.execute()) {
295            commands.add(new ChangePropertyCommand(selection, tag.getKey(), "".equals(tag.getValue()) ? null : tag.getValue()));
296        }
297        commitCommands(selection, commands);
298        return true;
299    }
300
301    /**
302     * Create and execute SequenceCommand with descriptive title
303     * @param commands
304     */
305    private static void commitCommands(Collection<OsmPrimitive> selection, List<Command> commands) {
306        if (!commands.isEmpty()) {
307            String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size());
308            String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size());
309            Main.main.undoRedo.add(
310                    new SequenceCommand(
311                            title1 + " " + title2,
312                            commands
313                    ));
314        }
315    }
316
317    @Override
318    protected void updateEnabledState() {
319        if (getCurrentDataSet() == null) {
320            setEnabled(false);
321            return;
322        }
323        // buffer listening slows down the program and is not very good for arbitrary text in buffer
324        setEnabled(!getCurrentDataSet().getSelected().isEmpty());
325    }
326
327    @Override
328    protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
329        setEnabled(selection!= null && !selection.isEmpty());
330    }
331}