001// License: GPL. For details, see LICENSE file.
002// Author: David Earl
003package org.openstreetmap.josm.actions;
004
005import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
006import static org.openstreetmap.josm.tools.I18n.tr;
007
008import java.awt.MouseInfo;
009import java.awt.Point;
010import java.awt.event.ActionEvent;
011import java.awt.event.KeyEvent;
012import java.util.ArrayList;
013import java.util.HashMap;
014import java.util.List;
015import java.util.Map;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.command.AddPrimitivesCommand;
019import org.openstreetmap.josm.data.coor.EastNorth;
020import org.openstreetmap.josm.data.osm.NodeData;
021import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
022import org.openstreetmap.josm.data.osm.PrimitiveData;
023import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy;
024import org.openstreetmap.josm.data.osm.PrimitiveDeepCopy.PasteBufferChangedListener;
025import org.openstreetmap.josm.data.osm.RelationData;
026import org.openstreetmap.josm.data.osm.RelationMemberData;
027import org.openstreetmap.josm.data.osm.WayData;
028import org.openstreetmap.josm.gui.ExtendedDialog;
029import org.openstreetmap.josm.gui.layer.Layer;
030import org.openstreetmap.josm.tools.Shortcut;
031
032/**
033 * Paste OSM primitives from clipboard to the current edit layer.
034 * @since 404
035 */
036public final class PasteAction extends JosmAction implements PasteBufferChangedListener {
037
038    /**
039     * Constructs a new {@code PasteAction}.
040     */
041    public PasteAction() {
042        super(tr("Paste"), "paste", tr("Paste contents of paste buffer."),
043                Shortcut.registerShortcut("system:paste", tr("Edit: {0}", tr("Paste")), KeyEvent.VK_V, Shortcut.CTRL), true);
044        putValue("help", ht("/Action/Paste"));
045        // CUA shortcut for paste (http://en.wikipedia.org/wiki/IBM_Common_User_Access#Description)
046        Main.registerActionShortcut(this,
047                Shortcut.registerShortcut("system:paste:cua", tr("Edit: {0}", tr("Paste")), KeyEvent.VK_INSERT, Shortcut.SHIFT));
048        Main.pasteBuffer.addPasteBufferChangedListener(this);
049    }
050
051    @Override
052    public void actionPerformed(ActionEvent e) {
053        if (!isEnabled())
054            return;
055        pasteData(Main.pasteBuffer, Main.pasteSource, e);
056    }
057
058    /**
059     * Paste OSM primitives from the given paste buffer and OSM data layer source to the current edit layer.
060     * @param pasteBuffer The paste buffer containing primitive ids to copy
061     * @param source The OSM data layer used to look for primitive ids
062     * @param e The ActionEvent that triggered this operation
063     */
064    public void pasteData(PrimitiveDeepCopy pasteBuffer, Layer source, ActionEvent e) {
065        /* Find the middle of the pasteBuffer area */
066        double maxEast = -1E100, minEast = 1E100, maxNorth = -1E100, minNorth = 1E100;
067        boolean incomplete = false;
068        for (PrimitiveData data : pasteBuffer.getAll()) {
069            if (data instanceof NodeData) {
070                NodeData n = (NodeData)data;
071                if (n.getEastNorth() != null) {
072                    double east = n.getEastNorth().east();
073                    double north = n.getEastNorth().north();
074                    if (east > maxEast) { maxEast = east; }
075                    if (east < minEast) { minEast = east; }
076                    if (north > maxNorth) { maxNorth = north; }
077                    if (north < minNorth) { minNorth = north; }
078                }
079            }
080            if (data.isIncomplete()) {
081                incomplete = true;
082            }
083        }
084
085        // Allow to cancel paste if there are incomplete primitives
086        if (incomplete) {
087            if (!confirmDeleteIncomplete()) return;
088        }
089
090        // default to paste in center of map (pasted via menu or cursor not in MapView)
091        EastNorth mPosition = Main.map.mapView.getCenter();
092        // We previously checked for modifier to know if the action has been trigerred via shortcut or via menu
093        // But this does not work if the shortcut is changed to a single key (see #9055)
094        // Observed behaviour: getActionCommand() returns Action.NAME when triggered via menu, but shortcut text when triggered with it
095        if (!getValue(NAME).equals(e.getActionCommand())) {
096            final Point mp = MouseInfo.getPointerInfo().getLocation();
097            final Point tl = Main.map.mapView.getLocationOnScreen();
098            final Point pos = new Point(mp.x-tl.x, mp.y-tl.y);
099            if(Main.map.mapView.contains(pos)) {
100                mPosition = Main.map.mapView.getEastNorth(pos.x, pos.y);
101            }
102        }
103
104        double offsetEast  = mPosition.east() - (maxEast + minEast)/2.0;
105        double offsetNorth = mPosition.north() - (maxNorth + minNorth)/2.0;
106
107        // Make a copy of pasteBuffer and map from old id to copied data id
108        List<PrimitiveData> bufferCopy = new ArrayList<PrimitiveData>();
109        List<PrimitiveData> toSelect = new ArrayList<PrimitiveData>();
110        Map<Long, Long> newNodeIds = new HashMap<Long, Long>();
111        Map<Long, Long> newWayIds = new HashMap<Long, Long>();
112        Map<Long, Long> newRelationIds = new HashMap<Long, Long>();
113        for (PrimitiveData data: pasteBuffer.getAll()) {
114            if (data.isIncomplete()) {
115                continue;
116            }
117            PrimitiveData copy = data.makeCopy();
118            copy.clearOsmMetadata();
119            if (data instanceof NodeData) {
120                newNodeIds.put(data.getUniqueId(), copy.getUniqueId());
121            } else if (data instanceof WayData) {
122                newWayIds.put(data.getUniqueId(), copy.getUniqueId());
123            } else if (data instanceof RelationData) {
124                newRelationIds.put(data.getUniqueId(), copy.getUniqueId());
125            }
126            bufferCopy.add(copy);
127            if (pasteBuffer.getDirectlyAdded().contains(data)) {
128                toSelect.add(copy);
129            }
130        }
131
132        // Update references in copied buffer
133        for (PrimitiveData data:bufferCopy) {
134            if (data instanceof NodeData) {
135                NodeData nodeData = (NodeData)data;
136                if (Main.main.getEditLayer() == source) {
137                    nodeData.setEastNorth(nodeData.getEastNorth().add(offsetEast, offsetNorth));
138                }
139            } else if (data instanceof WayData) {
140                List<Long> newNodes = new ArrayList<Long>();
141                for (Long oldNodeId: ((WayData)data).getNodes()) {
142                    Long newNodeId = newNodeIds.get(oldNodeId);
143                    if (newNodeId != null) {
144                        newNodes.add(newNodeId);
145                    }
146                }
147                ((WayData)data).setNodes(newNodes);
148            } else if (data instanceof RelationData) {
149                List<RelationMemberData> newMembers = new ArrayList<RelationMemberData>();
150                for (RelationMemberData member: ((RelationData)data).getMembers()) {
151                    OsmPrimitiveType memberType = member.getMemberType();
152                    Long newId = null;
153                    switch (memberType) {
154                    case NODE:
155                        newId = newNodeIds.get(member.getMemberId());
156                        break;
157                    case WAY:
158                        newId = newWayIds.get(member.getMemberId());
159                        break;
160                    case RELATION:
161                        newId = newRelationIds.get(member.getMemberId());
162                        break;
163                    }
164                    if (newId != null) {
165                        newMembers.add(new RelationMemberData(member.getRole(), memberType, newId));
166                    }
167                }
168                ((RelationData)data).setMembers(newMembers);
169            }
170        }
171
172        /* Now execute the commands to add the duplicated contents of the paste buffer to the map */
173
174        Main.main.undoRedo.add(new AddPrimitivesCommand(bufferCopy, toSelect));
175        Main.map.mapView.repaint();
176    }
177
178    protected boolean confirmDeleteIncomplete() {
179        ExtendedDialog ed = new ExtendedDialog(Main.parent,
180                tr("Delete incomplete members?"),
181                new String[] {tr("Paste without incomplete members"), tr("Cancel")});
182        ed.setButtonIcons(new String[] {"dialogs/relation/deletemembers.png", "cancel.png"});
183        ed.setContent(tr("The copied data contains incomplete objects.  "
184                + "When pasting the incomplete objects are removed.  "
185                + "Do you want to paste the data without the incomplete objects?"));
186        ed.showDialog();
187        return ed.getValue() == 1;
188    }
189
190    @Override
191    protected void updateEnabledState() {
192        if (getCurrentDataSet() == null || Main.pasteBuffer == null) {
193            setEnabled(false);
194            return;
195        }
196        setEnabled(!Main.pasteBuffer.isEmpty());
197    }
198
199    @Override
200    public void pasteBufferChanged(PrimitiveDeepCopy pasteBuffer) {
201        updateEnabledState();
202    }
203}