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