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

Last change on this file since 13842 was 13842, checked in by Don-vip, 6 years ago

see #16319 - scale properly all icons using ButtonSpec

  • Property svn:eol-style set to native
File size: 15.2 KB
RevLine 
[8378]1// License: GPL. For details, see LICENSE file.
[422]2package org.openstreetmap.josm.actions;
3
[2315]4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
[422]5import static org.openstreetmap.josm.tools.I18n.tr;
[6507]6import static org.openstreetmap.josm.tools.I18n.trn;
[422]7
8import java.awt.event.ActionEvent;
9import java.awt.event.KeyEvent;
[582]10import java.util.ArrayList;
[422]11import java.util.Collection;
[5132]12import java.util.Collections;
[582]13import java.util.HashSet;
[422]14import java.util.LinkedList;
[2113]15import java.util.List;
[10795]16import java.util.Objects;
[11553]17import java.util.Optional;
[422]18import java.util.Set;
19
20import javax.swing.JOptionPane;
21
22import org.openstreetmap.josm.Main;
23import org.openstreetmap.josm.command.ChangeCommand;
[3142]24import org.openstreetmap.josm.command.ChangeNodesCommand;
[422]25import org.openstreetmap.josm.command.Command;
26import org.openstreetmap.josm.command.DeleteCommand;
27import org.openstreetmap.josm.command.SequenceCommand;
[4315]28import org.openstreetmap.josm.data.coor.EastNorth;
[3596]29import org.openstreetmap.josm.data.coor.LatLon;
[12663]30import org.openstreetmap.josm.data.osm.DefaultNameFormatter;
[582]31import org.openstreetmap.josm.data.osm.Node;
[422]32import org.openstreetmap.josm.data.osm.OsmPrimitive;
[13611]33import org.openstreetmap.josm.data.osm.OsmUtils;
[2095]34import org.openstreetmap.josm.data.osm.TagCollection;
[422]35import org.openstreetmap.josm.data.osm.Way;
[2315]36import org.openstreetmap.josm.gui.HelpAwareOptionPane;
37import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
[12630]38import org.openstreetmap.josm.gui.MainApplication;
39import org.openstreetmap.josm.gui.MapView;
[6130]40import org.openstreetmap.josm.gui.Notification;
[2095]41import org.openstreetmap.josm.gui.conflict.tags.CombinePrimitiveResolverDialog;
42import org.openstreetmap.josm.gui.layer.OsmDataLayer;
[12846]43import org.openstreetmap.josm.spi.preferences.Config;
[2842]44import org.openstreetmap.josm.tools.CheckParameterUtil;
[2315]45import org.openstreetmap.josm.tools.ImageProvider;
[12620]46import org.openstreetmap.josm.tools.Logging;
[1084]47import org.openstreetmap.josm.tools.Shortcut;
[8919]48import org.openstreetmap.josm.tools.UserCancelException;
[4315]49
[422]50/**
[2095]51 * Merges a collection of nodes into one node.
[2512]52 *
[3134]53 * The "surviving" node will be the one with the lowest positive id.
54 * (I.e. it was uploaded to the server and is the oldest one.)
[3530]55 *
[3134]56 * However we use the location of the node that was selected *last*.
57 * The "surviving" node will be moved to that location if it is
58 * different from the last selected node.
[7509]59 *
[6202]60 * @since 422
[422]61 */
[1820]62public class MergeNodesAction extends JosmAction {
[422]63
[6202]64 /**
65 * Constructs a new {@code MergeNodesAction}.
66 */
[1169]67 public MergeNodesAction() {
68 super(tr("Merge Nodes"), "mergenodes", tr("Merge nodes into the oldest one."),
[4982]69 Shortcut.registerShortcut("tools:mergenodes", tr("Tool: {0}", tr("Merge Nodes")), KeyEvent.VK_M, Shortcut.DIRECT), true);
[3757]70 putValue("help", ht("/Action/MergeNodes"));
[1169]71 }
[422]72
[6084]73 @Override
[1169]74 public void actionPerformed(ActionEvent event) {
[1820]75 if (!isEnabled())
76 return;
[10382]77 Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getAllSelected();
[3713]78 List<Node> selectedNodes = OsmPrimitive.getFilteredList(selection, Node.class);
[12974]79 selectedNodes.removeIf(n -> n.isDeleted() || n.isIncomplete());
[3713]80
81 if (selectedNodes.size() == 1) {
[12630]82 MapView mapView = MainApplication.getMap().mapView;
83 List<Node> nearestNodes = mapView.getNearestNodes(
84 mapView.getPoint(selectedNodes.get(0)), selectedNodes, OsmPrimitive::isUsable);
[3713]85 if (nearestNodes.isEmpty()) {
[6130]86 new Notification(
87 tr("Please select at least two nodes to merge or one node that is close to another node."))
88 .setIcon(JOptionPane.WARNING_MESSAGE)
89 .show();
[3713]90 return;
91 }
92 selectedNodes.addAll(nearestNodes);
[1169]93 }
[422]94
[2095]95 Node targetNode = selectTargetNode(selectedNodes);
[12550]96 if (targetNode != null) {
97 Node targetLocationNode = selectTargetLocationNode(selectedNodes);
[12689]98 Command cmd = mergeNodes(selectedNodes, targetNode, targetLocationNode);
[12550]99 if (cmd != null) {
[12641]100 MainApplication.undoRedo.add(cmd);
[12636]101 getLayerManager().getEditLayer().data.setSelected(targetNode);
[12550]102 }
[2095]103 }
104 }
105
106 /**
[3134]107 * Select the location of the target node after merge.
108 *
109 * @param candidates the collection of candidate nodes
110 * @return the coordinates of this node are later used for the target node
111 */
[3713]112 public static Node selectTargetLocationNode(List<Node> candidates) {
[4315]113 int size = candidates.size();
114 if (size == 0)
115 throw new IllegalArgumentException("empty list");
[11108]116 if (size == 1) // to avoid division by 0 in mode 2
117 return candidates.get(0);
[4315]118
[12846]119 switch (Config.getPref().getInt("merge-nodes.mode", 0)) {
[7025]120 case 0:
[11108]121 return candidates.get(size - 1);
[7025]122 case 1:
[11481]123 double east1 = 0;
124 double north1 = 0;
[4315]125 for (final Node n : candidates) {
[11108]126 EastNorth en = n.getEastNorth();
127 east1 += en.east();
128 north1 += en.north();
[4315]129 }
[3596]130
[7025]131 return new Node(new EastNorth(east1 / size, north1 / size));
132 case 2:
[4315]133 final double[] weights = new double[size];
[3596]134
[4315]135 for (int i = 0; i < size; i++) {
136 final LatLon c1 = candidates.get(i).getCoor();
137 for (int j = i + 1; j < size; j++) {
138 final LatLon c2 = candidates.get(j).getCoor();
139 final double d = c1.distance(c2);
140 weights[i] += d;
141 weights[j] += d;
142 }
143 }
144
[11481]145 double east2 = 0;
146 double north2 = 0;
147 double weight = 0;
[4315]148 for (int i = 0; i < size; i++) {
149 final EastNorth en = candidates.get(i).getEastNorth();
150 final double w = weights[i];
[7025]151 east2 += en.east() * w;
152 north2 += en.north() * w;
[4315]153 weight += w;
154 }
155
[11481]156 if (weight == 0) // to avoid division by 0
157 return candidates.get(0);
158
[7025]159 return new Node(new EastNorth(east2 / weight, north2 / weight));
[4315]160 default:
[7859]161 throw new IllegalStateException("unacceptable merge-nodes.mode");
[4315]162 }
[3134]163 }
[3530]164
[3134]165 /**
[2341]166 * Find which node to merge into (i.e. which one will be left)
[2512]167 *
[2095]168 * @param candidates the collection of candidate nodes
169 * @return the selected target node
170 */
[5216]171 public static Node selectTargetNode(Collection<Node> candidates) {
[5989]172 Node oldestNode = null;
[2095]173 Node targetNode = null;
[3134]174 Node lastNode = null;
175 for (Node n : candidates) {
176 if (!n.isNew()) {
[5989]177 // Among existing nodes, try to keep the oldest used one
178 if (!n.getReferrers().isEmpty()) {
[11109]179 if (targetNode == null || n.getId() < targetNode.getId()) {
[5989]180 targetNode = n;
181 }
[11109]182 } else if (oldestNode == null || n.getId() < oldestNode.getId()) {
[5989]183 oldestNode = n;
[3134]184 }
185 }
186 lastNode = n;
[1169]187 }
[11553]188 return Optional.ofNullable(targetNode).orElse(oldestNode != null ? oldestNode : lastNode);
[1169]189 }
[422]190
[1169]191 /**
[2113]192 * Fixes the parent ways referring to one of the nodes.
[2512]193 *
[2202]194 * Replies null, if the ways could not be fixed, i.e. because a way would have to be deleted
[2113]195 * which is referred to by a relation.
[2512]196 *
[2113]197 * @param nodesToDelete the collection of nodes to be deleted
198 * @param targetNode the target node the other nodes are merged to
[2202]199 * @return a list of commands; null, if the ways could not be fixed
[2113]200 */
[3142]201 protected static List<Command> fixParentWays(Collection<Node> nodesToDelete, Node targetNode) {
[7005]202 List<Command> cmds = new ArrayList<>();
203 Set<Way> waysToDelete = new HashSet<>();
[2113]204
[2565]205 for (Way w: OsmPrimitive.getFilteredList(OsmPrimitive.getReferrer(nodesToDelete), Way.class)) {
[7005]206 List<Node> newNodes = new ArrayList<>(w.getNodesCount());
[2113]207 for (Node n: w.getNodes()) {
[8443]208 if (!nodesToDelete.contains(n) && !n.equals(targetNode)) {
[2113]209 newNodes.add(n);
[11109]210 } else if (newNodes.isEmpty() || !newNodes.get(newNodes.size()-1).equals(targetNode)) {
[2113]211 // make sure we collapse a sequence of deleted nodes
212 // to exactly one occurrence of the merged target node
213 newNodes.add(targetNode);
214 }
[8513]215 // else: drop the node
[2113]216 }
217 if (newNodes.size() < 2) {
[2565]218 if (w.getReferrers().isEmpty()) {
[2113]219 waysToDelete.add(w);
220 } else {
[2315]221 ButtonSpec[] options = new ButtonSpec[] {
222 new ButtonSpec(
223 tr("Abort Merging"),
[13842]224 new ImageProvider("cancel"),
[2315]225 tr("Click to abort merging nodes"),
226 null /* no special help topic */
227 )
228 };
229 HelpAwareOptionPane.showOptionDialog(
[2113]230 Main.parent,
[5059]231 tr("Cannot merge nodes: Would have to delete way {0} which is still used by {1}",
232 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(w),
[9473]233 DefaultNameFormatter.getInstance().formatAsHtmlUnorderedList(w.getReferrers(), 20)),
[2113]234 tr("Warning"),
[2315]235 JOptionPane.WARNING_MESSAGE,
236 null, /* no icon */
237 options,
238 options[0],
239 ht("/Action/MergeNodes#WaysToDeleteStillInUse")
[2113]240 );
241 return null;
242 }
[8510]243 } else if (newNodes.size() < 2 && w.getReferrers().isEmpty()) {
[2113]244 waysToDelete.add(w);
245 } else {
[3142]246 cmds.add(new ChangeNodesCommand(w, newNodes));
[2113]247 }
248 }
[2333]249 if (!waysToDelete.isEmpty()) {
250 cmds.add(new DeleteCommand(waysToDelete));
251 }
[2113]252 return cmds;
253 }
254
[6202]255 /**
256 * Merges the nodes in {@code nodes} at the specified node's location. Uses the dataset
257 * managed by {@code layer} as reference.
258 * @param layer layer the reference data layer. Must not be null
259 * @param nodes the collection of nodes. Ignored if null
260 * @param targetLocationNode this node's location will be used for the target node
[8291]261 * @throws IllegalArgumentException if {@code layer} is null
[6202]262 */
[5265]263 public static void doMergeNodes(OsmDataLayer layer, Collection<Node> nodes, Node targetLocationNode) {
[5216]264 if (nodes == null) {
[5265]265 return;
266 }
[7005]267 Set<Node> allNodes = new HashSet<>(nodes);
[5265]268 allNodes.add(targetLocationNode);
[6202]269 Node target;
270 if (nodes.contains(targetLocationNode) && !targetLocationNode.isNew()) {
271 target = targetLocationNode; // keep existing targetLocationNode as target to avoid unnecessary changes (see #2447)
272 } else {
273 target = selectTargetNode(allNodes);
274 }
[5265]275
[12550]276 if (target != null) {
[12689]277 Command cmd = mergeNodes(nodes, target, targetLocationNode);
[12550]278 if (cmd != null) {
[12641]279 MainApplication.undoRedo.add(cmd);
[12550]280 layer.data.setSelected(target);
281 }
[5265]282 }
283 }
284
[6202]285 /**
[12689]286 * Merges the nodes in {@code nodes} at the specified node's location.
[6202]287 *
288 * @param nodes the collection of nodes. Ignored if null.
289 * @param targetLocationNode this node's location will be used for the targetNode.
290 * @return The command necessary to run in order to perform action, or {@code null} if there is nothing to do
[8291]291 * @throws IllegalArgumentException if {@code layer} is null
[12689]292 * @since 12689
293 */
294 public static Command mergeNodes(Collection<Node> nodes, Node targetLocationNode) {
[5265]295 if (nodes == null) {
[5216]296 return null;
297 }
[7005]298 Set<Node> allNodes = new HashSet<>(nodes);
[5265]299 allNodes.add(targetLocationNode);
[13189]300 Node targetNode = selectTargetNode(allNodes);
301 if (targetNode == null) {
302 return null;
303 }
304 return mergeNodes(nodes, targetNode, targetLocationNode);
[3134]305 }
[3530]306
[2113]307 /**
[12689]308 * Merges the nodes in <code>nodes</code> onto one of the nodes.
[2095]309 *
310 * @param nodes the collection of nodes. Ignored if null.
311 * @param targetNode the target node the collection of nodes is merged to. Must not be null.
[3134]312 * @param targetLocationNode this node's location will be used for the targetNode.
[6202]313 * @return The command necessary to run in order to perform action, or {@code null} if there is nothing to do
[8291]314 * @throws IllegalArgumentException if layer is null
[2095]315 */
[12689]316 public static Command mergeNodes(Collection<Node> nodes, Node targetNode, Node targetLocationNode) {
[2842]317 CheckParameterUtil.ensureParameterNotNull(targetNode, "targetNode");
[5132]318 if (nodes == null) {
[2095]319 return null;
[5132]320 }
[422]321
[5132]322 try {
323 TagCollection nodeTags = TagCollection.unionOfAllPrimitives(nodes);
[422]324
[5132]325 // the nodes we will have to delete
326 //
[7005]327 Collection<Node> nodesToDelete = new HashSet<>(nodes);
[5132]328 nodesToDelete.remove(targetNode);
329
330 // fix the ways referring to at least one of the merged nodes
331 //
[9062]332 List<Command> wayFixCommands = fixParentWays(nodesToDelete, targetNode);
[5132]333 if (wayFixCommands == null) {
[1169]334 return null;
[5132]335 }
[9062]336 List<Command> cmds = new LinkedList<>(wayFixCommands);
[422]337
[5132]338 // build the commands
339 //
[7859]340 if (!targetNode.equals(targetLocationNode)) {
[6275]341 LatLon targetLocationCoor = targetLocationNode.getCoor();
[10795]342 if (!Objects.equals(targetNode.getCoor(), targetLocationCoor)) {
[6275]343 Node newTargetNode = new Node(targetNode);
344 newTargetNode.setCoor(targetLocationCoor);
345 cmds.add(new ChangeCommand(targetNode, newTargetNode));
346 }
[5132]347 }
[9062]348 cmds.addAll(CombinePrimitiveResolverDialog.launchIfNecessary(nodeTags, nodes, Collections.singleton(targetNode)));
[5132]349 if (!nodesToDelete.isEmpty()) {
350 cmds.add(new DeleteCommand(nodesToDelete));
351 }
[6507]352 return new SequenceCommand(/* for correct i18n of plural forms - see #9110 */
[6679]353 trn("Merge {0} node", "Merge {0} nodes", nodes.size(), nodes.size()), cmds);
[5132]354 } catch (UserCancelException ex) {
[12620]355 Logging.trace(ex);
[2113]356 return null;
[3134]357 }
[1169]358 }
[422]359
[1820]360 @Override
[2256]361 protected void updateEnabledState() {
[10548]362 updateEnabledStateOnCurrentSelection();
[2256]363 }
364
365 @Override
366 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
[13611]367 boolean ok = OsmUtils.isOsmCollectionEditable(selection);
368 if (ok) {
369 for (OsmPrimitive osm : selection) {
370 if (!(osm instanceof Node)) {
371 ok = false;
372 break;
373 }
[1169]374 }
375 }
376 setEnabled(ok);
377 }
[422]378}
Note: See TracBrowser for help on using the repository browser.