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