// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.Splinex;
import static org.openstreetmap.josm.plugins.Splinex.SplinexPlugin.EPSILON;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.geom.GeneralPath;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import javax.swing.Icon;
import javax.swing.JOptionPane;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.command.AddCommand;
import org.openstreetmap.josm.command.Command;
import org.openstreetmap.josm.command.SequenceCommand;
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.Way;
import org.openstreetmap.josm.data.preferences.IntegerProperty;
import org.openstreetmap.josm.gui.DefaultNameFormatter;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.NavigatableComponent;
import org.openstreetmap.josm.tools.ImageProvider;
public class Spline {
public static IntegerProperty PROP_SPLINEPOINTS = new IntegerProperty("edit.spline.num_points", 10);
public static class SNode {
public final Node node; // Endpoint
public EastNorth cprev, cnext; // Relative offsets of control points
public SNode(Node node) {
this.node = node;
cprev = new EastNorth(0, 0);
cnext = new EastNorth(0, 0);
}
}
private final ArrayList<SNode> nodes = new ArrayList<SNode>();
public SNode getFirstSegment() {
if (nodes.isEmpty())
return null;
return nodes.get(0);
}
public SNode getLastSegment() {
if (nodes.isEmpty())
return null;
return nodes.get(nodes.size() - 1);
}
public boolean isEmpty() {
return nodes.isEmpty();
}
public int nodeCount() {
return nodes.size();
}
public boolean isClosed() {
if (nodes.size() < 2)
return false;
return nodes.get(0) == nodes.get(nodes.size() - 1);
}
//int chkTime;
public void paint(Graphics2D g, MapView mv, Color curveColor, Color ctlColor, Point helperEndpoint, short direction) {
if (nodes.isEmpty())
return;
final GeneralPath curv = new GeneralPath();
final GeneralPath ctl = new GeneralPath();
Point2D cbPrev = null;
if (helperEndpoint != null && direction == -1) {
cbPrev = new Point2D.Double(helperEndpoint.x, helperEndpoint.y);
curv.moveTo(helperEndpoint.x, helperEndpoint.y);
}
for (SNode sn : nodes) {
Point2D pt = mv.getPoint2D(sn.node);
EastNorth en = sn.node.getEastNorth();
Point2D ca = mv.getPoint2D(en.add(sn.cprev));
Point2D cb = mv.getPoint2D(en.add(sn.cnext));
if (cbPrev != null || !isClosed()) {
ctl.moveTo(ca.getX(), ca.getY());
ctl.lineTo(pt.getX(), pt.getY());
ctl.lineTo(cb.getX(), cb.getY());
}
if (cbPrev == null)
curv.moveTo(pt.getX(), pt.getY());
else
curv.curveTo(cbPrev.getX(), cbPrev.getY(), ca.getX(), ca.getY(), pt.getX(), pt.getY());
cbPrev = cb;
}
if (helperEndpoint != null && direction == 1) {
curv.curveTo(cbPrev.getX(), cbPrev.getY(), helperEndpoint.getX(), helperEndpoint.getY(),
helperEndpoint.getX(), helperEndpoint.getY());
}
g.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g.setColor(curveColor);
g.draw(curv);
g.setStroke(new BasicStroke(1));
g.setColor(ctlColor);
g.draw(ctl);
/* if (chkTime > 0 || sht.chkCnt > 0) {
g.drawString(tr("Check count: {0}", sht.chkCnt), 10, 60);
g.drawString(tr("Check time: {0} us", chkTime), 10, 70);
}
chkTime = 0;
sht.chkCnt = 0; */
}
public enum SplinePoint {
ENDPOINT, CONTROL_PREV, CONTROL_NEXT
}
public class PointHandle {
public final int idx;
public final SNode sn;
public final SplinePoint point;
public PointHandle(int idx, SplinePoint point) {
if (point == null)
throw new IllegalArgumentException("Invalid SegmentPoint passed for PointHandle contructor");
this.idx = idx;
this.sn = nodes.get(idx);
this.point = point;
}
public PointHandle otherPoint(SplinePoint point) {
return new PointHandle(idx, point);
}
public Spline getSpline() {
return Spline.this;
}
public EastNorth getPoint() {
EastNorth en = sn.node.getEastNorth();
switch (point) {
case ENDPOINT:
return en;
case CONTROL_PREV:
return en.add(sn.cprev);
case CONTROL_NEXT:
return en.add(sn.cnext);
}
throw new AssertionError();
}
public void movePoint(EastNorth en) {
switch (point) {
case ENDPOINT:
sn.node.setEastNorth(en);
return;
case CONTROL_PREV:
sn.cprev = en.subtract(sn.node.getEastNorth());
return;
case CONTROL_NEXT:
sn.cnext = en.subtract(sn.node.getEastNorth());
return;
}
throw new AssertionError();
}
@Override
public boolean equals(Object other) {
if (!(other instanceof PointHandle))
return false;
PointHandle o = (PointHandle) other;
return this.sn == o.sn && this.point == o.point;
}
@Override
public int hashCode() {
return Objects.hash(sn, point);
}
}
public PointHandle getNearestPoint(MapView mv, Point2D point) {
PointHandle bestPH = null;
double bestDistSq = NavigatableComponent.PROP_SNAP_DISTANCE.get();
bestDistSq = bestDistSq * bestDistSq;
for (int i = 0; i < nodes.size(); i++) {
for (SplinePoint sp : SplinePoint.values()) {
PointHandle ph = new PointHandle(i, sp);
double distSq = point.distanceSq(mv.getPoint2D(ph.getPoint()));
if (distSq < bestDistSq) {
bestPH = ph;
bestDistSq = distSq;
}
}
}
return bestPH;
}
SplineHitTest sht = new SplineHitTest();
public boolean doesHit(double x, double y, MapView mv) {
//long start = System.nanoTime();
//sht.chkCnt = 0;
sht.setCoord(x, y, NavigatableComponent.PROP_SNAP_DISTANCE.get());
Point2D prev = null;
Point2D cbPrev = null;
for (SNode sn : nodes) {
Point2D pt = mv.getPoint2D(sn.node);
EastNorth en = sn.node.getEastNorth();
Point2D ca = mv.getPoint2D(en.add(sn.cprev));
if (cbPrev != null)
if (sht.checkCurve(prev.getX(), prev.getY(), cbPrev.getX(), cbPrev.getY(), ca.getX(), ca.getY(),
pt.getX(), pt.getY()))
return true;
cbPrev = mv.getPoint2D(en.add(sn.cnext));
prev = pt;
}
//chkTime = (int) ((System.nanoTime() - start) / 1000);
return false;
}
public void finishSpline() {
if (nodes.isEmpty())
return;
int detail = PROP_SPLINEPOINTS.get();
Way w = new Way();
List<Command> cmds = new LinkedList<>();
Iterator<SNode> it = nodes.iterator();
SNode sn = it.next();
if (sn.node.isDeleted())
cmds.add(new UndeleteNodeCommand(sn.node));
w.addNode(sn.node);
EastNorth a = sn.node.getEastNorth();
EastNorth ca = a.add(sn.cnext);
while (it.hasNext()) {
sn = it.next();
if (sn.node.isDeleted() && sn != nodes.get(0))
cmds.add(new UndeleteNodeCommand(sn.node));
EastNorth b = sn.node.getEastNorth();
EastNorth cb = b.add(sn.cprev);
if (!a.equalsEpsilon(ca, EPSILON) || !b.equalsEpsilon(cb, EPSILON))
for (int i = 1; i < detail; i++) {
Node n = new Node(Main.getProjection().eastNorth2latlon(
cubicBezier(a, ca, cb, b, (double) i / detail)));
if (n.getCoor().isOutSideWorld()) {
JOptionPane.showMessageDialog(Main.parent, tr("Spline goes outside of the world."));
return;
}
cmds.add(new AddCommand(n));
w.addNode(n);
}
w.addNode(sn.node);
a = b;
ca = a.add(sn.cnext);
}
if (!cmds.isEmpty())
cmds.add(new AddCommand(w));
Main.main.undoRedo.add(new FinishSplineCommand(cmds));
}
/**
* A cubic bezier method to calculate the point at t along the Bezier Curve
* give
*/
public static EastNorth cubicBezier(EastNorth a0, EastNorth a1, EastNorth a2, EastNorth a3, double t) {
return new EastNorth(cubicBezierPoint(a0.getX(), a1.getX(), a2.getX(), a3.getX(), t), cubicBezierPoint(
a0.getY(), a1.getY(), a2.getY(), a3.getY(), t));
}
/**
* The cubic Bezier equation.
*/
private static double cubicBezierPoint(double a0, double a1, double a2, double a3, double t) {
return Math.pow(1 - t, 3) * a0 + 3 * Math.pow(1 - t, 2) * t * a1 + 3 * (1 - t) * Math.pow(t, 2) * a2
+ Math.pow(t, 3) * a3;
}
public class AddSplineNodeCommand extends Command {
private final SNode sn;
private final boolean existing;
private final int idx;
boolean affected;
public AddSplineNodeCommand(SNode sn, boolean existing, int idx) {
this.sn = sn;
this.existing = existing;
this.idx = idx;
}
public AddSplineNodeCommand(SNode sn, boolean existing) {
this(sn, existing, nodes.size() - 1);
}
@Override
public boolean executeCommand() {
nodes.add(idx, sn);
if (!existing) {
getLayer().data.addPrimitive(sn.node);
sn.node.setModified(true);
affected = true;
}
return true;
}
@Override
public void undoCommand() {
if (!existing)
getLayer().data.removePrimitive(sn.node);
nodes.remove(idx);
affected = false;
}
@Override
public String getDescriptionText() {
if (existing)
return tr("Add an existing node to spline: {0}",
sn.node.getDisplayName(DefaultNameFormatter.getInstance()));
return tr("Add a new node to spline: {0}", sn.node.getDisplayName(DefaultNameFormatter.getInstance()));
}
@Override
public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
Collection<OsmPrimitive> added) {
if (!existing)
added.add(sn.node);
}
@Override
public Icon getDescriptionIcon() {
return ImageProvider.get("data", "node");
}
@Override
public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
return affected ? Collections.singleton(sn.node) : super.getParticipatingPrimitives();
}
}
public class DeleteSplineNodeCommand extends Command {
int idx;
SNode sn;
boolean wasDeleted;
boolean affected;
public DeleteSplineNodeCommand(int idx) {
this.idx = idx;
}
private boolean deleteUnderlying() {
return !sn.node.hasKeys() && sn.node.getReferrers().isEmpty() && (!isClosed() || idx < (nodes.size() - 1));
}
@Override
public boolean executeCommand() {
if (isClosed() && idx == 0)
idx = nodes.size() - 1;
sn = nodes.get(idx);
wasDeleted = sn.node.isDeleted();
if (deleteUnderlying()) {
sn.node.setDeleted(true);
affected = true;
}
nodes.remove(idx);
return true;
}
@Override
public void undoCommand() {
affected = false;
sn.node.setDeleted(wasDeleted);
nodes.add(idx, sn);
}
@Override
public String getDescriptionText() {
return tr("Delete spline node {0}", sn.node.getDisplayName(DefaultNameFormatter.getInstance()));
}
@Override
public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
Collection<OsmPrimitive> added) {
if (deleteUnderlying())
deleted.add(sn.node);
}
@Override
public Icon getDescriptionIcon() {
return ImageProvider.get("data", "node");
}
@Override
public Collection<? extends OsmPrimitive> getParticipatingPrimitives() {
return affected ? Collections.singleton(sn.node) : super.getParticipatingPrimitives();
}
}
public static class EditSplineCommand extends Command {
EastNorth cprev;
EastNorth cnext;
SNode sn;
public EditSplineCommand(SNode sn) {
this.sn = sn;
cprev = sn.cprev.add(0, 0);
cnext = sn.cnext.add(0, 0);
}
@Override
public boolean executeCommand() {
EastNorth en = sn.cprev;
sn.cprev = this.cprev;
this.cprev = en;
en = sn.cnext;
sn.cnext = this.cnext;
this.cnext = en;
return true;
}
@Override
public void undoCommand() {
executeCommand();
}
@Override
public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
Collection<OsmPrimitive> added) {
// This command doesn't touches OSM data
}
@Override
public String getDescriptionText() {
return "Edit spline";
}
@Override
public Icon getDescriptionIcon() {
return ImageProvider.get("data", "node");
}
}
public class CloseSplineCommand extends Command {
@Override
public boolean executeCommand() {
nodes.add(nodes.get(0));
return true;
}
@Override
public void undoCommand() {
nodes.remove(nodes.size() - 1);
}
@Override
public void fillModifiedData(Collection<OsmPrimitive> modified, Collection<OsmPrimitive> deleted,
Collection<OsmPrimitive> added) {
// This command doesn't touches OSM data
}
@Override
public String getDescriptionText() {
return "Close spline";
}
@Override
public Icon getDescriptionIcon() {
return ImageProvider.get("aligncircle");
}
}
public List<OsmPrimitive> getNodes() {
ArrayList<OsmPrimitive> result = new ArrayList<>(nodes.size());
for (SNode sn : nodes) {
result.add(sn.node);
}
return result;
}
public class FinishSplineCommand extends SequenceCommand {
public SNode[] saveSegments;
public FinishSplineCommand(Collection<Command> sequenz) {
super(tr("Finish spline"), sequenz);
}
@Override
public boolean executeCommand() {
saveSegments = new SNode[nodes.size()];
int i = 0;
for (SNode sn : nodes) {
saveSegments[i++] = sn;
}
nodes.clear();
return super.executeCommand();
}
@Override
public void undoCommand() {
super.undoCommand();
nodes.clear();
nodes.addAll(Arrays.asList(saveSegments));
}
}
}