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