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

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

Removed BackReferenceDataSet and CollectBackReferenceVisitor because we now have referrer support in OsmPrimitive.
This could cause some problems in the next few days. I'm sure I didn't test every possibly affected feature.

  • Property svn:eol-style set to native
File size: 10.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.Node;
28import org.openstreetmap.josm.data.osm.OsmPrimitive;
29import org.openstreetmap.josm.data.osm.RelationToChildReference;
30import org.openstreetmap.josm.data.osm.TagCollection;
31import org.openstreetmap.josm.data.osm.Way;
32import org.openstreetmap.josm.gui.DefaultNameFormatter;
33import org.openstreetmap.josm.gui.HelpAwareOptionPane;
34import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
35import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
36import org.openstreetmap.josm.gui.layer.OsmDataLayer;
37import org.openstreetmap.josm.tools.ImageProvider;
38import org.openstreetmap.josm.tools.Shortcut;
39/**
40 * Merges a collection of nodes into one node.
41 *
42 */
43public class MergeNodesAction extends JosmAction {
44
45 public MergeNodesAction() {
46 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
47 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.GROUP_EDIT), true);
48 putValue("help", ht("/Action/MergeNodesAction"));
49 }
50
51 public void actionPerformed(ActionEvent event) {
52 if (!isEnabled())
53 return;
54 Collection<OsmPrimitive> selection = getCurrentDataSet().getSelected();
55 LinkedHashSet<Node> selectedNodes = OsmPrimitive.getFilteredSet(selection, Node.class);
56 if (selectedNodes.size() < 2) {
57 JOptionPane.showMessageDialog(
58 Main.parent,
59 tr("Please select at least two nodes to merge."),
60 tr("Warning"),
61 JOptionPane.WARNING_MESSAGE
62 );
63 return;
64 }
65
66 Node targetNode = selectTargetNode(selectedNodes);
67 Command cmd = mergeNodes(Main.main.getEditLayer(), selectedNodes, targetNode);
68 if (cmd != null) {
69 Main.main.undoRedo.add(cmd);
70 Main.main.getEditLayer().data.setSelected(targetNode);
71 }
72 }
73
74 /**
75 * Find which node to merge into (i.e. which one will be left)
76 * The last selected node will become the target node the remaining
77 * nodes are merged to.
78 *
79 * @param candidates the collection of candidate nodes
80 * @return the selected target node
81 */
82 public static Node selectTargetNode(LinkedHashSet<Node> candidates) {
83 Node targetNode = null;
84 for (Node n : candidates) { // pick last one
85 targetNode = n;
86 }
87 return targetNode;
88 }
89
90 /**
91 * Fixes the parent ways referring to one of the nodes.
92 *
93 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
94 * which is referred to by a relation.
95 *
96 * @param nodesToDelete the collection of nodes to be deleted
97 * @param targetNode the target node the other nodes are merged to
98 * @return a list of commands; null, if the ways could not be fixed
99 */
100 protected static List<Command> fixParentWays(Collection<OsmPrimitive> nodesToDelete, Node targetNode) {
101 List<Command> cmds = new ArrayList<Command>();
102 Set<Way> waysToDelete = new HashSet<Way>();
103
104 for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) {
105 ArrayList<Node> newNodes = new ArrayList<Node>(w.getNodesCount());
106 for (Node n: w.getNodes()) {
107 if (! nodesToDelete.contains(n) && n != targetNode) {
108 newNodes.add(n);
109 } else if (newNodes.isEmpty()) {
110 newNodes.add(targetNode);
111 } else if (newNodes.get(newNodes.size()-1) != targetNode) {
112 // make sure we collapse a sequence of deleted nodes
113 // to exactly one occurrence of the merged target node
114 //
115 newNodes.add(targetNode);
116 } else {
117 // drop the node
118 }
119 }
120 if (newNodes.size() < 2) {
121 if (w.getReferrers().isEmpty()) {
122 waysToDelete.add(w);
123 } else {
124 ButtonSpec[] options = new ButtonSpec[] {
125 new ButtonSpec(
126 tr("Abort Merging"),
127 ImageProvider.get("cancel"),
128 tr("Click to abort merging nodes"),
129 null /* no special help topic */
130 )
131 };
132 HelpAwareOptionPane.showOptionDialog(
133 Main.parent,
134 tr(
135 "Cannot merge nodes: Would have to delete way ''{0}'' which is still used.",
136 w.getDisplayName(DefaultNameFormatter.getInstance())
137 ),
138 tr("Warning"),
139 JOptionPane.WARNING_MESSAGE,
140 null, /* no icon */
141 options,
142 options[0],
143 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
144 );
145 return null;
146 }
147 } else if(newNodes.size() < 2 && w.getReferrers().isEmpty()) {
148 waysToDelete.add(w);
149 } else {
150 Way newWay = new Way(w);
151 newWay.setNodes(newNodes);
152 cmds.add(new ChangeCommand(w, newWay));
153 }
154 }
155 if (!waysToDelete.isEmpty()) {
156 cmds.add(new DeleteCommand(waysToDelete));
157 }
158 return cmds;
159 }
160
161 /**
162 * Merges the nodes in <code>nodes</code> onto one of the nodes. Uses the dataset
163 * managed by <code>layer</code> as reference.
164 *
165 * @param layer layer the reference data layer. Must not be null.
166 * @param nodes the collection of nodes. Ignored if null.
167 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
168 * @throw IllegalArgumentException thrown if layer is null
169 */
170 public static Command mergeNodes(OsmDataLayer layer,Collection<Node> nodes, Node targetNode) {
171 if (layer == null)
172 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null.", "nodes"));
173 if (targetNode == null)
174 throw new IllegalArgumentException(tr("Parameter ''{0}'' must not be null.", "targetNode"));
175 if (nodes == null)
176 return null;
177
178
179 Set<RelationToChildReference> relationToNodeReferences = RelationToChildReference.getRelationToChildReferences(nodes);
180
181 // build the tag collection
182 //
183 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
184 combineTigerTags(nodeTags);
185 normalizeTagCollectionBeforeEditing(nodeTags, nodes);
186 TagCollection nodeTagsToEdit = new TagCollection(nodeTags);
187 completeTagCollectionForEditing(nodeTagsToEdit);
188
189 // launch a conflict resolution dialog, if necessary
190 //
191 CombinePrimitiveResolverDialog dialog = CombinePrimitiveResolverDialog.getInstance();
192 dialog.getTagConflictResolverModel().populate(nodeTagsToEdit, nodeTags.getKeysWithMultipleValues());
193 dialog.getRelationMemberConflictResolverModel().populate(relationToNodeReferences);
194 dialog.setTargetPrimitive(targetNode);
195 dialog.prepareDefaultDecisions();
196 // conflict resolution is necessary if there are conflicts in the merged tags
197 // or if at least one of the merged nodes is referred to by a relation
198 //
199 if (! nodeTags.isApplicableToPrimitive() || relationToNodeReferences.size() > 1) {
200 dialog.setVisible(true);
201 if (dialog.isCancelled())
202 return null;
203 }
204 LinkedList<Command> cmds = new LinkedList<Command>();
205
206 // the nodes we will have to delete
207 //
208 Collection<OsmPrimitive> nodesToDelete = new HashSet<OsmPrimitive>(nodes);
209 nodesToDelete.remove(targetNode);
210
211 // fix the ways referring to at least one of the merged nodes
212 //
213 Collection<Way> waysToDelete= new HashSet<Way>();
214 List<Command> wayFixCommands = fixParentWays(
215 nodesToDelete,
216 targetNode
217 );
218 if (wayFixCommands == null)
219 return null;
220 cmds.addAll(wayFixCommands);
221
222 // build the commands
223 //
224 if (!nodesToDelete.isEmpty()) {
225 cmds.add(new DeleteCommand(nodesToDelete));
226 }
227 if (!waysToDelete.isEmpty()) {
228 cmds.add(new DeleteCommand(waysToDelete));
229 }
230 cmds.addAll(dialog.buildResolutionCommands());
231 Command cmd = new SequenceCommand(tr("Merge {0} nodes", nodes.size()), cmds);
232 return cmd;
233 }
234
235 @Override
236 protected void updateEnabledState() {
237 if (getCurrentDataSet() == null) {
238 setEnabled(false);
239 } else {
240 updateEnabledState(getCurrentDataSet().getSelected());
241 }
242 }
243
244 @Override
245 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
246 if (selection == null || selection.isEmpty()) {
247 setEnabled(false);
248 return;
249 }
250 boolean ok = true;
251 if (selection.size() < 2) {
252 setEnabled(false);
253 return;
254 }
255 for (OsmPrimitive osm : selection) {
256 if (!(osm instanceof Node)) {
257 ok = false;
258 break;
259 }
260 }
261 setEnabled(ok);
262 }
263}
Note: See TracBrowser for help on using the repository browser.