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