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