source: josm/trunk/src/org/openstreetmap/josm/actions/MergeNodesAction.java@ 3134

Last change on this file since 3134 was 3134, checked in by bastiK, 17 years ago

fixed #4606 - merging a new and an already existing node (better selection of "surviving" node)

  • Property svn:eol-style set to native
File size: 12.1 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others. See LICENSE file for details.
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.combineTigerTags;
5import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.completeTagCollectionForEditing;
6import static org.openstreetmap.josm.gui.conflict.tags.TagConflictResolutionUtil.normalizeTagCollectionBeforeEditing;
7import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
8import static org.openstreetmap.josm.tools.I18n.tr;
9
10import java.awt.event.ActionEvent;
11import java.awt.event.KeyEvent;
12import java.util.ArrayList;
13import java.util.Collection;
14import java.util.HashSet;
15import java.util.LinkedHashSet;
16import java.util.LinkedList;
17import java.util.List;
18import java.util.Set;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.ChangeCommand;
24import org.openstreetmap.josm.command.Command;
25import org.openstreetmap.josm.command.DeleteCommand;
26import org.openstreetmap.josm.command.SequenceCommand;
27import org.openstreetmap.josm.data.osm.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.RelationToChildReference;
30import org.openstreetmap.josm.data.osm.TagCollection;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.gui.DefaultNameFormatter;
33import org.openstreetmap.josm.gui.HelpAwareOptionPane;
34import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
35import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
36import org.openstreetmap.josm.gui.layer.OsmDataLayer;
37import org.openstreetmap.josm.tools.CheckParameterUtil;
38import org.openstreetmap.josm.tools.ImageProvider;
39import org.openstreetmap.josm.tools.Shortcut;
40/**
41 * Merges a collection of nodes into one node.
42 *
43 * The "surviving" node will be the one with the lowest positive id.
44 * (I.e. it was uploaded to the server and is the oldest one.)
45 *
46 * However we use the location of the node that was selected *last*.
47 * The "surviving" node will be moved to that location if it is
48 * different from the last selected node.
49 */
50public class MergeNodesAction extends JosmAction {
51
52 public MergeNodesAction() {
53 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
54 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true);
55 putValue("help", ht("/Action/MergeNodesAction"));
56 }
57
58 public void actionPerformed(ActionEvent event) {
59 if (!isEnabled())
60 return;
61 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
62 LinkedHashSet<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
63 if (selectedNodes.size() < 2) {
64 JOptionPane.showMessageDialog(
65 Main.parent,
66 tr("Please select at least two nodes to merge."),
67 tr("Warning"),
68 JOptionPane.WARNING_MESSAGE
69 );
70 return;
71 }
72
73 Node targetNode = selectTargetNode(selectedNodes);
74 Node targetLocationNode = selectTargetLocationNode(selectedNodes);
75 Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode, targetLocationNode);
76 if (cmd != null) {
77 Main.main.undoRedo.add(cmd);
78 Main.main.getEditLayer().data.setSelected(targetNode);
79 }
80 }
81
82 /**
83 * Select the location of the target node after merge.
84 *
85 * @param candidates the collection of candidate nodes
86 * @return the coordinates of this node are later used for the target node
87 */
88 public static Node selectTargetLocationNode(LinkedHashSet<Node> candidates) {
89 Node targetNode = null;
90 for (Node n : candidates) { // pick last one
91 targetNode = n;
92 }
93 return targetNode;
94 }
95
96 /**
97 * Find which node to merge into (i.e. which one will be left)
98 *
99 * @param candidates the collection of candidate nodes
100 * @return the selected target node
101 */
102 public static Node selectTargetNode(LinkedHashSet<Node> candidates) {
103 Node targetNode = null;
104 Node lastNode = null;
105 for (Node n : candidates) {
106 if (!n.isNew()) {
107 if (targetNode == null) {
108 targetNode = n;
109 } else if (n.getId() < targetNode.getId()) {
110 targetNode = n;
111 }
112 }
113 lastNode = n;
114 }
115 if (targetNode == null) {
116 targetNode = lastNode;
117 }
118 return targetNode;
119 }
120
121
122 /**
123 * Fixes the parent ways referring to one of the nodes.
124 *
125 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
126 * which is referred to by a relation.
127 *
128 * @param nodesToDelete the collection of nodes to be deleted
129 * @param targetNode the target node the other nodes are merged to
130 * @return a list of commands; null, if the ways could not be fixed
131 */
132 protected static List<Command> fixParentWays(Collection<OsmPrimitive> nodesToDelete, Node targetNode) {
133 List<Command> cmds = new ArrayList<Command>();
134 Set<Way> waysToDelete = new HashSet<Way>();
135
136 for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) {
137 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
138 for (Node n: w.getNodes()) {
139 if (! nodesToDelete.contains(n) && n != targetNode) {
140 newNodes.add(n);
141 } else if (newNodes.isEmpty()) {
142 newNodes.add(targetNode);
143 } else if (newNodes.get(newNodes.size()-1) != targetNode) {
144 // make sure we collapse a sequence of deleted nodes
145 // to exactly one occurrence of the merged target node
146 //
147 newNodes.add(targetNode);
148 } else {
149 // drop the node
150 }
151 }
152 if (newNodes.size() < 2) {
153 if (w.getReferrers().isEmpty()) {
154 waysToDelete.add(w);
155 } else {
156 ButtonSpec[] options = new ButtonSpec[] {
157 new ButtonSpec(
158 tr("Abort Merging"),
159 ImageProvider.get("cancel"),
160 tr("Click to abort merging nodes"),
161 null /* no special help topic */
162 )
163 };
164 HelpAwareOptionPane.showOptionDialog(
165 Main.parent,
166 tr(
167 "Cannot merge nodes: Would have to delete way ''{0}'' which is still used.",
168 w.getDisplayName(DefaultNameFormatter.getInstance())
169 ),
170 tr("Warning"),
171 JOptionPane.WARNING_MESSAGE,
172 null, /* no icon */
173 options,
174 options[0],
175 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
176 );
177 return null;
178 }
179 } else if(newNodes.size() < 2 && w.getReferrers().isEmpty()) {
180 waysToDelete.add(w);
181 } else {
182 Way newWay = new Way(w);
183 newWay.setNodes(newNodes);
184 cmds.add(new ChangeCommand(w, newWay));
185 }
186 }
187 if (!waysToDelete.isEmpty()) {
188 cmds.add(new DeleteCommand(waysToDelete));
189 }
190 return cmds;
191 }
192
193 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode) {
194 return mergeNodes(layer, nodes, targetNode, targetNode);
195 }
196
197 /**
198 * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset
199 * managed by <code>layer</code> as reference.
200 *
201 * @param layer layer the reference data layer. Must not be null.
202 * @param nodes the collection of nodes. Ignored if null.
203 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
204 * @param targetLocationNode this node's location will be used for the targetNode.
205 * @throw IllegalArgumentException thrown if layer is null
206 */
207 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode, Node targetLocationNode) {
208 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
209 CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
210 if (nodes == null)
211 return null;
212
213 Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(nodes);
214
215 // build the tag collection
216 //
217 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
218 combineTigerTags(nodeTags);
219 normalizeTagCollectionBeforeEditing(nodeTags, nodes);
220 TagCollection nodeTagsToEdit = new TagCollection(nodeTags);
221 completeTagCollectionForEditing(nodeTagsToEdit);
222
223 // launch a conflict resolution dialog, if necessary
224 //
225 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
226 dialog.getTagConflictResolverModel().populate(nodeTagsToEdit, nodeTags.getKeysWithMultipleValues());
227 dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
228 dialog.setTargetPrimitive(targetNode);
229 dialog.prepareDefaultDecisions();
230 // conflict resolution is necessary if there are conflicts in the merged tags
231 // or if at least one of the merged nodes is referred to by a relation
232 //
233 if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
234 dialog.setVisible(true);
235 if (dialog.isCancelled())
236 return null;
237 }
238 LinkedList<Command> cmds = new LinkedList<Command>();
239
240 // the nodes we will have to delete
241 //
242 Collection<OsmPrimitive> nodesToDelete = new HashSet<OsmPrimitive>(nodes);
243 nodesToDelete.remove(targetNode);
244
245 // fix the ways referring to at least one of the merged nodes
246 //
247 Collection<Way> waysToDelete= new HashSet<Way>();
248 List<Command> wayFixCommands = fixParentWays(
249 nodesToDelete,
250 targetNode
251 );
252 if (wayFixCommands == null)
253 return null;
254 cmds.addAll(wayFixCommands);
255
256 // build the commands
257 //
258 if (targetNode != targetLocationNode) {
259 Node newTargetNode = new Node(targetNode);
260 newTargetNode.setCoor(targetLocationNode.getCoor());
261 cmds.add(new ChangeCommand(targetNode, newTargetNode));
262 }
263 if (!nodesToDelete.isEmpty()) {
264 cmds.add(new DeleteCommand(nodesToDelete));
265 }
266 if (!waysToDelete.isEmpty()) {
267 cmds.add(new DeleteCommand(waysToDelete));
268 }
269 cmds.addAll(dialog.buildResolutionCommands());
270 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
271 return cmd;
272 }
273
274 @Override
275 protected void updateEnabledState() {
276 if (getCurrentDataSet() == null) {
277 setEnabled(false);
278 } else {
279 updateEnabledState(getCurrentDataSet().getSelected());
280 }
281 }
282
283 @Override
284 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
285 if (selection == null || selection.isEmpty()) {
286 setEnabled(false);
287 return;
288 }
289 boolean ok = true;
290 if (selection.size() < 2) {
291 setEnabled(false);
292 return;
293 }
294 for (OsmPrimitive osm : selection) {
295 if (!(osm instanceof Node)) {
296 ok = false;
297 break;
298 }
299 }
300 setEnabled(ok);
301 }
302}
Note: See TracBrowser for help on using the repository browser.