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