// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.actions; import static org.openstreetmap.josm.gui.help.HelpUtil.ht; import static org.openstreetmap.josm.tools.I18n.tr; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; import javax.swing.JOptionPane; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.command.Command; import org.openstreetmap.josm.command.MoveCommand; import org.openstreetmap.josm.command.SequenceCommand; import org.openstreetmap.josm.data.osm.Node; import org.openstreetmap.josm.data.osm.OsmPrimitive; import org.openstreetmap.josm.data.osm.Way; import org.openstreetmap.josm.gui.Notification; import org.openstreetmap.josm.tools.Shortcut; /** * Distributes the selected nodes to equal distances along a line. * * @author Teemu Koskinen */ public final class DistributeAction extends JosmAction { /** * Constructs a new {@code DistributeAction}. */ public DistributeAction() { super(tr("Distribute Nodes"), "distribute", tr("Distribute the selected nodes to equal distances along a line."), Shortcut.registerShortcut("tools:distribute", tr("Tool: {0}", tr("Distribute Nodes")), KeyEvent.VK_B, Shortcut.SHIFT), true); putValue("help", ht("/Action/DistributeNodes")); } /** * Perform action. * Select method according to user selection. * Case 1: One Way (no self-crossing) and at most 2 nodes contains by this way: * Distribute nodes keeping order along the way * Case 2: Other * Distribute nodes */ @Override public void actionPerformed(ActionEvent e) { if (!isEnabled()) return; // Collect user selected objects Collection selected = getCurrentDataSet().getSelected(); Collection ways = new LinkedList<>(); Collection nodes = new HashSet<>(); for (OsmPrimitive osm : selected) { if (osm instanceof Node) { nodes.add((Node) osm); } else if (osm instanceof Way) { ways.add((Way) osm); } } Set ignoredNodes = removeNodesWithoutCoordinates(nodes); if (!ignoredNodes.isEmpty()) { Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size())); ignoredNodes.clear(); } // Switch between algorithms Collection cmds; if (checkDistributeWay(ways, nodes)) { cmds = distributeWay(ways, nodes); } else if (checkDistributeNodes(ways, nodes)) { cmds = distributeNodes(nodes); } else { new Notification( tr("Please select :\n" + "* One no self-crossing way with at most two of its nodes;\n" + "* Three nodes.")) .setIcon(JOptionPane.INFORMATION_MESSAGE) .setDuration(Notification.TIME_SHORT) .show(); return; } if (cmds.isEmpty()) { return; } // Do it! Main.main.undoRedo.add(new SequenceCommand(tr("Distribute Nodes"), cmds)); Main.map.repaint(); } /** * Test if one way, no self-crossing, is selected with at most two of its nodes. * @param ways Selected ways * @param nodes Selected nodes * @return true in this case */ private static boolean checkDistributeWay(Collection ways, Collection nodes) { if (ways.size() == 1 && nodes.size() <= 2) { Way w = ways.iterator().next(); Set unduplicated = new HashSet<>(w.getNodes()); if (unduplicated.size() != w.getNodesCount()) { // No self crossing way return false; } for (Node node: nodes) { if (!w.containsNode(node)) { return false; } } return true; } return false; } /** * Distribute nodes contained by a way, keeping nodes order. * If one or two nodes are selected, keep these nodes in place. * @param ways Selected ways, must be collection of size 1. * @param nodes Selected nodes, at most two nodes. * @return Collection of command to be executed. */ private static Collection distributeWay(Collection ways, Collection nodes) { Way w = ways.iterator().next(); Collection cmds = new LinkedList<>(); if (w.getNodesCount() == nodes.size() || w.getNodesCount() <= 2) { // Nothing to do return cmds; } double xa, ya; // Start point double dx, dy; // Segment increment if (nodes.isEmpty()) { Node na = w.firstNode(); nodes.add(na); Node nb = w.lastNode(); nodes.add(nb); xa = na.getEastNorth().east(); ya = na.getEastNorth().north(); dx = (nb.getEastNorth().east() - xa) / (w.getNodesCount() - 1); dy = (nb.getEastNorth().north() - ya) / (w.getNodesCount() - 1); } else if (nodes.size() == 1) { Node n = nodes.iterator().next(); int nIdx = w.getNodes().indexOf(n); Node na = w.firstNode(); Node nb = w.lastNode(); dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / (w.getNodesCount() - 1); dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / (w.getNodesCount() - 1); xa = n.getEastNorth().east() - dx * nIdx; ya = n.getEastNorth().north() - dy * nIdx; } else { Iterator it = nodes.iterator(); Node na = it.next(); Node nb = it.next(); List wayNodes = w.getNodes(); int naIdx = wayNodes.indexOf(na); int nbIdx = wayNodes.indexOf(nb); dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / (nbIdx - naIdx); dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / (nbIdx - naIdx); xa = na.getEastNorth().east() - dx * naIdx; ya = na.getEastNorth().north() - dy * naIdx; } for (int i = 0; i < w.getNodesCount(); i++) { Node n = w.getNode(i); if (!n.isLatLonKnown() || nodes.contains(n)) { continue; } double x = xa + i * dx; double y = ya + i * dy; cmds.add(new MoveCommand(n, x - n.getEastNorth().east(), y - n.getEastNorth().north())); } return cmds; } /** * Test if nodes oriented algorithm applies to the selection. * @param ways Selected ways * @param nodes Selected nodes * @return true in this case */ private static Boolean checkDistributeNodes(Collection ways, Collection nodes) { return ways.isEmpty() && nodes.size() >= 3; } /** * Distribute nodes when only nodes are selected. * The general algorithm here is to find the two selected nodes * that are furthest apart, and then to distribute all other selected * nodes along the straight line between these nodes. * @param nodes nodes to distribute * @return Commands to execute to perform action * @throws IllegalArgumentException if nodes is empty */ private static Collection distributeNodes(Collection nodes) { // Find from the selected nodes two that are the furthest apart. // Let's call them A and B. double distance = 0; Node nodea = null; Node nodeb = null; Collection itnodes = new LinkedList<>(nodes); for (Node n : nodes) { itnodes.remove(n); for (Node m : itnodes) { double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth())); if (dist > distance) { nodea = n; nodeb = m; distance = dist; } } } if (nodea == null || nodeb == null) { throw new IllegalArgumentException(); } // Remove the nodes A and B from the list of nodes to move nodes.remove(nodea); nodes.remove(nodeb); // Find out co-ords of A and B double ax = nodea.getEastNorth().east(); double ay = nodea.getEastNorth().north(); double bx = nodeb.getEastNorth().east(); double by = nodeb.getEastNorth().north(); // A list of commands to do Collection cmds = new LinkedList<>(); // Amount of nodes between A and B plus 1 int num = nodes.size()+1; // Current number of node int pos = 0; while (!nodes.isEmpty()) { pos++; Node s = null; // Find the node that is furthest from B (i.e. closest to A) distance = 0.0; for (Node n : nodes) { double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth())); if (dist > distance) { s = n; distance = dist; } } if (s != null) { // First move the node to A's position, then move it towards B double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num; double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num; cmds.add(new MoveCommand(s, dx, dy)); //remove moved node from the list nodes.remove(s); } } return cmds; } /** * Remove nodes without knowned coordinates from a collection. * @param col Collection of nodes to check * @return Set of nodes without coordinates */ private static Set removeNodesWithoutCoordinates(Collection col) { Set result = new HashSet<>(); for (Iterator it = col.iterator(); it.hasNext();) { Node n = it.next(); if (!n.isLatLonKnown()) { it.remove(); result.add(n); } } return result; } @Override protected void updateEnabledState() { if (getCurrentDataSet() == null) { setEnabled(false); } else { updateEnabledState(getCurrentDataSet().getSelected()); } } @Override protected void updateEnabledState(Collection selection) { setEnabled(selection != null && !selection.isEmpty()); } }