]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/gui/profile/ProfileChart.java
Version 15.2, November 2013
[GpsPrune.git] / tim / prune / gui / profile / ProfileChart.java
1 package tim.prune.gui.profile;
2
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.FlowLayout;
6 import java.awt.Graphics;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.awt.event.MouseEvent;
10 import java.awt.event.MouseListener;
11 import javax.swing.JLabel;
12 import javax.swing.JMenuItem;
13 import javax.swing.JPopupMenu;
14
15 import tim.prune.I18nManager;
16 import tim.prune.config.ColourScheme;
17 import tim.prune.config.Config;
18 import tim.prune.data.Field;
19 import tim.prune.data.FieldList;
20 import tim.prune.data.TrackInfo;
21 import tim.prune.gui.GenericDisplay;
22
23 /**
24  * Chart component for the profile display
25  */
26 public class ProfileChart extends GenericDisplay implements MouseListener
27 {
28         /** Inner class to handle popup menu clicks */
29         class MenuClicker implements ActionListener
30         {
31                 private Field _field = null;
32                 MenuClicker(Field inField) {_field = inField;}
33                 /** React to menu click by changing the field */
34                 public void actionPerformed(ActionEvent arg0) {
35                         changeView(_field);
36                 }
37         }
38
39         /** Current scale factor in x direction*/
40         private double _xScaleFactor = 0.0;
41         /** Data to show on chart */
42         private ProfileData _data = null;
43         /** Label for chart type, units */
44         private JLabel _label = null;
45         /** Right-click popup menu */
46         private JPopupMenu _popup = null;
47
48         /** Possible scales to use */
49         private static final int[] LINE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 10, 5, 2, 1};
50         /** Border width around black line */
51         private static final int BORDER_WIDTH = 6;
52         /** Minimum size for profile chart in pixels */
53         private static final Dimension MINIMUM_SIZE = new Dimension(200, 110);
54         /** Colour to use for text if no data found */
55         private static final Color COLOR_NODATA_TEXT = Color.GRAY;
56
57
58         /**
59          * Constructor
60          * @param inTrackInfo Track info object
61          */
62         public ProfileChart(TrackInfo inTrackInfo)
63         {
64                 super(inTrackInfo);
65                 _data = new AltitudeData(inTrackInfo.getTrack());
66                 addMouseListener(this);
67                 setLayout(new FlowLayout(FlowLayout.LEFT));
68                 _label = new JLabel("Altitude"); // text will be replaced later
69                 add(_label);
70                 makePopup();
71         }
72
73
74         /**
75          * Override minimum size method to restrict slider
76          */
77         public Dimension getMinimumSize()
78         {
79                 return MINIMUM_SIZE;
80         }
81
82         /**
83          * Override paint method to draw map
84          * @param g Graphics object
85          */
86         public void paint(Graphics g)
87         {
88                 super.paint(g);
89                 ColourScheme colourScheme = Config.getColourScheme();
90                 paintBackground(g, colourScheme);
91                 if (_track != null && _track.getNumPoints() > 0)
92                 {
93                         _label.setText(_data.getLabel());
94                         int width = getWidth();
95                         int height = getHeight();
96
97                         // Set up colours
98                         final Color barColour = colourScheme.getColour(ColourScheme.IDX_POINT);
99                         final Color rangeColour = colourScheme.getColour(ColourScheme.IDX_SELECTION);
100                         final Color currentColour = colourScheme.getColour(ColourScheme.IDX_PRIMARY);
101                         final Color secondColour = colourScheme.getColour(ColourScheme.IDX_SECONDARY);
102                         final Color lineColour = colourScheme.getColour(ColourScheme.IDX_LINES);
103
104                         // message if no data for the current field in track
105                         if (!_data.hasData())
106                         {
107                                 g.setColor(lineColour);
108                                 g.drawString(I18nManager.getText(_data.getNoDataKey()), 50, (height+_label.getHeight())/2);
109                                 paintChildren(g);
110                                 return;
111                         }
112
113                         // Find minimum and maximum values to plot
114                         double minValue = _data.getMinValue();
115                         double maxValue = _data.getMaxValue();
116                         if (maxValue <= minValue) {maxValue = minValue + 1; minValue--;}
117
118                         final int numPoints = _track.getNumPoints();
119                         _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH - 1) / numPoints;
120                         int usableHeight = height - 2 * BORDER_WIDTH - _label.getHeight();
121                         double yScaleFactor = 1.0 * usableHeight / (maxValue - minValue);
122                         int barWidth = (int) (_xScaleFactor + 1.0);
123                         int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
124                         // selection start, end
125                         int selectionStart = -1, selectionEnd = -1;
126                         if (_trackInfo.getSelection().hasRangeSelected()) {
127                                 selectionStart = _trackInfo.getSelection().getStart();
128                                 selectionEnd = _trackInfo.getSelection().getEnd();
129                         }
130
131                         // horizontal lines for scale - set to round numbers eg 500
132                         int lineScale = getLineScale(minValue, maxValue);
133                         int scaleValue = (int) (minValue/lineScale + 1) * lineScale;
134                         if (minValue < 0.0) {scaleValue -= lineScale;}
135                         int x = 0, y = 0;
136                         final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue));
137
138                         double value = 0.0;
139                         g.setColor(lineColour);
140                         if (lineScale >= 1)
141                         {
142                                 while (scaleValue < maxValue)
143                                 {
144                                         y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
145                                         g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
146                                         scaleValue += lineScale;
147                                 }
148                         }
149                         else if (minValue < 0.0)
150                         {
151                                 // just draw zero line
152                                 y = zeroY;
153                                 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
154                         }
155
156                         try
157                         {
158                                 // loop through points
159                                 g.setColor(barColour);
160                                 for (int p = 0; p < numPoints; p++)
161                                 {
162                                         x = (int) (_xScaleFactor * p) + 1;
163                                         if (p == selectionStart)
164                                                 g.setColor(rangeColour);
165                                         else if (p == (selectionEnd+1))
166                                                 g.setColor(barColour);
167                                         if (_data.hasData(p))
168                                         {
169                                                 value = _data.getData(p);
170                                                 // Normal case is the minimum value greater than zero
171                                                 if (minValue >= 0)
172                                                 {
173                                                         y = (int) (yScaleFactor * (value - minValue));
174                                                         g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
175                                                 }
176                                                 else if (value >= 0.0) {
177                                                         // Bar upwards from the zero line
178                                                         y = height-BORDER_WIDTH - (int) (yScaleFactor * (value - minValue));
179                                                         g.fillRect(BORDER_WIDTH+x, y, barWidth, zeroY - y);
180                                                 }
181                                                 else {
182                                                         // Bar downwards from the zero line
183                                                         int barHeight = (int) (yScaleFactor * value);
184                                                         g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight);
185                                                 }
186                                         }
187                                 }
188                                 // current point (make sure it's drawn last)
189                                 if (selectedPoint >= 0)
190                                 {
191                                         x = (int) (_xScaleFactor * selectedPoint) + 1;
192                                         g.setColor(secondColour);
193                                         g.fillRect(BORDER_WIDTH + x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2);
194                                         if (_data.hasData(selectedPoint))
195                                         {
196                                                 g.setColor(currentColour);
197                                                 value = _data.getData(selectedPoint);
198                                                 y = (int) (yScaleFactor * (value - minValue));
199                                                 g.fillRect(BORDER_WIDTH + x, height-BORDER_WIDTH - y, barWidth, y);
200                                         }
201                                 }
202                         }
203                         catch (NullPointerException npe) { // ignore, probably due to data being changed
204                         }
205                         // Draw numbers on top of the graph to mark scale
206                         if (lineScale >= 1)
207                         {
208                                 int textHeight = g.getFontMetrics().getHeight();
209                                 scaleValue = (int) (minValue / lineScale + 1) * lineScale;
210                                 if (minValue < 0.0) {scaleValue -= lineScale;}
211                                 y = 0;
212                                 g.setColor(currentColour);
213                                 while (scaleValue < maxValue)
214                                 {
215                                         y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
216                                         // Limit y so String isn't above border
217                                         if (y < (BORDER_WIDTH + textHeight)) {
218                                                 y = BORDER_WIDTH + textHeight;
219                                         }
220                                         g.drawString(""+scaleValue, BORDER_WIDTH + 5, y);
221                                         scaleValue += lineScale;
222                                 }
223                         }
224                         // Paint label on top
225                         paintChildren(g);
226                 }
227         }
228
229
230         /**
231          * Paint the background for the chart
232          * @param inG graphics object
233          * @param inColourScheme colour scheme
234          */
235         private void paintBackground(Graphics inG, ColourScheme inColourScheme)
236         {
237                 final int width = getWidth();
238                 final int height = getHeight();
239                 // Get colours
240                 final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
241                 final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
242                 // background
243                 inG.setColor(backgroundColour);
244                 inG.fillRect(0, 0, width, height);
245                 if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return;
246                 // Display message if no data to be displayed
247                 if (_track == null || _track.getNumPoints() <= 0)
248                 {
249                         inG.setColor(COLOR_NODATA_TEXT);
250                         inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
251                 }
252                 else {
253                         inG.setColor(borderColour);
254                         inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
255                                 width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
256                 }
257         }
258
259         /**
260          * Make the popup menu for right-clicking the chart
261          */
262         private synchronized void makePopup()
263         {
264                 _popup = new JPopupMenu();
265                 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
266                 altItem.addActionListener(new ActionListener() {
267                         public void actionPerformed(ActionEvent e)
268                         {
269                                 changeView(Field.ALTITUDE);
270                         }});
271                 _popup.add(altItem);
272                 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
273                 speedItem.addActionListener(new ActionListener() {
274                         public void actionPerformed(ActionEvent e)
275                         {
276                                 changeView(Field.SPEED);
277                         }});
278                 _popup.add(speedItem);
279                 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
280                 vertSpeedItem.addActionListener(new ActionListener() {
281                         public void actionPerformed(ActionEvent e)
282                         {
283                                 changeView(Field.VERTICAL_SPEED);
284                         }});
285                 _popup.add(vertSpeedItem);
286                 // Go through track's master field list, see if any other fields to list
287                 boolean addSeparator = true;
288                 FieldList fields = _track.getFieldList();
289                 for (int i=0; i<fields.getNumFields(); i++)
290                 {
291                         Field field = fields.getField(i);
292                         if (!field.isBuiltIn())
293                         {
294                                 if (addSeparator) {_popup.addSeparator();}
295                                 addSeparator = false;
296                                 JMenuItem item = new JMenuItem(field.getName());
297                                 item.addActionListener(new MenuClicker(field));
298                                 _popup.add(item);
299                         }
300                 }
301         }
302
303         /**
304          * Work out the scale for the horizontal lines
305          * @param inMin min value of data
306          * @param inMax max value of data
307          * @return scale separation, or -1 for no scale
308          */
309         private int getLineScale(double inMin, double inMax)
310         {
311                 if ((inMax - inMin) < 2.0) {
312                         return -1;
313                 }
314                 int numScales = LINE_SCALES.length;
315                 for (int i=0; i<numScales; i++)
316                 {
317                         int scale = LINE_SCALES[i];
318                         int numLines = (int)(inMax / scale) - (int)(inMin / scale);
319                         // Check for too many lines
320                         if (numLines > 10) return -1;
321                         // If more than 1 line then use this scale
322                         if (numLines > 1) return scale;
323                 }
324                 // no suitable scale found so just use minimum
325                 return LINE_SCALES[numScales-1];
326         }
327
328
329         /**
330          * Method to inform map that data has changed
331          */
332         public void dataUpdated(byte inUpdateType)
333         {
334                 // Try not to recalculate all the values unless necessary
335                 if (inUpdateType != SELECTION_CHANGED) {
336                         _data.init(Config.getUnitSet());
337                 }
338                 // Update the menu if necessary
339                 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
340                         makePopup();
341                 }
342                 repaint();
343         }
344
345         /**
346          * React to click on profile display
347          */
348         public void mouseClicked(MouseEvent e)
349         {
350                 if (_track == null || _track.getNumPoints() < 1) {return;}
351                 // left clicks
352                 if (!e.isMetaDown())
353                 {
354                         int xClick = e.getX();
355                         int yClick = e.getY();
356                         // Check click is within main area (not in border)
357                         if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
358                                 && yClick < (getHeight() - BORDER_WIDTH))
359                         {
360                                 // work out which data point is nearest and select it
361                                 int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
362                                 // If shift clicked, then extend selection
363                                 if (e.isShiftDown()) {
364                                         _trackInfo.extendSelection(pointNum);
365                                 }
366                                 else {
367                                         _trackInfo.selectPoint(pointNum);
368                                 }
369                         }
370                 }
371                 else {
372                         // right clicks
373                         _popup.show(this, e.getX(), e.getY());
374                 }
375         }
376
377         /**
378          * Called by clicking on popup menu to change the view
379          * @param inField field to show
380          */
381         private void changeView(Field inField)
382         {
383                 if (inField == Field.ALTITUDE)
384                 {
385                         if (!(_data instanceof AltitudeData)) {
386                                 _data = new AltitudeData(_track);
387                         }
388                 }
389                 else if (inField == Field.SPEED) {
390                         if (!(_data instanceof SpeedData)) {
391                                 _data = new SpeedData(_track);
392                         }
393                 }
394                 else if (inField == Field.VERTICAL_SPEED) {
395                         if (!(_data instanceof VerticalSpeedData)) {
396                                 _data = new VerticalSpeedData(_track);
397                         }
398                 }
399                 else
400                 {
401                         if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
402                                 _data = new ArbitraryData(_track, inField);
403                         }
404                 }
405                 _data.init(Config.getUnitSet());
406                 repaint();
407         }
408
409         /**
410          * mouse enter events ignored
411          */
412         public void mouseEntered(MouseEvent e)
413         {}
414
415         /**
416          * mouse exit events ignored
417          */
418         public void mouseExited(MouseEvent e)
419         {}
420
421         /**
422          * ignore mouse pressed for now too
423          */
424         public void mousePressed(MouseEvent e)
425         {}
426
427         /**
428          * and also ignore mouse released
429          */
430         public void mouseReleased(MouseEvent e)
431         {}
432 }