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