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