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

Last change on this file since 3775 was 3757, checked in by stoecker, 13 years ago

fix help topics

  • Property svn:eol-style set to native
File size: 12.6 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.LinkedList;
16import java.util.List;
17import java.util.Set;
18
19import javax.swing.JOptionPane;
20
21import org.openstreetmap.josm.Main;
22import org.openstreetmap.josm.command.ChangeCommand;
23import org.openstreetmap.josm.command.ChangeNodesCommand;
24import org.openstreetmap.josm.command.Command;
25import org.openstreetmap.josm.command.DeleteCommand;
26import org.openstreetmap.josm.command.SequenceCommand;
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.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/MergeNodes"));
57 }
58
59 public void actionPerformed(ActionEvent event) {
60 if (!isEnabled())
61 return;
62 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
63 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
64
65 if (selectedNodes.size() == 1) {
66 List<Node> nearestNodes = Main.map.mapView.getNearestNodes(Main.map.mapView.getPoint(selectedNodes.get(0)), selectedNodes, OsmPrimitive.isUsablePredicate);
67 if (nearestNodes.isEmpty()) {
68 JOptionPane.showMessageDialog(
69 Main.parent,
70 tr("Please select at least two nodes to merge or node that is close to another node."),
71 tr("Warning"),
72 JOptionPane.WARNING_MESSAGE
73 );
74
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 if (! Main.pref.getBoolean("merge-nodes.average-location", false)) {
97 Node targetNode = null;
98 for (final Node n : candidates) { // pick last one
99 targetNode = n;
100 }
101 return targetNode;
102 }
103
104 double lat = 0, lon = 0;
105 for (final Node n : candidates) {
106 lat += n.getCoor().lat();
107 lon += n.getCoor().lon();
108 }
109
110 return new Node(new LatLon(lat / candidates.size(), lon / candidates.size()));
111 }
112
113 /**
114 * Find which node to merge into (i.e. which one will be left)
115 *
116 * @param candidates the collection of candidate nodes
117 * @return the selected target node
118 */
119 public static Node selectTargetNode(List<Node> candidates) {
120 Node targetNode = null;
121 Node lastNode = null;
122 for (Node n : candidates) {
123 if (!n.isNew()) {
124 if (targetNode == null) {
125 targetNode = n;
126 } else if (n.getId() < targetNode.getId()) {
127 targetNode = n;
128 }
129 }
130 lastNode = n;
131 }
132 if (targetNode == null) {
133 targetNode = lastNode;
134 }
135 return targetNode;
136 }
137
138
139 /**
140 * Fixes the parent ways referring to one of the nodes.
141 *
142 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
143 * which is referred to by a relation.
144 *
145 * @param nodesToDelete the collection of nodes to be deleted
146 * @param targetNode the target node the other nodes are merged to
147 * @return a list of commands; null, if the ways could not be fixed
148 */
149 protected static List<Command> fixParentWays(Collection<Node> nodesToDelete, Node targetNode) {
150 List<Command> cmds = new ArrayList<Command>();
151 Set<Way> waysToDelete = new HashSet<Way>();
152
153 for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) {
154 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
155 for (Node n: w.getNodes()) {
156 if (! nodesToDelete.contains(n) && n != targetNode) {
157 newNodes.add(n);
158 } else if (newNodes.isEmpty()) {
159 newNodes.add(targetNode);
160 } else if (newNodes.get(newNodes.size()-1) != targetNode) {
161 // make sure we collapse a sequence of deleted nodes
162 // to exactly one occurrence of the merged target node
163 //
164 newNodes.add(targetNode);
165 } else {
166 // drop the node
167 }
168 }
169 if (newNodes.size() < 2) {
170 if (w.getReferrers().isEmpty()) {
171 waysToDelete.add(w);
172 } else {
173 ButtonSpec[] options = new ButtonSpec[] {
174 new ButtonSpec(
175 tr("Abort Merging"),
176 ImageProvider.get("cancel"),
177 tr("Click to abort merging nodes"),
178 null /* no special help topic */
179 )
180 };
181 HelpAwareOptionPane.showOptionDialog(
182 Main.parent,
183 tr(
184 "Cannot merge nodes: Would have to delete way ''{0}'' which is still used.",
185 w.getDisplayName(DefaultNameFormatter.getInstance())
186 ),
187 tr("Warning"),
188 JOptionPane.WARNING_MESSAGE,
189 null, /* no icon */
190 options,
191 options[0],
192 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
193 );
194 return null;
195 }
196 } else if(newNodes.size() < 2 && w.getReferrers().isEmpty()) {
197 waysToDelete.add(w);
198 } else {
199 cmds.add(new ChangeNodesCommand(w, newNodes));
200 }
201 }
202 if (!waysToDelete.isEmpty()) {
203 cmds.add(new DeleteCommand(waysToDelete));
204 }
205 return cmds;
206 }
207
208 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode) {
209 return mergeNodes(layer, nodes, targetNode, targetNode);
210 }
211
212 /**
213 * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset
214 * managed by <code>layer</code> as reference.
215 *
216 * @param layer layer the reference data layer. Must not be null.
217 * @param nodes the collection of nodes. Ignored if null.
218 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
219 * @param targetLocationNode this node's location will be used for the targetNode.
220 * @throw IllegalArgumentException thrown if layer is null
221 */
222 public static Command mergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetNode, Node targetLocationNode) {
223 CheckParameterUtil.ensureParameterNotNull(layer, "layer");
224 CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
225 if (nodes == null)
226 return null;
227
228 Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(nodes);
229
230 // build the tag collection
231 //
232 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
233 combineTigerTags(nodeTags);
234 normalizeTagCollectionBeforeEditing(nodeTags, nodes);
235 TagCollection nodeTagsToEdit = new TagCollection(nodeTags);
236 completeTagCollectionForEditing(nodeTagsToEdit);
237
238 // launch a conflict resolution dialog, if necessary
239 //
240 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
241 dialog.getTagConflictResolverModel().populate(nodeTagsToEdit, nodeTags.getKeysWithMultipleValues());
242 dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
243 dialog.setTargetPrimitive(targetNode);
244 dialog.prepareDefaultDecisions();
245 // conflict resolution is necessary if there are conflicts in the merged tags
246 // or if at least one of the merged nodes is referred to by a relation
247 //
248 if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
249 dialog.setVisible(true);
250 if (dialog.isCancelled())
251 return null;
252 }
253 LinkedList<Command> cmds = new LinkedList<Command>();
254
255 // the nodes we will have to delete
256 //
257 Collection<Node> nodesToDelete = new HashSet<Node>(nodes);
258 nodesToDelete.remove(targetNode);
259
260 // fix the ways referring to at least one of the merged nodes
261 //
262 Collection<Way> waysToDelete= new HashSet<Way>();
263 List<Command> wayFixCommands = fixParentWays(
264 nodesToDelete,
265 targetNode
266 );
267 if (wayFixCommands == null)
268 return null;
269 cmds.addAll(wayFixCommands);
270
271 // build the commands
272 //
273 if (targetNode != targetLocationNode) {
274 Node newTargetNode = new Node(targetNode);
275 newTargetNode.setCoor(targetLocationNode.getCoor());
276 cmds.add(new ChangeCommand(targetNode, newTargetNode));
277 }
278 cmds.addAll(dialog.buildResolutionCommands());
279 if (!nodesToDelete.isEmpty()) {
280 cmds.add(new DeleteCommand(nodesToDelete));
281 }
282 if (!waysToDelete.isEmpty()) {
283 cmds.add(new DeleteCommand(waysToDelete));
284 }
285 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
286 return cmd;
287 }
288
289 @Override
290 protected void updateEnabledState() {
291 if (getCurrentDataSet() == null) {
292 setEnabled(false);
293 } else {
294 updateEnabledState(getCurrentDataSet().getSelected());
295 }
296 }
297
298 @Override
299 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
300 if (selection == null || selection.isEmpty()) {
301 setEnabled(false);
302 return;
303 }
304 boolean ok = true;
305 for (OsmPrimitive osm : selection) {
306 if (!(osm instanceof Node)) {
307 ok = false;
308 break;
309 }
310 }
311 setEnabled(ok);
312 }
313}
Note: See TracBrowser for help on using the repository browser.