]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/function/charts/Charter.java
Version 18.1, September 2015
[GpsPrune.git] / tim / prune / function / charts / Charter.java
1 package tim.prune.function.charts;
2
3 import java.awt.BorderLayout;
4 import java.awt.FlowLayout;
5 import java.awt.GridLayout;
6 import java.awt.event.ActionEvent;
7 import java.awt.event.ActionListener;
8 import java.io.File;
9 import java.io.FileWriter;
10 import java.io.IOException;
11 import java.io.OutputStreamWriter;
12
13 import javax.swing.BorderFactory;
14 import javax.swing.BoxLayout;
15 import javax.swing.ButtonGroup;
16 import javax.swing.JButton;
17 import javax.swing.JCheckBox;
18 import javax.swing.JDialog;
19 import javax.swing.JFileChooser;
20 import javax.swing.JLabel;
21 import javax.swing.JOptionPane;
22 import javax.swing.JPanel;
23 import javax.swing.JRadioButton;
24 import javax.swing.JTextField;
25 import javax.swing.SwingConstants;
26
27 import tim.prune.App;
28 import tim.prune.ExternalTools;
29 import tim.prune.GenericFunction;
30 import tim.prune.I18nManager;
31 import tim.prune.config.Config;
32 import tim.prune.data.DataPoint;
33 import tim.prune.data.Distance;
34 import tim.prune.data.Field;
35 import tim.prune.data.Timestamp;
36 import tim.prune.data.Track;
37 import tim.prune.gui.profile.SpeedData;
38 import tim.prune.gui.profile.VerticalSpeedData;
39 import tim.prune.load.GenericFileFilter;
40
41 /**
42  * Class to manage the generation of charts using gnuplot
43  */
44 public class Charter extends GenericFunction
45 {
46         /** dialog object, cached */
47         private JDialog _dialog = null;
48         /** radio button for distance axis */
49         private JRadioButton _distanceRadio = null;
50         /** radio button for time axis */
51         private JRadioButton _timeRadio = null;
52         /** array of checkboxes for specifying y axes */
53         private JCheckBox[] _yAxesBoxes = null;
54         /** radio button for svg output */
55         private JRadioButton _svgRadio = null;
56         /** file chooser for saving svg file */
57         private JFileChooser _fileChooser = null;
58         /** text field for svg width */
59         private JTextField _svgWidthField = null;
60         /** text field for svg height */
61         private JTextField _svgHeightField = null;
62
63         /** Default dimensions of Svg file */
64         private static final String DEFAULT_SVG_WIDTH  = "800";
65         private static final String DEFAULT_SVG_HEIGHT = "400";
66
67
68         /**
69          * Constructor from superclass
70          * @param inApp app object
71          */
72         public Charter(App inApp)
73         {
74                 super(inApp);
75         }
76
77         /**
78          * @return key for function name
79          */
80         public String getNameKey()
81         {
82                 return "function.charts";
83         }
84
85         /**
86          * Show the dialog
87          */
88         public void begin()
89         {
90                 // First check if gnuplot is available
91                 if (!ExternalTools.isToolInstalled(ExternalTools.TOOL_GNUPLOT))
92                 {
93                         _app.showErrorMessage(getNameKey(), "dialog.charts.gnuplotnotfound");
94                         return;
95                 }
96                 // Make dialog window
97                 if (_dialog == null)
98                 {
99                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
100                         _dialog.setLocationRelativeTo(_parentFrame);
101                         _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
102                         _dialog.getContentPane().add(makeDialogComponents());
103                         _dialog.pack();
104                 }
105                 if (setupDialog(_app.getTrackInfo().getTrack())) {
106                         _dialog.setVisible(true);
107                 }
108                 else {
109                         _app.showErrorMessage(getNameKey(), "dialog.charts.needaltitudeortimes");
110                 }
111         }
112
113
114         /**
115          * Make the dialog components
116          * @return panel containing gui elements
117          */
118         private JPanel makeDialogComponents()
119         {
120                 JPanel dialogPanel = new JPanel();
121                 dialogPanel.setLayout(new BorderLayout());
122
123                 JPanel mainPanel = new JPanel();
124                 mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS));
125                 // x axis choice
126                 JPanel axisPanel = new JPanel();
127                 axisPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.xaxis")));
128                 _distanceRadio = new JRadioButton(I18nManager.getText("fieldname.distance"));
129                 _distanceRadio.setSelected(true);
130                 _timeRadio = new JRadioButton(I18nManager.getText("fieldname.time"));
131                 ButtonGroup axisGroup = new ButtonGroup();
132                 axisGroup.add(_distanceRadio); axisGroup.add(_timeRadio);
133                 axisPanel.add(_distanceRadio); axisPanel.add(_timeRadio);
134                 mainPanel.add(axisPanel);
135
136                 // y axis choices
137                 JPanel yPanel = new JPanel();
138                 yPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.yaxis")));
139                 _yAxesBoxes = new JCheckBox[4]; // dist altitude speed vertspeed (time not available on y axis)
140                 _yAxesBoxes[0] = new JCheckBox(I18nManager.getText("fieldname.distance"));
141                 _yAxesBoxes[1] = new JCheckBox(I18nManager.getText("fieldname.altitude"));
142                 _yAxesBoxes[1].setSelected(true);
143                 _yAxesBoxes[2] = new JCheckBox(I18nManager.getText("fieldname.speed"));
144                 _yAxesBoxes[3] = new JCheckBox(I18nManager.getText("fieldname.verticalspeed"));
145                 for (int i=0; i<4; i++) {
146                         yPanel.add(_yAxesBoxes[i]);
147                 }
148                 mainPanel.add(yPanel);
149
150                 // Add validation to prevent choosing invalid (ie dist/dist) combinations
151                 ActionListener xAxisListener = new ActionListener() {
152                         public void actionPerformed(ActionEvent e) {
153                                 enableYbox(0, _timeRadio.isSelected());
154                         }
155                 };
156                 _timeRadio.addActionListener(xAxisListener);
157                 _distanceRadio.addActionListener(xAxisListener);
158
159                 // output buttons
160                 JPanel outputPanel = new JPanel();
161                 outputPanel.setBorder(BorderFactory.createTitledBorder(I18nManager.getText("dialog.charts.output")));
162                 outputPanel.setLayout(new BorderLayout());
163                 JPanel radiosPanel = new JPanel();
164                 JRadioButton screenRadio = new JRadioButton(I18nManager.getText("dialog.charts.screen"));
165                 screenRadio.setSelected(true);
166                 _svgRadio = new JRadioButton(I18nManager.getText("dialog.charts.svg"));
167                 ButtonGroup outputGroup = new ButtonGroup();
168                 outputGroup.add(screenRadio); outputGroup.add(_svgRadio);
169                 radiosPanel.add(screenRadio); radiosPanel.add(_svgRadio);
170                 outputPanel.add(radiosPanel, BorderLayout.NORTH);
171                 // panel for svg width, height
172                 JPanel sizePanel = new JPanel();
173                 sizePanel.setLayout(new GridLayout(2, 2, 10, 1));
174                 JLabel widthLabel = new JLabel(I18nManager.getText("dialog.charts.svgwidth"));
175                 widthLabel.setHorizontalAlignment(SwingConstants.RIGHT);
176                 sizePanel.add(widthLabel);
177                 _svgWidthField = new JTextField(DEFAULT_SVG_WIDTH, 5);
178                 sizePanel.add(_svgWidthField);
179                 JLabel heightLabel = new JLabel(I18nManager.getText("dialog.charts.svgheight"));
180                 heightLabel.setHorizontalAlignment(SwingConstants.RIGHT);
181                 sizePanel.add(heightLabel);
182                 _svgHeightField = new JTextField(DEFAULT_SVG_HEIGHT, 5);
183                 sizePanel.add(_svgHeightField);
184
185                 outputPanel.add(sizePanel, BorderLayout.EAST);
186                 mainPanel.add(outputPanel);
187                 dialogPanel.add(mainPanel, BorderLayout.CENTER);
188
189                 // button panel on bottom
190                 JPanel buttonPanel = new JPanel();
191                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
192                 // ok button
193                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
194                 okButton.addActionListener(new ActionListener() {
195                         public void actionPerformed(ActionEvent e) {
196                                 showChart(_app.getTrackInfo().getTrack());
197                                 _dialog.setVisible(false);
198                         }
199                 });
200                 buttonPanel.add(okButton);
201                 // Cancel button
202                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
203                 cancelButton.addActionListener(new ActionListener() {
204                         public void actionPerformed(ActionEvent e) {
205                                 _dialog.setVisible(false);
206                         }
207                 });
208                 buttonPanel.add(cancelButton);
209                 dialogPanel.add(buttonPanel, BorderLayout.SOUTH);
210                 return dialogPanel;
211         }
212
213
214         /**
215          * Set up the dialog according to the track contents
216          * @param inTrack track object
217          * @return true if it's all ok
218          */
219         private boolean setupDialog(Track inTrack)
220         {
221                 boolean hasTimes = inTrack.hasData(Field.TIMESTAMP);
222                 boolean hasAltitudes = inTrack.hasAltitudeData();
223                 _timeRadio.setEnabled(hasTimes);
224
225                 // Add checks to prevent choosing unavailable combinations
226                 if (!hasTimes) {
227                         _distanceRadio.setSelected(true);
228                 }
229                 enableYbox(0, !_distanceRadio.isSelected());
230                 enableYbox(1, hasAltitudes);
231                 enableYbox(2, hasTimes);
232                 enableYbox(3, hasTimes && hasAltitudes);
233                 return (hasTimes || hasAltitudes);
234         }
235
236
237         /**
238          * Enable or disable the given y axis checkbox
239          * @param inIndex index of checkbox
240          * @param inFlag true to enable
241          */
242         private void enableYbox(int inIndex, boolean inFlag)
243         {
244                 _yAxesBoxes[inIndex].setEnabled(inFlag);
245                 if (!inFlag) {
246                         _yAxesBoxes[inIndex].setSelected(inFlag);
247                 }
248         }
249
250         /**
251          * Show the chart for the specified track
252          * @param inTrack track object containing data
253          */
254         private void showChart(Track inTrack)
255         {
256                 int numCharts = 0;
257                 for (int i=0; i<_yAxesBoxes.length; i++) {
258                         if (_yAxesBoxes[i].isSelected()) {
259                                 numCharts++;
260                         }
261                 }
262                 // Select default chart if none selected
263                 if (numCharts == 0) {
264                         _yAxesBoxes[1].setSelected(true);
265                         numCharts = 1;
266                 }
267                 int[] heights = getHeights(numCharts);
268
269                 boolean showSvg = _svgRadio.isSelected();
270                 File svgFile = null;
271                 if (showSvg) {
272                         svgFile = selectSvgFile();
273                         if (svgFile == null) {showSvg = false;}
274                 }
275                 OutputStreamWriter writer = null;
276                 try
277                 {
278                         final String gnuplotPath = Config.getConfigString(Config.KEY_GNUPLOT_PATH);
279                         Process process = Runtime.getRuntime().exec(gnuplotPath + " -persist");
280                         writer = new OutputStreamWriter(process.getOutputStream());
281                         if (showSvg)
282                         {
283                                 writer.write("set terminal svg size " + getSvgValue(_svgWidthField, DEFAULT_SVG_WIDTH) + " "
284                                         + getSvgValue(_svgHeightField, DEFAULT_SVG_HEIGHT) + "\n");
285                                 writer.write("set out '" + svgFile.getAbsolutePath() + "'\n");
286                         }
287                         else {
288                                 // For screen output, gnuplot should use the default terminal (windows or x11 or wxt or something)
289                         }
290                         if (numCharts > 1) {
291                                 writer.write("set multiplot layout " + numCharts + ",1\n");
292                         }
293                         // Loop over possible charts
294                         int chartNum = 0;
295                         for (int c=0; c<_yAxesBoxes.length; c++)
296                         {
297                                 if (_yAxesBoxes[c].isSelected())
298                                 {
299                                         writer.write("set size 1," + (0.01*heights[chartNum*2+1]) + "\n");
300                                         writer.write("set origin 0," + (0.01*heights[chartNum*2]) + "\n");
301                                         writeChart(writer, inTrack, _distanceRadio.isSelected(), c);
302                                         chartNum++;
303                                 }
304                         }
305                         // Close multiplot if open
306                         if (numCharts > 1) {
307                                 writer.write("unset multiplot\n");
308                         }
309                 }
310                 catch (Exception e) {
311                         _app.showErrorMessageNoLookup(getNameKey(), e.getMessage());
312                 }
313                 finally {
314                         try {
315                                 // Close writer
316                                 if (writer != null) writer.close();
317                         }
318                         catch (Exception e) {} // ignore
319                 }
320         }
321
322
323         /**
324          * Parse the given text field's value and return as string
325          * @param inField text field to read from
326          * @param inDefault default value if not valid
327          * @return value of svg dimension as string
328          */
329         private static String getSvgValue(JTextField inField, String inDefault)
330         {
331                 int value = 0;
332                 try {
333                         value = Integer.parseInt(inField.getText());
334                 }
335                 catch (Exception e) {} // ignore, value stays zero
336                 if (value > 0) {
337                         return "" + value;
338                 }
339                 return inDefault;
340         }
341
342
343         /**
344          * Write out the selected chart to the given Writer object
345          * @param inWriter writer object
346          * @param inTrack Track containing data
347          * @param inDistance true if x axis is distance
348          * @param inYaxis index of y axis
349          * @throws IOException if writing error occurred
350          */
351         private static void writeChart(OutputStreamWriter inWriter, Track inTrack, boolean inDistance, int inYaxis)
352         throws IOException
353         {
354                 ChartSeries xValues = null, yValues = null;
355                 ChartSeries distValues = getDistanceValues(inTrack);
356                 // Choose x values according to axis
357                 if (inDistance) {
358                         xValues = distValues;
359                 }
360                 else {
361                         xValues = getTimeValues(inTrack);
362                 }
363                 // Choose y values according to axis
364                 switch (inYaxis)
365                 {
366                 case 0: // y axis is distance
367                         yValues = distValues;
368                         break;
369                 case 1: // y axis is altitude
370                         yValues = getAltitudeValues(inTrack);
371                         break;
372                 case 2: // y axis is speed
373                         yValues = getSpeedValues(inTrack);
374                         break;
375                 case 3: // y axis is vertical speed
376                         yValues = getVertSpeedValues(inTrack);
377                         break;
378                 }
379                 // Make a temporary data file for the output (one per subchart)
380                 File tempFile = File.createTempFile("gpsprunedata", null);
381                 tempFile.deleteOnExit();
382                 // write out values for x and y to temporary file
383                 FileWriter tempFileWriter = null;
384                 try {
385                         tempFileWriter = new FileWriter(tempFile);
386                         tempFileWriter.write("# Temporary data file for GpsPrune charts\n\n");
387                         for (int i=0; i<inTrack.getNumPoints(); i++) {
388                                 if (xValues.hasData(i) && yValues.hasData(i)) {
389                                         tempFileWriter.write("" + xValues.getData(i) + ", " + yValues.getData(i) + "\n");
390                                 }
391                         }
392                 }
393                 catch (IOException ioe) { // rethrow
394                         throw ioe;
395                 }
396                 finally {
397                         try {
398                                 tempFileWriter.close();
399                         }
400                         catch (Exception e) {}
401                 }
402
403                 // Sort out units to use
404                 final String distLabel = I18nManager.getText(Config.getUnitSet().getDistanceUnit().getShortnameKey());
405                 final String altLabel  = I18nManager.getText(Config.getUnitSet().getAltitudeUnit().getShortnameKey());
406                 final String speedLabel = I18nManager.getText(Config.getUnitSet().getSpeedUnit().getShortnameKey());
407                 final String vertSpeedLabel = I18nManager.getText(Config.getUnitSet().getVerticalSpeedUnit().getShortnameKey());
408
409                 // Set x axis label
410                 if (inDistance) {
411                         inWriter.write("set xlabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
412                 }
413                 else {
414                         inWriter.write("set xlabel '" + I18nManager.getText("fieldname.time") + " (" + I18nManager.getText("units.hours") + ")'\n");
415                 }
416
417                 // set other labels and plot chart
418                 String chartTitle = null;
419                 switch (inYaxis)
420                 {
421                 case 0: // y axis is distance
422                         inWriter.write("set ylabel '" + I18nManager.getText("fieldname.distance") + " (" + distLabel + ")'\n");
423                         chartTitle = I18nManager.getText("fieldname.distance");
424                         break;
425                 case 1: // y axis is altitude
426                         inWriter.write("set ylabel '" + I18nManager.getText("fieldname.altitude") + " (" + altLabel + ")'\n");
427                         chartTitle = I18nManager.getText("fieldname.altitude");
428                         break;
429                 case 2: // y axis is speed
430                         inWriter.write("set ylabel '" + I18nManager.getText("fieldname.speed") + " (" + speedLabel + ")'\n");
431                         chartTitle = I18nManager.getText("fieldname.speed");
432                         break;
433                 case 3: // y axis is vertical speed
434                         inWriter.write("set ylabel '" + I18nManager.getText("fieldname.verticalspeed") + " (" + vertSpeedLabel + ")'\n");
435                         chartTitle = I18nManager.getText("fieldname.verticalspeed");
436                         break;
437                 }
438                 inWriter.write("set style fill solid 0.5 border -1\n");
439                 inWriter.write("plot '" + tempFile.getAbsolutePath() + "' title '" + chartTitle + "' with filledcurve y1=0 lt rgb \"#009000\"\n");
440         }
441
442
443         /**
444          * Calculate the distance values for each point in the given track
445          * @param inTrack track object
446          * @return distance values in a ChartSeries object
447          */
448         private static ChartSeries getDistanceValues(Track inTrack)
449         {
450                 // Calculate distances and fill in in values array
451                 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
452                 double totalRads = 0;
453                 DataPoint prevPoint = null, currPoint = null;
454                 for (int i=0; i<inTrack.getNumPoints(); i++)
455                 {
456                         currPoint = inTrack.getPoint(i);
457                         if (prevPoint != null && !currPoint.isWaypoint() && !currPoint.getSegmentStart())
458                         {
459                                 totalRads += DataPoint.calculateRadiansBetween(prevPoint, currPoint);
460                         }
461
462                         // distance values use currently configured units
463                         values.setData(i, Distance.convertRadiansToDistance(totalRads));
464
465                         prevPoint = currPoint;
466                 }
467                 return values;
468         }
469
470         /**
471          * Calculate the time values for each point in the given track
472          * @param inTrack track object
473          * @return time values in a ChartSeries object
474          */
475         private static ChartSeries getTimeValues(Track inTrack)
476         {
477                 // Calculate times and fill in in values array
478                 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
479                 double seconds = 0.0;
480                 Timestamp prevTimestamp = null;
481                 DataPoint currPoint = null;
482                 for (int i=0; i<inTrack.getNumPoints(); i++)
483                 {
484                         currPoint = inTrack.getPoint(i);
485                         if (currPoint.hasTimestamp())
486                         {
487                                 if (!currPoint.getSegmentStart() && prevTimestamp != null) {
488                                         seconds += (currPoint.getTimestamp().getMillisecondsSince(prevTimestamp) / 1000.0);
489                                 }
490                                 values.setData(i, seconds / 60.0 / 60.0);
491                                 prevTimestamp = currPoint.getTimestamp();
492                         }
493                 }
494                 return values;
495         }
496
497         /**
498          * Calculate the altitude values for each point in the given track
499          * @param inTrack track object
500          * @return altitude values in a ChartSeries object
501          */
502         private static ChartSeries getAltitudeValues(Track inTrack)
503         {
504                 ChartSeries values = new ChartSeries(inTrack.getNumPoints());
505                 final double multFactor = Config.getUnitSet().getAltitudeUnit().getMultFactorFromStd();
506                 for (int i=0; i<inTrack.getNumPoints(); i++) {
507                         if (inTrack.getPoint(i).hasAltitude()) {
508                                 values.setData(i, inTrack.getPoint(i).getAltitude().getMetricValue() * multFactor);
509                         }
510                 }
511                 return values;
512         }
513
514         /**
515          * Calculate the speed values for each point in the given track
516          * @param inTrack track object
517          * @return speed values in a ChartSeries object
518          */
519         private static ChartSeries getSpeedValues(Track inTrack)
520         {
521                 // Calculate speeds using the same formula as the profile chart
522                 SpeedData speeds = new SpeedData(inTrack);
523                 speeds.init(Config.getUnitSet());
524
525                 final int numPoints = inTrack.getNumPoints();
526                 ChartSeries values = new ChartSeries(numPoints);
527                 // Loop over collected points
528                 for (int i=0; i<numPoints; i++)
529                 {
530                         if (speeds.hasData(i))
531                         {
532                                 values.setData(i, speeds.getData(i));
533                         }
534                 }
535                 return values;
536         }
537
538         /**
539          * Calculate the vertical speed values for each point in the given track
540          * @param inTrack track object
541          * @return vertical speed values in a ChartSeries object
542          */
543         private static ChartSeries getVertSpeedValues(Track inTrack)
544         {
545                 // Calculate speeds using the same formula as the profile chart
546                 VerticalSpeedData speeds = new VerticalSpeedData(inTrack);
547                 speeds.init(Config.getUnitSet());
548
549                 final int numPoints = inTrack.getNumPoints();
550                 ChartSeries values = new ChartSeries(numPoints);
551                 // Loop over collected points
552                 for (int i=0; i<numPoints; i++)
553                 {
554                         if (speeds.hasData(i))
555                         {
556                                 values.setData(i, speeds.getData(i));
557                         }
558                 }
559                 return values;
560         }
561
562
563         /**
564          * Select a file to write for the SVG output
565          * @return selected File object or null if cancelled
566          */
567         private File selectSvgFile()
568         {
569                 if (_fileChooser == null)
570                 {
571                         _fileChooser = new JFileChooser();
572                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
573                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
574                         _fileChooser.setAcceptAllFileFilterUsed(false);
575                         // start from directory in config which should be set
576                         String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
577                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
578                 }
579                 boolean chooseAgain = true;
580                 while (chooseAgain)
581                 {
582                         chooseAgain = false;
583                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
584                         {
585                                 // OK pressed and file chosen
586                                 File file = _fileChooser.getSelectedFile();
587                                 // Check file extension
588                                 if (!file.getName().toLowerCase().endsWith(".svg")) {
589                                         file = new File(file.getAbsolutePath() + ".svg");
590                                 }
591                                 // Check if file exists and if necessary prompt for overwrite
592                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
593                                 if (!file.exists() || (file.canWrite() && JOptionPane.showOptionDialog(_parentFrame,
594                                                 I18nManager.getText("dialog.save.overwrite.text"),
595                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
596                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
597                                         == JOptionPane.YES_OPTION))
598                                 {
599                                         return file;
600                                 }
601                                 chooseAgain = true;
602                         }
603                 }
604                 // Cancel pressed so no file selected
605                 return null;
606         }
607
608
609         /**
610          * @param inNumCharts number of charts to draw
611          * @return array of ints describing position and height of each subchart
612          */
613         private static int[] getHeights(int inNumCharts)
614         {
615                 if (inNumCharts <= 1) {return new int[] {0, 100};}
616                 if (inNumCharts == 2) {return new int[] {25, 75, 0, 25};}
617                 if (inNumCharts == 3) {return new int[] {40, 60, 20, 20, 0, 20};}
618                 return new int[] {54, 46, 36, 18, 18, 18, 0, 18};
619         }
620 }