]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/gui/profile/ProfileChart.java
466fda2175de2418569297c0722c6cfb1a2fd3c0
[GpsPrune.git] / src / 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         /** Inner class to remember a single index */
43         class PointIndex
44         {
45                 public int index = -1;
46                 public boolean hasValue = false;
47                 public PointIndex()
48                 {
49                         index = -1;
50                         hasValue = false;
51                 }
52                 /** Set a single value */
53                 public void set(int inValue)
54                 {
55                         index = inValue;
56                         hasValue = (inValue != -1);
57                 }
58                 /** Add an index to the minimum calculation */
59                 public void setMin(PointIndex other)
60                 {
61                         if (!other.hasValue) {return;}
62                         if (!hasValue) {
63                                 index = other.index;
64                                 hasValue = other.hasValue;
65                         }
66                         else {
67                                 index = Math.min(index, other.index);
68                         }
69                 }
70                 /** Add an index to the maximum calculation */
71                 public void setMax(PointIndex other)
72                 {
73                         if (!other.hasValue) {return;}
74                         if (!hasValue) {
75                                 index = other.index;
76                                 hasValue = other.hasValue;
77                         }
78                         else {
79                                 index = Math.max(index, other.index);
80                         }
81                 }
82                 /** @return true if two Indexes are equal */
83                 public boolean equals(PointIndex other)
84                 {
85                         if (!hasValue || !other.hasValue) {
86                                 return hasValue == other.hasValue;
87                         }
88                         return index == other.index;
89                 }
90         }
91
92         /** Inner class to remember previous chart parameters */
93         class ChartParameters
94         {
95                 public PointIndex selectedPoint = new PointIndex();
96                 public PointIndex rangeStart = new PointIndex(), rangeEnd = new PointIndex();
97                 public void clear()
98                 {
99                         selectedPoint.hasValue = false;
100                         rangeStart.hasValue = false;
101                         rangeEnd.hasValue = false;
102                 }
103                 /** Get the minimum index which has changed between two sets of parameters */
104                 public int getMinChangedIndex(ChartParameters other)
105                 {
106                         PointIndex minIndex = new PointIndex();
107                         if (!selectedPoint.equals(other.selectedPoint)) {
108                                 minIndex.setMin(selectedPoint);
109                                 minIndex.setMin(other.selectedPoint);
110                         }
111                         if (!rangeStart.equals(other.rangeStart)) {
112                                 minIndex.setMin(rangeStart);
113                                 minIndex.setMin(other.rangeStart);
114                         }
115                         if (!rangeEnd.equals(other.rangeEnd)) {
116                                 minIndex.setMin(rangeEnd);
117                                 minIndex.setMin(other.rangeEnd);
118                         }
119                         return minIndex.index;
120                 }
121                 /** Get the maximum index which has changed between two sets of parameters */
122                 public int getMaxChangedIndex(ChartParameters other)
123                 {
124                         PointIndex maxIndex = new PointIndex();
125                         if (!selectedPoint.equals(other.selectedPoint)) {
126                                 maxIndex.setMax(selectedPoint);
127                                 maxIndex.setMax(other.selectedPoint);
128                         }
129                         if (!rangeStart.equals(other.rangeStart)) {
130                                 maxIndex.setMax(rangeStart);
131                                 maxIndex.setMax(other.rangeStart);
132                         }
133                         if (!rangeEnd.equals(other.rangeEnd)) {
134                                 maxIndex.setMax(rangeEnd);
135                                 maxIndex.setMax(other.rangeEnd);
136                         }
137                         return maxIndex.index;
138                 }
139                 /** @return true if the parameters are completely empty (cleared) */
140                 public boolean isEmpty()
141                 {
142                         return !selectedPoint.hasValue && !rangeStart.hasValue && !rangeEnd.hasValue;
143                 }
144         }
145
146         /** Current scale factor in x direction*/
147         private double _xScaleFactor = 0.0;
148         /** Data to show on chart */
149         private ProfileData _data = null;
150         /** Label for chart type, units */
151         private JLabel _label = null;
152         /** Right-click popup menu */
153         private JPopupMenu _popup = null;
154         /** Parameters last time chart was drawn */
155         private ChartParameters _previousParameters = new ChartParameters();
156
157         /** Possible scales to use */
158         private static final int[] LINE_SCALES = {10000, 5000, 2000, 1000, 500, 200, 100, 50, 10, 5, 2, 1};
159         /** Border width around black line */
160         private static final int BORDER_WIDTH = 6;
161         /** Minimum size for profile chart in pixels */
162         private static final Dimension MINIMUM_SIZE = new Dimension(200, 110);
163         /** Colour to use for text if no data found */
164         private static final Color COLOR_NODATA_TEXT = Color.GRAY;
165
166
167         /**
168          * Constructor
169          * @param inTrackInfo Track info object
170          */
171         public ProfileChart(TrackInfo inTrackInfo)
172         {
173                 super(inTrackInfo);
174                 _data = new AltitudeData(inTrackInfo.getTrack());
175                 addMouseListener(this);
176                 setLayout(new FlowLayout(FlowLayout.LEFT));
177                 _label = new JLabel("Altitude"); // text will be replaced later
178                 add(_label);
179                 makePopup();
180         }
181
182
183         /**
184          * Override minimum size method to restrict slider
185          */
186         public Dimension getMinimumSize()
187         {
188                 return MINIMUM_SIZE;
189         }
190
191         /**
192          * Override paint method to draw map
193          * @param g Graphics object
194          */
195         public void paint(Graphics g)
196         {
197                 super.paint(g);
198                 // Set antialiasing depending on Config
199                 ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_ANTIALIASING,
200                         Config.getConfigBoolean(Config.KEY_ANTIALIAS) ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF);
201                 ColourScheme colourScheme = Config.getColourScheme();
202                 paintBackground(g, colourScheme);
203
204                 if (_track == null || _track.getNumPoints() <= 0)
205                 {
206                         return;
207                 }
208
209                 _label.setText(_data.getLabel());
210                 int width = getWidth();
211                 int height = getHeight();
212
213                 // Set up colours
214                 final Color barColour = colourScheme.getColour(ColourScheme.IDX_POINT);
215                 final Color rangeColour = colourScheme.getColour(ColourScheme.IDX_SELECTION);
216                 final Color currentColour = colourScheme.getColour(ColourScheme.IDX_PRIMARY);
217                 final Color secondColour = colourScheme.getColour(ColourScheme.IDX_SECONDARY);
218                 final Color lineColour = colourScheme.getColour(ColourScheme.IDX_LINES);
219
220                 // message if no data for the current field in track
221                 if (!_data.hasData())
222                 {
223                         g.setColor(lineColour);
224                         g.drawString(I18nManager.getText(_data.getNoDataKey()), 50, (height+_label.getHeight())/2);
225                         paintChildren(g);
226                         return;
227                 }
228
229                 // Find minimum and maximum values to plot
230                 double minValue = _data.getMinValue();
231                 double maxValue = _data.getMaxValue();
232                 if (maxValue <= minValue) {maxValue = minValue + 1; minValue--;}
233
234                 final int numPoints = _track.getNumPoints();
235                 _xScaleFactor = 1.0 * (width - 2 * BORDER_WIDTH - 1) / numPoints;
236                 int usableHeight = height - 2 * BORDER_WIDTH - _label.getHeight();
237                 double yScaleFactor = 1.0 * usableHeight / (maxValue - minValue);
238                 int barWidth = (int) (_xScaleFactor + 1.0);
239                 int selectedPoint = _trackInfo.getSelection().getCurrentPointIndex();
240                 // selection start, end
241                 int selectionStart = -1, selectionEnd = -1;
242                 if (_trackInfo.getSelection().hasRangeSelected()) {
243                         selectionStart = _trackInfo.getSelection().getStart();
244                         selectionEnd = _trackInfo.getSelection().getEnd();
245                 }
246
247                 int y = 0;
248                 double value = 0.0;
249                 // horizontal lines for scale - set to round numbers eg 500
250                 final int lineScale = getLineScale(minValue, maxValue);
251                 double scaleValue = Math.ceil(minValue/lineScale) * lineScale;
252                 final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue));
253
254                 g.setColor(lineColour);
255                 if (lineScale >= 1)
256                 {
257                         while (scaleValue < maxValue)
258                         {
259                                 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
260                                 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
261                                 scaleValue += lineScale;
262                         }
263                 }
264                 else if (minValue < 0.0)
265                 {
266                         // just draw zero line
267                         y = zeroY;
268                         g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
269                 }
270
271                 try
272                 {
273                         // loop through points
274                         g.setColor(barColour);
275                         for (int p = 0; p < numPoints; p++)
276                         {
277                                 if (p == selectionStart)
278                                         g.setColor(rangeColour);
279                                 else if (p == (selectionEnd+1))
280                                         g.setColor(barColour);
281
282                                 final int x = (int) (_xScaleFactor * p) + 1;
283                                 if (_data.hasData(p))
284                                 {
285                                         value = _data.getData(p);
286                                         // Normal case is the minimum value greater than zero
287                                         if (minValue >= 0)
288                                         {
289                                                 y = (int) (yScaleFactor * (value - minValue));
290                                                 g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
291                                         }
292                                         else if (value >= 0.0) {
293                                                 // Bar upwards from the zero line
294                                                 y = height-BORDER_WIDTH - (int) (yScaleFactor * (value - minValue));
295                                                 g.fillRect(BORDER_WIDTH+x, y, barWidth, zeroY - y);
296                                         }
297                                         else {
298                                                 // Bar downwards from the zero line
299                                                 int barHeight = (int) (yScaleFactor * value);
300                                                 g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight);
301                                         }
302                                 }
303                         }
304
305                         // current point (make sure it's drawn last)
306                         if (selectedPoint >= 0)
307                         {
308                                 final int sel_x = (int) (_xScaleFactor * selectedPoint) + 1;
309                                 g.setColor(secondColour);
310                                 g.fillRect(BORDER_WIDTH + sel_x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2);
311                                 if (_data.hasData(selectedPoint))
312                                 {
313                                         g.setColor(currentColour);
314                                         value = _data.getData(selectedPoint);
315                                         y = (int) (yScaleFactor * (value - minValue));
316                                         g.fillRect(BORDER_WIDTH + sel_x, height-BORDER_WIDTH - y, barWidth, y);
317                                 }
318                         }
319                 }
320                 catch (NullPointerException npe) { // ignore, probably due to data being changed
321                 }
322                 // Draw numbers on top of the graph to mark scale
323                 if (lineScale >= 1)
324                 {
325                         int textHeight = g.getFontMetrics().getHeight();
326                         scaleValue = (int) (minValue / lineScale + 1) * lineScale;
327                         if (minValue < 0.0) {scaleValue -= lineScale;}
328                         y = 0;
329                         g.setColor(currentColour);
330                         while (scaleValue < maxValue)
331                         {
332                                 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
333                                 // Limit y so String isn't above border
334                                 if (y < (BORDER_WIDTH + textHeight)) {
335                                         y = BORDER_WIDTH + textHeight;
336                                 }
337                                 g.drawString(""+(int)scaleValue, BORDER_WIDTH + 5, y);
338                                 scaleValue += lineScale;
339                         }
340                 }
341                 // Paint label on top
342                 paintChildren(g);
343         }
344
345
346         /**
347          * Paint the background for the chart
348          * @param inG graphics object
349          * @param inColourScheme colour scheme
350          */
351         private void paintBackground(Graphics inG, ColourScheme inColourScheme)
352         {
353                 final int width = getWidth();
354                 final int height = getHeight();
355                 // Get colours
356                 final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
357                 final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
358                 // background
359                 inG.setColor(backgroundColour);
360                 inG.fillRect(0, 0, width, height);
361                 if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return;
362                 // Display message if no data to be displayed
363                 if (_track == null || _track.getNumPoints() <= 0)
364                 {
365                         inG.setColor(COLOR_NODATA_TEXT);
366                         inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
367                 }
368                 else
369                 {
370                         inG.setColor(borderColour);
371                         inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
372                                 width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
373                 }
374         }
375
376         /**
377          * Make the popup menu for right-clicking the chart
378          */
379         private synchronized void makePopup()
380         {
381                 _popup = new JPopupMenu();
382                 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
383                 altItem.addActionListener(new ActionListener() {
384                         public void actionPerformed(ActionEvent e)
385                         {
386                                 changeView(Field.ALTITUDE);
387                         }});
388                 _popup.add(altItem);
389                 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
390                 speedItem.addActionListener(new ActionListener() {
391                         public void actionPerformed(ActionEvent e)
392                         {
393                                 changeView(Field.SPEED);
394                         }});
395                 _popup.add(speedItem);
396                 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
397                 vertSpeedItem.addActionListener(new ActionListener() {
398                         public void actionPerformed(ActionEvent e)
399                         {
400                                 changeView(Field.VERTICAL_SPEED);
401                         }});
402                 _popup.add(vertSpeedItem);
403                 // Go through track's master field list, see if any other fields to list
404                 boolean addSeparator = true;
405                 FieldList fields = _track.getFieldList();
406                 for (int i=0; i<fields.getNumFields(); i++)
407                 {
408                         Field field = fields.getField(i);
409                         if (!field.isBuiltIn())
410                         {
411                                 if (addSeparator) {_popup.addSeparator();}
412                                 addSeparator = false;
413                                 JMenuItem item = new JMenuItem(field.getName());
414                                 item.addActionListener(new MenuClicker(field));
415                                 _popup.add(item);
416                         }
417                 }
418         }
419
420         /**
421          * Work out the scale for the horizontal lines
422          * @param inMin min value of data
423          * @param inMax max value of data
424          * @return scale separation, or -1 for no scale
425          */
426         private int getLineScale(double inMin, double inMax)
427         {
428                 if ((inMax - inMin) < 2.0) {
429                         return -1;
430                 }
431                 int numScales = LINE_SCALES.length;
432                 for (int i=0; i<numScales; i++)
433                 {
434                         int scale = LINE_SCALES[i];
435                         int numLines = (int)(inMax / scale) - (int)(inMin / scale);
436                         // Check for too many lines
437                         if (numLines > 10) return -1;
438                         // If more than 1 line then use this scale
439                         if (numLines > 1) return scale;
440                 }
441                 // no suitable scale found so just use minimum
442                 return LINE_SCALES[numScales-1];
443         }
444
445
446         /**
447          * Method to inform map that data has changed
448          */
449         public void dataUpdated(byte inUpdateType)
450         {
451                 // Try not to recalculate all the values unless necessary
452                 if (inUpdateType != SELECTION_CHANGED)
453                 {
454                         _data.init(Config.getUnitSet());
455                         _previousParameters.clear();
456                 }
457                 // Update the menu if necessary
458                 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
459                         makePopup();
460                 }
461                 if (inUpdateType == SELECTION_CHANGED) {
462                         triggerPartialRepaint();
463                 }
464                 else
465                 {
466                         repaint();
467                 }
468         }
469
470         /**
471          * For performance reasons, only repaint the part of the graphics affected by
472          * the change in selection
473          */
474         private void triggerPartialRepaint()
475         {
476                 ChartParameters currentParameters = new ChartParameters();
477                 currentParameters.selectedPoint.set(_trackInfo.getSelection().getCurrentPointIndex());
478                 if (_trackInfo.getSelection().hasRangeSelected())
479                 {
480                         currentParameters.rangeStart.set(_trackInfo.getSelection().getStart());
481                         currentParameters.rangeEnd.set(_trackInfo.getSelection().getEnd());
482                 }
483
484                 int minPointIndex = currentParameters.getMinChangedIndex(_previousParameters);
485                 minPointIndex = Math.max(minPointIndex, 0);
486                 int maxPointIndex = currentParameters.getMaxChangedIndex(_previousParameters);
487                 if (maxPointIndex < minPointIndex) {
488                         maxPointIndex = _trackInfo.getTrack().getNumPoints() - 1;
489                 }
490                 // System.out.println("Redraw from index: " + minPointIndex + " to " + maxPointIndex);
491                 _previousParameters = currentParameters;
492                 final int region_x = (int) (_xScaleFactor * minPointIndex) + BORDER_WIDTH;
493                 final int region_width = (int) (_xScaleFactor * (maxPointIndex-minPointIndex+2)) + 2;
494                 repaint(region_x, 0, region_width, getHeight());
495         }
496
497         /**
498          * React to click on profile display
499          */
500         public void mouseClicked(MouseEvent e)
501         {
502                 if (_track == null || _track.getNumPoints() < 1) {return;}
503                 // left clicks
504                 if (!e.isMetaDown())
505                 {
506                         int xClick = e.getX();
507                         int yClick = e.getY();
508                         // Check click is within main area (not in border)
509                         if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
510                                 && yClick < (getHeight() - BORDER_WIDTH))
511                         {
512                                 // work out which data point is nearest and select it
513                                 int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
514                                 // If shift clicked, then extend selection
515                                 if (e.isShiftDown()) {
516                                         _trackInfo.extendSelection(pointNum);
517                                 }
518                                 else {
519                                         _trackInfo.selectPoint(pointNum);
520                                 }
521                         }
522                 }
523                 else {
524                         // right clicks
525                         _popup.show(this, e.getX(), e.getY());
526                 }
527         }
528
529         /**
530          * Called by clicking on popup menu to change the view
531          * @param inField field to show
532          */
533         private void changeView(Field inField)
534         {
535                 if (inField == Field.ALTITUDE)
536                 {
537                         if (!(_data instanceof AltitudeData)) {
538                                 _data = new AltitudeData(_track);
539                         }
540                 }
541                 else if (inField == Field.SPEED) {
542                         if (!(_data instanceof SpeedData)) {
543                                 _data = new SpeedData(_track);
544                         }
545                 }
546                 else if (inField == Field.VERTICAL_SPEED) {
547                         if (!(_data instanceof VerticalSpeedData)) {
548                                 _data = new VerticalSpeedData(_track);
549                         }
550                 }
551                 else
552                 {
553                         if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
554                                 _data = new ArbitraryData(_track, inField);
555                         }
556                 }
557                 _data.init(Config.getUnitSet());
558                 repaint();
559         }
560
561         /**
562          * mouse enter events ignored
563          */
564         public void mouseEntered(MouseEvent e)
565         {}
566
567         /**
568          * mouse exit events ignored
569          */
570         public void mouseExited(MouseEvent e)
571         {}
572
573         /**
574          * ignore mouse pressed for now too
575          */
576         public void mousePressed(MouseEvent e)
577         {}
578
579         /**
580          * and also ignore mouse released
581          */
582         public void mouseReleased(MouseEvent e)
583         {}
584 }