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}