// 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 } }