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

Last change on this file since 6156 was 6130, checked in by bastiK, 11 years ago

see #6963 - converted popups to notifications for all actions in the Tools menu (TODO: Simplify Way)

  • Property svn:eol-style set to native
File size: 14.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.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6
7import java.awt.event.ActionEvent;
8import java.awt.event.KeyEvent;
9import java.util.ArrayList;
10import java.util.Collection;
11import java.util.Collections;
12import java.util.HashSet;
13import java.util.LinkedList;
14import java.util.List;
15import java.util.Set;
16
17import javax.swing.JOptionPane;
18
19import org.openstreetmap.josm.Main;
20import org.openstreetmap.josm.command.ChangeCommand;
21import org.openstreetmap.josm.command.ChangeNodesCommand;
22import org.openstreetmap.josm.command.Command;
23import org.openstreetmap.josm.command.DeleteCommand;
24import org.openstreetmap.josm.command.SequenceCommand;
25import org.openstreetmap.josm.corrector.UserCancelException;
26import org.openstreetmap.josm.data.coor.EastNorth;
27import org.openstreetmap.josm.data.coor.LatLon;
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.Notification;
37import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
38import org.openstreetmap.josm.gui.layer.OsmDataLayer;
39import org.openstreetmap.josm.tools.CheckParameterUtil;
40import org.openstreetmap.josm.tools.ImageProvider;
41import org.openstreetmap.josm.tools.Shortcut;
42
43/**
44 * Merges a collection of nodes into one node.
45 *
46 * The "surviving" node will be the one with the lowest positive id.
47 * (I.e. it was uploaded to the server and is the oldest one.)
48 *
49 * However we use the location of the node that was selected *last*.
50 * The "surviving" node will be moved to that location if it is
51 * different from the last selected node.
52 */
53public class MergeNodesAction extends JosmAction {
54
55 public MergeNodesAction() {
56 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
57 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.DIRECT), true);
58 putValue("help", ht("/Action/MergeNodes"));
59 }
60
61 @Override
62 public void actionPerformed(ActionEvent event) {
63 if (!isEnabled())
64 return;
65 Collection<OsmPrimitive> selection = getCurrentDataSet().getAllSelected();
66 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
67
68 if (selectedNodes.size() == 1) {
69 List<Node> nearestNodes = Main.map.mapView.getNearestNodes(Main.map.mapView.getPoint(selectedNodes.get(0)), selectedNodes, OsmPrimitive.isUsablePredicate);
70 if (nearestNodes.isEmpty()) {
71 new Notification(
72 tr("Please select at least two nodes to merge or one node that is close to another node."))
73 .setIcon(JOptionPane.WARNING_MESSAGE)
74 .show();
75 return;
76 }
77 selectedNodes.addAll(nearestNodes);
78 }
79
80 Node targetNode = selectTargetNode(selectedNodes);
81 Node targetLocationNode = selectTargetLocationNode(selectedNodes);
82 Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode, targetLocationNode);
83 if (cmd != null) {
84 Main.main.undoRedo.add(cmd);
85 Main.main.getEditLayer().data.setSelected(targetNode);
86 }
87 }
88
89 /**
90 * Select the location of the target node after merge.
91 *
92 * @param candidates the collection of candidate nodes
93 * @return the coordinates of this node are later used for the target node
94 */
95 public static Node selectTargetLocationNode(List<Node> candidates) {
96 int size = candidates.size();
97 if (size == 0)
98 throw new IllegalArgumentException("empty list");
99
100 switch (Main.pref.getInteger("merge-nodes.mode", 0)) {
101 case 0: {
102 Node targetNode = candidates.get(size - 1);
103 for (final Node n : candidates) { // pick last one
104 targetNode = n;
105 }
106 return targetNode;
107 }
108 case 1: {
109 double east = 0, north = 0;
110 for (final Node n : candidates) {
111 east += n.getEastNorth().east();
112 north += n.getEastNorth().north();
113 }
114
115 return new Node(new EastNorth(east / size, north / size));
116 }
117 case 2: {
118 final double[] weights = new double[size];
119
120 for (int i = 0; i < size; i++) {
121 final LatLon c1 = candidates.get(i).getCoor();
122 for (int j = i + 1; j < size; j++) {
123 final LatLon c2 = candidates.get(j).getCoor();
124 final double d = c1.distance(c2);
125 weights[i] += d;
126 weights[j] += d;
127 }
128 }
129
130 double east = 0, north = 0, weight = 0;
131 for (int i = 0; i < size; i++) {
132 final EastNorth en = candidates.get(i).getEastNorth();
133 final double w = weights[i];
134 east += en.east() * w;
135 north += en.north() * w;
136 weight += w;
137 }
138
139 return new Node(new EastNorth(east / weight, north / weight));
140 }
141 default:
142 throw new RuntimeException("unacceptable merge-nodes.mode");
143 }
144
145 }
146
147 /**
148 * Find which node to merge into (i.e. which one will be left)
149 *
150 * @param candidates the collection of candidate nodes
151 * @return the selected target node
152 */
153 public static Node selectTargetNode(Collection<Node> candidates) {
154 Node oldestNode = null;
155 Node targetNode = null;
156 Node lastNode = null;
157 for (Node n : candidates) {
158 if (!n.isNew()) {
159 // Among existing nodes, try to keep the oldest used one
160 if (!n.getReferrers().isEmpty()) {
161 if (targetNode == null) {
162 targetNode = n;
163 } else if (n.getId() < targetNode.getId()) {
164 targetNode = n;
165 }
166 } else if (oldestNode == null) {
167 oldestNode = n;
168 } else if (n.getId() < oldestNode.getId()) {
169 oldestNode = n;
170 }
171 }
172 lastNode = n;
173 }
174 if (targetNode == null) {
175 targetNode = (oldestNode != null ? oldestNode : lastNode);
176 }
177 return targetNode;
178 }
179
180
181 /**
182 * Fixes the parent ways referring to one of the nodes.
183 *
184 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
185 * which is referred to by a relation.
186 *
187 * @param nodesToDelete the collection of nodes to be deleted
188 * @param targetNode the target node the other nodes are merged to
189 * @return a list of commands; null, if the ways could not be fixed
190 */
191 protected static List<Command> fixParentWays(Collection<Node> nodesToDelete, Node targetNode) {
192 List<Command> cmds = new ArrayList<Command>();
193 Set<Way> waysToDelete = new HashSet<Way>();
194
195 for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) {
196 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
197 for (Node n: w.getNodes()) {
198 if (! nodesToDelete.contains(n) && n != targetNode) {
199 newNodes.add(n);
200 } else if (newNodes.isEmpty()) {
201 newNodes.add(targetNode);
202 } else if (newNodes.get(newNodes.size()-1) != targetNode) {
203 // make sure we collapse a sequence of deleted nodes
204 // to exactly one occurrence of the merged target node
205 //
206 newNodes.add(targetNode);
207 } else {
208 // drop the node
209 }
210 }
211 if (newNodes.size() < 2) {
212 if (w.getReferrers().isEmpty()) {
213 waysToDelete.add(w);
214 } else {
215 ButtonSpec[] options = new ButtonSpec[] {
216 new ButtonSpec(
217 tr("Abort Merging"),
218 ImageProvider.get("cancel"),
219 tr("Click to abort merging nodes"),
220 null /* no special help topic */
221 )
222 };
223 HelpAwareOptionPane.showOptionDialog(
224 Main.parent,
225 tr("Cannot merge nodes: Would have to delete way {0} which is still used by {1}",
226 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(w),
227 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(w.getReferrers())),
228 tr("Warning"),
229 JOptionPane.WARNING_MESSAGE,
230 null, /* no icon */
231 options,
232 options[0],
233 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
234 );
235 return null;
236 }
237 } else if(newNodes.size() < 2 && w.getReferrers().isEmpty()) {
238 waysToDelete.add(w);
239 } else {
240 cmds.add(new ChangeNodesCommand(w, newNodes));
241 }
242 }
243 if (!waysToDelete.isEmpty()) {
244 cmds.add(new DeleteCommand(waysToDelete));
245 }
246 return cmds;
247 }
248
249 public static void doMergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetLocationNode) {
250 if (nodes == null) {
251 return;
252 }
253 Set<Node> allNodes = new HashSet<Node>(nodes);
254 allNodes.add(targetLocationNode);
255 Node target = selectTargetNode(allNodes);
256
257 Command cmd = mergeNodes(layer, nodes, target, targetLocationNode);
258 if (cmd != null) {
259 Main.main.undoRedo.add(cmd);
260 getCurrentDataSet().setSelected(target);
261 }
262 }
263
264 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetLocationNode) {
265 if (nodes == null) {
266 return null;
267 }
268 Set<Node> allNodes = new HashSet<Node>(nodes);
269 allNodes.add(targetLocationNode);
270 return mergeNodes(layer, nodes, selectTargetNode(allNodes), targetLocationNode);
271 }
272
273 /**
274 * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset
275 * managed by <code>layer</code> as reference.
276 *
277 * @param layer layer the reference data layer. Must not be null.
278 * @param nodes the collection of nodes. Ignored if null.
279 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
280 * @param targetLocationNode this node's location will be used for the targetNode.
281 * @throws IllegalArgumentException thrown if layer is null
282 */
283 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode, Node targetLocationNode) {
284 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
285 CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
286 if (nodes == null) {
287 return null;
288 }
289
290 Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(nodes);
291
292 try {
293 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
294 List<Command> resultion = CombinePrimitiveResolverDialog.launchIfNecessary(nodeTags, nodes, Collections.singleton(targetNode));
295 LinkedList<Command> cmds = new LinkedList<Command>();
296
297 // the nodes we will have to delete
298 //
299 Collection<Node> nodesToDelete = new HashSet<Node>(nodes);
300 nodesToDelete.remove(targetNode);
301
302 // fix the ways referring to at least one of the merged nodes
303 //
304 Collection<Way> waysToDelete = new HashSet<Way>();
305 List<Command> wayFixCommands = fixParentWays(
306 nodesToDelete,
307 targetNode);
308 if (wayFixCommands == null) {
309 return null;
310 }
311 cmds.addAll(wayFixCommands);
312
313 // build the commands
314 //
315 if (targetNode != targetLocationNode) {
316 Node newTargetNode = new Node(targetNode);
317 newTargetNode.setCoor(targetLocationNode.getCoor());
318 cmds.add(new ChangeCommand(targetNode, newTargetNode));
319 }
320 cmds.addAll(resultion);
321 if (!nodesToDelete.isEmpty()) {
322 cmds.add(new DeleteCommand(nodesToDelete));
323 }
324 if (!waysToDelete.isEmpty()) {
325 cmds.add(new DeleteCommand(waysToDelete));
326 }
327 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
328 return cmd;
329 } catch (UserCancelException ex) {
330 return null;
331 }
332 }
333
334 @Override
335 protected void updateEnabledState() {
336 if (getCurrentDataSet() == null) {
337 setEnabled(false);
338 } else {
339 updateEnabledState(getCurrentDataSet().getAllSelected());
340 }
341 }
342
343 @Override
344 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
345 if (selection == null || selection.isEmpty()) {
346 setEnabled(false);
347 return;
348 }
349 boolean ok = true;
350 for (OsmPrimitive osm : selection) {
351 if (!(osm instanceof Node)) {
352 ok = false;
353 break;
354 }
355 }
356 setEnabled(ok);
357 }
358}
Note: See TracBrowser for help on using the repository browser.