// 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.hypot; import static java.lang.Math.max; import static java.lang.Math.tan; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.angle; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.closest; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.cpf; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.intersection; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.loc; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.normalize; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.relativePoint; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.geom.FlatteningPathIterator; import java.awt.geom.Line2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.NavigableMap; import java.util.Set; import java.util.TreeMap; import org.openstreetmap.josm.plugins.turnlanes.model.Junction; import org.openstreetmap.josm.plugins.turnlanes.model.Road; import org.openstreetmap.josm.plugins.turnlanes.model.Turn; class JunctionGui { private final class TurnConnection extends InteractiveElement { private final Turn turn; private Point2D dragBegin; private double dragOffsetX = 0; private double dragOffsetY = 0; TurnConnection(Turn turn) { this.turn = turn; } @Override void paint(Graphics2D g2d, State state) { if (isVisible(state)) { g2d.setStroke(getContainer().getConnectionStroke()); g2d.setColor(isRemoveDragOffset() ? GuiContainer.RED : GuiContainer.GREEN); g2d.translate(dragOffsetX, dragOffsetY); g2d.draw(getPath()); g2d.translate(-dragOffsetX, -dragOffsetY); } } private Path2D getPath() { final Path2D path = new Path2D.Double(); final LaneGui laneGui = getContainer().getGui(turn.getFrom()); final RoadGui roadGui = getContainer().getGui(turn.getTo().getRoad()); path.moveTo(laneGui.outgoing.getCenter().getX(), laneGui.outgoing.getCenter().getY()); Junction j = laneGui.getModel().getOutgoingJunction(); for (Road v : turn.getVia()) { final PathIterator it; if (v.getFromEnd().getJunction().equals(j)) { it = getContainer().getGui(v).getLaneMiddle(true).getIterator(); j = v.getToEnd().getJunction(); } else { it = getContainer().getGui(v).getLaneMiddle(false).getIterator(); j = v.getFromEnd().getJunction(); } path.append(it, true); } path.lineTo(roadGui.getConnector(turn.getTo()).getCenter().getX(), roadGui.getConnector(turn.getTo()).getCenter() .getY()); return path; } private boolean isVisible(State state) { if (state instanceof State.AllTurns) { return true; } else if (state instanceof State.OutgoingActive) { return turn.getFrom().equals(((State.OutgoingActive) state).getLane().getModel()); } else if (state instanceof State.IncomingActive) { return turn.getTo().equals(((State.IncomingActive) state).getRoadEnd()); } return false; } @Override boolean contains(Point2D p, State state) { if (!isVisible(state)) { return false; } final PathIterator it = new FlatteningPathIterator(getPath().getPathIterator(null), 0.05 / getContainer() .getMpp()); final double[] coords = new double[6]; double lastX = 0; double lastY = 0; while (!it.isDone()) { if (it.currentSegment(coords) == PathIterator.SEG_LINETO) { final Point2D closest = closest(new Line2D.Double(lastX, lastY, coords[0], coords[1]), p); if (p.distance(closest) <= strokeWidth() / 2) { return true; } } lastX = coords[0]; lastY = coords[1]; it.next(); } return false; } private double strokeWidth() { final BasicStroke stroke = (BasicStroke) getContainer().getConnectionStroke(); return stroke.getLineWidth(); } @Override Type getType() { return Type.TURN_CONNECTION; } @Override int getZIndex() { return 0; } @Override boolean beginDrag(double x, double y) { dragBegin = new Point2D.Double(x, y); dragOffsetX = 0; dragOffsetY = 0; return true; } @Override State drag(double x, double y, InteractiveElement target, State old) { dragOffsetX = x - dragBegin.getX(); dragOffsetY = y - dragBegin.getY(); return old; } @Override State drop(double x, double y, InteractiveElement target, State old) { drag(x, y, target, old); if (isRemoveDragOffset()) { turn.remove(); } dragBegin = null; dragOffsetX = 0; dragOffsetY = 0; return new State.Dirty(old); } private boolean isRemoveDragOffset() { final double r = getContainer().getGui(turn.getFrom().getRoad()).connectorRadius; final double max = r - strokeWidth() / 2; return hypot(dragOffsetX, dragOffsetY) > max; } } private static final class Corner { final double x1; final double y1; final double cx1; final double cy1; final double cx2; final double cy2; final double x2; final double y2; Corner(Point2D c1, Point2D cp1, Point2D cp2, Point2D c2) { this.x1 = c1.getX(); this.y1 = c1.getY(); this.cx1 = cp1.getX(); this.cy1 = cp1.getY(); this.cx2 = cp2.getX(); this.cy2 = cp2.getY(); this.x2 = c2.getX(); this.y2 = c2.getY(); } @Override public String toString() { return "Corner [x1=" + x1 + ", y1=" + y1 + ", cx1=" + cx1 + ", cy1=" + cy1 + ", cx2=" + cx2 + ", cy2=" + cy2 + ", x2=" + x2 + ", y2=" + y2 + "]"; } } private final class Linkage implements Comparable<Linkage> { final RoadGui roadGui; final Road.End roadEnd; final double angle; double lTrim; double rTrim; Linkage(Road.End roadEnd) { this.roadGui = getContainer().getGui(roadEnd.getRoad()); this.roadEnd = roadEnd; this.angle = normalize(roadGui.getAngle(roadEnd) + PI); roads.put(angle, this); } @Override public int compareTo(Linkage o) { return Double.compare(angle, o.angle); } public void trimLeft(Linkage right) { right.trimRight(this); final Line2D leftCurb = roadGui.getLeftCurb(roadEnd); final Line2D rightCurb = right.roadGui.getRightCurb(right.roadEnd); final double leftAngle = angle(leftCurb); final double rightAngle = angle(rightCurb); final Point2D isect; if (abs(PI - normalize(rightAngle - leftAngle)) > PI / 12) { isect = intersection(leftCurb, rightCurb); } else { isect = GuiUtil.relativePoint(leftCurb.getP1(), roadGui.getWidth(roadEnd) / 2, angle); } if (Math.abs(leftAngle - angle(leftCurb.getP1(), isect)) < 0.1) { lTrim = leftCurb.getP1().distance(isect); } } private void trimRight(Linkage left) { final Line2D rightCurb = roadGui.getRightCurb(roadEnd); final Line2D leftCurb = left.roadGui.getLeftCurb(left.roadEnd); final double rightAngle = angle(rightCurb); final double leftAngle = angle(leftCurb); final Point2D isect; if (abs(PI - normalize(rightAngle - leftAngle)) > PI / 12) { isect = intersection(rightCurb, leftCurb); } else { isect = GuiUtil.relativePoint(rightCurb.getP1(), roadGui.getWidth(roadEnd) / 2, angle); } if (Math.abs(rightAngle - angle(rightCurb.getP1(), isect)) < 0.1) { rTrim = rightCurb.getP1().distance(isect); } } public void trimAdjust() { final double MAX_TAN = tan(PI / 2 - MAX_ANGLE); final double sin = roadGui.getWidth(roadEnd); final double cos = abs(lTrim - rTrim); final double tan = sin / cos; if (tan < MAX_TAN) { lTrim = max(lTrim, rTrim - sin / MAX_TAN); rTrim = max(rTrim, lTrim - sin / MAX_TAN); } lTrim += container.getLaneWidth() / 2; rTrim += container.getLaneWidth() / 2; } } // max angle between corners private static final double MAX_ANGLE = Math.toRadians(30); private final GuiContainer container; private final Junction junction; final double x; final double y; private final NavigableMap<Double, Linkage> roads = new TreeMap<>(); private final Path2D area = new Path2D.Double(); JunctionGui(GuiContainer container, Junction j) { this.container = container; this.junction = j; container.register(this); final Point2D loc = container.translateAndScale(loc(j.getNode())); this.x = loc.getX(); this.y = loc.getY(); final Set<Road> done = new HashSet<>(); for (Road r : j.getRoads()) { if (!done.contains(r)) { done.add(r); if (r.getFromEnd().getJunction().equals(j)) { new Linkage(r.getFromEnd()); } if (r.getToEnd().getJunction().equals(j)) { new Linkage(r.getToEnd()); } } } recalculate(); } void recalculate() { for (Linkage l : roads.values()) { l.lTrim = 0; l.rTrim = 0; } area.reset(); if (roads.size() < 2) { return; } Linkage last = roads.lastEntry().getValue(); for (Linkage l : roads.values()) { l.trimLeft(last); last = l; } for (Linkage l : roads.values()) { l.trimAdjust(); } boolean first = true; for (Corner c : corners()) { if (first) { area.moveTo(c.x1, c.y1); first = false; } else { area.lineTo(c.x1, c.y1); } area.curveTo(c.cx1, c.cy1, c.cx2, c.cy2, c.x2, c.y2); } area.closePath(); } private Iterable<Corner> corners() { final List<Corner> result = new ArrayList<>(roads.size()); Linkage last = roads.lastEntry().getValue(); for (Linkage l : roads.values()) { result.add(corner(last, l)); last = l; } return result; } private Corner corner(Linkage right, Linkage left) { final Line2D rightCurb = right.roadGui.getRightCurb(right.roadEnd); final Line2D leftCurb = left.roadGui.getLeftCurb(left.roadEnd); final double rightAngle = angle(rightCurb); final double leftAngle = angle(leftCurb); final double delta = normalize(leftAngle - rightAngle); final boolean wide = delta > PI; final double a = wide ? max(0, delta - (PI + 2 * MAX_ANGLE)) : delta; final double cpf1 = cpf(a, container.getLaneWidth() / 2 + (wide ? right.roadGui.getWidth(right.roadEnd) : 0)); final double cpf2 = cpf(a, container.getLaneWidth() / 2 + (wide ? left.roadGui.getWidth(left.roadEnd) : 0)); final Point2D c1 = relativePoint(rightCurb.getP1(), cpf1, right.angle + PI); final Point2D c2 = relativePoint(leftCurb.getP1(), cpf2, left.angle + PI); return new Corner(rightCurb.getP1(), c1, c2, leftCurb.getP1()); } public Set<RoadGui> getRoads() { final Set<RoadGui> result = new HashSet<>(); for (Linkage l : roads.values()) { result.add(l.roadGui); } return Collections.unmodifiableSet(result); } double getLeftTrim(Road.End end) { return getLinkage(end).lTrim; } private Linkage getLinkage(Road.End end) { final double a = normalize(getContainer().getGui(end.getRoad()).getAngle(end) + PI); final Map.Entry<Double, Linkage> e = roads.floorEntry(a); return e != null ? e.getValue() : null; } double getRightTrim(Road.End end) { return getLinkage(end).rTrim; } Point2D getPoint() { return new Point2D.Double(x, y); } public GuiContainer getContainer() { return container; } public Junction getModel() { return junction; } public List<InteractiveElement> paint(Graphics2D g2d) { g2d.setColor(new Color(96, 96, 96)); g2d.fill(area); final List<InteractiveElement> result = new ArrayList<>(); if (getModel().isPrimary()) { for (Road.End r : new HashSet<>(getModel().getRoadEnds())) { for (Turn t : r.getTurns()) { result.add(new TurnConnection(t)); } } } return result; } public Rectangle2D getBounds() { return area.getBounds2D(); } }