]> gitweb.fperrin.net Git - GpsPrune.git/blob - src/tim/prune/gui/profile/ProfileChart.java
Version 20.2, January 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                 if (_track.getNumPoints() < 1)
386                 {
387                         _popup = null;
388                         return;
389                 }
390                 _popup = new JPopupMenu();
391                 JMenuItem altItem = new JMenuItem(I18nManager.getText("fieldname.altitude"));
392                 altItem.addActionListener(new ActionListener() {
393                         public void actionPerformed(ActionEvent e)
394                         {
395                                 changeView(Field.ALTITUDE);
396                         }});
397                 _popup.add(altItem);
398                 JMenuItem speedItem = new JMenuItem(I18nManager.getText("fieldname.speed"));
399                 speedItem.addActionListener(new ActionListener() {
400                         public void actionPerformed(ActionEvent e)
401                         {
402                                 changeView(Field.SPEED);
403                         }});
404                 _popup.add(speedItem);
405                 JMenuItem vertSpeedItem = new JMenuItem(I18nManager.getText("fieldname.verticalspeed"));
406                 vertSpeedItem.addActionListener(new ActionListener() {
407                         public void actionPerformed(ActionEvent e)
408                         {
409                                 changeView(Field.VERTICAL_SPEED);
410                         }});
411                 _popup.add(vertSpeedItem);
412                 // Go through track's master field list, see if any other fields to list
413                 boolean addSeparator = true;
414                 FieldList fields = _track.getFieldList();
415                 for (int i=0; i<fields.getNumFields(); i++)
416                 {
417                         Field field = fields.getField(i);
418                         if (!field.isBuiltIn())
419                         {
420                                 if (addSeparator) {_popup.addSeparator();}
421                                 addSeparator = false;
422                                 JMenuItem item = new JMenuItem(field.getName());
423                                 item.addActionListener(new MenuClicker(field));
424                                 _popup.add(item);
425                         }
426                 }
427         }
428
429         /**
430          * Work out the scale for the horizontal lines
431          * @param inMin min value of data
432          * @param inMax max value of data
433          * @return scale separation, or -1 for no scale
434          */
435         private int getLineScale(double inMin, double inMax)
436         {
437                 if ((inMax - inMin) < 2.0) {
438                         return -1;
439                 }
440                 int numScales = LINE_SCALES.length;
441                 for (int i=0; i<numScales; i++)
442                 {
443                         int scale = LINE_SCALES[i];
444                         int numLines = (int)(inMax / scale) - (int)(inMin / scale);
445                         // Check for too many lines
446                         if (numLines > 10) return -1;
447                         // If more than 1 line then use this scale
448                         if (numLines > 1) return scale;
449                 }
450                 // no suitable scale found so just use minimum
451                 return LINE_SCALES[numScales-1];
452         }
453
454
455         /**
456          * Method to inform map that data has changed
457          */
458         public void dataUpdated(byte inUpdateType)
459         {
460                 // Try not to recalculate all the values unless necessary
461                 if (inUpdateType != SELECTION_CHANGED)
462                 {
463                         _data.init(Config.getUnitSet());
464                         _previousParameters.clear();
465                 }
466                 // Update the menu if necessary
467                 if ((inUpdateType & DATA_ADDED_OR_REMOVED) > 0) {
468                         makePopup();
469                 }
470
471                 ChartParameters currentParameters = new ChartParameters();
472                 currentParameters.selectedPoint.set(_trackInfo.getSelection().getCurrentPointIndex());
473                 if (_trackInfo.getSelection().hasRangeSelected())
474                 {
475                         currentParameters.rangeStart.set(_trackInfo.getSelection().getStart());
476                         currentParameters.rangeEnd.set(_trackInfo.getSelection().getEnd());
477                 }
478                 if (inUpdateType == SELECTION_CHANGED)
479                 {
480                         triggerPartialRepaint(currentParameters);
481                 }
482                 else
483                 {
484                         repaint();
485                 }
486                 _previousParameters = currentParameters;
487         }
488
489         /**
490          * For performance reasons, only repaint the part of the graphics affected by
491          * the change in selection
492          * @param currentParameters - contains the current selected point, range
493          */
494         private void triggerPartialRepaint(ChartParameters currentParameters)
495         {
496                 int minPointIndex = currentParameters.getMinChangedIndex(_previousParameters);
497                 minPointIndex = Math.max(minPointIndex, 0);
498                 int maxPointIndex = currentParameters.getMaxChangedIndex(_previousParameters);
499                 if (maxPointIndex < minPointIndex) {
500                         maxPointIndex = _trackInfo.getTrack().getNumPoints() - 1;
501                 }
502                 // System.out.println("Redraw from index: " + minPointIndex + " to " + maxPointIndex);
503                 final int region_x = (int) (_xScaleFactor * minPointIndex) + BORDER_WIDTH - 2;
504                 final int region_width = (int) (_xScaleFactor * (maxPointIndex-minPointIndex+2)) + 6;
505                 repaint(region_x, 0, region_width, getHeight());
506                 // System.out.println("Partial repaint, x=" + region_x + ", region_width=" + region_width);
507         }
508
509         /**
510          * React to click on profile display
511          */
512         public void mouseClicked(MouseEvent e)
513         {
514                 if (_track == null || _track.getNumPoints() < 1) {return;}
515                 // left clicks
516                 if (!e.isMetaDown())
517                 {
518                         int xClick = e.getX();
519                         int yClick = e.getY();
520                         // Check click is within main area (not in border)
521                         if (xClick > BORDER_WIDTH && yClick > BORDER_WIDTH && xClick < (getWidth() - BORDER_WIDTH)
522                                 && yClick < (getHeight() - BORDER_WIDTH))
523                         {
524                                 // work out which data point is nearest and select it
525                                 int pointNum = (int) ((e.getX() - BORDER_WIDTH) / _xScaleFactor);
526                                 // If shift clicked, then extend selection
527                                 if (e.isShiftDown()) {
528                                         _trackInfo.extendSelection(pointNum);
529                                 }
530                                 else {
531                                         _trackInfo.selectPoint(pointNum);
532                                 }
533                         }
534                 }
535                 else if (_popup != null)
536                 {
537                         // right clicks
538                         _popup.show(this, e.getX(), e.getY());
539                 }
540         }
541
542         /**
543          * Called by clicking on popup menu to change the view
544          * @param inField field to show
545          */
546         private void changeView(Field inField)
547         {
548                 if (inField == Field.ALTITUDE)
549                 {
550                         if (!(_data instanceof AltitudeData)) {
551                                 _data = new AltitudeData(_track);
552                         }
553                 }
554                 else if (inField == Field.SPEED)
555                 {
556                         if (!(_data instanceof SpeedData)) {
557                                 _data = new SpeedData(_track);
558                         }
559                 }
560                 else if (inField == Field.VERTICAL_SPEED)
561                 {
562                         if (!(_data instanceof VerticalSpeedData)) {
563                                 _data = new VerticalSpeedData(_track);
564                         }
565                 }
566                 else
567                 {
568                         if (!(_data instanceof ArbitraryData) || ((ArbitraryData)_data).getField() != inField) {
569                                 _data = new ArbitraryData(_track, inField);
570                         }
571                 }
572                 _data.init(Config.getUnitSet());
573                 repaint();
574         }
575
576         /**
577          * mouse enter events ignored
578          */
579         public void mouseEntered(MouseEvent e)
580         {}
581
582         /**
583          * mouse exit events ignored
584          */
585         public void mouseExited(MouseEvent e)
586         {}
587
588         /**
589          * ignore mouse pressed for now too
590          */
591         public void mousePressed(MouseEvent e)
592         {}
593
594         /**
595          * and also ignore mouse released
596          */
597         public void mouseReleased(MouseEvent e)
598         {}
599 }