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 /** Current scale factor in x direction*/
43 private double _xScaleFactor = 0.0;
44 /** Data to show on chart */
45 private ProfileData _data = null;
46 /** Label for chart type, units */
47 private JLabel _label = null;
48 /** Right-click popup menu */
49 private JPopupMenu _popup = null;
51 /** Possible scales to use */
52 private static final int[] LINE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 10, 5, 2, 1};
53 /** Border width around black line */
54 private static final int BORDER_WIDTH = 6;
55 /** Minimum size for profile chart in pixels */
56 private static final Dimension MINIMUM_SIZE = new Dimension(200, 110);
57 /** Colour to use for text if no data found */
58 private static final Color COLOR_NODATA_TEXT = Color.GRAY;
63 * @param inTrackInfo Track info object
65 public ProfileChart(TrackInfo inTrackInfo)
68 _data = new AltitudeData(inTrackInfo.getTrack());
69 addMouseListener(this);
70 setLayout(new FlowLayout(FlowLayout.LEFT));
71 _label = new JLabel("Altitude"); // text will be replaced later
78 * Override minimum size method to restrict slider
80 public Dimension getMinimumSize()
86 * Override paint method to draw map
87 * @param g Graphics object
89 public void paint(Graphics g)
92 // Set antialiasing depending on Config
93 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
94 Config.getConfigBoolean(Config.KEY_ANTIALIAS) ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
95 ColourScheme colourScheme = Config.getColourScheme();
96 paintBackground(g, colourScheme);
97 if (_track != null && _track.getNumPoints() > 0)
99 _label.setText(_data.getLabel());
100 int width = getWidth();
101 int height = getHeight();
104 final Color barColour = colourScheme.getColour(ColourScheme.IDX_POINT);
105 final Color rangeColour = colourScheme.getColour(ColourScheme.IDX_SELECTION);
106 final Color currentColour = colourScheme.getColour(ColourScheme.IDX_PRIMARY);
107 final Color secondColour = colourScheme.getColour(ColourScheme.IDX_SECONDARY);
108 final Color lineColour = colourScheme.getColour(ColourScheme.IDX_LINES);
110 // message if no data for the current field in track
111 if (!_data.hasData())
113 g.setColor(lineColour);
114 g.drawString(I18nManager.getText(_data.getNoDataKey()), 50, (height+_label.getHeight())/2);
119 // Find minimum and maximum values to plot
120 double minValue = _data.getMinValue();
121 double maxValue = _data.getMaxValue();
122 if (maxValue <= minValue) {maxValue = minValue + 1; minValue--;}
124 final int numPoints = _track.getNumPoints();
125 _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH - 1) / numPoints;
126 int usableHeight = height - 2 * BORDER_WIDTH - _label.getHeight();
127 double yScaleFactor = 1.0 * usableHeight / (maxValue - minValue);
128 int barWidth = (int) (_xScaleFactor + 1.0);
129 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
130 // selection start, end
131 int selectionStart = -1, selectionEnd = -1;
132 if (_trackInfo.getSelection().hasRangeSelected()) {
133 selectionStart = _trackInfo.getSelection().getStart();
134 selectionEnd = _trackInfo.getSelection().getEnd();
137 // horizontal lines for scale - set to round numbers eg 500
138 int lineScale = getLineScale(minValue, maxValue);
139 int scaleValue = (int) (minValue/lineScale + 1) * lineScale;
140 if (minValue < 0.0) {scaleValue -= lineScale;}
142 final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue));
145 g.setColor(lineColour);
148 while (scaleValue < maxValue)
150 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
151 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
152 scaleValue += lineScale;
155 else if (minValue < 0.0)
157 // just draw zero line
159 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
164 // loop through points
165 g.setColor(barColour);
166 for (int p = 0; p < numPoints; p++)
168 x = (int) (_xScaleFactor * p) + 1;
169 if (p == selectionStart)
170 g.setColor(rangeColour);
171 else if (p == (selectionEnd+1))
172 g.setColor(barColour);
173 if (_data.hasData(p))
175 value = _data.getData(p);
176 // Normal case is the minimum value greater than zero
179 y = (int) (yScaleFactor * (value - minValue));
180 g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
182 else if (value >= 0.0) {
183 // Bar upwards from the zero line
184 y = height-BORDER_WIDTH - (int) (yScaleFactor * (value - minValue));
185 g.fillRect(BORDER_WIDTH+x, y, barWidth, zeroY - y);
188 // Bar downwards from the zero line
189 int barHeight = (int) (yScaleFactor * value);
190 g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight);
194 // current point (make sure it's drawn last)
195 if (selectedPoint >= 0)
197 x = (int) (_xScaleFactor * selectedPoint) + 1;
198 g.setColor(secondColour);
199 g.fillRect(BORDER_WIDTH + x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2);
200 if (_data.hasData(selectedPoint))
202 g.setColor(currentColour);
203 value = _data.getData(selectedPoint);
204 y = (int) (yScaleFactor * (value - minValue));
205 g.fillRect(BORDER_WIDTH + x, height-BORDER_WIDTH - y, barWidth, y);
209 catch (NullPointerException npe) { // ignore, probably due to data being changed
211 // Draw numbers on top of the graph to mark scale
214 int textHeight = g.getFontMetrics().getHeight();
215 scaleValue = (int) (minValue / lineScale + 1) * lineScale;
216 if (minValue < 0.0) {scaleValue -= lineScale;}
218 g.setColor(currentColour);
219 while (scaleValue < maxValue)
221 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
222 // Limit y so String isn't above border
223 if (y < (BORDER_WIDTH + textHeight)) {
224 y = BORDER_WIDTH + textHeight;
226 g.drawString(""+scaleValue, BORDER_WIDTH + 5, y);
227 scaleValue += lineScale;
230 // Paint label on top
237 * Paint the background for the chart
238 * @param inG graphics object
239 * @param inColourScheme colour scheme
241 private void paintBackground(Graphics inG, ColourScheme inColourScheme)
243 final int width = getWidth();
244 final int height = getHeight();
246 final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
247 final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
249 inG.setColor(backgroundColour);
250 inG.fillRect(0, 0, width, height);
251 if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return;
252 // Display message if no data to be displayed
253 if (_track == null || _track.getNumPoints() <= 0)
255 inG.setColor(COLOR_NODATA_TEXT);
256 inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
259 inG.setColor(borderColour);
260 inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
261 width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
266 * Make the popup menu for right-clicking the chart
268 private synchronized void makePopup()
270 _popup = new JPopupMenu();
271 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
272 altItem.addActionListener(new ActionListener() {
273 public void actionPerformed(ActionEvent e)
275 changeView(Field.ALTITUDE);
278 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
279 speedItem.addActionListener(new ActionListener() {
280 public void actionPerformed(ActionEvent e)
282 changeView(Field.SPEED);
284 _popup.add(speedItem);
285 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
286 vertSpeedItem.addActionListener(new ActionListener() {
287 public void actionPerformed(ActionEvent e)
289 changeView(Field.VERTICAL_SPEED);
291 _popup.add(vertSpeedItem);
292 // Go through track's master field list, see if any other fields to list
293 boolean addSeparator = true;
294 FieldList fields = _track.getFieldList();
295 for (int i=0; i<fields.getNumFields(); i++)
297 Field field = fields.getField(i);
298 if (!field.isBuiltIn())
300 if (addSeparator) {_popup.addSeparator();}
301 addSeparator = false;
302 JMenuItem item = new JMenuItem(field.getName());
303 item.addActionListener(new MenuClicker(field));
310 * Work out the scale for the horizontal lines
311 * @param inMin min value of data
312 * @param inMax max value of data
313 * @return scale separation, or -1 for no scale
315 private int getLineScale(double inMin, double inMax)
317 if ((inMax - inMin) < 2.0) {
320 int numScales = LINE_SCALES.length;
321 for (int i=0; i<numScales; i++)
323 int scale = LINE_SCALES[i];
324 int numLines = (int)(inMax / scale) - (int)(inMin / scale);
325 // Check for too many lines
326 if (numLines > 10) return -1;
327 // If more than 1 line then use this scale
328 if (numLines > 1) return scale;
330 // no suitable scale found so just use minimum
331 return LINE_SCALES[numScales-1];
336 * Method to inform map that data has changed
338 public void dataUpdated(byte inUpdateType)
340 // Try not to recalculate all the values unless necessary
341 if (inUpdateType != SELECTION_CHANGED) {
342 _data.init(Config.getUnitSet());
344 // Update the menu if necessary
345 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
352 * React to click on profile display
354 public void mouseClicked(MouseEvent e)
356 if (_track == null || _track.getNumPoints() < 1) {return;}
360 int xClick = e.getX();
361 int yClick = e.getY();
362 // Check click is within main area (not in border)
363 if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
364 && yClick < (getHeight() - BORDER_WIDTH))
366 // work out which data point is nearest and select it
367 int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
368 // If shift clicked, then extend selection
369 if (e.isShiftDown()) {
370 _trackInfo.extendSelection(pointNum);
373 _trackInfo.selectPoint(pointNum);
379 _popup.show(this, e.getX(), e.getY());
384 * Called by clicking on popup menu to change the view
385 * @param inField field to show
387 private void changeView(Field inField)
389 if (inField == Field.ALTITUDE)
391 if (!(_data instanceof AltitudeData)) {
392 _data = new AltitudeData(_track);
395 else if (inField == Field.SPEED) {
396 if (!(_data instanceof SpeedData)) {
397 _data = new SpeedData(_track);
400 else if (inField == Field.VERTICAL_SPEED) {
401 if (!(_data instanceof VerticalSpeedData)) {
402 _data = new VerticalSpeedData(_track);
407 if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
408 _data = new ArbitraryData(_track, inField);
411 _data.init(Config.getUnitSet());
416 * mouse enter events ignored
418 public void mouseEntered(MouseEvent e)
422 * mouse exit events ignored
424 public void mouseExited(MouseEvent e)
428 * ignore mouse pressed for now too
430 public void mousePressed(MouseEvent e)
434 * and also ignore mouse released
436 public void mouseReleased(MouseEvent e)