/**
*
*/
package com.tilusnet.josm.plugins.alignways;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Collections;
import javax.swing.Icon;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.osm.Node;
import org.openstreetmap.josm.data.osm.OsmPrimitive;
import org.openstreetmap.josm.data.osm.WaySegment;
import org.openstreetmap.josm.data.projection.Projections;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* Executes one of the desired geometric actions
* needed to align the ways.
*
* @author tilusnet <tilusnet@gmail.com>
*/
public class AlignWaysCmdKeepLength extends Command {
enum AlignableStatus {
ALGN_VALID,
ALGN_INV_CONNECTED_UNSHARED_PIVOT,
ALGN_INV_OUTSIDE_WORLD,
ALGN_INV_TOOMANY_CONNECTED_WS, // for AlignWaysCmdKeepAngles
ALGN_INV_ANGLE_PRESERVING_CONFLICT, // for AlignWaysCmdKeepAngles
ALGN_INV_XPOINT_FALLSOUT // for AlignWaysCmdKeepAngles
}
final AlignWaysAlgnSegment algnSeg;
/**
* The nodes that will move as result of the aligning.
*/
final Collection<Node> displaceableNodes;
/**
* Maps the alignee nodes with their calculated coordinates.
* Useful for validation.
*/
final Map<Node,EastNorth> calculatedNodes = new HashMap<>();
/**
* Set of nodes that were affected by the last command execution.
*/
Collection<Node> lastAffectedNodes;
/**
* Pivot point
*/
final EastNorth pivot;
/**
* Computed rotation angle to rotate the segment
*
*/
private final double rotationAngle;
/**
* List of all old states of the objects.
*/
final Map<Node, Node> oldNodes = new HashMap<>();
/**
* Creates an AlignWaysRotateCommand.
* TODO Limitation (for now): constructor assumes areSegsAlignable() returns true.
*/
public AlignWaysCmdKeepLength() {
lastAffectedNodes = null;
algnSeg = AlignWaysSegmentMgr.getInstance(Main.map.mapView)
.getAlgnSeg();
WaySegment algnWS = algnSeg.getSegment();
WaySegment refWS = AlignWaysSegmentMgr.getInstance(Main.map.mapView)
.getRefSeg().getSegment();
this.pivot = algnSeg.getCurrPivotCoord();
this.displaceableNodes = algnSeg.getSegmentEndPoints();
EastNorth enRefNode1 = refWS.getFirstNode().getEastNorth();
EastNorth enRefNode2 = refWS.getSecondNode().getEastNorth();
EastNorth enAlgnNode1 = algnWS.getFirstNode().getEastNorth();
EastNorth enAlgnNode2 = algnWS.getSecondNode().getEastNorth();
// Calculate the rotation angle
double refAngle = Math.atan2(enRefNode1.north() - enRefNode2.north(),
enRefNode1.east() - enRefNode2.east());
double algnAngle = Math.atan2(
enAlgnNode1.north() - enAlgnNode2.north(), enAlgnNode1.east()
- enAlgnNode2.east());
rotationAngle = normalise_angle(refAngle - algnAngle);
// Rotate using the rotation matrix known in algebra:
// [ x' ] = [ cos(Phi) -sin(Phi) ] [ x ]
// [ y' ] [ sin(Phi) cos(Phi) ] [ y ]
for (Node n : displaceableNodes) {
double cosPhi = Math.cos(rotationAngle);
double sinPhi = Math.sin(rotationAngle);
EastNorth oldEastNorth = n.getEastNorth();
double x = oldEastNorth.east() - pivot.east();
double y = oldEastNorth.north() - pivot.north();
double nx = cosPhi * x - sinPhi * y + pivot.east();
double ny = sinPhi * x + cosPhi * y + pivot.north();
calculatedNodes.put(n, new EastNorth(nx, ny));
}
/*
* For debug only
* String s = "Ref Angle: " + refAngle + " (" + Math.toDegrees(refAngle)
* + ")\n";
* s += "Algn Angle: " + algnAngle + " (" + Math.toDegrees(algnAngle)
* + ")\n";
* s += "Rotation angle: " + rotationAngle + " ("
* + Math.toDegrees(rotationAngle) + ")";
*/
}
/**
* Helper for actually rotating the nodes.
*
* @param setModified
* - true if rotated nodes should be flagged "modified"
*/
private void rotateNodes(boolean setModified) {
// "Backup" state
lastAffectedNodes = new HashSet<>();
for (Node n : this.displaceableNodes) {
Node nodeBackup = new Node(n);
// Set other fields that clone doesn't copy
nodeBackup.setEastNorth(n.getEastNorth());
oldNodes.put(n, nodeBackup);
lastAffectedNodes.add(n);
}
// Physical change occurs here:
for (Node n : displaceableNodes) {
n.setEastNorth(calculatedNodes.get(n));
if (setModified) {
n.setModified(true);
}
}
algnSeg.updatePivotsEndpoints();
}
/**
* Make sure angle is in interval ( -Pi/2, Pi/2 ].
*/
private double normalise_angle(double a) {
while (a > Math.PI) {
a -= 2 * Math.PI;
}
while (a <= -Math.PI) {
a += 2 * Math.PI;
}
if (a > Math.PI / 2) {
a -= Math.PI;
} else if (a < -Math.PI / 2) {
a += Math.PI;
}
return a;
}
@Override
public String getDescriptionText() {
return tr("Align way segment");
}
@Override
public Icon getDescriptionIcon() {
return ImageProvider.get("alignways");
}
@Override
public void fillModifiedData(Collection<OsmPrimitive> modified,
Collection<OsmPrimitive> deleted, Collection<OsmPrimitive> added) {
for (OsmPrimitive osm : displaceableNodes) {
modified.add(osm);
}
}
@Override
public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
Collection<OsmPrimitive> prims = new HashSet<>(displaceableNodes);
prims.add(algnSeg.getSegment().way);
return Collections.unmodifiableCollection(prims);
}
@Override
public boolean executeCommand() {
rotateNodes(true);
return true;
}
@Override
public void undoCommand() {
for (Node n : displaceableNodes) {
Node oldNode = oldNodes.get(n);
n.setCoor(oldNode.getCoor());
}
algnSeg.updatePivotsEndpoints();
}
public Collection<Node> getPrevAffectedNodes() {
return lastAffectedNodes;
}
/**
* Returns true if the two selected segments are alignable.
* They are not if they are connected *and* the pivot is not the connection
* node.
*/
AlignableStatus areSegsAlignable() {
Collection<Node> algnNodes = displaceableNodes;
Collection<Node> refNodes = AlignWaysSegmentMgr
.getInstance(Main.map.mapView).getRefSeg()
.getSegmentEndPoints();
// First check if the pivot node of the alignee exists in the reference:
// in this case the pivot is the shared node and alignment is possible
for (Node nR : refNodes) {
if (nR.getEastNorth().equals(pivot))
return AlignableStatus.ALGN_VALID;
}
// Otherwise if the segments are connected, alignment is not possible
for (Node nA : algnNodes) {
for (Node nR : refNodes) {
if (nA.equals(nR))
return AlignableStatus.ALGN_INV_CONNECTED_UNSHARED_PIVOT;
}
}
// Deny action if the nodes would end up outside world
for (EastNorth en : calculatedNodes.values()) {
if (Projections.inverseProject(en).isOutSideWorld())
return AlignableStatus.ALGN_INV_OUTSIDE_WORLD;
}
// In all other cases alignment is possible
return AlignableStatus.ALGN_VALID;
}
/**
* Validates the circumstances of the alignment command to be executed.
*
* @return true if the aligning action can be done, false otherwise.
*/
public boolean executable() {
AlignableStatus stat = areSegsAlignable();
if (stat != AlignableStatus.ALGN_VALID) {
reportInvalidCommand(stat);
return false;
} else
// Action valid
return true;
}
/**
* Reports invalid alignable statuses on screen in dialog boxes.
*
* @param stat The invalid status to report
*/
void reportInvalidCommand(AlignableStatus stat) {
String statMsg;
switch (stat) {
case ALGN_INV_CONNECTED_UNSHARED_PIVOT:
statMsg = tr("Please select two segments that don''t share any nodes\n"
+ " or put the pivot on their common node.\n");
break;
case ALGN_INV_OUTSIDE_WORLD:
statMsg = tr("Aligning would result nodes ''outside the world''.\n"
+ "Alignment not possible.\n");
break;
default:
statMsg = tr("Undocumented problem occured.\n");
break;
}
JOptionPane.showMessageDialog(
Main.parent,
tr(statMsg),
tr("AlignWayS: Alignment not possible"),
JOptionPane.WARNING_MESSAGE
);
}
}