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    }