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}