--- /dev/null
+package tim.prune.gui.profile;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import javax.swing.JLabel;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+
+import tim.prune.I18nManager;
+import tim.prune.config.ColourScheme;
+import tim.prune.config.Config;
+import tim.prune.data.TrackInfo;
+import tim.prune.gui.GenericDisplay;
+
+/**
+ * Chart component for the profile display
+ */
+public class ProfileChart extends GenericDisplay implements MouseListener
+{
+ /** Current scale factor in x direction*/
+ private double _xScaleFactor = 0.0;
+ /** Data to show on chart */
+ private ProfileData _data = null;
+ /** Label for chart type, units */
+ private JLabel _label = null;
+ /** Right-click popup menu */
+ private JPopupMenu _popup = null;
+
+ /** Possible scales to use */
+ private static final int[] LINE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 10, 5};
+ /** Border width around black line */
+ private static final int BORDER_WIDTH = 6;
+ /** Minimum size for profile chart in pixels */
+ private static final Dimension MINIMUM_SIZE = new Dimension(200, 110);
+ /** Colour to use for text if no data found */
+ private static final Color COLOR_NODATA_TEXT = Color.GRAY;
+ /** Chart type */
+ private static enum ChartType {ALTITUDE, SPEED};
+
+
+ /**
+ * Constructor
+ * @param inTrackInfo Track info object
+ */
+ public ProfileChart(TrackInfo inTrackInfo)
+ {
+ super(inTrackInfo);
+ _data = new AltitudeData(inTrackInfo.getTrack());
+ addMouseListener(this);
+ setLayout(new FlowLayout(FlowLayout.LEFT));
+ _label = new JLabel("Altitude");
+ add(_label);
+ makePopup();
+ }
+
+
+ /**
+ * Override minimum size method to restrict slider
+ */
+ public Dimension getMinimumSize()
+ {
+ return MINIMUM_SIZE;
+ }
+
+ /**
+ * Override paint method to draw map
+ * @param g Graphics object
+ */
+ public void paint(Graphics g)
+ {
+ super.paint(g);
+ ColourScheme colourScheme = Config.getColourScheme();
+ paintBackground(g, colourScheme);
+ if (_track != null && _track.getNumPoints() > 0)
+ {
+ _data.init();
+ _label.setText(_data.getLabel());
+ int width = getWidth();
+ int height = getHeight();
+
+ // Set up colours
+ final Color barColour = colourScheme.getColour(ColourScheme.IDX_POINT);
+ final Color rangeColour = colourScheme.getColour(ColourScheme.IDX_SELECTION);
+ final Color currentColour = colourScheme.getColour(ColourScheme.IDX_PRIMARY);
+ final Color secondColour = colourScheme.getColour(ColourScheme.IDX_SECONDARY);
+ final Color lineColour = colourScheme.getColour(ColourScheme.IDX_LINES);
+
+ // message if no data for the current field in track
+ if (!_data.hasData())
+ {
+ g.setColor(lineColour);
+ g.drawString(I18nManager.getText(_data.getNoDataKey()), 50, (height+_label.getHeight())/2);
+ paintChildren(g);
+ return;
+ }
+
+ // Find minimum and maximum values to plot
+ double minValue = _data.getMinValue();
+ double maxValue = _data.getMaxValue();
+ if (maxValue <= minValue) {maxValue = minValue + 1;}
+
+ final int numPoints = _track.getNumPoints();
+ _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH - 1) / numPoints;
+ int usableHeight = height - 2 * BORDER_WIDTH - _label.getHeight();
+ double yScaleFactor = 1.0 * usableHeight / (maxValue - minValue);
+ int barWidth = (int) (_xScaleFactor + 1.0);
+ int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
+ // selection start, end
+ int selectionStart = -1, selectionEnd = -1;
+ if (_trackInfo.getSelection().hasRangeSelected()) {
+ selectionStart = _trackInfo.getSelection().getStart();
+ selectionEnd = _trackInfo.getSelection().getEnd();
+ }
+
+ // horizontal lines for scale - set to round numbers eg 500
+ int lineScale = getLineScale(minValue, maxValue);
+ int scaleValue = (int) (minValue/lineScale + 1) * lineScale;
+ int x = 0, y = 0;
+ double value = 0.0;
+ if (lineScale > 1)
+ {
+ g.setColor(lineColour);
+ while (scaleValue < maxValue)
+ {
+ y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
+ g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
+ scaleValue += lineScale;
+ }
+ }
+
+ try
+ {
+ // loop through points
+ g.setColor(barColour);
+ for (int p = 0; p < numPoints; p++)
+ {
+ x = (int) (_xScaleFactor * p) + 1;
+ if (p == selectionStart)
+ g.setColor(rangeColour);
+ else if (p == (selectionEnd+1))
+ g.setColor(barColour);
+ if (_data.hasData(p))
+ {
+ value = _data.getData(p);
+ y = (int) (yScaleFactor * (value - minValue));
+ g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
+ }
+ }
+ // current point (make sure it's drawn last)
+ if (_data.hasData(selectedPoint))
+ {
+ x = (int) (_xScaleFactor * selectedPoint) + 1;
+ g.setColor(secondColour);
+ g.fillRect(BORDER_WIDTH + x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2);
+ g.setColor(currentColour);
+ value = _data.getData(selectedPoint);
+ y = (int) (yScaleFactor * (value - minValue));
+ g.fillRect(BORDER_WIDTH + x, height-BORDER_WIDTH - y, barWidth, y);
+ }
+ }
+ catch (NullPointerException npe) { // ignore, probably due to data being changed
+ }
+ // Draw numbers on top of the graph to mark scale
+ if (lineScale > 1)
+ {
+ int textHeight = g.getFontMetrics().getHeight();
+ scaleValue = (int) (minValue / lineScale + 1) * lineScale;
+ y = 0;
+ g.setColor(currentColour);
+ while (scaleValue < maxValue)
+ {
+ y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
+ // Limit y so String isn't above border
+ if (y < (BORDER_WIDTH + textHeight)) {
+ y = BORDER_WIDTH + textHeight;
+ }
+ g.drawString(""+scaleValue, BORDER_WIDTH + 5, y);
+ scaleValue += lineScale;
+ }
+ }
+ // Paint label on top
+ paintChildren(g);
+ }
+ }
+
+
+ /**
+ * Paint the background for the chart
+ * @param inG graphics object
+ * @param inColourScheme colour scheme
+ */
+ private void paintBackground(Graphics inG, ColourScheme inColourScheme)
+ {
+ final int width = getWidth();
+ final int height = getHeight();
+ // Get colours
+ final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
+ final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
+ // background
+ inG.setColor(backgroundColour);
+ inG.fillRect(0, 0, width, height);
+ if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return;
+ // Display message if no data to be displayed
+ if (_track == null || _track.getNumPoints() <= 0)
+ {
+ inG.setColor(COLOR_NODATA_TEXT);
+ inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
+ }
+ else {
+ inG.setColor(borderColour);
+ inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
+ width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
+ }
+ }
+
+ /**
+ * Make the popup menu for right-clicking the chart
+ */
+ private void makePopup()
+ {
+ _popup = new JPopupMenu();
+ JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
+ altItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ changeView(ChartType.ALTITUDE);
+ }});
+ _popup.add(altItem);
+ JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
+ speedItem.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e)
+ {
+ changeView(ChartType.SPEED);
+ }});
+ _popup.add(speedItem);
+ }
+
+ /**
+ * Work out the scale for the horizontal lines
+ * @param inMin min value of data
+ * @param inMax max value of data
+ * @return scale separation, or -1 for no scale
+ */
+ private int getLineScale(double inMin, double inMax)
+ {
+ if ((inMax - inMin) < 5 || inMax < 0) {
+ return -1;
+ }
+ int numScales = LINE_SCALES.length;
+ for (int i=0; i<numScales; i++)
+ {
+ int scale = LINE_SCALES[i];
+ int numLines = (int)(inMax / scale) - (int)(inMin / scale);
+ // Check for too many lines
+ if (numLines > 10) return -1;
+ // If more than 1 line then use this scale
+ if (numLines > 1) return scale;
+ }
+ // no suitable scale found so just use minimum
+ return LINE_SCALES[numScales-1];
+ }
+
+
+ /**
+ * Method to inform map that data has changed
+ */
+ public void dataUpdated(byte inUpdateType)
+ {
+ _data.init();
+ repaint();
+ }
+
+ /**
+ * React to click on profile display
+ */
+ public void mouseClicked(MouseEvent e)
+ {
+ if (_track == null || _track.getNumPoints() < 1) {return;}
+ // left clicks
+ if (!e.isMetaDown())
+ {
+ int xClick = e.getX();
+ int yClick = e.getY();
+ // Check click is within main area (not in border)
+ if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
+ && yClick < (getHeight() - BORDER_WIDTH))
+ {
+ // work out which data point is nearest and select it
+ int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
+ // If shift clicked, then extend selection
+ if (e.isShiftDown()) {
+ _trackInfo.extendSelection(pointNum);
+ }
+ else {
+ _trackInfo.selectPoint(pointNum);
+ }
+ }
+ }
+ else {
+ // right clicks
+ _popup.show(this, e.getX(), e.getY());
+ }
+ }
+
+ /**
+ * Called by clicking on popup menu to change the view
+ * @param inType selected chart type
+ */
+ private void changeView(ChartType inType)
+ {
+ if (inType == ChartType.ALTITUDE && !(_data instanceof AltitudeData))
+ {
+ _data = new AltitudeData(_track);
+ }
+ else if (inType == ChartType.SPEED && !(_data instanceof SpeedData)) {
+ _data = new SpeedData(_track);
+ }
+ _data.init();
+ repaint();
+ }
+
+ /**
+ * mouse enter events ignored
+ */
+ public void mouseEntered(MouseEvent e)
+ {}
+
+ /**
+ * mouse exit events ignored
+ */
+ public void mouseExited(MouseEvent e)
+ {}
+
+ /**
+ * ignore mouse pressed for now too
+ */
+ public void mousePressed(MouseEvent e)
+ {}
+
+ /**
+ * and also ignore mouse released
+ */
+ public void mouseReleased(MouseEvent e)
+ {}
+}