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 (https://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; 067 double minEast = 1E100; 068 double maxNorth = -1E100; 069 double minNorth = 1E100; 070 boolean incomplete = false; 071 for (PrimitiveData data : pasteBuffer.getAll()) { 072 if (data instanceof NodeData) { 073 NodeData n = (NodeData) data; 074 if (n.getEastNorth() != null) { 075 double east = n.getEastNorth().east(); 076 double north = n.getEastNorth().north(); 077 if (east > maxEast) { 078 maxEast = east; 079 } 080 if (east < minEast) { 081 minEast = east; 082 } 083 if (north > maxNorth) { 084 maxNorth = north; 085 } 086 if (north < minNorth) { 087 minNorth = north; 088 } 089 } 090 } 091 if (data.isIncomplete()) { 092 incomplete = true; 093 } 094 } 095 096 // Allow to cancel paste if there are incomplete primitives 097 if (incomplete && !confirmDeleteIncomplete()) { 098 return; 099 } 100 101 // default to paste in center of map (pasted via menu or cursor not in MapView) 102 EastNorth mPosition = Main.map.mapView.getCenter(); 103 // We previously checked for modifier to know if the action has been trigerred via shortcut or via menu 104 // But this does not work if the shortcut is changed to a single key (see #9055) 105 // Observed behaviour: getActionCommand() returns Action.NAME when triggered via menu, but shortcut text when triggered with it 106 if (e != null && !getValue(NAME).equals(e.getActionCommand())) { 107 final Point mp = MouseInfo.getPointerInfo().getLocation(); 108 final Point tl = Main.map.mapView.getLocationOnScreen(); 109 final Point pos = new Point(mp.x-tl.x, mp.y-tl.y); 110 if (Main.map.mapView.contains(pos)) { 111 mPosition = Main.map.mapView.getEastNorth(pos.x, pos.y); 112 } 113 } 114 115 double offsetEast = mPosition.east() - (maxEast + minEast)/2.0; 116 double offsetNorth = mPosition.north() - (maxNorth + minNorth)/2.0; 117 118 // Make a copy of pasteBuffer and map from old id to copied data id 119 List<PrimitiveData> bufferCopy = new ArrayList<>(); 120 List<PrimitiveData> toSelect = new ArrayList<>(); 121 Map<Long, Long> newNodeIds = new HashMap<>(); 122 Map<Long, Long> newWayIds = new HashMap<>(); 123 Map<Long, Long> newRelationIds = new HashMap<>(); 124 for (PrimitiveData data: pasteBuffer.getAll()) { 125 if (data.isIncomplete()) { 126 continue; 127 } 128 PrimitiveData copy = data.makeCopy(); 129 copy.clearOsmMetadata(); 130 if (data instanceof NodeData) { 131 newNodeIds.put(data.getUniqueId(), copy.getUniqueId()); 132 } else if (data instanceof WayData) { 133 newWayIds.put(data.getUniqueId(), copy.getUniqueId()); 134 } else if (data instanceof RelationData) { 135 newRelationIds.put(data.getUniqueId(), copy.getUniqueId()); 136 } 137 bufferCopy.add(copy); 138 if (pasteBuffer.getDirectlyAdded().contains(data)) { 139 toSelect.add(copy); 140 } 141 } 142 143 // Update references in copied buffer 144 for (PrimitiveData data:bufferCopy) { 145 if (data instanceof NodeData) { 146 NodeData nodeData = (NodeData) data; 147 if (Main.getLayerManager().getEditLayer() == source) { 148 nodeData.setEastNorth(nodeData.getEastNorth().add(offsetEast, offsetNorth)); 149 } 150 } else if (data instanceof WayData) { 151 List<Long> newNodes = new ArrayList<>(); 152 for (Long oldNodeId: ((WayData) data).getNodes()) { 153 Long newNodeId = newNodeIds.get(oldNodeId); 154 if (newNodeId != null) { 155 newNodes.add(newNodeId); 156 } 157 } 158 ((WayData) data).setNodes(newNodes); 159 } else if (data instanceof RelationData) { 160 List<RelationMemberData> newMembers = new ArrayList<>(); 161 for (RelationMemberData member: ((RelationData) data).getMembers()) { 162 OsmPrimitiveType memberType = member.getMemberType(); 163 Long newId; 164 switch (memberType) { 165 case NODE: 166 newId = newNodeIds.get(member.getMemberId()); 167 break; 168 case WAY: 169 newId = newWayIds.get(member.getMemberId()); 170 break; 171 case RELATION: 172 newId = newRelationIds.get(member.getMemberId()); 173 break; 174 default: throw new AssertionError(); 175 } 176 if (newId != null) { 177 newMembers.add(new RelationMemberData(member.getRole(), memberType, newId)); 178 } 179 } 180 ((RelationData) data).setMembers(newMembers); 181 } 182 } 183 184 /* Now execute the commands to add the duplicated contents of the paste buffer to the map */ 185 Main.main.undoRedo.add(new AddPrimitivesCommand(bufferCopy, toSelect)); 186 Main.map.mapView.repaint(); 187 } 188 189 private static boolean confirmDeleteIncomplete() { 190 ExtendedDialog ed = new ExtendedDialog(Main.parent, 191 tr("Delete incomplete members?"), 192 new String[] {tr("Paste without incomplete members"), tr("Cancel")}); 193 ed.setButtonIcons(new String[] {"dialogs/relation/deletemembers", "cancel"}); 194 ed.setContent(tr("The copied data contains incomplete objects. " 195 + "When pasting the incomplete objects are removed. " 196 + "Do you want to paste the data without the incomplete objects?")); 197 ed.showDialog(); 198 return ed.getValue() == 1; 199 } 200 201 @Override 202 protected void updateEnabledState() { 203 if (getLayerManager().getEditDataSet() == null || Main.pasteBuffer == null) { 204 setEnabled(false); 205 return; 206 } 207 setEnabled(!Main.pasteBuffer.isEmpty()); 208 } 209 210 @Override 211 public void pasteBufferChanged(PrimitiveDeepCopy pasteBuffer) { 212 updateEnabledState(); 213 } 214}