// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.graphview.plugin.layer;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Line2D;
import java.util.List;
import javax.swing.Action;
import javax.swing.Icon;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.actions.RenameLayerAction;
import org.openstreetmap.josm.data.Bounds;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.coor.LatLon;
import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.gui.dialogs.LayerListDialog;
import org.openstreetmap.josm.gui.dialogs.LayerListPopup;
import org.openstreetmap.josm.gui.layer.Layer;
import org.openstreetmap.josm.plugins.graphview.core.graph.GraphEdge;
import org.openstreetmap.josm.plugins.graphview.core.graph.GraphNode;
import org.openstreetmap.josm.plugins.graphview.core.graph.WayGraph;
import org.openstreetmap.josm.plugins.graphview.core.graph.WayGraphObserver;
import org.openstreetmap.josm.plugins.graphview.core.property.GraphEdgeSegments;
import org.openstreetmap.josm.plugins.graphview.core.transition.Segment;
import org.openstreetmap.josm.plugins.graphview.core.transition.SegmentNode;
import org.openstreetmap.josm.plugins.graphview.core.util.GraphUtil;
import org.openstreetmap.josm.plugins.graphview.core.visualisation.ColorScheme;
import org.openstreetmap.josm.plugins.graphview.core.visualisation.LatLonCoords;
import org.openstreetmap.josm.plugins.graphview.core.visualisation.NodePositioner;
import org.openstreetmap.josm.plugins.graphview.core.visualisation.NonMovingNodePositioner;
import org.openstreetmap.josm.plugins.graphview.plugin.preferences.GraphViewPreferences;
import org.openstreetmap.josm.tools.ImageProvider;
/**
* layer for displaying the graph visualization
*/
public class GraphViewLayer extends Layer implements WayGraphObserver {
private static final int NODE_RADIUS = 5;
/**
* offset from real position if {@link GraphViewPreferences#getSeparateDirections()} is active,
* causes the graph nodes for the two directions to be visually distinguishable.
* Positive values move "forward" direction to the right.
*/
private static final double DIRECTIONAL_OFFSET = 20;
private static final double OFFSET_ANGLE = 0.5 * Math.PI;
private static final double MIN_QUAD_DISTANCE_FOR_OFFSET = 4;
private static final boolean CONNECT_ALL_NODE_PAIRS = false;
/** an arrow head that points along the x-axis to (0,0) */
private static final Shape ARROW_HEAD;
/** arrow head core, can be colored differently and is rendered on top */
private static final Shape ARROW_HEAD_CORE;
static {
Polygon head = new Polygon();
head.addPoint( 0, 0);
head.addPoint(-22, +6);
head.addPoint(-22, -6);
ARROW_HEAD = head;
Polygon headCore = new Polygon();
headCore.addPoint(-12, 0);
headCore.addPoint(-19, +2);
headCore.addPoint(-19, -2);
ARROW_HEAD_CORE = headCore;
}
private WayGraph wayGraph = null;
private ColorScheme colorScheme = null;
private Double arrowheadPlacement = null;
private NodePositioner nodePositioner = new NonMovingNodePositioner();
public GraphViewLayer() {
super("Graph view");
}
/** sets the WayGraph that is to be displayed by this layer, may be null */
public void setWayGraph(WayGraph wayGraph) {
if (this.wayGraph != null) {
this.wayGraph.deleteObserver(this);
}
this.wayGraph = wayGraph;
if (wayGraph != null) {
wayGraph.addObserver(this);
}
}
/** sets the ColorScheme that is to be used for choosing colors, may be null */
public void setColorScheme(ColorScheme colorScheme) {
this.colorScheme = colorScheme;
invalidate();
}
/** sets the arrowhead placement (relative offset from edge start) */
public void setArrowheadPlacement(double arrowheadPlacement) {
assert arrowheadPlacement >= 0 && arrowheadPlacement <= 1;
this.arrowheadPlacement = arrowheadPlacement;
invalidate();
}
/**
* sets the NodePositioner that is to be used for determining node placement,
* null will cause a {@link NonMovingNodePositioner} to be used.
*/
public void setNodePositioner(NodePositioner nodePositioner) {
this.nodePositioner = nodePositioner;
if (nodePositioner == null) {
this.nodePositioner = new NonMovingNodePositioner();
}
invalidate();
}
@Override
public Icon getIcon() {
return ImageProvider.get("layer", "graphview");
}
private void paintGraphNode(final GraphNode node, final Graphics g, final MapView mv) {
Color color = colorScheme != null ? colorScheme.getNodeColor(node) : Color.LIGHT_GRAY;
Point p = getNodePoint(node, mv);
paintNode(g, p, color);
}
public static void paintNode(final Graphics g, Point p, Color color) {
g.setColor(color);
g.fillOval(p.x - NODE_RADIUS, p.y - NODE_RADIUS, 2 * NODE_RADIUS, 2 * NODE_RADIUS);
}
private void paintGraphEdge(final GraphEdge e, final Graphics2D g2D, final MapView mv,
boolean drawLine, boolean drawDirectionIndicator) {
if (!CONNECT_ALL_NODE_PAIRS && GraphViewPreferences.getInstance().getSeparateDirections()) {
//don't paint edges between nodes from the same SegmentNode and simply inverted Segment
if (e.getStartNode().getSegmentNode() == e.getTargetNode().getSegmentNode()
&& e.getStartNode().getSegment().getNode2() == e.getTargetNode().getSegment().getNode1()
&& e.getStartNode().getSegment().getNode1() == e.getTargetNode().getSegment().getNode2()) {
return;
}
}
/* draw line(s) */
g2D.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
List<Segment> edgeSegments = e.getPropertyValue(GraphEdgeSegments.PROPERTY);
if (edgeSegments.size() > 0) {
Segment firstSegment = edgeSegments.get(0);
Segment lastSegment = edgeSegments.get(edgeSegments.size() - 1);
//draw segments
for (Segment segment : edgeSegments) {
Color color = Color.WHITE;
if (colorScheme != null) {
color = colorScheme.getSegmentColor(segment);
}
g2D.setColor(color);
Point p1 = getNodePoint(segment.getNode1(), mv);
Point p2 = getNodePoint(segment.getNode2(), mv);
if (segment == firstSegment) {
p1 = getNodePoint(e.getStartNode(), mv);
}
if (segment == lastSegment) {
p2 = getNodePoint(e.getTargetNode(), mv);
}
if (drawLine) {
g2D.draw(new Line2D.Float(p1.x, p1.y, p2.x, p2.y));
}
}
} else {
Color color = GraphViewPreferences.getInstance().getSegmentColor();
g2D.setColor(color);
Point p1 = getNodePoint(e.getStartNode(), mv);
Point p2 = getNodePoint(e.getTargetNode(), mv);
if (drawLine) {
g2D.draw(new Line2D.Float(p1.x, p1.y, p2.x, p2.y));
}
}
/* draw arrow head (note: color of last segment is still set) */
{
Point p1 = getNodePoint(e.getStartNode(), mv);
Point p2 = getNodePoint(e.getTargetNode(), mv);
if (edgeSegments.size() > 1) {
Segment lastSegment = edgeSegments.get(edgeSegments.size() - 1);
p1 = getNodePoint(lastSegment.getNode1(), mv);
}
if (drawDirectionIndicator) {
paintArrowhead(g2D, p1, p2, arrowheadPlacement, null,
GraphViewPreferences.getInstance().getArrowheadFillColor());
}
}
}
public static void paintArrowhead(Graphics2D g2D,
Point p1, Point p2, Double arrowheadPlacement2,
Color casingColor, Color fillColor) {
Point pTip = new Point(
(int) (p1.x + arrowheadPlacement2 * (p2.x - p1.x)),
(int) (p1.y + arrowheadPlacement2 * (p2.y - p1.y)));
double angle = angleFromXAxis(p1, p2); // angle between x-axis and [p1,p2]
{ //draw head shape
if (casingColor != null) {
g2D.setColor(casingColor);
}
Shape head = ARROW_HEAD;
head = AffineTransform.getRotateInstance(angle).createTransformedShape(head);
head = AffineTransform.getTranslateInstance(pTip.x, pTip.y).createTransformedShape(head);
g2D.fill(head);
}
{ //draw head core shape
if (fillColor != null) {
g2D.setColor(fillColor);
}
Shape headCore = ARROW_HEAD_CORE;
headCore = AffineTransform.getRotateInstance(angle).createTransformedShape(headCore);
headCore = AffineTransform.getTranslateInstance(pTip.x, pTip.y).createTransformedShape(headCore);
g2D.fill(headCore);
}
}
private Point getNodePoint(GraphNode node, MapView mv) {
Point nodePoint = getNodePoint(nodePositioner.getPosition(node), mv);
if (GraphViewPreferences.getInstance().getSeparateDirections()
&& !GraphUtil.isEndNode(node)) {
SegmentNode node1 = node.getSegment().getNode1();
SegmentNode node2 = node.getSegment().getNode2();
Point node1Point = getNodePoint(node1, mv);
Point node2Point = getNodePoint(node2, mv);
double segmentX = node2Point.getX() - node1Point.getX();
double segmentY = node2Point.getY() - node1Point.getY();
if (segmentX*segmentX + segmentY*segmentY >= MIN_QUAD_DISTANCE_FOR_OFFSET) {
double rotatedX = Math.cos(OFFSET_ANGLE) * segmentX - Math.sin(OFFSET_ANGLE) * segmentY;
double rotatedY = Math.sin(OFFSET_ANGLE) * segmentX + Math.cos(OFFSET_ANGLE) * segmentY;
double segmentLength = Math.sqrt(rotatedX * rotatedX + rotatedY * rotatedY);
double normalizedX = rotatedX / segmentLength;
double normalizedY = rotatedY / segmentLength;
nodePoint.x += DIRECTIONAL_OFFSET * normalizedX;
nodePoint.y += DIRECTIONAL_OFFSET * normalizedY;
}
}
return nodePoint;
}
private static Point getNodePoint(SegmentNode node, MapView mv) {
LatLonCoords coords = new LatLonCoords(node.getLat(), node.getLon());
return getNodePoint(coords, mv);
}
private static Point getNodePoint(LatLonCoords coords, MapView mv) {
LatLon latLon = new LatLon(coords.getLat(), coords.getLon());
EastNorth eastNorth = Main.getProjection().latlon2eastNorth(latLon);
return mv.getPoint(eastNorth);
}
/**
* calculates the angle between the x axis and a vector given by two points
* @param p1 first point for vector; != null
* @param p2 second point for vector; != null
* @return angle in radians, in range [-Pi .. +Pi]
*/
private static double angleFromXAxis(Point p1, Point p2) {
assert p1 != null && p2 != null;
final float vecX = p2.x - p1.x;
final float vecY = p2.y - p1.y;
final float vecLength = (float) Math.sqrt(vecX*vecX + vecY*vecY);
final float dotProductVecAxis = vecX;
float angle = (float) Math.acos(dotProductVecAxis / vecLength);
if (p2.y < p1.y) {
angle = -angle;
}
assert -Math.PI*0.5 < angle && angle <= Math.PI*0.5;
return angle;
}
@Override
public void paint(final Graphics2D g, final MapView mv, Bounds bounds) {
if (wayGraph != null) {
for (GraphNode n : wayGraph.getNodes()) {
paintGraphNode(n, g, mv);
}
for (GraphEdge e : wayGraph.getEdges()) {
paintGraphEdge(e, g, mv, true, false);
}
for (GraphEdge e : wayGraph.getEdges()) {
//draw arrowheads last to make sure they end up on top
paintGraphEdge(e, g, mv, false, true);
}
}
}
@Override
public String getToolTipText() {
return tr("Routing graph calculated by the GraphView plugin");
}
@Override
public void mergeFrom(Layer from) {
throw new AssertionError(tr("GraphView layer is not mergable"));
}
@Override
public boolean isMergable(Layer other) {
return false;
}
@Override
public void visitBoundingBox(BoundingXYVisitor v) {
}
@Override
public Object getInfoComponent() {
return getToolTipText();
}
@Override
public Action[] getMenuEntries() {
return new Action[] {
LayerListDialog.getInstance().createShowHideLayerAction(),
LayerListDialog.getInstance().createDeleteLayerAction(),
SeparatorLayerAction.INSTANCE,
new RenameLayerAction(null, this),
SeparatorLayerAction.INSTANCE,
new LayerListPopup.InfoAction(this)};
}
@Override
public void update(WayGraph wayGraph) {
assert wayGraph == this.wayGraph;
invalidate();
}
}