1 package tim.prune.gui.profile;
4 import java.awt.Dimension;
5 import java.awt.FlowLayout;
6 import java.awt.Graphics;
7 import java.awt.Graphics2D;
8 import java.awt.RenderingHints;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ActionListener;
11 import java.awt.event.MouseEvent;
12 import java.awt.event.MouseListener;
14 import javax.swing.JLabel;
15 import javax.swing.JMenuItem;
16 import javax.swing.JPopupMenu;
18 import tim.prune.I18nManager;
19 import tim.prune.config.ColourScheme;
20 import tim.prune.config.Config;
21 import tim.prune.data.Field;
22 import tim.prune.data.FieldList;
23 import tim.prune.data.TrackInfo;
24 import tim.prune.gui.GenericDisplay;
27 * Chart component for the profile display
29 public class ProfileChart extends GenericDisplay implements MouseListener
31 /** Inner class to handle popup menu clicks */
32 class MenuClicker implements ActionListener
34 private Field _field = null;
35 MenuClicker(Field inField) {_field = inField;}
36 /** React to menu click by changing the field */
37 public void actionPerformed(ActionEvent arg0) {
42 /** Inner class to remember a single index */
45 public int index = -1;
46 public boolean hasValue = false;
52 /** Set a single value */
53 public void set(int inValue)
56 hasValue = (inValue != -1);
58 /** Add an index to the minimum calculation */
59 public void setMin(PointIndex other)
61 if (!other.hasValue) {return;}
67 index = Math.min(index, other.index);
70 /** Add an index to the maximum calculation */
71 public void setMax(PointIndex other)
73 if (!other.hasValue) {return;}
79 index = Math.max(index, other.index);
82 /** @return true if two Indexes are equal */
83 public boolean equals(PointIndex other)
85 if (!hasValue || !other.hasValue) {
86 return hasValue == other.hasValue;
88 return index == other.index;
92 /** Inner class to remember previous chart parameters */
95 public PointIndex selectedPoint = new PointIndex();
96 public PointIndex rangeStart = new PointIndex(), rangeEnd = new PointIndex();
99 selectedPoint.hasValue = false;
100 rangeStart.hasValue = false;
101 rangeEnd.hasValue = false;
103 /** Get the minimum index which has changed between two sets of parameters */
104 public int getMinChangedIndex(ChartParameters other)
106 PointIndex minIndex = new PointIndex();
107 if (!selectedPoint.equals(other.selectedPoint)) {
108 minIndex.setMin(selectedPoint);
109 minIndex.setMin(other.selectedPoint);
111 if (!rangeStart.equals(other.rangeStart)) {
112 minIndex.setMin(rangeStart);
113 minIndex.setMin(other.rangeStart);
115 if (!rangeEnd.equals(other.rangeEnd)) {
116 minIndex.setMin(rangeEnd);
117 minIndex.setMin(other.rangeEnd);
119 return minIndex.index;
121 /** Get the maximum index which has changed between two sets of parameters */
122 public int getMaxChangedIndex(ChartParameters other)
124 PointIndex maxIndex = new PointIndex();
125 if (!selectedPoint.equals(other.selectedPoint)) {
126 maxIndex.setMax(selectedPoint);
127 maxIndex.setMax(other.selectedPoint);
129 if (!rangeStart.equals(other.rangeStart)) {
130 maxIndex.setMax(rangeStart);
131 maxIndex.setMax(other.rangeStart);
133 if (!rangeEnd.equals(other.rangeEnd)) {
134 maxIndex.setMax(rangeEnd);
135 maxIndex.setMax(other.rangeEnd);
137 return maxIndex.index;
139 /** @return true if the parameters are completely empty (cleared) */
140 public boolean isEmpty()
142 return !selectedPoint.hasValue && !rangeStart.hasValue && !rangeEnd.hasValue;
146 /** Current scale factor in x direction*/
147 private double _xScaleFactor = 0.0;
148 /** Data to show on chart */
149 private ProfileData _data = null;
150 /** Label for chart type, units */
151 private JLabel _label = null;
152 /** Right-click popup menu */
153 private JPopupMenu _popup = null;
154 /** Parameters last time chart was drawn */
155 private ChartParameters _previousParameters = new ChartParameters();
157 /** Possible scales to use */
158 private static final int[] LINE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 10, 5, 2, 1};
159 /** Border width around black line */
160 private static final int BORDER_WIDTH = 6;
161 /** Minimum size for profile chart in pixels */
162 private static final Dimension MINIMUM_SIZE = new Dimension(200, 110);
163 /** Colour to use for text if no data found */
164 private static final Color COLOR_NODATA_TEXT = Color.GRAY;
169 * @param inTrackInfo Track info object
171 public ProfileChart(TrackInfo inTrackInfo)
174 _data = new AltitudeData(inTrackInfo.getTrack());
175 addMouseListener(this);
176 setLayout(new FlowLayout(FlowLayout.LEFT));
177 _label = new JLabel("Altitude"); // text will be replaced later
184 * Override minimum size method to restrict slider
186 public Dimension getMinimumSize()
192 * Override paint method to draw map
193 * @param g Graphics object
195 public void paint(Graphics g)
198 // Set antialiasing depending on Config
199 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
200 Config.getConfigBoolean(Config.KEY_ANTIALIAS) ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
201 ColourScheme colourScheme = Config.getColourScheme();
202 paintBackground(g, colourScheme);
204 if (_track == null || _track.getNumPoints() <= 0)
209 _label.setText(_data.getLabel());
210 int width = getWidth();
211 int height = getHeight();
214 final Color barColour = colourScheme.getColour(ColourScheme.IDX_POINT);
215 final Color rangeColour = colourScheme.getColour(ColourScheme.IDX_SELECTION);
216 final Color currentColour = colourScheme.getColour(ColourScheme.IDX_PRIMARY);
217 final Color secondColour = colourScheme.getColour(ColourScheme.IDX_SECONDARY);
218 final Color lineColour = colourScheme.getColour(ColourScheme.IDX_LINES);
220 // message if no data for the current field in track
221 if (!_data.hasData())
223 g.setColor(lineColour);
224 g.drawString(I18nManager.getText(_data.getNoDataKey()), 50, (height+_label.getHeight())/2);
229 // Find minimum and maximum values to plot
230 double minValue = _data.getMinValue();
231 double maxValue = _data.getMaxValue();
232 if (maxValue <= minValue) {maxValue = minValue + 1; minValue--;}
234 final int numPoints = _track.getNumPoints();
235 _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH - 1) / numPoints;
236 int usableHeight = height - 2 * BORDER_WIDTH - _label.getHeight();
237 double yScaleFactor = 1.0 * usableHeight / (maxValue - minValue);
238 int barWidth = (int) (_xScaleFactor + 1.0);
239 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
240 // selection start, end
241 int selectionStart = -1, selectionEnd = -1;
242 if (_trackInfo.getSelection().hasRangeSelected())
244 selectionStart = _trackInfo.getSelection().getStart();
245 selectionEnd = _trackInfo.getSelection().getEnd();
250 // horizontal lines for scale - set to round numbers eg 500
251 final int lineScale = getLineScale(minValue, maxValue);
252 double scaleValue = Math.ceil(minValue/lineScale) * lineScale;
253 final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue));
255 g.setColor(lineColour);
258 while (scaleValue < maxValue)
260 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
261 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
262 scaleValue += lineScale;
265 else if (minValue < 0.0)
267 // just draw zero line
269 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
274 // loop through points
275 g.setColor(barColour);
276 for (int p = 0; p < numPoints; p++)
278 if (p == selectionStart)
279 g.setColor(rangeColour);
280 else if (p == (selectionEnd+1))
281 g.setColor(barColour);
283 final int x = (int) (_xScaleFactor * p) + 1;
284 if (_data.hasData(p))
286 value = _data.getData(p);
287 // Normal case is the minimum value greater than zero
290 y = (int) (yScaleFactor * (value - minValue));
291 g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
293 else if (value >= 0.0)
295 // Bar upwards from the zero line
296 y = height-BORDER_WIDTH - (int) (yScaleFactor * (value - minValue));
297 g.fillRect(BORDER_WIDTH+x, y, barWidth, zeroY - y);
301 // Bar downwards from the zero line
302 int barHeight = (int) (yScaleFactor * value);
303 g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight);
308 // current point (make sure it's drawn last)
309 if (selectedPoint >= 0)
311 final int sel_x = (int) (_xScaleFactor * selectedPoint) + 1;
312 g.setColor(secondColour);
313 g.fillRect(BORDER_WIDTH + sel_x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2);
314 if (_data.hasData(selectedPoint))
316 g.setColor(currentColour);
317 value = _data.getData(selectedPoint);
318 y = (int) (yScaleFactor * (value - minValue));
319 g.fillRect(BORDER_WIDTH + sel_x, height-BORDER_WIDTH - y, barWidth, y);
323 catch (NullPointerException npe)
324 { // ignore, probably due to data being changed
326 // Draw numbers on top of the graph to mark scale
329 int textHeight = g.getFontMetrics().getHeight();
330 scaleValue = (int) (minValue / lineScale + 1) * lineScale;
331 if (minValue < 0.0) {scaleValue -= lineScale;}
333 g.setColor(currentColour);
334 while (scaleValue < maxValue)
336 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
337 // Limit y so String isn't above border
338 if (y < (BORDER_WIDTH + textHeight)) {
339 y = BORDER_WIDTH + textHeight;
341 g.drawString(""+(int)scaleValue, BORDER_WIDTH + 5, y);
342 scaleValue += lineScale;
345 // Paint label on top
351 * Paint the background for the chart
352 * @param inG graphics object
353 * @param inColourScheme colour scheme
355 private void paintBackground(Graphics inG, ColourScheme inColourScheme)
357 final int width = getWidth();
358 final int height = getHeight();
360 final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
361 final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
363 inG.setColor(backgroundColour);
364 inG.fillRect(0, 0, width, height);
365 if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return;
366 // Display message if no data to be displayed
367 if (_track == null || _track.getNumPoints() <= 0)
369 inG.setColor(COLOR_NODATA_TEXT);
370 inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
374 inG.setColor(borderColour);
375 inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
376 width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
381 * Make the popup menu for right-clicking the chart
383 private synchronized void makePopup()
385 if (_track.getNumPoints() < 1)
390 _popup = new JPopupMenu();
391 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
392 altItem.addActionListener(new ActionListener() {
393 public void actionPerformed(ActionEvent e)
395 changeView(Field.ALTITUDE);
398 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
399 speedItem.addActionListener(new ActionListener() {
400 public void actionPerformed(ActionEvent e)
402 changeView(Field.SPEED);
404 _popup.add(speedItem);
405 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
406 vertSpeedItem.addActionListener(new ActionListener() {
407 public void actionPerformed(ActionEvent e)
409 changeView(Field.VERTICAL_SPEED);
411 _popup.add(vertSpeedItem);
412 // Go through track's master field list, see if any other fields to list
413 boolean addSeparator = true;
414 FieldList fields = _track.getFieldList();
415 for (int i=0; i<fields.getNumFields(); i++)
417 Field field = fields.getField(i);
418 if (!field.isBuiltIn())
420 if (addSeparator) {_popup.addSeparator();}
421 addSeparator = false;
422 JMenuItem item = new JMenuItem(field.getName());
423 item.addActionListener(new MenuClicker(field));
430 * Work out the scale for the horizontal lines
431 * @param inMin min value of data
432 * @param inMax max value of data
433 * @return scale separation, or -1 for no scale
435 private int getLineScale(double inMin, double inMax)
437 if ((inMax - inMin) < 2.0) {
440 int numScales = LINE_SCALES.length;
441 for (int i=0; i<numScales; i++)
443 int scale = LINE_SCALES[i];
444 int numLines = (int)(inMax / scale) - (int)(inMin / scale);
445 // Check for too many lines
446 if (numLines > 10) return -1;
447 // If more than 1 line then use this scale
448 if (numLines > 1) return scale;
450 // no suitable scale found so just use minimum
451 return LINE_SCALES[numScales-1];
456 * Method to inform map that data has changed
458 public void dataUpdated(byte inUpdateType)
460 // Try not to recalculate all the values unless necessary
461 if (inUpdateType != SELECTION_CHANGED)
463 _data.init(Config.getUnitSet());
464 _previousParameters.clear();
466 // Update the menu if necessary
467 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
471 ChartParameters currentParameters = new ChartParameters();
472 currentParameters.selectedPoint.set(_trackInfo.getSelection().getCurrentPointIndex());
473 if (_trackInfo.getSelection().hasRangeSelected())
475 currentParameters.rangeStart.set(_trackInfo.getSelection().getStart());
476 currentParameters.rangeEnd.set(_trackInfo.getSelection().getEnd());
478 if (inUpdateType == SELECTION_CHANGED)
480 triggerPartialRepaint(currentParameters);
486 _previousParameters = currentParameters;
490 * For performance reasons, only repaint the part of the graphics affected by
491 * the change in selection
492 * @param currentParameters - contains the current selected point, range
494 private void triggerPartialRepaint(ChartParameters currentParameters)
496 int minPointIndex = currentParameters.getMinChangedIndex(_previousParameters);
497 minPointIndex = Math.max(minPointIndex, 0);
498 int maxPointIndex = currentParameters.getMaxChangedIndex(_previousParameters);
499 if (maxPointIndex < minPointIndex) {
500 maxPointIndex = _trackInfo.getTrack().getNumPoints() - 1;
502 // System.out.println("Redraw from index: " + minPointIndex + " to " + maxPointIndex);
503 final int region_x = (int) (_xScaleFactor * minPointIndex) + BORDER_WIDTH - 2;
504 final int region_width = (int) (_xScaleFactor * (maxPointIndex-minPointIndex+2)) + 6;
505 repaint(region_x, 0, region_width, getHeight());
506 // System.out.println("Partial repaint, x=" + region_x + ", region_width=" + region_width);
510 * React to click on profile display
512 public void mouseClicked(MouseEvent e)
514 if (_track == null || _track.getNumPoints() < 1) {return;}
518 int xClick = e.getX();
519 int yClick = e.getY();
520 // Check click is within main area (not in border)
521 if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
522 && yClick < (getHeight() - BORDER_WIDTH))
524 // work out which data point is nearest and select it
525 int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
526 // If shift clicked, then extend selection
527 if (e.isShiftDown()) {
528 _trackInfo.extendSelection(pointNum);
531 _trackInfo.selectPoint(pointNum);
535 else if (_popup != null)
538 _popup.show(this, e.getX(), e.getY());
543 * Called by clicking on popup menu to change the view
544 * @param inField field to show
546 private void changeView(Field inField)
548 if (inField == Field.ALTITUDE)
550 if (!(_data instanceof AltitudeData)) {
551 _data = new AltitudeData(_track);
554 else if (inField == Field.SPEED)
556 if (!(_data instanceof SpeedData)) {
557 _data = new SpeedData(_track);
560 else if (inField == Field.VERTICAL_SPEED)
562 if (!(_data instanceof VerticalSpeedData)) {
563 _data = new VerticalSpeedData(_track);
568 if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
569 _data = new ArbitraryData(_track, inField);
572 _data.init(Config.getUnitSet());
577 * mouse enter events ignored
579 public void mouseEntered(MouseEvent e)
583 * mouse exit events ignored
585 public void mouseExited(MouseEvent e)
589 * ignore mouse pressed for now too
591 public void mousePressed(MouseEvent e)
595 * and also ignore mouse released
597 public void mouseReleased(MouseEvent e)