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