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