// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.plugins.elevation.gui;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.border.Border;
import org.openstreetmap.josm.data.gpx.WayPoint;
import org.openstreetmap.josm.plugins.elevation.ElevationHelper;
import org.openstreetmap.josm.plugins.elevation.IElevationModel;
import org.openstreetmap.josm.plugins.elevation.IElevationProfile;
import org.openstreetmap.josm.plugins.elevation.gpx.ElevationWayPointKind;
/**
* Provides the panel showing the elevation profile.
* @author Oliver Wieland <oliver.wieland@online.de>
*
*/
public class ElevationProfilePanel extends JPanel implements ComponentListener, MouseMotionListener {
/**
* Serial version UID
*/
private static final long serialVersionUID = -7343429725259575319L;
private static final int BOTTOM_TEXT_Y_OFFSET = 7;
private IElevationModel model;
private Rectangle plotArea;
private final IElevationProfileRenderer renderer = new DefaultElevationProfileRenderer();
private int selectedIndex = -1;
private final List<IElevationProfileSelectionListener> selectionChangedListeners = new ArrayList<>();
private boolean isPainting;
private int step = 0;
/**
* Constructs a new ElevationProfilePanel with the given elevation profile.
* @param profile The elevation profile to show in the panel.
*/
public ElevationProfilePanel(IElevationModel profile) {
super();
this.model = profile;
setDoubleBuffered(true);
setBackground(Color.WHITE);
createOrUpdatePlotArea();
addComponentListener(this);
addMouseMotionListener(this);
Font lFont = getFont().deriveFont(9.0f);
setFont(lFont);
}
/**
* Gets the elevation profile instance.
*/
public IElevationModel getProfile() {
return model;
}
/**
* Sets the new elevation profile instance.
*/
public void setElevationModel(IElevationModel model) {
if (this.model != model) {
this.model = model;
invalidate();
}
}
/**
* Gets the plot area coordinates.
*/
public Rectangle getPlotArea() {
return plotArea;
}
/**
* Sets the plot area coordinates.
*/
public void setPlotArea(Rectangle plotArea) {
this.plotArea = plotArea;
}
/**
* Gets the selected index of the bar.
*/
public int getSelectedIndex() {
return selectedIndex;
}
/**
* Sets the selected index of the bar.
*/
public void setSelectedIndex(int selectedIndex) {
this.selectedIndex = selectedIndex;
if (model != null) {
model.setCurrentProfile(selectedIndex);
}
}
/**
* Gets the selected (highlighted) way point.
* @return The selected way point or null, if no way point is selected.
*/
public WayPoint getSelectedWayPoint() {
if (model == null) return null;
IElevationProfile profile = model.getCurrentProfile();
int selWp = this.selectedIndex * step;
if (profile != null && profile.getWayPoints() != null && selWp > 0 && profile.getWayPoints().size() > selWp) {
return profile.getWayPoints().get(selWp);
} else {
return null;
}
}
/**
* Adds a selection listener.
* @param listener The listener instance to add.
*/
public void addSelectionListener(IElevationProfileSelectionListener listener) {
if (listener == null) return;
selectionChangedListeners.add(listener);
}
/**
* Removes a selection listener from the list.
* @param listener The listener instance to remove.
*/
public void removeSelectionListener(IElevationProfileSelectionListener listener) {
if (listener == null) return;
selectionChangedListeners.remove(listener);
}
/**
* Removes all selection listeners.
*/
public void removeAllSelectionListeners() {
selectionChangedListeners.clear();
}
protected void fireSelectionChanged(WayPoint selWayPoint) {
for (IElevationProfileSelectionListener listener : selectionChangedListeners) {
listener.selectedWayPointChanged(selWayPoint);
}
}
@Override
public void paint(Graphics g) {
isPainting = true;
try {
super.paint(g);
createOrUpdatePlotArea();
int y1 = getPlotBottom();
g.setColor(Color.DARK_GRAY);
g.drawLine(plotArea.x, plotArea.y, plotArea.x, plotArea.y
+ plotArea.height);
g.drawLine(plotArea.x, plotArea.y + plotArea.height, plotArea.x
+ plotArea.width, plotArea.y + plotArea.height);
if (model != null) {
IElevationProfile profile = model.getCurrentProfile();
if (profile != null && profile.hasElevationData()) {
// Draw start and end date
drawAlignedString(formatDate(profile.getStart()), 5, y1 + BOTTOM_TEXT_Y_OFFSET,
TextAlignment.Left, g);
drawAlignedString(formatDate(profile.getEnd()),
getPlotRight(), y1 + BOTTOM_TEXT_Y_OFFSET, TextAlignment.Right, g);
// Show SRTM indicator
if (ElevationHelper.hasSrtmData(profile.getBounds())) {
String txt = "SRTM";
drawAlignedString(txt, getPlotHCenter(), y1 + BOTTOM_TEXT_Y_OFFSET, TextAlignment.Centered, g);
}
drawProfile(g);
drawElevationLines(g);
} else {
// No profile or profile supports no elevation data
drawAlignedString(tr("(No elevation data)"), getPlotHCenter(),
getPlotVCenter(), TextAlignment.Centered, g);
}
}
} finally {
isPainting = false;
}
}
/**
* Draw a string with a specified alignment.
* @param s The text to display.
* @param x The x coordinate.
* @param y The y coordinate.
* @param align The text alignment.
* @param g The graphics context.
* @return The resulting rectangle of the drawn string.
*/
private Rectangle drawAlignedString(String s, int x, int y,
TextAlignment align, Graphics g) {
FontMetrics fm = g.getFontMetrics();
int w = fm.stringWidth(s);
int h = fm.getHeight();
int xoff = w / 2;
int yoff = h / 2;
if (align == TextAlignment.Left) {
xoff = 0;
}
if (align == TextAlignment.Right) {
xoff = w;
}
g.drawString(s, x - xoff, y + yoff);
return new Rectangle(x - xoff, y - yoff, w, h);
}
/**
* Draw a string which is horizontally centered around (x,y).
* @param s The text to display.
* @param x The x coordinate.
* @param y The y coordinate.
* @param g The graphics context.
* @return The resulting rectangle of the drawn string.
private void drawHCenteredString(String s, int x, int y, Graphics g) {
drawAlignedString(s, x, y, TextAlignment.Centered, g);
}*/
/**
* Formats the date in a predefined manner: "21. Oct 2010, 12:10".
*/
private String formatDate(Date date) {
Format formatter = new SimpleDateFormat("d MMM yy, HH:mm");
return formatter.format(date);
}
/**
* Helper function to draw elevation axes.
*/
private void drawElevationLines(Graphics g) {
IElevationProfile profile = model.getCurrentProfile();
double diff = profile.getHeightDifference();
if (diff == 0.0) {
return;
}
double z10 = Math.floor(Math.log10(diff));
double scaleUnit = Math.pow(10, z10); // scale unit, e. g. 100 for
// values below 1000
int upperLimit = (int) (Math.round(Math.ceil(profile.getMaxHeight()
/ scaleUnit)) * scaleUnit);
int lowerLimit = (int) (Math.round(Math.floor(profile.getMinHeight()
/ scaleUnit)) * scaleUnit);
int su = (int) scaleUnit;
for (int i = lowerLimit; i <= upperLimit; i += su) {
int yLine = getYForEelevation(i);
// check bounds
if (yLine <= getPlotBottom() && yLine >= getPlotTop()) {
String txt = ElevationHelper.getElevationText(i);
Rectangle r = drawAlignedString(txt, getPlotHCenter(), yLine - 2,
TextAlignment.Right, g);
r.grow(2, 2);
// Draw left and right line segment
g.drawLine(getPlotLeftAxis(), yLine, r.x,
yLine);
g.drawLine(r.x + r.width, yLine, getPlotRight(),
yLine);
// Draw label with shadow
g.setColor(Color.WHITE);
drawAlignedString(txt, getPlotHCenter() + 1, yLine - 1,
TextAlignment.Right, g);
g.setColor(Color.BLACK);
drawAlignedString(txt, getPlotHCenter(), yLine - 2,
TextAlignment.Right, g);
}
}
}
/**
* Gets the x value of the left border for axes (slightly smaller than the
* left x).
*/
private int getPlotLeftAxis() {
return plotArea.x - 3;
}
/**
* Gets the x value of the left border.
*/
private int getPlotLeft() {
return plotArea.x + 1;
}
/**
* Gets the horizontal center coordinate (mid between left and right x).
*/
private int getPlotHCenter() {
return (getPlotLeft() + getPlotRight()) / 2;
}
/**
* Gets the vertical center coordinate (mid between top and bottom y).
*/
private int getPlotVCenter() {
return (getPlotTop() + getPlotBottom()) / 2;
}
/**
* Gets the x value of the right border.
*/
private int getPlotRight() {
return plotArea.x + plotArea.width - 1;
}
private int getPlotBottom() {
return plotArea.y + plotArea.height - 1;
}
private int getPlotTop() {
return plotArea.y + 1;
}
/**
* Gets for an elevation value the according y coordinate in the plot area.
* @return The y coordinate in the plot area.
*/
private int getYForEelevation(int elevation) {
int y1 = getPlotBottom();
IElevationProfile profile = model.getCurrentProfile();
if (!profile.hasElevationData()) {
return y1;
}
double diff = profile.getHeightDifference();
return y1 - (int) Math.round(((elevation - profile.getMinHeight()) / diff * plotArea.height));
}
/**
* Draws the elevation profile
*/
private void drawProfile(Graphics g) {
IElevationProfile profile = model.getCurrentProfile();
int nwp = profile.getNumberOfWayPoints();
int n = Math.min(plotArea.width, nwp);
if (n == 0) return; // nothing to draw
// compute step size in panel (add 1 to make sure that
// the complete range fits into panel
step = (nwp / n) + 1;
int yBottom = getPlotBottom();
Color oldC = g.getColor();
for (int i = 0, ip = 0; i < n && ip < nwp; i++, ip += step) {
WayPoint wpt = profile.getWayPoints().get(ip);
int eleVal = (int) ElevationHelper.getElevation(wpt);
Color c = renderer.getColorForWaypoint(profile, wpt,
ElevationWayPointKind.Plain);
// draw cursor
if (i == this.selectedIndex) {
g.setColor(Color.BLACK);
drawAlignedString(ElevationHelper.getElevationText(eleVal),
(getPlotRight() + getPlotLeft()) / 2,
getPlotBottom() + 6,
TextAlignment.Centered,
g);
c = renderer.getColorForWaypoint(profile, wpt, ElevationWayPointKind.Highlighted);
}
int yEle = getYForEelevation(eleVal);
int x = getPlotLeft() + i;
g.setColor(c);
g.drawLine(x, yBottom, x, yEle);
g.setColor(ElevationColors.EPLightBlue);
}
g.setColor(oldC);
}
@Override
protected void paintBorder(Graphics g) {
super.paintBorder(g);
Border loweredbevel = BorderFactory.createLoweredBevelBorder();
this.setBorder(loweredbevel);
}
/**
* Determines the size of the plot area depending on the panel size.
*/
private void createOrUpdatePlotArea() {
Dimension caSize = getSize();
if (plotArea == null) {
plotArea = new Rectangle(0, 0, caSize.width, caSize.height);
} else {
plotArea.width = caSize.width;
plotArea.height = caSize.height;
}
plotArea.setLocation(0, 0);
plotArea.grow(-10, -15);
}
@Override
public void componentHidden(ComponentEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void componentMoved(ComponentEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void componentResized(ComponentEvent arg0) {
createOrUpdatePlotArea();
}
@Override
public void componentShown(ComponentEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mouseDragged(MouseEvent arg0) {
// TODO Auto-generated method stub
}
@Override
public void mouseMoved(MouseEvent arg0) {
if (isPainting || arg0.isControlDown() || arg0.isAltDown() || arg0.isShiftDown()) arg0.consume();
int x = arg0.getX();
int l = this.getX();
int pl = this.getPlotLeft();
int newIdx = x - l - pl;
if (newIdx != this.selectedIndex && newIdx >= 0) {
this.selectedIndex = newIdx;
this.repaint();
fireSelectionChanged(getSelectedWayPoint());
}
}
@Override
public String getToolTipText() {
WayPoint wpt = getSelectedWayPoint();
if (wpt != null) {
return String.format("%s: %s", ElevationHelper.getTimeText(wpt), ElevationHelper.getElevationText(wpt));
}
return super.getToolTipText();
}
}