001 // License: GPL. Copyright 2007 by Immanuel Scholz and others 002 package org.openstreetmap.josm.actions; 003 004 import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005 import static org.openstreetmap.josm.tools.I18n.tr; 006 import static org.openstreetmap.josm.tools.I18n.trn; 007 008 import java.awt.event.ActionEvent; 009 import java.awt.event.KeyEvent; 010 import java.util.ArrayList; 011 import java.util.Collection; 012 import java.util.HashMap; 013 import java.util.List; 014 import java.util.Map; 015 016 import org.openstreetmap.josm.Main; 017 import org.openstreetmap.josm.command.ChangePropertyCommand; 018 import org.openstreetmap.josm.command.Command; 019 import org.openstreetmap.josm.command.SequenceCommand; 020 import org.openstreetmap.josm.data.osm.OsmPrimitive; 021 import org.openstreetmap.josm.data.osm.OsmPrimitiveType; 022 import org.openstreetmap.josm.data.osm.PrimitiveData; 023 import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy; 024 import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy.PasteBufferChangedListener; 025 import org.openstreetmap.josm.data.osm.Tag; 026 import org.openstreetmap.josm.data.osm.TagCollection; 027 import org.openstreetmap.josm.gui.conflict.tags.PasteTagsConflictResolverDialog; 028 import org.openstreetmap.josm.tools.Shortcut; 029 030 /** 031 * Action, to paste all tags from one primitive to another. 032 * 033 * It will take the primitive from the copy-paste buffer an apply all its tags 034 * to the selected primitive(s). 035 * 036 * @author David Earl 037 */ 038 public final class PasteTagsAction extends JosmAction implements PasteBufferChangedListener { 039 040 public PasteTagsAction() { 041 super(tr("Paste Tags"), "pastetags", 042 tr("Apply tags of contents of paste buffer to all selected items."), 043 Shortcut.registerShortcut("system:pastestyle", tr("Edit: {0}", tr("Paste Tags")), 044 KeyEvent.VK_V, Shortcut.CTRL_SHIFT), true); 045 Main.pasteBuffer.addPasteBufferChangedListener(this); 046 putValue("help", ht("/Action/PasteTags")); 047 } 048 049 public static class TagPaster { 050 051 private final Collection<PrimitiveData> source; 052 private final Collection<OsmPrimitive> target; 053 private final List<Tag> commands = new ArrayList<Tag>(); 054 055 public TagPaster(Collection<PrimitiveData> source, Collection<OsmPrimitive> target) { 056 this.source = source; 057 this.target = target; 058 } 059 060 /** 061 * Replies true if the source for tag pasting is heterogeneous, i.e. if it doesn't consist of 062 * {@link OsmPrimitive}s of exactly one type 063 */ 064 protected boolean isHeteogeneousSource() { 065 int count = 0; 066 count = !getSourcePrimitivesByType(OsmPrimitiveType.NODE).isEmpty() ? count + 1 : count; 067 count = !getSourcePrimitivesByType(OsmPrimitiveType.WAY).isEmpty() ? count + 1 : count; 068 count = !getSourcePrimitivesByType(OsmPrimitiveType.RELATION).isEmpty() ? count + 1 : count; 069 return count > 1; 070 } 071 072 /** 073 * Replies all primitives of type <code>type</code> in the current selection. 074 * 075 * @param <T> 076 * @param type the type 077 * @return all primitives of type <code>type</code> in the current selection. 078 */ 079 protected <T extends PrimitiveData> Collection<? extends PrimitiveData> getSourcePrimitivesByType(OsmPrimitiveType type) { 080 return PrimitiveData.getFilteredList(source, type); 081 } 082 083 /** 084 * Replies the collection of tags for all primitives of type <code>type</code> in the current 085 * selection 086 * 087 * @param <T> 088 * @param type the type 089 * @return the collection of tags for all primitives of type <code>type</code> in the current 090 * selection 091 */ 092 protected <T extends OsmPrimitive> TagCollection getSourceTagsByType(OsmPrimitiveType type) { 093 return TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type)); 094 } 095 096 /** 097 * Replies true if there is at least one tag in the current selection for primitives of 098 * type <code>type</code> 099 * 100 * @param <T> 101 * @param type the type 102 * @return true if there is at least one tag in the current selection for primitives of 103 * type <code>type</code> 104 */ 105 protected <T extends OsmPrimitive> boolean hasSourceTagsByType(OsmPrimitiveType type) { 106 return ! getSourceTagsByType(type).isEmpty(); 107 } 108 109 protected void buildChangeCommand(Collection<? extends OsmPrimitive> selection, TagCollection tc) { 110 for (String key : tc.getKeys()) { 111 commands.add(new Tag(key, tc.getValues(key).iterator().next())); 112 } 113 } 114 115 protected Map<OsmPrimitiveType, Integer> getSourceStatistics() { 116 HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>(); 117 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 118 if (!getSourceTagsByType(type).isEmpty()) { 119 ret.put(type, getSourcePrimitivesByType(type).size()); 120 } 121 } 122 return ret; 123 } 124 125 protected Map<OsmPrimitiveType, Integer> getTargetStatistics() { 126 HashMap<OsmPrimitiveType, Integer> ret = new HashMap<OsmPrimitiveType, Integer>(); 127 for (OsmPrimitiveType type: OsmPrimitiveType.dataValues()) { 128 int count = OsmPrimitive.getFilteredList(target, type.getOsmClass()).size(); 129 if (count > 0) { 130 ret.put(type, count); 131 } 132 } 133 return ret; 134 } 135 136 /** 137 * Pastes the tags from a homogeneous source (the {@link Main#pasteBuffer}s selection consisting 138 * of one type of {@link OsmPrimitive}s only). 139 * 140 * Tags from a homogeneous source can be pasted to a heterogeneous target. All target primitives, 141 * regardless of their type, receive the same tags. 142 */ 143 protected void pasteFromHomogeneousSource() { 144 TagCollection tc = null; 145 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 146 TagCollection tc1 = getSourceTagsByType(type); 147 if (!tc1.isEmpty()) { 148 tc = tc1; 149 } 150 } 151 if (tc == null) 152 // no tags found to paste. Abort. 153 return; 154 155 if (!tc.isApplicableToPrimitive()) { 156 PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent); 157 dialog.populate(tc, getSourceStatistics(), getTargetStatistics()); 158 dialog.setVisible(true); 159 if (dialog.isCanceled()) 160 return; 161 buildChangeCommand(target, dialog.getResolution()); 162 } else { 163 // no conflicts in the source tags to resolve. Just apply the tags 164 // to the target primitives 165 // 166 buildChangeCommand(target, tc); 167 } 168 } 169 170 /** 171 * Replies true if there is at least one primitive of type <code>type</code> 172 * is in the target collection 173 * 174 * @param <T> 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 <T extends OsmPrimitive> boolean hasTargetPrimitives(Class<T> type) { 180 return !OsmPrimitive.getFilteredList(target, type).isEmpty(); 181 } 182 183 /** 184 * Replies true if this a heterogeneous source can be pasted without conflict to targets 185 * 186 * @param targets the collection of target primitives 187 * @return true if this a heterogeneous source can be pasted without conflicts to targets 188 */ 189 protected boolean canPasteFromHeterogeneousSourceWithoutConflict(Collection<OsmPrimitive> targets) { 190 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 191 if (hasTargetPrimitives(type.getOsmClass())) { 192 TagCollection tc = TagCollection.unionOfAllPrimitives(getSourcePrimitivesByType(type)); 193 if (!tc.isEmpty() && ! tc.isApplicableToPrimitive()) 194 return false; 195 } 196 } 197 return true; 198 } 199 200 /** 201 * Pastes the tags in the current selection of the paste buffer to a set of target 202 * primitives. 203 */ 204 protected void pasteFromHeterogeneousSource() { 205 if (canPasteFromHeterogeneousSourceWithoutConflict(target)) { 206 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 207 if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) { 208 buildChangeCommand(target, getSourceTagsByType(type)); 209 } 210 } 211 } else { 212 PasteTagsConflictResolverDialog dialog = new PasteTagsConflictResolverDialog(Main.parent); 213 dialog.populate( 214 getSourceTagsByType(OsmPrimitiveType.NODE), 215 getSourceTagsByType(OsmPrimitiveType.WAY), 216 getSourceTagsByType(OsmPrimitiveType.RELATION), 217 getSourceStatistics(), 218 getTargetStatistics() 219 ); 220 dialog.setVisible(true); 221 if (dialog.isCanceled()) 222 return; 223 for (OsmPrimitiveType type : OsmPrimitiveType.dataValues()) { 224 if (hasSourceTagsByType(type) && hasTargetPrimitives(type.getOsmClass())) { 225 buildChangeCommand(OsmPrimitive.getFilteredList(target, type.getOsmClass()), dialog.getResolution(type)); 226 } 227 } 228 } 229 } 230 231 public List<Tag> execute() { 232 commands.clear(); 233 if (isHeteogeneousSource()) { 234 pasteFromHeterogeneousSource(); 235 } else { 236 pasteFromHomogeneousSource(); 237 } 238 return commands; 239 } 240 241 } 242 243 public void actionPerformed(ActionEvent e) { 244 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected(); 245 246 if (selection.isEmpty()) 247 return; 248 249 TagPaster tagPaster = new TagPaster(Main.pasteBuffer.getDirectlyAdded(), selection); 250 251 List<Command> commands = new ArrayList<Command>(); 252 for (Tag tag: tagPaster.execute()) { 253 commands.add(new ChangePropertyCommand(selection, tag.getKey(), "".equals(tag.getValue())?null:tag.getValue())); 254 } 255 if (!commands.isEmpty()) { 256 String title1 = trn("Pasting {0} tag", "Pasting {0} tags", commands.size(), commands.size()); 257 String title2 = trn("to {0} object", "to {0} objects", selection.size(), selection.size()); 258 Main.main.undoRedo.add( 259 new SequenceCommand( 260 title1 + " " + title2, 261 commands 262 )); 263 } 264 265 } 266 267 @Override public void pasteBufferChanged(PrimitiveDeepCopy newPasteBuffer) { 268 updateEnabledState(); 269 } 270 271 @Override 272 protected void updateEnabledState() { 273 if (getCurrentDataSet() == null || Main.pasteBuffer == null) { 274 setEnabled(false); 275 return; 276 } 277 setEnabled( 278 !getCurrentDataSet().getSelected().isEmpty() 279 && !TagCollection.unionOfAllPrimitives(Main.pasteBuffer.getDirectlyAdded()).isEmpty() 280 ); 281 } 282 283 @Override 284 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 285 setEnabled( 286 selection!= null && !selection.isEmpty() 287 && !TagCollection.unionOfAllPrimitives(Main.pasteBuffer.getDirectlyAdded()).isEmpty() 288 ); 289 } 290 }