001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.actions; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.marktr; 006import static org.openstreetmap.josm.tools.I18n.tr; 007 008import java.awt.event.ActionEvent; 009import java.awt.event.KeyEvent; 010import java.util.Collection; 011import java.util.HashSet; 012import java.util.Iterator; 013import java.util.LinkedList; 014import java.util.List; 015import java.util.Set; 016 017import javax.swing.JOptionPane; 018 019import org.openstreetmap.josm.Main; 020import org.openstreetmap.josm.command.Command; 021import org.openstreetmap.josm.command.MoveCommand; 022import org.openstreetmap.josm.command.SequenceCommand; 023import org.openstreetmap.josm.data.osm.Node; 024import org.openstreetmap.josm.data.osm.OsmPrimitive; 025import org.openstreetmap.josm.data.osm.Way; 026import org.openstreetmap.josm.gui.Notification; 027import org.openstreetmap.josm.tools.Shortcut; 028 029/** 030 * Distributes the selected nodes to equal distances along a line. 031 * 032 * @author Teemu Koskinen 033 */ 034public final class DistributeAction extends JosmAction { 035 036 private static final String ACTION_SHORT_NAME = marktr("Distribute Nodes"); 037 038 /** 039 * Constructs a new {@code DistributeAction}. 040 */ 041 public DistributeAction() { 042 super(tr(ACTION_SHORT_NAME), "distribute", 043 tr("Distribute the selected nodes to equal distances along a line."), 044 Shortcut.registerShortcut("tools:distribute", 045 tr("Tool: {0}", tr(ACTION_SHORT_NAME)), 046 KeyEvent.VK_B, Shortcut.SHIFT), 047 true); 048 putValue("help", ht("/Action/DistributeNodes")); 049 } 050 051 /** 052 * Perform action. 053 * Select method according to user selection. 054 * Case 1: One Way (no self-crossing) and at most 2 nodes contains by this way: 055 * Distribute nodes keeping order along the way 056 * Case 2: Other 057 * Distribute nodes 058 */ 059 @Override 060 public void actionPerformed(ActionEvent e) { 061 if (!isEnabled()) 062 return; 063 064 // Collect user selected objects 065 Collection<OsmPrimitive> selected = getCurrentDataSet().getSelected(); 066 Collection<Way> ways = new LinkedList<>(); 067 Collection<Node> nodes = new HashSet<>(); 068 for (OsmPrimitive osm : selected) { 069 if (osm instanceof Node) { 070 nodes.add((Node) osm); 071 } else if (osm instanceof Way) { 072 ways.add((Way) osm); 073 } 074 } 075 076 Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes); 077 if (!ignoredNodes.isEmpty()) { 078 Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size())); 079 ignoredNodes.clear(); 080 } 081 082 // Switch between algorithms 083 Collection<Command> cmds; 084 if (checkDistributeWay(ways, nodes)) { 085 cmds = distributeWay(ways, nodes); 086 } else if (checkDistributeNodes(ways, nodes)) { 087 cmds = distributeNodes(nodes); 088 } else { 089 new Notification( 090 tr("Please select :\n" + 091 "* One no self-crossing way with at most two of its nodes;\n" + 092 "* Three nodes.")) 093 .setIcon(JOptionPane.INFORMATION_MESSAGE) 094 .setDuration(Notification.TIME_SHORT) 095 .show(); 096 return; 097 } 098 099 if (cmds.isEmpty()) { 100 return; 101 } 102 103 // Do it! 104 Main.main.undoRedo.add(new SequenceCommand(tr(ACTION_SHORT_NAME), cmds)); 105 Main.map.repaint(); 106 } 107 108 /** 109 * Test if one way, no self-crossing, is selected with at most two of its nodes. 110 * @param ways Selected ways 111 * @param nodes Selected nodes 112 * @return true in this case 113 */ 114 private static boolean checkDistributeWay(Collection<Way> ways, Collection<Node> nodes) { 115 if (ways.size() == 1 && nodes.size() <= 2) { 116 Way w = ways.iterator().next(); 117 Set<Node> unduplicated = new HashSet<>(w.getNodes()); 118 if (unduplicated.size() != w.getNodesCount()) { 119 // No self crossing way 120 return false; 121 } 122 for (Node node: nodes) { 123 if (!w.containsNode(node)) { 124 return false; 125 } 126 } 127 return true; 128 } 129 return false; 130 } 131 132 /** 133 * Distribute nodes contained by a way, keeping nodes order. 134 * If one or two nodes are selected, keep these nodes in place. 135 * @param ways Selected ways, must be collection of size 1. 136 * @param nodes Selected nodes, at most two nodes. 137 * @return Collection of command to be executed. 138 */ 139 private static Collection<Command> distributeWay(Collection<Way> ways, 140 Collection<Node> nodes) { 141 Way w = ways.iterator().next(); 142 Collection<Command> cmds = new LinkedList<>(); 143 144 if (w.getNodesCount() == nodes.size() || w.getNodesCount() <= 2) { 145 // Nothing to do 146 return cmds; 147 } 148 149 double xa, ya; // Start point 150 double dx, dy; // Segment increment 151 if (nodes.isEmpty()) { 152 Node na = w.firstNode(); 153 nodes.add(na); 154 Node nb = w.lastNode(); 155 nodes.add(nb); 156 xa = na.getEastNorth().east(); 157 ya = na.getEastNorth().north(); 158 dx = (nb.getEastNorth().east() - xa) / (w.getNodesCount() - 1); 159 dy = (nb.getEastNorth().north() - ya) / (w.getNodesCount() - 1); 160 } else if (nodes.size() == 1) { 161 Node n = nodes.iterator().next(); 162 int nIdx = w.getNodes().indexOf(n); 163 Node na = w.firstNode(); 164 Node nb = w.lastNode(); 165 dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / 166 (w.getNodesCount() - 1); 167 dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / 168 (w.getNodesCount() - 1); 169 xa = n.getEastNorth().east() - dx * nIdx; 170 ya = n.getEastNorth().north() - dy * nIdx; 171 } else { 172 Iterator<Node> it = nodes.iterator(); 173 Node na = it.next(); 174 Node nb = it.next(); 175 List<Node> wayNodes = w.getNodes(); 176 int naIdx = wayNodes.indexOf(na); 177 int nbIdx = wayNodes.indexOf(nb); 178 dx = (nb.getEastNorth().east() - na.getEastNorth().east()) / (nbIdx - naIdx); 179 dy = (nb.getEastNorth().north() - na.getEastNorth().north()) / (nbIdx - naIdx); 180 xa = na.getEastNorth().east() - dx * naIdx; 181 ya = na.getEastNorth().north() - dy * naIdx; 182 } 183 184 for (int i = 0; i < w.getNodesCount(); i++) { 185 Node n = w.getNode(i); 186 if (!n.isLatLonKnown() || nodes.contains(n)) { 187 continue; 188 } 189 double x = xa + i * dx; 190 double y = ya + i * dy; 191 cmds.add(new MoveCommand(n, x - n.getEastNorth().east(), 192 y - n.getEastNorth().north())); 193 } 194 return cmds; 195 } 196 197 /** 198 * Test if nodes oriented algorithm applies to the selection. 199 * @param ways Selected ways 200 * @param nodes Selected nodes 201 * @return true in this case 202 */ 203 private static Boolean checkDistributeNodes(Collection<Way> ways, Collection<Node> nodes) { 204 return ways.isEmpty() && nodes.size() >= 3; 205 } 206 207 /** 208 * Distribute nodes when only nodes are selected. 209 * The general algorithm here is to find the two selected nodes 210 * that are furthest apart, and then to distribute all other selected 211 * nodes along the straight line between these nodes. 212 * @param nodes nodes to distribute 213 * @return Commands to execute to perform action 214 */ 215 private static Collection<Command> distributeNodes(Collection<Node> nodes) { 216 // Find from the selected nodes two that are the furthest apart. 217 // Let's call them A and B. 218 double distance = 0; 219 220 Node nodea = null; 221 Node nodeb = null; 222 223 Collection<Node> itnodes = new LinkedList<>(nodes); 224 for (Node n : nodes) { 225 itnodes.remove(n); 226 for (Node m : itnodes) { 227 double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth())); 228 if (dist > distance) { 229 nodea = n; 230 nodeb = m; 231 distance = dist; 232 } 233 } 234 } 235 236 // Remove the nodes A and B from the list of nodes to move 237 nodes.remove(nodea); 238 nodes.remove(nodeb); 239 240 // Find out co-ords of A and B 241 double ax = nodea.getEastNorth().east(); 242 double ay = nodea.getEastNorth().north(); 243 double bx = nodeb.getEastNorth().east(); 244 double by = nodeb.getEastNorth().north(); 245 246 // A list of commands to do 247 Collection<Command> cmds = new LinkedList<>(); 248 249 // Amount of nodes between A and B plus 1 250 int num = nodes.size()+1; 251 252 // Current number of node 253 int pos = 0; 254 while (!nodes.isEmpty()) { 255 pos++; 256 Node s = null; 257 258 // Find the node that is furthest from B (i.e. closest to A) 259 distance = 0.0; 260 for (Node n : nodes) { 261 double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth())); 262 if (dist > distance) { 263 s = n; 264 distance = dist; 265 } 266 } 267 268 // First move the node to A's position, then move it towards B 269 double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num; 270 double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num; 271 272 cmds.add(new MoveCommand(s, dx, dy)); 273 274 //remove moved node from the list 275 nodes.remove(s); 276 } 277 278 return cmds; 279 } 280 281 /** 282 * Remove nodes without knowned coordinates from a collection. 283 * @param col Collection of nodes to check 284 * @return Set of nodes without coordinates 285 */ 286 private static Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) { 287 Set<Node> result = new HashSet<>(); 288 for (Iterator<Node> it = col.iterator(); it.hasNext();) { 289 Node n = it.next(); 290 if (!n.isLatLonKnown()) { 291 it.remove(); 292 result.add(n); 293 } 294 } 295 return result; 296 } 297 298 @Override 299 protected void updateEnabledState() { 300 if (getCurrentDataSet() == null) { 301 setEnabled(false); 302 } else { 303 updateEnabledState(getCurrentDataSet().getSelected()); 304 } 305 } 306 307 @Override 308 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 309 setEnabled(selection != null && !selection.isEmpty()); 310 } 311}