// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.utilsplugin2.actions;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.JosmAction;
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;
/**
* Pastes relation membership from objects in the paste buffer onto selected object(s).
*
* @author Zverik
*/
public class AlignWayNodesAction extends JosmAction {
private static final String TITLE = tr("Align Way Nodes");
private static final double MOVE_THRESHOLD = 1e-9;
/**
* Constructs a new {@code AlignWayNodesAction}.
*/
public AlignWayNodesAction() {
super(TITLE, "dumbutils/alignwaynodes", tr("Align nodes in a way"),
Shortcut.registerShortcut("tools:alignwaynodes", tr("Tool: {0}", tr("Align Way Nodes")), KeyEvent.VK_L, Shortcut.SHIFT),
true);
}
@Override
public void actionPerformed(ActionEvent e) {
Collection<OsmPrimitive> selection = getLayerManager().getEditDataSet().getSelected();
Set<Node> selectedNodes = filterNodes(selection);
int selectedNodesCount = selectedNodes.size();
Set<Way> ways = findCommonWays(selectedNodes);
if (ways == null || ways.size() != 1 || selectedNodesCount == 0)
return;
Way way = ways.iterator().next();
if (way.getNodesCount() < (way.isClosed() ? 4 : 3)) {
new Notification(tr("The way with selected nodes can not be straightened."))
.setIcon(JOptionPane.ERROR_MESSAGE).show();
return;
}
// Prepare a list of nodes to align
int firstNodePos = findFirstNode(way, selectedNodes);
int lastNodePos = way.isClosed() ? firstNodePos : way.getNodesCount();
List<Node> nodes = new ArrayList<>();
int i = firstNodePos;
boolean iterated = false;
while (!iterated || i != lastNodePos) {
Node node = way.getNode(i);
if (selectedNodes.contains(node)) {
nodes.add(node);
selectedNodes.remove(node);
if (selectedNodesCount == 1) {
nodes.add(0, way.getNode(i > 0 ? i - 1 : way.isClosed() ? way.getNodesCount() - 2 : i + 2));
nodes.add(way.getNode(i + 1 < way.getNodesCount() ? i + 1 : way.isClosed() ? 1 : i - 2));
}
if (selectedNodes.isEmpty())
break;
} else if (selectedNodesCount == 2 && selectedNodes.size() == 1)
nodes.add(node);
i++;
if (i >= way.getNodesCount() && way.isClosed())
i = 0;
iterated = true;
}
if (nodes.size() < 3) {
new Notification(tr("Internal error: number of nodes is {0}.", nodes.size()))
.setIcon(JOptionPane.ERROR_MESSAGE).show();
return;
}
// Now, we have an ordered list of nodes, of which idx 0 and N-1 serve as guides
// and 1..N-2 should be aligned with them
List<Command> commands = new ArrayList<>();
double ax = nodes.get(0).getEastNorth().east();
double ay = nodes.get(0).getEastNorth().north();
double bx = nodes.get(nodes.size() - 1).getEastNorth().east();
double by = nodes.get(nodes.size() - 1).getEastNorth().north();
for (i = 1; i + 1 < nodes.size(); i++) {
Node n = nodes.get(i);
// Algorithm is copied from org.openstreetmap.josm.actions.AlignInLineAction
double nx = n.getEastNorth().east();
double ny = n.getEastNorth().north();
if (ax == bx) {
// Special case if AB is vertical...
nx = ax;
} else if (ay == by) {
// ...or horizontal
ny = ay;
} else {
// Otherwise calculate position by solving y=mx+c (simplified)
double m1 = (by - ay) / (bx - ax);
double c1 = ay - (ax * m1);
double m2 = (-1) / m1;
double c2 = ny - (nx * m2);
nx = (c2 - c1) / (m1 - m2);
ny = (m1 * nx) + c1;
}
// Add the command to move the node to its new position.
if (Math.abs(nx - n.getEastNorth().east()) > MOVE_THRESHOLD && Math.abs(ny - n.getEastNorth().north()) > MOVE_THRESHOLD)
commands.add(new MoveCommand(n, nx - n.getEastNorth().east(), ny - n.getEastNorth().north()));
}
if (!commands.isEmpty())
Main.main.undoRedo.add(new SequenceCommand(TITLE, commands));
}
@Override
protected void updateEnabledState() {
updateEnabledStateOnCurrentSelection();
}
@Override
protected void updateEnabledState(Collection<? extends OsmPrimitive> selection) {
Set<Node> nodes = filterNodes(selection);
Set<Way> ways = findCommonWays(nodes);
setEnabled(ways != null && ways.size() == 1 && !nodes.isEmpty());
}
private Set<Way> findCommonWays(Set<Node> nodes) {
Set<Way> ways = null;
for (Node n : nodes) {
List<Way> referrers = OsmPrimitive.getFilteredList(n.getReferrers(), Way.class);
if (ways == null)
ways = new HashSet<>(referrers);
else {
ways.retainAll(referrers);
}
}
return ways;
}
private Set<Node> filterNodes(Collection<? extends OsmPrimitive> selection) {
Set<Node> result = new HashSet<>();
if (selection != null) {
for (OsmPrimitive p : selection) {
if (p instanceof Node)
result.add((Node) p);
}
}
return result;
}
/**
* Find the largest empty span between nodes and returns the index of the node right after it.
*
* TODO: not the maximum node count, but maximum distance!
*/
private int findFirstNode(Way way, Set<Node> nodes) {
int pos = 0;
while (pos < way.getNodesCount() && !nodes.contains(way.getNode(pos))) {
pos++;
}
if (pos >= way.getNodesCount())
return 0;
if (!way.isClosed() || nodes.size() <= 1)
return pos;
// now, way is closed
boolean fullCircle = false;
int maxLength = 0;
int lastPos = 0;
while (!fullCircle) {
int length = 0;
boolean skippedFirst = false;
while (!(skippedFirst && nodes.contains(way.getNode(pos)))) {
skippedFirst = true;
length++;
pos++;
if (pos >= way.getNodesCount()) {
pos = 0;
fullCircle = true;
}
}
if (length > maxLength) {
maxLength = length;
lastPos = pos;
}
}
return lastPos;
}
}