]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/gui/profile/ProfileChart.java
Version 20.4, May 2021
[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 = true;
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 = true;
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                 {
244                         selectionStart = _trackInfo.getSelection().getStart();
245                         selectionEnd = _trackInfo.getSelection().getEnd();
246                 }
247
248                 int y = 0;
249                 double value = 0.0;
250                 // horizontal lines for scale - set to round numbers eg 500
251                 final int lineScale = getLineScale(minValue, maxValue);
252                 double scaleValue = Math.ceil(minValue/lineScale) * lineScale;
253                 final int zeroY = height - BORDER_WIDTH - (int) (yScaleFactor * (0.0 - minValue));
254
255                 g.setColor(lineColour);
256                 if (lineScale >= 1)
257                 {
258                         while (scaleValue < maxValue)
259                         {
260                                 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
261                                 g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
262                                 scaleValue += lineScale;
263                         }
264                 }
265                 else if (minValue < 0.0)
266                 {
267                         // just draw zero line
268                         y = zeroY;
269                         g.drawLine(BORDER_WIDTH + 1, y, width - BORDER_WIDTH - 1, y);
270                 }
271
272                 try
273                 {
274                         // loop through points
275                         g.setColor(barColour);
276                         for (int p = 0; p < numPoints; p++)
277                         {
278                                 if (p == selectionStart)
279                                         g.setColor(rangeColour);
280                                 else if (p == (selectionEnd+1))
281                                         g.setColor(barColour);
282
283                                 final int x = (int) (_xScaleFactor * p) + 1;
284                                 if (_data.hasData(p))
285                                 {
286                                         value = _data.getData(p);
287                                         // Normal case is the minimum value greater than zero
288                                         if (minValue >= 0)
289                                         {
290                                                 y = (int) (yScaleFactor * (value - minValue));
291                                                 g.fillRect(BORDER_WIDTH+x, height-BORDER_WIDTH - y, barWidth, y);
292                                         }
293                                         else if (value >= 0.0)
294                                         {
295                                                 // Bar upwards from the zero line
296                                                 y = height-BORDER_WIDTH - (int) (yScaleFactor * (value - minValue));
297                                                 g.fillRect(BORDER_WIDTH+x, y, barWidth, zeroY - y);
298                                         }
299                                         else
300                                         {
301                                                 // Bar downwards from the zero line
302                                                 int barHeight = (int) (yScaleFactor * value);
303                                                 g.fillRect(BORDER_WIDTH+x, zeroY, barWidth, -barHeight);
304                                         }
305                                 }
306                         }
307
308                         // current point (make sure it's drawn last)
309                         if (selectedPoint >= 0)
310                         {
311                                 final int sel_x = (int) (_xScaleFactor * selectedPoint) + 1;
312                                 g.setColor(secondColour);
313                                 g.fillRect(BORDER_WIDTH + sel_x, height-usableHeight-BORDER_WIDTH+1, barWidth, usableHeight-2);
314                                 if (_data.hasData(selectedPoint))
315                                 {
316                                         g.setColor(currentColour);
317                                         value = _data.getData(selectedPoint);
318                                         y = (int) (yScaleFactor * (value - minValue));
319                                         g.fillRect(BORDER_WIDTH + sel_x, height-BORDER_WIDTH - y, barWidth, y);
320                                 }
321                         }
322                 }
323                 catch (NullPointerException npe)
324                 { // ignore, probably due to data being changed
325                 }
326                 // Draw numbers on top of the graph to mark scale
327                 if (lineScale >= 1)
328                 {
329                         int textHeight = g.getFontMetrics().getHeight();
330                         scaleValue = (int) (minValue / lineScale + 1) * lineScale;
331                         if (minValue < 0.0) {scaleValue -= lineScale;}
332                         y = 0;
333                         g.setColor(currentColour);
334                         while (scaleValue < maxValue)
335                         {
336                                 y = height - BORDER_WIDTH - (int) (yScaleFactor * (scaleValue - minValue));
337                                 // Limit y so String isn't above border
338                                 if (y < (BORDER_WIDTH + textHeight)) {
339                                         y = BORDER_WIDTH + textHeight;
340                                 }
341                                 g.drawString(""+(int)scaleValue, BORDER_WIDTH + 5, y);
342                                 scaleValue += lineScale;
343                         }
344                 }
345                 // Paint label on top
346                 paintChildren(g);
347         }
348
349
350         /**
351          * Paint the background for the chart
352          * @param inG graphics object
353          * @param inColourScheme colour scheme
354          */
355         private void paintBackground(Graphics inG, ColourScheme inColourScheme)
356         {
357                 final int width = getWidth();
358                 final int height = getHeight();
359                 // Get colours
360                 final Color borderColour = inColourScheme.getColour(ColourScheme.IDX_BORDERS);
361                 final Color backgroundColour = inColourScheme.getColour(ColourScheme.IDX_BACKGROUND);
362                 // background
363                 inG.setColor(backgroundColour);
364                 inG.fillRect(0, 0, width, height);
365                 if (width < 2*BORDER_WIDTH || height < 2*BORDER_WIDTH) return;
366                 // Display message if no data to be displayed
367                 if (_track == null || _track.getNumPoints() <= 0)
368                 {
369                         inG.setColor(COLOR_NODATA_TEXT);
370                         inG.drawString(I18nManager.getText("display.nodata"), 50, height/2);
371                 }
372                 else
373                 {
374                         inG.setColor(borderColour);
375                         inG.drawRect(BORDER_WIDTH, BORDER_WIDTH + _label.getHeight(),
376                                 width - 2*BORDER_WIDTH, height-2*BORDER_WIDTH-_label.getHeight());
377                 }
378         }
379
380         /**
381          * Make the popup menu for right-clicking the chart
382          */
383         private synchronized void makePopup()
384         {
385                 _popup = new JPopupMenu();
386                 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
387                 altItem.addActionListener(new ActionListener() {
388                         public void actionPerformed(ActionEvent e)
389                         {
390                                 changeView(Field.ALTITUDE);
391                         }});
392                 _popup.add(altItem);
393                 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
394                 speedItem.addActionListener(new ActionListener() {
395                         public void actionPerformed(ActionEvent e)
396                         {
397                                 changeView(Field.SPEED);
398                         }});
399                 _popup.add(speedItem);
400                 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
401                 vertSpeedItem.addActionListener(new ActionListener() {
402                         public void actionPerformed(ActionEvent e)
403                         {
404                                 changeView(Field.VERTICAL_SPEED);
405                         }});
406                 _popup.add(vertSpeedItem);
407                 // Go through track's master field list, see if any other fields to list
408                 boolean addSeparator = true;
409                 FieldList fields = _track.getFieldList();
410                 for (int i=0; i<fields.getNumFields(); i++)
411                 {
412                         Field field = fields.getField(i);
413                         if (!field.isBuiltIn())
414                         {
415                                 if (addSeparator) {_popup.addSeparator();}
416                                 addSeparator = false;
417                                 JMenuItem item = new JMenuItem(field.getName());
418                                 item.addActionListener(new MenuClicker(field));
419                                 _popup.add(item);
420                         }
421                 }
422         }
423
424         /**
425          * Work out the scale for the horizontal lines
426          * @param inMin min value of data
427          * @param inMax max value of data
428          * @return scale separation, or -1 for no scale
429          */
430         private int getLineScale(double inMin, double inMax)
431         {
432                 if ((inMax - inMin) < 2.0) {
433                         return -1;
434                 }
435                 int numScales = LINE_SCALES.length;
436                 for (int i=0; i<numScales; i++)
437                 {
438                         int scale = LINE_SCALES[i];
439                         int numLines = (int)(inMax / scale) - (int)(inMin / scale);
440                         // Check for too many lines
441                         if (numLines > 10) return -1;
442                         // If more than 1 line then use this scale
443                         if (numLines > 1) return scale;
444                 }
445                 // no suitable scale found so just use minimum
446                 return LINE_SCALES[numScales-1];
447         }
448
449
450         /**
451          * Method to inform map that data has changed
452          */
453         public void dataUpdated(byte inUpdateType)
454         {
455                 // Try not to recalculate all the values unless necessary
456                 if (inUpdateType != SELECTION_CHANGED)
457                 {
458                         _data.init(Config.getUnitSet());
459                         _previousParameters.clear();
460                 }
461                 // Update the menu if necessary
462                 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
463                         makePopup();
464                 }
465
466                 ChartParameters currentParameters = new ChartParameters();
467                 currentParameters.selectedPoint.set(_trackInfo.getSelection().getCurrentPointIndex());
468                 if (_trackInfo.getSelection().hasRangeSelected())
469                 {
470                         currentParameters.rangeStart.set(_trackInfo.getSelection().getStart());
471                         currentParameters.rangeEnd.set(_trackInfo.getSelection().getEnd());
472                 }
473                 if (inUpdateType == SELECTION_CHANGED)
474                 {
475                         triggerPartialRepaint(currentParameters);
476                 }
477                 else
478                 {
479                         repaint();
480                 }
481                 _previousParameters = currentParameters;
482         }
483
484         /**
485          * For performance reasons, only repaint the part of the graphics affected by
486          * the change in selection
487          * @param currentParameters - contains the current selected point, range
488          */
489         private void triggerPartialRepaint(ChartParameters currentParameters)
490         {
491                 int minPointIndex = currentParameters.getMinChangedIndex(_previousParameters);
492                 minPointIndex = Math.max(minPointIndex, 0);
493                 int maxPointIndex = currentParameters.getMaxChangedIndex(_previousParameters);
494                 if (maxPointIndex < minPointIndex) {
495                         maxPointIndex = _trackInfo.getTrack().getNumPoints() - 1;
496                 }
497                 // System.out.println("Redraw from index: " + minPointIndex + " to " + maxPointIndex);
498                 final int region_x = (int) (_xScaleFactor * minPointIndex) + BORDER_WIDTH - 2;
499                 final int region_width = (int) (_xScaleFactor * (maxPointIndex-minPointIndex+2)) + 6;
500                 repaint(region_x, 0, region_width, getHeight());
501                 // System.out.println("Partial repaint, x=" + region_x + ", region_width=" + region_width);
502         }
503
504         /**
505          * React to click on profile display
506          */
507         public void mouseClicked(MouseEvent e)
508         {
509                 if (_track == null || _track.getNumPoints() < 1) {return;}
510                 // left clicks
511                 if (!e.isMetaDown())
512                 {
513                         int xClick = e.getX();
514                         int yClick = e.getY();
515                         // Check click is within main area (not in border)
516                         if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
517                                 && yClick < (getHeight() - BORDER_WIDTH))
518                         {
519                                 // work out which data point is nearest and select it
520                                 int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
521                                 // If shift clicked, then extend selection
522                                 if (e.isShiftDown()) {
523                                         _trackInfo.extendSelection(pointNum);
524                                 }
525                                 else {
526                                         _trackInfo.selectPoint(pointNum);
527                                 }
528                         }
529                 }
530                 else
531                 {
532                         // right clicks
533                         _popup.show(this, e.getX(), e.getY());
534                 }
535         }
536
537         /**
538          * Called by clicking on popup menu to change the view
539          * @param inField field to show
540          */
541         private void changeView(Field inField)
542         {
543                 if (inField == Field.ALTITUDE)
544                 {
545                         if (!(_data instanceof AltitudeData)) {
546                                 _data = new AltitudeData(_track);
547                         }
548                 }
549                 else if (inField == Field.SPEED)
550                 {
551                         if (!(_data instanceof SpeedData)) {
552                                 _data = new SpeedData(_track);
553                         }
554                 }
555                 else if (inField == Field.VERTICAL_SPEED)
556                 {
557                         if (!(_data instanceof VerticalSpeedData)) {
558                                 _data = new VerticalSpeedData(_track);
559                         }
560                 }
561                 else
562                 {
563                         if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
564                                 _data = new ArbitraryData(_track, inField);
565                         }
566                 }
567                 _data.init(Config.getUnitSet());
568                 repaint();
569         }
570
571         /**
572          * mouse enter events ignored
573          */
574         public void mouseEntered(MouseEvent e)
575         {}
576
577         /**
578          * mouse exit events ignored
579          */
580         public void mouseExited(MouseEvent e)
581         {}
582
583         /**
584          * ignore mouse pressed for now too
585          */
586         public void mousePressed(MouseEvent e)
587         {}
588
589         /**
590          * and also ignore mouse released
591          */
592         public void mouseReleased(MouseEvent e)
593         {}
594 }