// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.elevation.gui;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.MultipleGradientPaint.CycleMethod;
import java.awt.Point;
import java.awt.RadialGradientPaint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import org.openstreetmap.josm.data.gpx.WayPoint;
import org.openstreetmap.josm.gui.MapView;
import org.openstreetmap.josm.plugins.elevation.ElevationHelper;
import org.openstreetmap.josm.plugins.elevation.IElevationProfile;
import org.openstreetmap.josm.plugins.elevation.gpx.ElevationWayPointKind;
import org.openstreetmap.josm.tools.CheckParameterUtil;
/**
* Provides default rendering for elevation profile layer.
* @author Oliver Wieland <oliver.wieland@online.de>
*/
public class DefaultElevationProfileRenderer implements
IElevationProfileRenderer {
private static final int ROUND_RECT_RADIUS = 6;
/**
*
*/
private static final int TRIANGLE_BASESIZE = 24;
/**
*
*/
private static final int BASIC_WPT_RADIUS = 1;
private static final int BIG_WPT_RADIUS = BASIC_WPT_RADIUS * 16;
// predefined colors
private static final Color HIGH_COLOR = ElevationColors.EPMidBlue;
private static final Color LOW_COLOR = ElevationColors.EPMidBlue;
private static final Color START_COLOR = Color.GREEN;
private static final Color END_POINT = Color.RED;
private static final Color LEVEL_GAIN_COLOR = Color.GREEN;
private static final Color LEVEL_LOSS_COLOR = Color.RED;
private static final Color MARKER_POINT = Color.YELLOW;
// Predefined radians
private static final double RAD_180 = Math.PI;
// private static final double RAD_270 = Math.PI * 1.5;
private static final double RAD_90 = Math.PI * 0.5;
private final List<Rectangle> forbiddenRects = new ArrayList<>();
@Override
public Color getColorForWaypoint(IElevationProfile profile, WayPoint wpt,
ElevationWayPointKind kind) {
if (wpt == null || profile == null) {
System.err.println(String.format(
"Cannot determine color: prof=%s, wpt=%s", profile, wpt));
return null;
}
switch (kind) {
case Plain:
return Color.LIGHT_GRAY;
case ElevationLevelLoss:
return LEVEL_LOSS_COLOR;
case ElevationLevelGain:
return LEVEL_GAIN_COLOR;
case Highlighted:
return Color.ORANGE;
case ElevationGainHigh:
return Color.getHSBColor(0.3f, 1.0f, 1.0f); // green
case ElevationLossHigh:
return Color.getHSBColor(0, 1.0f, 1.0f); // red
case ElevationGainLow:
return Color.getHSBColor(0.3f, 0.5f, 1.0f); // green with low sat
case ElevationLossLow:
return Color.getHSBColor(0, 0.5f, 1.0f); // red with low sat
case FullHour:
return MARKER_POINT;
case MaxElevation:
return HIGH_COLOR;
case MinElevation:
return LOW_COLOR;
case StartPoint:
return START_COLOR;
case EndPoint:
return END_POINT;
default:
break;
}
throw new RuntimeException("Unknown way point kind: " + kind);
}
@Override
public void renderWayPoint(Graphics g, IElevationProfile profile,
MapView mv, WayPoint wpt, ElevationWayPointKind kind) {
CheckParameterUtil.ensureParameterNotNull(g, "graphics");
CheckParameterUtil.ensureParameterNotNull(profile, "profile");
CheckParameterUtil.ensureParameterNotNull(mv, "map view");
if (wpt == null) {
System.err.println(String.format(
"Cannot paint: mv=%s, prof=%s, wpt=%s", mv, profile, wpt));
return;
}
switch (kind) {
case MinElevation:
case MaxElevation:
renderMinMaxPoint(g, profile, mv, wpt, kind);
break;
case EndPoint:
case StartPoint:
renderStartEndPoint(g, profile, mv, wpt, kind);
break;
default:
renderRegularWayPoint(g, profile, mv, wpt, kind);
break;
}
}
@Override
public void renderLine(Graphics g, IElevationProfile profile,
MapView mv, WayPoint wpt1, WayPoint wpt2, ElevationWayPointKind kind) {
CheckParameterUtil.ensureParameterNotNull(g, "graphics");
CheckParameterUtil.ensureParameterNotNull(profile, "profile");
CheckParameterUtil.ensureParameterNotNull(mv, "map view");
if (wpt1 == null || wpt2 == null) {
System.err.println(String.format(
"Cannot paint line: mv=%s, prof=%s, kind = %s", mv, profile, kind));
return;
}
// obtain and set color
g.setColor(getColorForWaypoint(profile, wpt2, kind));
// transform to view
Point pnt1 = mv.getPoint(wpt1.getEastNorth());
Point pnt2 = mv.getPoint(wpt2.getEastNorth());
// use thick line, if possible
if (g instanceof Graphics2D) {
Graphics2D g2 = (Graphics2D) g;
Stroke oldS = g2.getStroke();
try {
g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));
g2.drawLine(pnt1.x, pnt1.y, pnt2.x, pnt2.y);
} finally {
// must be restored; otherwise other layers may using this style, too
g2.setStroke(oldS);
}
} else {
// only poor man's graphics
g.drawLine(pnt1.x, pnt1.y, pnt2.x, pnt2.y);
}
}
/**
* Renders a regular way point.
*
* @param g
* The graphics context.
* @param profile
* The elevation profile.
* @param mv
* The map view instance.
* @param wpt
* The way point to render.
* @param kind
* The way point kind (start, end, max,...).
*/
private void renderRegularWayPoint(Graphics g, IElevationProfile profile,
MapView mv, WayPoint wpt, ElevationWayPointKind kind) {
Color c = getColorForWaypoint(profile, wpt, kind);
Point pnt = mv.getPoint(wpt.getEastNorth());
/* Paint full hour label */
if (kind == ElevationWayPointKind.FullHour) {
int hour = ElevationHelper.getHourOfWayPoint(wpt);
drawLabel(String.format("%02d:00", hour), pnt.x, pnt.y
+ g.getFontMetrics().getHeight(), g);
}
/* Paint label for elevation levels */
if (kind == ElevationWayPointKind.ElevationLevelGain || kind == ElevationWayPointKind.ElevationLevelLoss) {
int ele = ((int) Math.rint(ElevationHelper.getElevation(wpt) / 100.0)) * 100;
drawLabelWithTriangle(ElevationHelper.getElevationText(ele), pnt.x, pnt.y
+ g.getFontMetrics().getHeight(), g, Color.darkGray, 8,
getColorForWaypoint(profile, wpt, kind),
kind == ElevationWayPointKind.ElevationLevelGain ? TriangleDir.Up : TriangleDir.Down);
}
/* Paint cursor labels */
if (kind == ElevationWayPointKind.Highlighted) {
drawSphere(g, Color.WHITE, c, pnt.x, pnt.y, BIG_WPT_RADIUS);
drawLabel(ElevationHelper.getTimeText(wpt), pnt.x, pnt.y
- g.getFontMetrics().getHeight() - 5, g);
drawLabel(ElevationHelper.getElevationText(wpt), pnt.x, pnt.y
+ g.getFontMetrics().getHeight() + 5, g);
}
}
/**
* Renders a min/max point
*
* @param g
* The graphics context.
* @param profile
* The elevation profile.
* @param mv
* The map view instance.
* @param wpt
* The way point to render.
* @param kind
* The way point kind (start, end, max,...).
*/
private void renderMinMaxPoint(Graphics g, IElevationProfile profile,
MapView mv, WayPoint wpt, ElevationWayPointKind kind) {
Color c = getColorForWaypoint(profile, wpt, kind);
int eleH = (int) ElevationHelper.getElevation(wpt);
Point pnt = mv.getPoint(wpt.getEastNorth());
TriangleDir td = TriangleDir.Up;
switch (kind) {
case MaxElevation:
td = TriangleDir.Up;
break;
case MinElevation:
td = TriangleDir.Down;
break;
case EndPoint:
td = TriangleDir.Left;
break;
case StartPoint:
td = TriangleDir.Right;
break;
default:
return; // nothing to do
}
drawRegularTriangle(g, c, td, pnt.x, pnt.y,
DefaultElevationProfileRenderer.TRIANGLE_BASESIZE);
drawLabel(ElevationHelper.getElevationText(eleH), pnt.x, pnt.y
+ g.getFontMetrics().getHeight(), g, c);
}
/**
* Draws a regular triangle.
*
* @param g
* The graphics context.
* @param c
* The fill color of the triangle.
* @param dir
* The direction of the triangle
* @param x
* The x coordinate in the graphics context.
* @param y
* The y coordinate in the graphics context.
* @param baseLength
* The side length in pixel of the triangle.
*/
private void drawRegularTriangle(Graphics g, Color c, TriangleDir dir,
int x, int y, int baseLength) {
if (baseLength < 2)
return; // cannot render triangle
int b2 = baseLength >> 1;
// coordinates for upwards directed triangle
Point[] p = new Point[3];
for (int i = 0; i < p.length; i++) {
p[i] = new Point();
}
p[0].x = -b2;
p[0].y = b2;
p[1].x = b2;
p[1].y = b2;
p[2].x = 0;
p[2].y = -b2;
Triangle t = new Triangle(p[0], p[1], p[2]);
// rotation angle in rad
double theta = 0.0;
switch (dir) {
case Up:
theta = 0.0;
break;
case Down:
theta = RAD_180;
break;
case Left:
theta = -RAD_90;
break;
case Right:
theta = RAD_90;
break;
}
// rotate shape
AffineTransform at = AffineTransform.getRotateInstance(theta);
Shape tRot = at.createTransformedShape(t);
// translate shape
AffineTransform at2 = AffineTransform.getTranslateInstance(x, y);
Shape ts = at2.createTransformedShape(tRot);
// draw the shape
Graphics2D g2 = (Graphics2D) g;
if (g2 != null) {
Color oldC = g2.getColor();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(c);
g2.fill(ts);
g2.setColor(oldC);
}
}
/**
* Renders a start/end point.
*
* @param g
* The graphics context.
* @param profile
* The elevation profile.
* @param mv
* The map view instance.
* @param wpt
* The way point to render.
* @param kind
* The way point kind (start, end, max,...).
*/
private void renderStartEndPoint(Graphics g, IElevationProfile profile,
MapView mv, WayPoint wpt, ElevationWayPointKind kind) {
Color c = getColorForWaypoint(profile, wpt, kind);
Point pnt = mv.getPoint(wpt.getEastNorth());
drawSphere(g, Color.WHITE, c, pnt.x, pnt.y, BIG_WPT_RADIUS);
}
/**
* Draws a shaded sphere.
*
* @param g
* The graphics context.
* @param firstCol
* The focus color (usually white).
* @param secondCol
* The sphere color.
* @param x
* The x coordinate of the sphere center.
* @param y
* The y coordinate of the sphere center.
* @param radius
* The radius of the sphere.
*/
private void drawSphere(Graphics g, Color firstCol, Color secondCol, int x,
int y, int radius) {
Point2D center = new Point2D.Float(x, y);
Point2D focus = new Point2D.Float(x - (radius * 0.6f), y
- (radius * 0.6f));
float[] dist = {0.1f, 0.2f, 1.0f};
Color[] colors = {firstCol, secondCol, Color.DARK_GRAY};
RadialGradientPaint p = new RadialGradientPaint(center, radius, focus,
dist, colors, CycleMethod.NO_CYCLE);
Graphics2D g2 = (Graphics2D) g;
if (g2 != null) {
g2.setPaint(p);
int r2 = radius / 2;
g2.fillOval(x - r2, y - r2, radius, radius);
}
}
/**
* Draws a label within a filled rounded rectangle with standard gradient colors.
*
* @param s
* The text to draw.
* @param x
* The x coordinate of the label.
* @param y
* The y coordinate of the label.
* @param g
* The graphics context.
*/
private void drawLabel(String s, int x, int y, Graphics g) {
drawLabel(s, x, y, g, Color.GRAY);
}
/**
* Draws a label within a filled rounded rectangle with the specified second gradient color (first color is <tt>Color.WHITE</tt>).
*
* @param s
* The text to draw.
* @param x
* The x coordinate of the label.
* @param y
* The y coordinate of the label.
* @param g
* The graphics context.
* @param secondGradColor
* The second color of the gradient.
*/
private void drawLabel(String s, int x, int y, Graphics g,
Color secondGradColor) {
Graphics2D g2d = (Graphics2D) g;
int width = g.getFontMetrics(g.getFont()).stringWidth(s) + 10;
int height = g.getFont().getSize() + g.getFontMetrics().getLeading()
+ 5;
Rectangle r = new Rectangle(x - (width / 2), y - (height / 2), width,
height);
if (isForbiddenArea(r)) {
return; // no space left, skip this label
}
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
GradientPaint gradient = new GradientPaint(x, y, Color.WHITE, x, y
+ (height / 2), secondGradColor, false);
g2d.setPaint(gradient);
g2d.fillRoundRect(r.x, r.y, r.width, r.height, ROUND_RECT_RADIUS,
ROUND_RECT_RADIUS);
g2d.setColor(Color.BLACK);
g2d.drawRoundRect(r.x, r.y, r.width, r.height, ROUND_RECT_RADIUS,
ROUND_RECT_RADIUS);
g2d.drawString(s, x - (width / 2) + 5, y + (height / 2) - 3);
forbiddenRects.add(r);
}
/**
* Draws a label with an additional triangle on the left side.
*
* @param s
* The text to draw.
* @param x
* The x coordinate of the label.
* @param y
* The y coordinate of the label.
* @param g
* The graphics context.
* @param secondGradColor
* The second color of the gradient.
* @param baseLength
* The base length of the triangle in pixels.
* @param triangleColor
* The color of the triangle.
* @param triangleDir
* The direction of the triangle.
*/
private void drawLabelWithTriangle(String s, int x, int y, Graphics g,
Color secondGradColor, int baseLength, Color triangleColor,
TriangleDir triangleDir) {
Graphics2D g2d = (Graphics2D) g;
int width = g.getFontMetrics(g.getFont()).stringWidth(s) + 10 + baseLength + 5;
int height = g.getFont().getSize() + g.getFontMetrics().getLeading() + 5;
Rectangle r = new Rectangle(x - (width / 2), y - (height / 2), width, height);
if (isForbiddenArea(r)) {
return; // no space left, skip this label
}
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
GradientPaint gradient = new GradientPaint(x, y, Color.WHITE, x, y
+ (height / 2), secondGradColor, false);
g2d.setPaint(gradient);
g2d.fillRoundRect(r.x, r.y, r.width, r.height, ROUND_RECT_RADIUS,
ROUND_RECT_RADIUS);
g2d.setColor(Color.BLACK);
g2d.drawRoundRect(r.x, r.y, r.width, r.height, ROUND_RECT_RADIUS,
ROUND_RECT_RADIUS);
g2d.drawString(s, x - (width / 2) + 8 + baseLength, y + (height / 2) - 3);
drawRegularTriangle(g2d, triangleColor, triangleDir, r.x + baseLength,
r.y + baseLength, baseLength);
forbiddenRects.add(r);
}
/**
* Checks, if the rectangle has been 'reserved' by an previous draw action.
*
* @param r
* The area to check for.
* @return true, if area is already occupied by another rectangle.
*/
private boolean isForbiddenArea(Rectangle r) {
for (Rectangle rTest : forbiddenRects) {
if (r.intersects(rTest))
return true;
}
return false;
}
@Override
public void beginRendering() {
forbiddenRects.clear();
}
@Override
public void finishRendering() {
// nothing to do currently
}
}