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

Last change on this file since 2102 was 2102, checked in by Gubaer, 15 years ago

see #3466: Merge nodes produce bad results

  • Property svn:eol-style set to native
File size: 10.9 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.actions;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.event.ActionEvent;
7import java.awt.event.KeyEvent;
8import java.util.ArrayList;
9import java.util.Collection;
10import java.util.HashSet;
11import java.util.LinkedList;
12import java.util.Set;
13
14import javax.swing.JOptionPane;
15
16import org.openstreetmap.josm.Main;
17import org.openstreetmap.josm.command.ChangeCommand;
18import org.openstreetmap.josm.command.Command;
19import org.openstreetmap.josm.command.DeleteCommand;
20import org.openstreetmap.josm.command.SequenceCommand;
21import org.openstreetmap.josm.data.osm.BackreferencedDataSet;
22import org.openstreetmap.josm.data.osm.Node;
23import org.openstreetmap.josm.data.osm.OsmPrimitive;
24import org.openstreetmap.josm.data.osm.Tag;
25import org.openstreetmap.josm.data.osm.TagCollection;
26import org.openstreetmap.josm.data.osm.Way;
27import org.openstreetmap.josm.data.osm.BackreferencedDataSet.RelationToChildReference;
28import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
29import org.openstreetmap.josm.gui.layer.OsmDataLayer;
30import org.openstreetmap.josm.tools.Shortcut;
31
32
33/**
34 * Merges a collection of nodes into one node.
35 *
36 */
37public class MergeNodesAction extends JosmAction {
38
39 public MergeNodesAction() {
40 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
41 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true);
42 }
43
44 public void actionPerformed(ActionEvent event) {
45 if (!isEnabled())
46 return;
47 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
48 Set<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
49 if (selectedNodes.size() < 2) {
50 JOptionPane.showMessageDialog(
51 Main.parent,
52 tr("Please select at least two nodes to merge."),
53 tr("Warning"),
54 JOptionPane.WARNING_MESSAGE
55 );
56 return;
57 }
58
59
60 Node targetNode = selectTargetNode(selectedNodes);
61 Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode);
62 if (cmd != null) {
63 Main.main.undoRedo.add(cmd);
64 Main.main.getEditLayer().data.setSelected(targetNode);
65 }
66 }
67
68 protected static void completeTagCollectionWithMissingTags(TagCollection tc, Collection<Node> mergedNodes) {
69 for (String key: tc.getKeys()) {
70 // make sure the empty value is in the tag set if a tag is not present
71 // on all merged nodes
72 //
73 for (Node n: mergedNodes) {
74 if (n.get(key) == null) {
75 tc.add(new Tag(key)); // add a tag with key and empty value
76 }
77 }
78 }
79 // remove irrelevant tags
80 //
81 tc.removeByKey("created_by");
82 }
83
84 protected static void completeTagCollectionForEditing(TagCollection tc) {
85 for (String key: tc.getKeys()) {
86 // make sure the empty value is in the tag set such that we can delete the tag
87 // in the conflict dialog if necessary
88 //
89 tc.add(new Tag(key,""));
90 }
91 }
92
93 /**
94 * Selects a node out of a collection of candidate nodes. The selected
95 * node will become the target node the remaining nodes are merged to.
96 *
97 * @param candidates the collection of candidate nodes
98 * @return the selected target node
99 */
100 public static Node selectTargetNode(Collection<Node> candidates) {
101 // Find which node to merge into (i.e. which one will be left)
102 // - this should be combined from two things:
103 // 1. It will be the first node in the list that has a
104 // positive ID number, OR the first node.
105 // 2. It will be at the position of the first node in the
106 // list.
107 //
108 // *However* - there is the problem that the selection list is
109 // _not_ in the order that the nodes were clicked on, meaning
110 // that the user doesn't know which node will be chosen (so
111 // (2) is not implemented yet.) :-(
112 Node targetNode = null;
113 for (Node n: candidates) {
114 if (n.getId() > 0) {
115 targetNode = n;
116 break;
117 }
118 }
119 if (targetNode == null) {
120 // an arbitrary node
121 targetNode = candidates.iterator().next();
122 }
123 return targetNode;
124 }
125
126 /**
127 * Merges the nodes in <code>node</code> onto one of the nodes. Uses the dataset
128 * managed by <code>layer</code> as reference.
129 *
130 * @param layer the reference data layer. Must not be null.
131 * @param nodes the collection of nodes. Ignored if null.
132 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
133 * @throws IllegalArgumentException thrown if layer is null
134 * @throws IllegalArgumentException thrown if targetNode is null
135 *
136 */
137 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode) throws IllegalArgumentException{
138 if (layer == null)
139 throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "nodes"));
140 if (targetNode == null)
141 throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "targetNode"));
142
143 if (nodes == null)
144 return null;
145 nodes.remove(null); // just in case
146 BackreferencedDataSet backreferences = new BackreferencedDataSet(layer.data);
147 backreferences.build();
148 return mergeNodes(layer,backreferences, nodes, targetNode);
149 }
150
151 /**
152 * Merges the nodes in <code>node</code> onto one of the nodes. Uses the dataset
153 * managed by <code>layer</code> as reference. <code>backreferences</code> is precomputed
154 * collection of all parent/child references in the dataset.
155 *
156 * @param layer layer the reference data layer. Must not be null.
157 * @param backreferences if null, backreferneces are first computed from layer.data; otherwise
158 * backreferences.getSource() == layer.data must hold
159 * @param nodes the collection of nodes. Ignored if null.
160 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
161 * @throw IllegalArgumentException thrown if layer is null
162 * @throw IllegalArgumentException thrown if backreferences.getSource() != layer.data
163 */
164 public static Command mergeNodes(OsmDataLayer layer, BackreferencedDataSet backreferences, Collection<Node> nodes, Node targetNode) {
165 if (layer == null)
166 throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "nodes"));
167 if (targetNode == null)
168 throw new IllegalArgumentException(tr("parameter ''{0}'' must not be null", "targetNode"));
169 if (nodes == null)
170 return null;
171 if (backreferences == null) {
172 backreferences = new BackreferencedDataSet(layer.data);
173 backreferences.build();
174 }
175
176 Set<RelationToChildReference> relationToNodeReferences = backreferences.getRelationToChildReferences(nodes);
177
178 // build the tag collection
179 //
180 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
181 completeTagCollectionWithMissingTags(nodeTags, nodes);
182 TagCollection nodeTagsToEdit = new TagCollection(nodeTags);
183 completeTagCollectionForEditing(nodeTagsToEdit);
184
185 // launch a conflict resolution dialog, if necessary
186 //
187 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
188 dialog.getTagConflictResolverModel().populate(nodeTagsToEdit);
189 dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
190 dialog.setTargetPrimitive(targetNode);
191 dialog.prepareDefaultDecisions();
192 if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
193 dialog.setVisible(true);
194 if (dialog.isCancelled())
195 return null;
196 }
197 LinkedList<Command> cmds = new LinkedList<Command>();
198
199 // the nodes we will have to delete
200 //
201 Collection<OsmPrimitive> nodesToDelete = new HashSet<OsmPrimitive>(nodes);
202 nodesToDelete.remove(targetNode);
203
204 // change the ways referring to at least one of the merge nodes
205 //
206 Collection<Way> waysToDelete= new HashSet<Way>();
207 for (Way w : OsmPrimitive.getFilteredList(backreferences.getParents(nodesToDelete), Way.class)) {
208 // OK - this way contains one or more nodes to change
209 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
210 for (Node n: w.getNodes()) {
211 if (! nodesToDelete.contains(n)) {
212 newNodes.add(n);
213 } else {
214 newNodes.add(targetNode);
215 }
216 }
217 if (newNodes.size() < 2) {
218 if (backreferences.getParents(w).isEmpty()) {
219 waysToDelete.add(w);
220 } else {
221 JOptionPane.showMessageDialog(
222 Main.parent,
223 tr("Cannot merge nodes: " +
224 "Would have to delete a way that is still used."),
225 tr("Warning"),
226 JOptionPane.WARNING_MESSAGE
227 );
228 return null;
229 }
230 } else if(newNodes.size() < 2 && backreferences.getParents(w).isEmpty()) {
231 waysToDelete.add(w);
232 } else {
233 Way newWay = new Way(w);
234 newWay.setNodes(newNodes);
235 cmds.add(new ChangeCommand(w, newWay));
236 }
237 }
238
239 // build the commands
240 //
241 if (!nodesToDelete.isEmpty()) {
242 cmds.add(new DeleteCommand(nodesToDelete));
243 }
244 if (!waysToDelete.isEmpty()) {
245 cmds.add(new DeleteCommand(waysToDelete));
246 }
247 cmds.addAll(dialog.buildResolutionCommands());
248 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
249 return cmd;
250 }
251
252
253 /**
254 * Enable the "Merge Nodes" menu option if more then one node is selected
255 */
256 @Override
257 public void updateEnabledState() {
258 if (getCurrentDataSet() == null || getCurrentDataSet().getSelected().isEmpty()) {
259 setEnabled(false);
260 return;
261 }
262 boolean ok = true;
263 if (getCurrentDataSet().getSelected().size() < 2) {
264 setEnabled(false);
265 return;
266 }
267 for (OsmPrimitive osm : getCurrentDataSet().getSelected()) {
268 if (!(osm instanceof Node)) {
269 ok = false;
270 break;
271 }
272 }
273 setEnabled(ok);
274 }
275}
Note: See TracBrowser for help on using the repository browser.