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 }