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.tr; 006 007import java.awt.event.ActionEvent; 008import java.awt.event.KeyEvent; 009import java.util.Collection; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.LinkedList; 013import java.util.Set; 014 015import javax.swing.JOptionPane; 016 017import org.openstreetmap.josm.Main; 018import org.openstreetmap.josm.command.Command; 019import org.openstreetmap.josm.command.MoveCommand; 020import org.openstreetmap.josm.command.SequenceCommand; 021import org.openstreetmap.josm.data.osm.Node; 022import org.openstreetmap.josm.data.osm.OsmPrimitive; 023import org.openstreetmap.josm.data.osm.Way; 024import org.openstreetmap.josm.gui.Notification; 025import org.openstreetmap.josm.tools.Shortcut; 026 027/** 028 * Distributes the selected nodes to equal distances along a line. 029 * 030 * @author Teemu Koskinen 031 */ 032public final class DistributeAction extends JosmAction { 033 034 /** 035 * Constructs a new {@code DistributeAction}. 036 */ 037 public DistributeAction() { 038 super(tr("Distribute Nodes"), "distribute", tr("Distribute the selected nodes to equal distances along a line."), 039 Shortcut.registerShortcut("tools:distribute", tr("Tool: {0}", tr("Distribute Nodes")), KeyEvent.VK_B, 040 Shortcut.SHIFT), true); 041 putValue("help", ht("/Action/DistributeNodes")); 042 } 043 044 /** 045 * The general algorithm here is to find the two selected nodes 046 * that are furthest apart, and then to distribute all other selected 047 * nodes along the straight line between these nodes. 048 */ 049 @Override 050 public void actionPerformed(ActionEvent e) { 051 if (!isEnabled()) 052 return; 053 Collection<OsmPrimitive> sel = getCurrentDataSet().getSelected(); 054 Collection<Node> nodes = new LinkedList<>(); 055 Collection<Node> itnodes = new LinkedList<>(); 056 for (OsmPrimitive osm : sel) 057 if (osm instanceof Node) { 058 nodes.add((Node)osm); 059 itnodes.add((Node)osm); 060 } 061 // special case if no single nodes are selected and exactly one way is: 062 // then use the way's nodes 063 if (nodes.isEmpty() && (sel.size() == 1)) { 064 for (OsmPrimitive osm : sel) 065 if (osm instanceof Way) { 066 nodes.addAll(((Way)osm).getNodes()); 067 itnodes.addAll(((Way)osm).getNodes()); 068 } 069 } 070 071 Set<Node> ignoredNodes = removeNodesWithoutCoordinates(nodes); 072 ignoredNodes.addAll(removeNodesWithoutCoordinates(itnodes)); 073 if (!ignoredNodes.isEmpty()) { 074 Main.warn(tr("Ignoring {0} nodes with null coordinates", ignoredNodes.size())); 075 ignoredNodes.clear(); 076 } 077 078 if (nodes.size() < 3) { 079 new Notification( 080 tr("Please select at least three nodes.")) 081 .setIcon(JOptionPane.INFORMATION_MESSAGE) 082 .setDuration(Notification.TIME_SHORT) 083 .show(); 084 return; 085 } 086 087 // Find from the selected nodes two that are the furthest apart. 088 // Let's call them A and B. 089 double distance = 0; 090 091 Node nodea = null; 092 Node nodeb = null; 093 094 for (Node n : nodes) { 095 itnodes.remove(n); 096 for (Node m : itnodes) { 097 double dist = Math.sqrt(n.getEastNorth().distance(m.getEastNorth())); 098 if (dist > distance) { 099 nodea = n; 100 nodeb = m; 101 distance = dist; 102 } 103 } 104 } 105 106 // Remove the nodes A and B from the list of nodes to move 107 nodes.remove(nodea); 108 nodes.remove(nodeb); 109 110 // Find out co-ords of A and B 111 double ax = nodea.getEastNorth().east(); 112 double ay = nodea.getEastNorth().north(); 113 double bx = nodeb.getEastNorth().east(); 114 double by = nodeb.getEastNorth().north(); 115 116 // A list of commands to do 117 Collection<Command> cmds = new LinkedList<>(); 118 119 // Amount of nodes between A and B plus 1 120 int num = nodes.size()+1; 121 122 // Current number of node 123 int pos = 0; 124 while (!nodes.isEmpty()) { 125 pos++; 126 Node s = null; 127 128 // Find the node that is furthest from B (i.e. closest to A) 129 distance = 0.0; 130 for (Node n : nodes) { 131 double dist = Math.sqrt(nodeb.getEastNorth().distance(n.getEastNorth())); 132 if (dist > distance) { 133 s = n; 134 distance = dist; 135 } 136 } 137 138 // First move the node to A's position, then move it towards B 139 double dx = ax - s.getEastNorth().east() + (bx-ax)*pos/num; 140 double dy = ay - s.getEastNorth().north() + (by-ay)*pos/num; 141 142 cmds.add(new MoveCommand(s, dx, dy)); 143 144 //remove moved node from the list 145 nodes.remove(s); 146 } 147 148 // Do it! 149 Main.main.undoRedo.add(new SequenceCommand(tr("Distribute Nodes"), cmds)); 150 Main.map.repaint(); 151 } 152 153 private Set<Node> removeNodesWithoutCoordinates(Collection<Node> col) { 154 Set<Node> result = new HashSet<>(); 155 for (Iterator<Node> it = col.iterator(); it.hasNext();) { 156 Node n = it.next(); 157 if (n.getCoor() == null) { 158 it.remove(); 159 result.add(n); 160 } 161 } 162 return result; 163 } 164 165 @Override 166 protected void updateEnabledState() { 167 if (getCurrentDataSet() == null) { 168 setEnabled(false); 169 } else { 170 updateEnabledState(getCurrentDataSet().getSelected()); 171 } 172 } 173 174 @Override 175 protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) { 176 setEnabled(selection != null && !selection.isEmpty()); 177 } 178}