001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.util.ArrayList; 011import java.util.Collection; 012import java.util.EnumMap; 013import java.util.List; 014import java.util.Map; 015import java.util.Map.Entry; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.command.ChangePropertyCommand; 019import org.openstreetmap.josm.command.Command; 020import org.openstreetmap.josm.command.SequenceCommand; 021import org.openstreetmap.josm.data.osm.DataSet; 022import org.openstreetmap.josm.data.osm.OsmPrimitive; 023import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 024import org.openstreetmap.josm.data.osm.PrimitiveData; 025import org.openstreetmap.josm.data.osm.Tag; 026import org.openstreetmap.josm.data.osm.TagCollection; 027import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog; 028import org.openstreetmap.josm.tools.I18n; 029import org.openstreetmap.josm.tools.Shortcut; 030import org.openstreetmap.josm.tools.TextTagParser; 031import org.openstreetmap.josm.tools.Utils; 032 033/** 034 * Action, to paste all tags from one primitive to another. 035 * 036 * It will take the primitive from the copy-paste buffer an apply all its tags 037 * to the selected primitive(s). 038 * 039 * @author David Earl 040 */ 041public final class PasteTagsAction extends JosmAction { 042 043 private static final String help = ht("/Action/PasteTags"); 044 045 /** 046 * Constructs a new {@code PasteTagsAction}. 047 */ 048 public PasteTagsAction() { 049 super(tr("Paste Tags"), "pastetags", 050 tr("Apply tags of contents of paste buffer to all selected items."), 051 Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), 052 KeyEvent.VK_V, Shortcut.CTRL_SHIFT), true); 053 putValue("help", help); 054 } 055 056 public static class TagPaster { 057 058 private final Collection<PrimitiveData> source; 059 private final Collection<OsmPrimitive> target; 060 private final List<Tag> tags = new ArrayList<>(); 061 062 /** 063 * Constructs a new {@code TagPaster}. 064 * @param source source primitives 065 * @param target target primitives 066 */ 067 public TagPaster(Collection<PrimitiveData> source, Collection<OsmPrimitive> target) { 068 this.source = source; 069 this.target = target; 070 } 071 072 /** 073 * Determines if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of 074 * {@link OsmPrimitive}s of exactly one type 075 * @return true if the source for tag pasting is heterogeneous 076 */ 077 protected boolean isHeterogeneousSource() { 078 int count = 0; 079 count = !getSourcePrimitivesByType(OsmPrimitiveType.NODE).isEmpty() ? (count + 1) : count; 080 count = !getSourcePrimitivesByType(OsmPrimitiveType.WAY).isEmpty() ? (count + 1) : count; 081 count = !getSourcePrimitivesByType(OsmPrimitiveType.RELATION).isEmpty() ? (count + 1) : count; 082 return count > 1; 083 } 084 085 /** 086 * Replies all primitives of type <code>type</code> in the current selection. 087 * 088 * @param type the type 089 * @return all primitives of type <code>type</code> in the current selection. 090 */ 091 protected Collection<? extends PrimitiveData> getSourcePrimitivesByType(OsmPrimitiveType type) { 092 return PrimitiveData.getFilteredList(source, type); 093 } 094 095 /** 096 * Replies the collection of tags for all primitives of type <code>type</code> in the current 097 * selection 098 * 099 * @param type the type 100 * @return the collection of tags for all primitives of type <code>type</code> in the current 101 * selection 102 */ 103 protected TagCollection getSourceTagsByType(OsmPrimitiveType type) { 104 return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type)); 105 } 106 107 /** 108 * Replies true if there is at least one tag in the current selection for primitives of 109 * type <code>type</code> 110 * 111 * @param type the type 112 * @return true if there is at least one tag in the current selection for primitives of 113 * type <code>type</code> 114 */ 115 protected boolean hasSourceTagsByType(OsmPrimitiveType type) { 116 return !getSourceTagsByType(type).isEmpty(); 117 } 118 119 protected void buildTags(TagCollection tc) { 120 for (String key : tc.getKeys()) { 121 tags.add(new Tag(key, tc.getValues(key).iterator().next())); 122 } 123 } 124 125 protected Map<OsmPrimitiveType, Integer> getSourceStatistics() { 126 Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class); 127 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 128 if (!getSourceTagsByType(type).isEmpty()) { 129 ret.put(type, getSourcePrimitivesByType(type).size()); 130 } 131 } 132 return ret; 133 } 134 135 protected Map<OsmPrimitiveType, Integer> getTargetStatistics() { 136 Map<OsmPrimitiveType, Integer> ret = new EnumMap<>(OsmPrimitiveType.class); 137 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 138 int count = OsmPrimitive.getFilteredList(target, type.getOsmClass()).size(); 139 if (count > 0) { 140 ret.put(type, count); 141 } 142 } 143 return ret; 144 } 145 146 /** 147 * Pastes the tags from a homogeneous source (the {@link Main#pasteBuffer}s selection consisting 148 * of one type of {@link OsmPrimitive}s only). 149 * 150 * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives, 151 * regardless of their type, receive the same tags. 152 */ 153 protected void pasteFromHomogeneousSource() { 154 TagCollection tc = null; 155 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 156 TagCollection tc1 = getSourceTagsByType(type); 157 if (!tc1.isEmpty()) { 158 tc = tc1; 159 } 160 } 161 if (tc == null) 162 // no tags found to paste. Abort. 163 return; 164 165 if (!tc.isApplicableToPrimitive()) { 166 PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent); 167 dialog.populate(tc, getSourceStatistics(), getTargetStatistics()); 168 dialog.setVisible(true); 169 if (dialog.isCanceled()) 170 return; 171 buildTags(dialog.getResolution()); 172 } else { 173 // no conflicts in the source tags to resolve. Just apply the tags 174 // to the target primitives 175 // 176 buildTags(tc); 177 } 178 } 179 180 /** 181 * Replies true if there is at least one primitive of type <code>type</code> 182 * is in the target collection 183 * 184 * @param type the type to look for 185 * @return true if there is at least one primitive of type <code>type</code> in the collection 186 * <code>selection</code> 187 */ 188 protected boolean hasTargetPrimitives(Class<? extends OsmPrimitive> type) { 189 return !OsmPrimitive.getFilteredList(target, type).isEmpty(); 190 } 191 192 /** 193 * Replies true if this a heterogeneous source can be pasted without conflict to targets 194 * 195 * @return true if this a heterogeneous source can be pasted without conflicts to targets 196 */ 197 protected boolean canPasteFromHeterogeneousSourceWithoutConflict() { 198 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 199 if (hasTargetPrimitives(type.getOsmClass())) { 200 TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type)); 201 if (!tc.isEmpty() && !tc.isApplicableToPrimitive()) 202 return false; 203 } 204 } 205 return true; 206 } 207 208 /** 209 * Pastes the tags in the current selection of the paste buffer to a set of target primitives. 210 */ 211 protected void pasteFromHeterogeneousSource() { 212 if (canPasteFromHeterogeneousSourceWithoutConflict()) { 213 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 214 if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) { 215 buildTags(getSourceTagsByType(type)); 216 } 217 } 218 } else { 219 PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent); 220 dialog.populate( 221 getSourceTagsByType(OsmPrimitiveType.NODE), 222 getSourceTagsByType(OsmPrimitiveType.WAY), 223 getSourceTagsByType(OsmPrimitiveType.RELATION), 224 getSourceStatistics(), 225 getTargetStatistics() 226 ); 227 dialog.setVisible(true); 228 if (dialog.isCanceled()) 229 return; 230 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 231 if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) { 232 buildTags(dialog.getResolution(type)); 233 } 234 } 235 } 236 } 237 238 /** 239 * Performs the paste operation. 240 * @return list of tags 241 */ 242 public List<Tag> execute() { 243 tags.clear(); 244 if (isHeterogeneousSource()) { 245 pasteFromHeterogeneousSource(); 246 } else { 247 pasteFromHomogeneousSource(); 248 } 249 return tags; 250 } 251 252 } 253 254 @Override 255 public void actionPerformed(ActionEvent e) { 256 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected(); 257 258 if (selection.isEmpty()) 259 return; 260 261 String buf = Utils.getClipboardContent(); 262 if (buf == null || buf.isEmpty() || buf.matches(CopyAction.CLIPBOARD_REGEXP)) { 263 pasteTagsFromJOSMBuffer(selection); 264 } else { 265 // Paste tags from arbitrary text 266 pasteTagsFromText(selection, buf); 267 } 268 } 269 270 /** 271 * Paste tags from arbitrary text, not using JOSM buffer 272 * @param selection selected primitives 273 * @param text text containing tags 274 * @return true if action was successful 275 * @see TextTagParser#readTagsFromText 276 */ 277 public static boolean pasteTagsFromText(Collection<OsmPrimitive> selection, String text) { 278 Map<String, String> tags = TextTagParser.readTagsFromText(text); 279 if (tags == null || tags.isEmpty()) { 280 TextTagParser.showBadBufferMessage(help); 281 return false; 282 } 283 if (!TextTagParser.validateTags(tags)) return false; 284 285 List<Command> commands = new ArrayList<>(tags.size()); 286 for (Entry<String, String> entry: tags.entrySet()) { 287 String v = entry.getValue(); 288 commands.add(new ChangePropertyCommand(selection, entry.getKey(), "".equals(v) ? null : v)); 289 } 290 commitCommands(selection, commands); 291 return !commands.isEmpty(); 292 } 293 294 /** 295 * Paste tags from JOSM buffer 296 * @param selection objects that will have the tags 297 * @return false if JOSM buffer was empty 298 */ 299 public static boolean pasteTagsFromJOSMBuffer(Collection<OsmPrimitive> selection) { 300 List<PrimitiveData> directlyAdded = Main.pasteBuffer.getDirectlyAdded(); 301 if (directlyAdded == null || directlyAdded.isEmpty()) return false; 302 303 PasteTagsAction.TagPaster tagPaster = new PasteTagsAction.TagPaster(directlyAdded, selection); 304 List<Command> commands = new ArrayList<>(); 305 for (Tag tag : tagPaster.execute()) { 306 commands.add(new ChangePropertyCommand(selection, tag.getKey(), "".equals(tag.getValue()) ? null : tag.getValue())); 307 } 308 commitCommands(selection, commands); 309 return true; 310 } 311 312 /** 313 * Create and execute SequenceCommand with descriptive title 314 * @param selection selected primitives 315 * @param commands the commands to perform in a sequential command 316 */ 317 private static void commitCommands(Collection<OsmPrimitive> selection, List<Command> commands) { 318 if (!commands.isEmpty()) { 319 String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size()); 320 String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size()); 321 @I18n.QuirkyPluralString 322 final String title = title1 + ' ' + title2; 323 Main.main.undoRedo.add( 324 new SequenceCommand( 325 title, 326 commands 327 )); 328 } 329 } 330 331 @Override 332 protected void updateEnabledState() { 333 DataSet ds = getLayerManager().getEditDataSet(); 334 if (ds == null) { 335 setEnabled(false); 336 return; 337 } 338 // buffer listening slows down the program and is not very good for arbitrary text in buffer 339 setEnabled(!ds.selectionEmpty()); 340 } 341 342 @Override 343 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 344 setEnabled(selection != null && !selection.isEmpty()); 345 } 346}