]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/gui/profile/ProfileChart.java
b2d0d0df7020c36964f692a4eb1378768b58abee
[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                         int scaleValue = (int) (minValue/lineScale + 1) * lineScale;
140                         if (minValue < 0.0) {scaleValue -= lineScale;}
141                         int x = 0, y = 0;
142                         final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue));
143
144                         double value = 0.0;
145                         g.setColor(lineColour);
146                         if (lineScale >= 1)
147                         {
148                                 while (scaleValue < maxValue)
149                                 {
150                                         y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
151                                         g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
152                                         scaleValue += lineScale;
153                                 }
154                         }
155                         else if (minValue < 0.0)
156                         {
157                                 // just draw zero line
158                                 y = zeroY;
159                                 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
160                         }
161
162                         try
163                         {
164                                 // loop through points
165                                 g.setColor(barColour);
166                                 for (int p = 0; p < numPoints; p++)
167                                 {
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))
174                                         {
175                                                 value = _data.getData(p);
176                                                 // Normal case is the minimum value greater than zero
177                                                 if (minValue >= 0)
178                                                 {
179                                                         y = (int) (yScaleFactor * (value - minValue));
180                                                         g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
181                                                 }
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);
186                                                 }
187                                                 else {
188                                                         // Bar downwards from the zero line
189                                                         int barHeight = (int) (yScaleFactor * value);
190                                                         g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight);
191                                                 }
192                                         }
193                                 }
194                                 // current point (make sure it's drawn last)
195                                 if (selectedPoint >= 0)
196                                 {
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))
201                                         {
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);
206                                         }
207                                 }
208                         }
209                         catch (NullPointerException npe) { // ignore, probably due to data being changed
210                         }
211                         // Draw numbers on top of the graph to mark scale
212                         if (lineScale >= 1)
213                         {
214                                 int textHeight = g.getFontMetrics().getHeight();
215                                 scaleValue = (int) (minValue / lineScale + 1) * lineScale;
216                                 if (minValue < 0.0) {scaleValue -= lineScale;}
217                                 y = 0;
218                                 g.setColor(currentColour);
219                                 while (scaleValue < maxValue)
220                                 {
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;
225                                         }
226                                         g.drawString(""+scaleValue, BORDER_WIDTH + 5, y);
227                                         scaleValue += lineScale;
228                                 }
229                         }
230                         // Paint label on top
231                         paintChildren(g);
232                 }
233         }
234
235
236         /**
237          * Paint the background for the chart
238          * @param inG graphics object
239          * @param inColourScheme colour scheme
240          */
241         private void paintBackground(Graphics inG, ColourScheme inColourScheme)
242         {
243                 final int width = getWidth();
244                 final int height = getHeight();
245                 // Get colours
246                 final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
247                 final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
248                 // 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)
254                 {
255                         inG.setColor(COLOR_NODATA_TEXT);
256                         inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
257                 }
258                 else {
259                         inG.setColor(borderColour);
260                         inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
261                                 width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
262                 }
263         }
264
265         /**
266          * Make the popup menu for right-clicking the chart
267          */
268         private synchronized void makePopup()
269         {
270                 _popup = new JPopupMenu();
271                 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
272                 altItem.addActionListener(new ActionListener() {
273                         public void actionPerformed(ActionEvent e)
274                         {
275                                 changeView(Field.ALTITUDE);
276                         }});
277                 _popup.add(altItem);
278                 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
279                 speedItem.addActionListener(new ActionListener() {
280                         public void actionPerformed(ActionEvent e)
281                         {
282                                 changeView(Field.SPEED);
283                         }});
284                 _popup.add(speedItem);
285                 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
286                 vertSpeedItem.addActionListener(new ActionListener() {
287                         public void actionPerformed(ActionEvent e)
288                         {
289                                 changeView(Field.VERTICAL_SPEED);
290                         }});
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++)
296                 {
297                         Field field = fields.getField(i);
298                         if (!field.isBuiltIn())
299                         {
300                                 if (addSeparator) {_popup.addSeparator();}
301                                 addSeparator = false;
302                                 JMenuItem item = new JMenuItem(field.getName());
303                                 item.addActionListener(new MenuClicker(field));
304                                 _popup.add(item);
305                         }
306                 }
307         }
308
309         /**
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
314          */
315         private int getLineScale(double inMin, double inMax)
316         {
317                 if ((inMax - inMin) < 2.0) {
318                         return -1;
319                 }
320                 int numScales = LINE_SCALES.length;
321                 for (int i=0; i<numScales; i++)
322                 {
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;
329                 }
330                 // no suitable scale found so just use minimum
331                 return LINE_SCALES[numScales-1];
332         }
333
334
335         /**
336          * Method to inform map that data has changed
337          */
338         public void dataUpdated(byte inUpdateType)
339         {
340                 // Try not to recalculate all the values unless necessary
341                 if (inUpdateType != SELECTION_CHANGED) {
342                         _data.init(Config.getUnitSet());
343                 }
344                 // Update the menu if necessary
345                 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
346                         makePopup();
347                 }
348                 repaint();
349         }
350
351         /**
352          * React to click on profile display
353          */
354         public void mouseClicked(MouseEvent e)
355         {
356                 if (_track == null || _track.getNumPoints() < 1) {return;}
357                 // left clicks
358                 if (!e.isMetaDown())
359                 {
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))
365                         {
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);
371                                 }
372                                 else {
373                                         _trackInfo.selectPoint(pointNum);
374                                 }
375                         }
376                 }
377                 else {
378                         // right clicks
379                         _popup.show(this, e.getX(), e.getY());
380                 }
381         }
382
383         /**
384          * Called by clicking on popup menu to change the view
385          * @param inField field to show
386          */
387         private void changeView(Field inField)
388         {
389                 if (inField == Field.ALTITUDE)
390                 {
391                         if (!(_data instanceof AltitudeData)) {
392                                 _data = new AltitudeData(_track);
393                         }
394                 }
395                 else if (inField == Field.SPEED) {
396                         if (!(_data instanceof SpeedData)) {
397                                 _data = new SpeedData(_track);
398                         }
399                 }
400                 else if (inField == Field.VERTICAL_SPEED) {
401                         if (!(_data instanceof VerticalSpeedData)) {
402                                 _data = new VerticalSpeedData(_track);
403                         }
404                 }
405                 else
406                 {
407                         if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
408                                 _data = new ArbitraryData(_track, inField);
409                         }
410                 }
411                 _data.init(Config.getUnitSet());
412                 repaint();
413         }
414
415         /**
416          * mouse enter events ignored
417          */
418         public void mouseEntered(MouseEvent e)
419         {}
420
421         /**
422          * mouse exit events ignored
423          */
424         public void mouseExited(MouseEvent e)
425         {}
426
427         /**
428          * ignore mouse pressed for now too
429          */
430         public void mousePressed(MouseEvent e)
431         {}
432
433         /**
434          * and also ignore mouse released
435          */
436         public void mouseReleased(MouseEvent e)
437         {}
438 }