// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.turnlanes.gui; import static java.lang.Math.PI; import static java.lang.Math.abs; import static java.lang.Math.cos; import static java.lang.Math.hypot; import static java.lang.Math.max; import static java.lang.Math.min; import static java.lang.Math.signum; import static java.lang.Math.sin; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.angle; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.cpf; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.minAngleDiff; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.relativePoint; import java.awt.geom.Line2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.util.NoSuchElementException; /** * A path that allows constructing offset curves/parallel curves with a somewhat crude straight * skeleton implementation. * * @author Ben Schulz */ abstract class Path { private static final class SimplePathIterator implements PathIterator { private final SimplePathIterator previous; private final int type; private final double[] coords; private boolean done = false; SimplePathIterator(SimplePathIterator previous, int type, double... coords) { this.previous = previous; this.type = type; this.coords = coords; } SimplePathIterator(int type, double... coords) { this(null, type, coords); } @Override public int getWindingRule() { return WIND_NON_ZERO; } @Override public boolean isDone() { return done; } @Override public void next() { if (previous != null && !previous.isDone()) { previous.next(); } else { done = true; } } @Override public int currentSegment(float[] coords) { if (previous != null && !previous.isDone()) { return previous.currentSegment(coords); } else if (done) { throw new NoSuchElementException("Iterator is already done."); } for (int i = 0; i < 6; ++i) { coords[i] = (float) this.coords[i]; } return type; } @Override public int currentSegment(double[] coords) { if (previous != null && !previous.isDone()) { return previous.currentSegment(coords); } else if (done) { throw new NoSuchElementException("Iterator is already done."); } for (int i = 0; i < 6; ++i) { coords[i] = this.coords[i]; } return type; } } private static final class Line extends Path { private final Path previous; private final double endX; private final double endY; private final double angle; private final double length; Line(Path previous, double x, double y, double length) { this.previous = previous; this.endX = x; this.endY = y; this.angle = angle(previous.getEnd(), getEnd()); this.length = length; } @Override public Point2D getStart() { return previous.getStart(); } @Override public Point2D getEnd() { return new Point2D.Double(endX, endY); } @Override public double getEndAngle() { return angle; } @Override public double getLength() { return previous.getLength() + length; } @Override public Path offset(double ws, double m1, double m2, double we) { return offsetInternal(ws, m1, m2, we, angle); } @Override Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) { final double PL = previous.getLength(); final double ML = PL + length; final Path prev = previous.offsetInternal(ws, m1, m2, we, angle); final double wStart = PL <= m1 ? ws : m2 <= PL ? we : ws + (PL - m1) * (we - ws) / (m2 - m1); final Point2D from = prev.getEnd(); final Point2D to = offsetEnd(wStart, endAngle); if (abs(minAngleDiff(angle, angle(from, to))) > PI / 100) { return previous.offsetInternal(ws, m1, m2, we, endAngle); } if (ML <= m1) { return simpleOffset(prev, ws, endAngle); } else if (m2 <= PL) { return simpleOffset(prev, we, endAngle); } final double LL = from.distance(to); final Point2D m1o = PL <= m1 ? relativePoint(prev.getEnd(), LL * (m1 - PL) / length, angle) : null; final Point2D m2t = m2 <= ML ? relativePoint(getEnd(), LL * (ML - m2) / length, angle + PI) : null; final Point2D m2o = m2t == null ? null : relativePoint(m2t, we, (angle + endAngle - PI) / 2); if (m1o != null && m2o != null) { final Line l1 = new Line(prev, m1o.getX(), m1o.getY(), m1 - PL); final Line l2 = new Line(l1, m2o.getX(), m2o.getY(), m2 - m1); final Point2D end = offsetEnd(we, endAngle); return new Line(l2, end.getX(), end.getY(), ML - m2); } else if (m1o != null) { final Line l1 = new Line(prev, m1o.getX(), m1o.getY(), m1 - PL); final double w = ws + (ML - m1) * (we - ws) / (m2 - m1); final Point2D end = offsetEnd(w, endAngle); return new Line(l1, end.getX(), end.getY(), ML - m1); } else if (m2o != null) { final Line l2 = new Line(prev, m2o.getX(), m2o.getY(), m2 - PL); final Point2D end = offsetEnd(we, endAngle); return new Line(l2, end.getX(), end.getY(), ML - m2); } else { final double w = ws + (PL - m1 + length) * (we - ws) / (m2 - m1); final Point2D end = offsetEnd(w, endAngle); return new Line(prev, end.getX(), end.getY(), length); } } private Path simpleOffset(Path prev, double w, double endAngle) { final Point2D offset = offsetEnd(w, endAngle); return new Line(prev, offset.getX(), offset.getY(), length); } private Point2D offsetEnd(double w, double endAngle) { final double da2 = minAngleDiff(angle, endAngle) / 2; final double hypotenuse = w / cos(da2); return relativePoint(getEnd(), hypotenuse, angle + PI / 2 + da2); } @Override public SimplePathIterator getIterator() { return new SimplePathIterator(previous.getIteratorInternal(angle), PathIterator.SEG_LINETO, endX, endY, 0, 0, 0, 0); } @Override public Path subpath(double from, double to) { final double PL = previous.getLength(); final double ML = PL + length; if (from > ML) { throw new IllegalArgumentException("from > length"); } else if (to > ML) { throw new IllegalArgumentException("to > length"); } if (to < PL) { return previous.subpath(from, to); } final Point2D end = to < ML ? getPoint(to) : new Point2D.Double(endX, endY); final double EL = min(ML, to); if (PL <= from) { final Point2D start = getPoint(from); return new Line(new Start(start.getX(), start.getY(), angle), end.getX(), end.getY(), EL - from); } else { return new Line(previous.subpath(from, PL), end.getX(), end.getY(), EL - PL); } } @Override public Point2D getPoint(double offset) { final double PL = previous.getLength(); final double ML = PL + length; if (offset > ML) { throw new IllegalArgumentException("offset > length"); } if (offset <= ML && offset >= PL) { final double LL = previous.getEnd().distance(getEnd()); return relativePoint(getEnd(), LL * (ML - offset) / length, angle + PI); } else { return previous.getPoint(offset); } } @Override SimplePathIterator getIteratorInternal(double endAngle) { return getIterator(); } } // TODO curves are still somewhat broken private static class Curve extends Path { private final Path previous; private final double height; private final double centerX; private final double centerY; private final double centerToFromAngle; private final double endX; private final double endY; private final double fromAngle; private final double fromRadius; private final double toRadius; private final double angle; private final double length; private Curve(Path previous, double r1, double r2, double a, double length, double fromAngle) { this.previous = previous; this.fromAngle = fromAngle; this.fromRadius = r1; this.toRadius = r2; this.angle = a; this.length = length; final Point2D from = previous.getEnd(); this.centerToFromAngle = fromAngle - signum(a) * PI / 2; final Point2D center = relativePoint(from, r1, centerToFromAngle + PI); final double toAngle = centerToFromAngle + a; this.endX = center.getX() + r2 * cos(toAngle); this.endY = center.getY() - r2 * sin(toAngle); this.centerX = center.getX(); this.centerY = center.getY(); final double y = new Line2D.Double(center, from).ptLineDist(endX, endY); this.height = y / sin(angle); } Curve(Path previous, double r1, double r2, double a, double length) { this(previous, r1, r2, a, length, previous.getEndAngle()); } @Override public Point2D getStart() { return previous.getStart(); } @Override public Point2D getEnd() { return new Point2D.Double(endX, endY); } @Override public double getEndAngle() { return fromAngle + angle; } @Override public double getLength() { return previous.getLength() + length; } @Override public Path offset(double ws, double m1, double m2, double we) { return offsetInternal(ws, m1, m2, we, previous.getEndAngle() + angle); } @Override Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) { final double PL = previous.getLength(); final double ML = PL + length; final Path prev = previous.offsetInternal(ws, m1, m2, we, fromAngle); if (ML <= m1) { return simpleOffset(prev, ws); } else if (m2 <= PL) { return simpleOffset(prev, we); } final double s = signum(angle); if (PL < m1 && m2 < ML) { final double l1 = m1 - PL; final double a1 = angle(l1); final double r1 = radius(a1) - s * ws; final Curve c1 = new Curve(prev, fromRadius - ws, r1, offsetAngle(prev, a1), l1, fromAngle); final double l2 = m2 - m1; final double a2 = angle(l2); final double r2 = radius(a2) - s * we; final Curve c2 = new Curve(c1, r1, r2, a2 - a1, l2); return new Curve(c2, r2, toRadius - s * we, angle - a2, ML - m2); } else if (PL < m1) { final double l1 = m1 - PL; final double a1 = angle(l1); final double r1 = radius(a1) - s * ws; final Curve c1 = new Curve(prev, fromRadius - s * ws, r1, offsetAngle(prev, a1), l1, fromAngle); final double w = ws + (ML - m1) * (we - ws) / (m2 - m1); return new Curve(c1, r1, toRadius - s * w, angle - a1, ML - m1); } else if (m2 < ML) { final double w = ws + (PL - m1) * (we - ws) / (m2 - m1); final double l2 = m2 - PL; final double a2 = angle(l2); final double r2 = radius(a2) - s * we; final Curve c2 = new Curve(prev, fromRadius - s * w, r2, offsetAngle(prev, a2), l2, fromAngle); return new Curve(c2, r2, toRadius - s * we, angle - a2, ML - m2); } else { final double w1 = ws + (PL - m1) * (we - ws) / (m2 - m1); final double w2 = we - (m2 - ML) * (we - ws) / (m2 - m1); return new Curve(prev, fromRadius - s * w1, toRadius - s * w2, offsetAngle(prev, angle), length, fromAngle); } } private double angle(double l) { return l * angle / length; } private double radius(double a) { return hypot(fromRadius * cos(a), height * sin(a)); } private double offsetAngle(Path prev, double a) { return a; // + GuiUtil.normalize(previous.getEndAngle() // - prev.getEndAngle()); } private Path simpleOffset(Path prev, double w) { final double s = signum(angle); return new Curve(prev, fromRadius - s * w, toRadius - s * w, offsetAngle(prev, angle), length, fromAngle); } @Override public SimplePathIterator getIterator() { return getIteratorInternal(previous.getEndAngle() + angle); } @Override public Path subpath(double from, double to) { final double PL = previous.getLength(); final double ML = PL + length; if (from > ML) { throw new IllegalArgumentException("from > length"); } else if (to > ML) { throw new IllegalArgumentException("to > length"); } if (to < PL) { return previous.subpath(from, to); } final double toA = to < ML ? angle(to - PL) : angle; final double toR = to < ML ? radius(toA) : toRadius; final double fromA = from > PL ? angle(from - PL) : 0; final double fromR = from > PL ? radius(fromA) : fromRadius; final double a = toA - fromA; final double l = min(ML, to) - max(PL, from); if (from >= PL) { final Point2D start = getPoint(from); final double fa = fromAngle + fromA; return new Curve(new Start(start.getX(), start.getY(), fa), fromR, toR, a, l, fa); } else { return new Curve(previous.subpath(from, PL), fromR, toR, a, l, fromAngle); } } @Override public Point2D getPoint(double offset) { final double PL = previous.getLength(); final double ML = PL + length; if (offset <= ML && offset >= PL) { final double a = abs(angle(offset - PL)); final double w = fromRadius * cos(a); final double h = -height * sin(a); final double r = centerToFromAngle; // rotation angle final double x = w * cos(r) + h * sin(r); final double y = -w * sin(r) + h * cos(r); return new Point2D.Double(centerX + x, centerY + y); } else { return previous.getPoint(offset); } } @Override SimplePathIterator getIteratorInternal(double endAngle) { final Point2D cp1 = relativePoint(previous.getEnd(), cpf(angle, fromRadius), previous.getEndAngle()); final Point2D cp2 = relativePoint(getEnd(), cpf(angle, toRadius), endAngle + PI); return new SimplePathIterator(previous.getIteratorInternal(getEndAngle()), PathIterator.SEG_CUBICTO, // cp1.getX(), cp1.getY(), cp2.getX(), cp2.getY(), endX, endY // ); } } private static class Start extends Path { private final double x; private final double y; private final double endAngle; Start(double x, double y, double endAngle) { this.x = x; this.y = y; this.endAngle = endAngle; } Start(double x, double y) { this(x, y, Double.NaN); } @Override public Point2D getStart() { return new Point2D.Double(x, y); } @Override public Point2D getEnd() { return new Point2D.Double(x, y); } @Override public double getEndAngle() { if (Double.isNaN(endAngle)) { throw new UnsupportedOperationException(); } return endAngle; } @Override public double getLength() { return 0; } @Override public Path offset(double ws, double m1, double m2, double we) { throw new UnsupportedOperationException(); } @Override Path offsetInternal(double ws, double m1, double m2, double we, double endAngle) { final Point2D offset = relativePoint(getStart(), ws, endAngle + PI / 2); return new Start(offset.getX(), offset.getY(), endAngle); } @Override public SimplePathIterator getIterator() { return new SimplePathIterator(PathIterator.SEG_MOVETO, x, y, 0, 0, 0, 0); } @Override public Path subpath(double from, double to) { if (from > to) { throw new IllegalArgumentException("from > to"); } if (from < 0) { throw new IllegalArgumentException("from < 0"); } return this; } @Override public Point2D getPoint(double offset) { if (offset == 0) { return getEnd(); } else { throw new IllegalArgumentException(Double.toString(offset)); } } @Override SimplePathIterator getIteratorInternal(double endAngle) { return new SimplePathIterator(PathIterator.SEG_MOVETO, x, y, 0, 0, 0, 0); } } public static Path create(double x, double y) { return new Start(x, y); } public Path lineTo(double x, double y, double length) { return new Line(this, x, y, length); } public Path curveTo(double r1, double r2, double a, double length) { return new Curve(this, r1, r2, a, length); } public abstract Path offset(double ws, double m1, double m2, double we); abstract Path offsetInternal(double ws, double m1, double m2, double we, double endAngle); public abstract double getLength(); public abstract double getEndAngle(); public abstract Point2D getStart(); public abstract Point2D getEnd(); public abstract SimplePathIterator getIterator(); abstract SimplePathIterator getIteratorInternal(double endAngle); public abstract Path subpath(double from, double to); public Path subpath(double from, double to, boolean fixArgs) { if (fixArgs) { from = min(max(from, 0), getLength()); to = min(max(to, 0), getLength()); } return subpath(from, to); } public abstract Point2D getPoint(double offset); public Point2D getPoint(double offset, boolean fixArgs) { if (fixArgs) { offset = min(max(offset, 0), getLength()); } return getPoint(offset); } }