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

Last change on this file since 2341 was 2341, checked in by Gubaer, 14 years ago

applied #3798: patch by bastiK: Merge nodes tool - respect selection order

  • Property svn:eol-style set to native
File size: 12.5 KB
Line 
1//License: GPL. Copyright 2007 by Immanuel Scholz and others
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.BackreferencedDataSet;
28import org.openstreetmap.josm.data.osm.Node;
29import org.openstreetmap.josm.data.osm.OsmPrimitive;
30import org.openstreetmap.josm.data.osm.TagCollection;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.data.osm.BackreferencedDataSet.RelationToChildReference;
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.ImageProvider;
39import org.openstreetmap.josm.tools.Shortcut;
40/**
41 * Merges a collection of nodes into one node.
42 *
43 */
44public class MergeNodesAction extends JosmAction {
45
46 public MergeNodesAction() {
47 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
48 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true);
49 putValue("help", ht("/Action/MergeNodesAction"));
50 }
51
52 public void actionPerformed(ActionEvent event) {
53 if (!isEnabled())
54 return;
55 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
56 LinkedHashSet<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
57 if (selectedNodes.size() < 2) {
58 JOptionPane.showMessageDialog(
59 Main.parent,
60 tr("Please select at least two nodes to merge."),
61 tr("Warning"),
62 JOptionPane.WARNING_MESSAGE
63 );
64 return;
65 }
66
67 Node targetNode = selectTargetNode(selectedNodes);
68 Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode);
69 if (cmd != null) {
70 Main.main.undoRedo.add(cmd);
71 Main.main.getEditLayer().data.setSelected(targetNode);
72 }
73 }
74
75 /**
76 * Find which node to merge into (i.e. which one will be left)
77 * The last selected node will become the target node the remaining
78 * nodes are merged to.
79 *
80 * @param candidates the collection of candidate nodes
81 * @return the selected target node
82 */
83 public static Node selectTargetNode(LinkedHashSet<Node> candidates) {
84 Node targetNode = null;
85 for (Node n : candidates) { // pick last one
86 targetNode = n;
87 }
88 return targetNode;
89 }
90
91 /**
92 * Merges the nodes in <code>node</code> onto one of the nodes. Uses the dataset
93 * managed by <code>layer</code> as reference.
94 *
95 * @param layer the reference data layer. Must not be null.
96 * @param nodes the collection of nodes. Ignored if null.
97 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
98 * @throws IllegalArgumentException thrown if layer is null
99 * @throws IllegalArgumentException thrown if targetNode is null
100 *
101 */
102 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode) throws IllegalArgumentException{
103 if (layer == null)
104 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null.", "nodes"));
105 if (targetNode == null)
106 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null.", "targetNode"));
107
108 if (nodes == null)
109 return null;
110 nodes.remove(null); // just in case
111 BackreferencedDataSet backreferences = new BackreferencedDataSet(layer.data);
112 backreferences.build();
113 return mergeNodes(layer,backreferences, nodes, targetNode);
114 }
115
116 /**
117 * Fixes the parent ways referring to one of the nodes.
118 *
119 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
120 * which is referred to by a relation.
121 *
122 * @param backreferences the backreference data set
123 * @param nodesToDelete the collection of nodes to be deleted
124 * @param targetNode the target node the other nodes are merged to
125 * @return a list of commands; null, if the ways could not be fixed
126 */
127 protected static List<Command> fixParentWays(BackreferencedDataSet backreferences, Collection<OsmPrimitive> nodesToDelete, Node targetNode) {
128 List<Command> cmds = new ArrayList<Command>();
129 Set<Way> waysToDelete = new HashSet<Way>();
130
131 for (Way w: OsmPrimitive.getFilteredList(backreferences.getParents(nodesToDelete), Way.class)) {
132 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
133 for (Node n: w.getNodes()) {
134 if (! nodesToDelete.contains(n) && n != targetNode) {
135 newNodes.add(n);
136 } else if (newNodes.isEmpty()) {
137 newNodes.add(targetNode);
138 } else if (newNodes.get(newNodes.size()-1) != targetNode) {
139 // make sure we collapse a sequence of deleted nodes
140 // to exactly one occurrence of the merged target node
141 //
142 newNodes.add(targetNode);
143 } else {
144 // drop the node
145 }
146 }
147 if (newNodes.size() < 2) {
148 if (backreferences.getParents(w).isEmpty()) {
149 waysToDelete.add(w);
150 } else {
151 ButtonSpec[] options = new ButtonSpec[] {
152 new ButtonSpec(
153 tr("Abort Merging"),
154 ImageProvider.get("cancel"),
155 tr("Click to abort merging nodes"),
156 null /* no special help topic */
157 )
158 };
159 HelpAwareOptionPane.showOptionDialog(
160 Main.parent,
161 tr(
162 "Cannot merge nodes: Would have to delete way ''{0}'' which is still used.",
163 w.getDisplayName(DefaultNameFormatter.getInstance())
164 ),
165 tr("Warning"),
166 JOptionPane.WARNING_MESSAGE,
167 null, /* no icon */
168 options,
169 options[0],
170 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
171 );
172 return null;
173 }
174 } else if(newNodes.size() < 2 && backreferences.getParents(w).isEmpty()) {
175 waysToDelete.add(w);
176 } else {
177 Way newWay = new Way(w);
178 newWay.setNodes(newNodes);
179 cmds.add(new ChangeCommand(w, newWay));
180 }
181 }
182 if (!waysToDelete.isEmpty()) {
183 cmds.add(new DeleteCommand(waysToDelete));
184 }
185 return cmds;
186 }
187
188 /**
189 * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset
190 * managed by <code>layer</code> as reference. <code>backreferences</code> is a precomputed
191 * collection of all parent/child references in the dataset.
192 *
193 * @param layer layer the reference data layer. Must not be null.
194 * @param backreferences if null, backreferences are first computed from layer.data; otherwise
195 * backreferences.getSource() == layer.data must be true
196 * @param nodes the collection of nodes. Ignored if null.
197 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
198 * @throw IllegalArgumentException thrown if layer is null
199 * @throw IllegalArgumentException thrown if backreferences.getSource() != layer.data
200 */
201 public static Command mergeNodes(OsmDataLayer layer, BackreferencedDataSet backreferences, Collection<Node> nodes, Node targetNode) {
202 if (layer == null)
203 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null.", "nodes"));
204 if (targetNode == null)
205 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null.", "targetNode"));
206 if (nodes == null)
207 return null;
208 if (backreferences == null) {
209 backreferences = new BackreferencedDataSet(layer.data);
210 backreferences.build();
211 }
212
213 Set<RelationToChildReference> relationToNodeReferences = backreferences.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 backreferences,
250 nodesToDelete,
251 targetNode
252 );
253 if (wayFixCommands == null)
254 return null;
255 cmds.addAll(wayFixCommands);
256
257 // build the commands
258 //
259 if (!nodesToDelete.isEmpty()) {
260 cmds.add(new DeleteCommand(nodesToDelete));
261 }
262 if (!waysToDelete.isEmpty()) {
263 cmds.add(new DeleteCommand(waysToDelete));
264 }
265 cmds.addAll(dialog.buildResolutionCommands());
266 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
267 return cmd;
268 }
269
270 @Override
271 protected void updateEnabledState() {
272 if (getCurrentDataSet() == null) {
273 setEnabled(false);
274 } else {
275 updateEnabledState(getCurrentDataSet().getSelected());
276 }
277 }
278
279 @Override
280 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
281 if (selection == null || selection.isEmpty()) {
282 setEnabled(false);
283 return;
284 }
285 boolean ok = true;
286 if (selection.size() < 2) {
287 setEnabled(false);
288 return;
289 }
290 for (OsmPrimitive osm : selection) {
291 if (!(osm instanceof Node)) {
292 ok = false;
293 break;
294 }
295 }
296 setEnabled(ok);
297 }
298}
Note: See TracBrowser for help on using the repository browser.