// License: GPL. For details, see LICENSE file. package org.openstreetmap.josm.plugins.turnlanes.gui; import static java.lang.Math.max; import static java.lang.Math.min; import static org.openstreetmap.josm.plugins.turnlanes.gui.GuiUtil.area; import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Composite; import java.awt.Font; import java.awt.Graphics2D; import java.awt.Shape; import java.awt.geom.Ellipse2D; import java.awt.geom.Path2D; import java.awt.geom.PathIterator; import java.awt.geom.Point2D; import java.awt.geom.Rectangle2D; import java.text.DecimalFormat; import java.text.NumberFormat; import java.util.ArrayList; import java.util.List; import org.openstreetmap.josm.plugins.turnlanes.gui.RoadGui.IncomingConnector; import org.openstreetmap.josm.plugins.turnlanes.model.Lane; import org.openstreetmap.josm.plugins.turnlanes.model.Road; final class LaneGui { final class LengthSlider extends InteractiveElement { private final Point2D center = new Point2D.Double(); private final Ellipse2D circle = new Ellipse2D.Double(); private Point2D dragDelta; private LengthSlider() {} @Override public void paint(Graphics2D g2d, State state) { if (isVisible(state)) { g2d.setColor(Color.BLUE); g2d.fill(circle); final String len = METER_FORMAT.format(getLength() * getRoad().getContainer().getMpp()); final Rectangle2D bounds = circle.getBounds2D(); g2d.setFont(g2d.getFont().deriveFont(Font.BOLD, (float) bounds.getHeight())); g2d.drawString(len, (float) bounds.getMaxX(), (float) bounds.getMaxY()); } } private boolean isVisible(State state) { if (state instanceof State.OutgoingActive) { return LaneGui.this.equals(((State.OutgoingActive) state).getLane()); } return false; } @Override public boolean contains(Point2D p, State state) { return isVisible(state) && circle.contains(p); } @Override public Type getType() { return Type.INCOMING_CONNECTOR; } @Override boolean beginDrag(double x, double y) { dragDelta = new Point2D.Double(center.getX() - x, center.getY() - y); return true; } @Override State drag(double x, double y, InteractiveElement target, State old) { move(x + dragDelta.getX(), y + dragDelta.getY(), false); return new State.Dirty(old); } @Override State drop(double x, double y, InteractiveElement target, State old) { move(x + dragDelta.getX(), y + dragDelta.getY(), true); return old; } void move(double x, double y, boolean updateModel) { final double r = getRoad().connectorRadius; final double offset = getRoad().getOffset(x, y); final double newLength = getModel().getOutgoingRoadEnd().isFromEnd() ? offset : getRoad().getLength() - offset; final double adjustedLength = min(max(newLength, 0.1), getRoad().getLength()); length = adjustedLength; if (updateModel) { getModel().setLength(adjustedLength * getRoad().getContainer().getMpp()); } center.setLocation(x, y); circle.setFrame(x - r, y - r, 2 * r, 2 * r); } public void move(Point2D loc) { final double x = loc.getX(); final double y = loc.getY(); final double r = getRoad().connectorRadius; center.setLocation(x, y); circle.setFrame(x - r, y - r, 2 * r, 2 * r); } @Override int getZIndex() { return 2; } } final class OutgoingConnector extends InteractiveElement { private final Point2D center = new Point2D.Double(); private final Ellipse2D circle = new Ellipse2D.Double(); private Point2D dragLocation; private IncomingConnector dropTarget; private OutgoingConnector() {} @Override public void paintBackground(Graphics2D g2d, State state) { if (isActive(state)) { final Composite old = g2d.getComposite(); g2d.setComposite(((AlphaComposite) old).derive(0.2f)); g2d.setColor(new Color(255, 127, 31)); LaneGui.this.fill(g2d); g2d.setComposite(old); } if (dragLocation != null) { final State.Connecting s = (State.Connecting) state; final Path2D path = new Path2D.Double(); path.moveTo(center.getX(), center.getY()); final List<RoadGui.ViaConnector> vias = s.getViaConnectors(); for (int i = 0; i < vias.size() - 1; i += 2) { final RoadGui.ViaConnector v = vias.get(i); final PathIterator it = v.getRoad().getLaneMiddle(v.getRoadEnd().isFromEnd()).getIterator(); path.append(it, true); } if ((vias.size() & 1) != 0) { final RoadGui.ViaConnector last = vias.get(vias.size() - 1); path.lineTo(last.getCenter().getX(), last.getCenter().getY()); } if (dropTarget == null) { g2d.setColor(GuiContainer.RED); path.lineTo(dragLocation.getX(), dragLocation.getY()); } else { g2d.setColor(GuiContainer.GREEN); path.lineTo(dropTarget.getCenter().getX(), dropTarget.getCenter().getY()); } g2d.setStroke(getContainer().getConnectionStroke()); g2d.draw(path); } } @Override public void paint(Graphics2D g2d, State state) { if (isVisible(state)) { final Composite old = g2d.getComposite(); if (isActive(state)) { g2d.setComposite(((AlphaComposite) old).derive(1f)); } g2d.setColor(Color.WHITE); g2d.fill(circle); g2d.setComposite(old); } } private boolean isActive(State state) { return state instanceof State.OutgoingActive && LaneGui.this.equals(((State.OutgoingActive) state).getLane()); } private boolean isVisible(State state) { if (state instanceof State.Connecting) { return ((State.Connecting) state).getLane().equals(getModel()); } return !getRoad().getModel().isPrimary() && getModel().getOutgoingJunction().isPrimary(); } @Override public boolean contains(Point2D p, State state) { return isVisible(state) && (circle.contains(p) || LaneGui.this.contains(p)); } @Override public Type getType() { return Type.OUTGOING_CONNECTOR; } @Override public State activate(State old) { return new State.OutgoingActive(LaneGui.this); } @Override boolean beginDrag(double x, double y) { return circle.contains(x, y); } @Override State.Connecting drag(double x, double y, InteractiveElement target, State old) { dragLocation = new Point2D.Double(x, y); dropTarget = null; if (!(old instanceof State.Connecting)) { return new State.Connecting(getModel()); } final State.Connecting s = (State.Connecting) old; if (target != null && target.getType() == Type.INCOMING_CONNECTOR) { dropTarget = (IncomingConnector) target; return (s.getViaConnectors().size() & 1) == 0 ? s : s.pop(); } else if (target != null && target.getType() == Type.VIA_CONNECTOR) { return s.next((RoadGui.ViaConnector) target); } return s; } @Override State drop(double x, double y, InteractiveElement target, State old) { final State.Connecting s = drag(x, y, target, old); dragLocation = null; if (dropTarget == null) { return activate(old); } final List<Road> via = new ArrayList<>(); assert (s.getViaConnectors().size() & 1) == 0; for (int i = 0; i < s.getViaConnectors().size(); i += 2) { final RoadGui.ViaConnector a = s.getViaConnectors().get(i); final RoadGui.ViaConnector b = s.getViaConnectors().get(i + 1); assert a.getRoadEnd().getOppositeEnd().equals(b.getRoadEnd()); via.add(a.getRoadEnd().getRoad()); } getModel().addTurn(via, dropTarget.getRoadEnd()); dropTarget = null; return new State.Dirty(activate(old)); } public Point2D getCenter() { return (Point2D) center.clone(); } void move(double x, double y) { final double r = getRoad().connectorRadius; center.setLocation(x, y); circle.setFrame(x - r, y - r, 2 * r, 2 * r); } @Override int getZIndex() { return 1; } } static final NumberFormat METER_FORMAT = new DecimalFormat("0.0m"); private final RoadGui road; private final Lane lane; final Path2D area = new Path2D.Double(); final OutgoingConnector outgoing = new OutgoingConnector(); final LengthSlider lengthSlider; private Shape clip; private double length; LaneGui(RoadGui road, Lane lane) { this.road = road; this.lane = lane; this.lengthSlider = lane.isExtra() ? new LengthSlider() : null; this.length = lane.isExtra() ? lane.getLength() / road.getContainer().getMpp() : Double.NaN; } public double getLength() { return lane.isExtra() ? length : road.getLength(); } public Lane getModel() { return lane; } public RoadGui getRoad() { return road; } public GuiContainer getContainer() { return getRoad().getContainer(); } public Path recalculate(Path inner, Path2D innerLine) { area.reset(); final double W = getContainer().getLaneWidth(); final double L = getLength(); final double WW = 3 / getContainer().getMpp(); final LaneGui left = left(); final Lane leftModel = left == null ? null : left.getModel(); final double leftLength = leftModel == null || !leftModel.getOutgoingRoadEnd().equals(getModel().getOutgoingRoadEnd()) ? Double.NEGATIVE_INFINITY : leftModel.getKind() == Lane.Kind.EXTRA_LEFT ? left.getLength() : L; final Path outer; if (getModel().getKind() == Lane.Kind.EXTRA_LEFT) { final double AL = 30 / getContainer().getMpp(); final double SL = max(L, leftLength + AL); outer = inner.offset(W, SL, SL + AL, 0); area(area, inner.subpath(0, L, true), outer.subpath(0, L + WW, true)); lengthSlider.move(inner.getPoint(L, true)); if (L > leftLength) { innerLine.append(inner.subpath(leftLength + WW, L, true).getIterator(), leftLength >= 0 || getModel().getOutgoingRoadEnd().isFromEnd()); final Point2D op = outer.getPoint(L + WW, true); innerLine.lineTo(op.getX(), op.getY()); } } else if (getModel().getKind() == Lane.Kind.EXTRA_RIGHT) { outer = inner.offset(W, L, L + WW, 0); area(area, inner.subpath(0, L + WW, true), outer.subpath(0, L, true)); lengthSlider.move(outer.getPoint(L, true)); } else { outer = inner.offset(W, -1, -1, W); area(area, inner, outer); if (leftLength < L) { innerLine.append(inner.subpath(leftLength + WW, L, true).getIterator(), leftLength >= 0 || getModel().getOutgoingRoadEnd().isFromEnd()); } } return outer; } private LaneGui left() { final List<LaneGui> lanes = getRoad().getLanes(getModel().getOutgoingRoadEnd()); final int i = lanes.indexOf(this); return i > 0 ? lanes.get(i - 1) : null; } public void fill(Graphics2D g2d) { final Shape old = g2d.getClip(); g2d.clip(clip); g2d.fill(area); g2d.setClip(old); } public void setClip(Shape clip) { this.clip = clip; } public boolean contains(Point2D p) { return area.contains(p) && clip.contains(p); } }