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