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

Last change on this file since 3530 was 3530, checked in by stoecker, 14 years ago

fix array preferences

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