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

Last change on this file since 2284 was 2273, checked in by jttt, 15 years ago

Replace testing for id <= 0 with isNew() method

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