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