001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.gui.datatransfer.importers;
003
004import java.awt.datatransfer.UnsupportedFlavorException;
005import java.io.IOException;
006import java.util.ArrayList;
007import java.util.Arrays;
008import java.util.Collection;
009import java.util.EnumMap;
010import java.util.HashMap;
011import java.util.List;
012import java.util.Map;
013
014import javax.swing.TransferHandler.TransferSupport;
015
016import org.openstreetmap.josm.command.ChangePropertyCommand;
017import org.openstreetmap.josm.command.Command;
018import org.openstreetmap.josm.data.osm.IPrimitive;
019import org.openstreetmap.josm.data.osm.Node;
020import org.openstreetmap.josm.data.osm.OsmDataManager;
021import org.openstreetmap.josm.data.osm.OsmPrimitive;
022import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
023import org.openstreetmap.josm.data.osm.Tag;
024import org.openstreetmap.josm.data.osm.TagCollection;
025import org.openstreetmap.josm.data.osm.TagMap;
026import org.openstreetmap.josm.gui.MainApplication;
027import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog;
028import org.openstreetmap.josm.gui.datatransfer.data.PrimitiveTagTransferData;
029
030/**
031 * This class helps pasting tags from other primitives. It handles resolving conflicts.
032 * @author Michael Zangl
033 * @since 10737
034 */
035public class PrimitiveTagTransferPaster extends AbstractTagPaster {
036    /**
037     * Create a new {@link PrimitiveTagTransferPaster}
038     */
039    public PrimitiveTagTransferPaster() {
040        super(PrimitiveTagTransferData.FLAVOR);
041    }
042
043    @Override
044    public boolean importTagsOn(TransferSupport support, Collection<? extends OsmPrimitive> selection)
045            throws UnsupportedFlavorException, IOException {
046        Object o = support.getTransferable().getTransferData(df);
047        if (!(o instanceof PrimitiveTagTransferData))
048            return false;
049        PrimitiveTagTransferData data = (PrimitiveTagTransferData) o;
050
051        TagPasteSupport tagPaster = new TagPasteSupport(data, selection);
052        List<Command> commands = new ArrayList<>();
053        for (Tag tag : tagPaster.execute()) {
054            Map<String, String> tags = new HashMap<>(1);
055            tags.put(tag.getKey(), "".equals(tag.getValue()) ? null : tag.getValue());
056            ChangePropertyCommand cmd = new ChangePropertyCommand(OsmDataManager.getInstance().getEditDataSet(), selection, tags);
057            if (cmd.getObjectsNumber() > 0) {
058                commands.add(cmd);
059            }
060        }
061        commitCommands(selection, commands);
062        return true;
063    }
064
065    @Override
066    protected Map<String, String> getTags(TransferSupport support) throws UnsupportedFlavorException, IOException {
067        PrimitiveTagTransferData data = (PrimitiveTagTransferData) support.getTransferable().getTransferData(df);
068
069        TagPasteSupport tagPaster = new TagPasteSupport(data, Arrays.asList(new Node()));
070        return new TagMap(tagPaster.execute());
071    }
072
073    private static class TagPasteSupport {
074        private final PrimitiveTagTransferData data;
075        private final Collection<? extends IPrimitive> selection;
076        private final List<Tag> tags = new ArrayList<>();
077
078        /**
079         * Constructs a new {@code TagPasteSupport}.
080         * @param data source tags to paste
081         * @param selection target primitives
082         */
083        TagPasteSupport(PrimitiveTagTransferData data, Collection<? extends IPrimitive> selection) {
084            super();
085            this.data = data;
086            this.selection = selection;
087        }
088
089        /**
090         * Pastes the tags from a homogeneous source (the selection consisting
091         * of one type of {@link OsmPrimitive}s only).
092         *
093         * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives,
094         * regardless of their type, receive the same tags.
095         */
096        protected void pasteFromHomogeneousSource() {
097            TagCollection tc = null;
098            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
099                TagCollection tc1 = data.getForPrimitives(type);
100                if (!tc1.isEmpty()) {
101                    tc = tc1;
102                }
103            }
104            if (tc == null)
105                // no tags found to paste. Abort.
106                return;
107
108            if (!tc.isApplicableToPrimitive()) {
109                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(MainApplication.getMainFrame());
110                dialog.populate(tc, data.getStatistics(), getTargetStatistics());
111                dialog.setVisible(true);
112                if (dialog.isCanceled())
113                    return;
114                buildTags(dialog.getResolution());
115            } else {
116                // no conflicts in the source tags to resolve. Just apply the tags to the target primitives
117                buildTags(tc);
118            }
119        }
120
121        /**
122         * Replies true if this a heterogeneous source can be pasted without conflict to targets
123         *
124         * @return true if this a heterogeneous source can be pasted without conflicts to targets
125         */
126        protected boolean canPasteFromHeterogeneousSourceWithoutConflict() {
127            for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
128                if (hasTargetPrimitives(type)) {
129                    TagCollection tc = data.getForPrimitives(type);
130                    if (!tc.isEmpty() && !tc.isApplicableToPrimitive())
131                        return false;
132                }
133            }
134            return true;
135        }
136
137        /**
138         * Pastes the tags in the current selection of the paste buffer to a set of target primitives.
139         */
140        protected void pasteFromHeterogeneousSource() {
141            if (canPasteFromHeterogeneousSourceWithoutConflict()) {
142                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
143                    if (!data.getForPrimitives(type).isEmpty() && hasTargetPrimitives(type)) {
144                        buildTags(data.getForPrimitives(type));
145                    }
146                }
147            } else {
148                PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(MainApplication.getMainFrame());
149                dialog.populate(
150                        data.getForPrimitives(OsmPrimitiveType.NODE),
151                        data.getForPrimitives(OsmPrimitiveType.WAY),
152                        data.getForPrimitives(OsmPrimitiveType.RELATION),
153                        data.getStatistics(),
154                        getTargetStatistics()
155                );
156                dialog.setVisible(true);
157                if (dialog.isCanceled())
158                    return;
159                for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) {
160                    if (!data.getForPrimitives(type).isEmpty() && hasTargetPrimitives(type)) {
161                        buildTags(dialog.getResolution(type));
162                    }
163                }
164            }
165        }
166
167        protected Map<OsmPrimitiveType, Integer> getTargetStatistics() {
168            Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class);
169            for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) {
170                int count = (int) selection.stream().filter(p -> type == p.getType()).count();
171                if (count > 0) {
172                    ret.put(type, count);
173                }
174            }
175            return ret;
176        }
177
178        /**
179         * Replies true if there is at least one primitive of type <code>type</code>
180         * is in the target collection
181         *
182         * @param type  the type to look for
183         * @return true if there is at least one primitive of type <code>type</code> in the collection
184         * <code>selection</code>
185         */
186        protected boolean hasTargetPrimitives(OsmPrimitiveType type) {
187            return selection.stream().anyMatch(p -> type == p.getType());
188        }
189
190        protected void buildTags(TagCollection tc) {
191            for (String key : tc.getKeys()) {
192                tags.add(new Tag(key, tc.getValues(key).iterator().next()));
193            }
194        }
195
196        /**
197         * Performs the paste operation.
198         * @return list of tags
199         */
200        public List<Tag> execute() {
201            tags.clear();
202            if (data.isHeterogeneousSource()) {
203                pasteFromHeterogeneousSource();
204            } else {
205                pasteFromHomogeneousSource();
206            }
207            return tags;
208        }
209
210        @Override
211        public String toString() {
212            return "PasteSupport [data=" + data + ", selection=" + selection + ']';
213        }
214    }
215}