]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/PovExporter.java
Version 14, October 2012
[GpsPrune.git] / tim / prune / save / PovExporter.java
1 package tim.prune.save;
2
3 import java.awt.BorderLayout;
4 import java.awt.Component;
5 import java.awt.FlowLayout;
6 import java.awt.GridLayout;
7 import java.awt.event.ActionEvent;
8 import java.awt.event.ActionListener;
9 import java.io.File;
10 import java.io.FileWriter;
11 import java.io.IOException;
12 import java.util.ArrayList;
13 import java.util.Iterator;
14
15 import javax.swing.BorderFactory;
16 import javax.swing.BoxLayout;
17 import javax.swing.ButtonGroup;
18 import javax.swing.JButton;
19 import javax.swing.JDialog;
20 import javax.swing.JFileChooser;
21 import javax.swing.JLabel;
22 import javax.swing.JOptionPane;
23 import javax.swing.JPanel;
24 import javax.swing.JRadioButton;
25 import javax.swing.JTextField;
26 import javax.swing.SwingConstants;
27
28 import tim.prune.App;
29 import tim.prune.I18nManager;
30 import tim.prune.UpdateMessageBroker;
31 import tim.prune.config.Config;
32 import tim.prune.data.NumberUtils;
33 import tim.prune.data.Track;
34 import tim.prune.function.Export3dFunction;
35 import tim.prune.gui.DialogCloser;
36 import tim.prune.load.GenericFileFilter;
37 import tim.prune.threedee.LineDialog;
38 import tim.prune.threedee.ThreeDModel;
39
40 /**
41  * Class to export a 3d scene of the track to a specified Pov file
42  */
43 public class PovExporter extends Export3dFunction
44 {
45         private Track _track = null;
46         private JDialog _dialog = null;
47         private JFileChooser _fileChooser = null;
48         private String _cameraX = null, _cameraY = null, _cameraZ = null;
49         private JTextField _cameraXField = null, _cameraYField = null, _cameraZField = null;
50         private JTextField _fontName = null, _altitudeFactorField = null;
51         private JRadioButton _ballsAndSticksButton = null;
52
53         // defaults
54         private static final double DEFAULT_CAMERA_DISTANCE = 30.0;
55         private static final String DEFAULT_FONT_FILE = "crystal.ttf";
56
57
58         /**
59          * Constructor
60          * @param inApp App object
61          */
62         public PovExporter(App inApp)
63         {
64                 super(inApp);
65                 _track = inApp.getTrackInfo().getTrack();
66                 // Set default camera coordinates
67                 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
68         }
69
70         /** Get the name key */
71         public String getNameKey() {
72                 return "function.exportpov";
73         }
74
75         /**
76          * Set the coordinates for the camera (can be any scale)
77          * @param inX X coordinate of camera
78          * @param inY Y coordinate of camera
79          * @param inZ Z coordinate of camera
80          */
81         public void setCameraCoordinates(double inX, double inY, double inZ)
82         {
83                 // calculate distance from origin
84                 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
85                 if (cameraDist > 0.0)
86                 {
87                         _cameraX = NumberUtils.formatNumber(inX / cameraDist * DEFAULT_CAMERA_DISTANCE, 5);
88                         _cameraY = NumberUtils.formatNumber(inY / cameraDist * DEFAULT_CAMERA_DISTANCE, 5);
89                         // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system!
90                         _cameraZ = NumberUtils.formatNumber(-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE, 5);
91                 }
92         }
93
94
95         /**
96          * Show the dialog to select options and export file
97          */
98         public void begin()
99         {
100                 // Make dialog window to select angles, colours etc
101                 if (_dialog == null)
102                 {
103                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
104                         _dialog.setLocationRelativeTo(_parentFrame);
105                         _dialog.getContentPane().add(makeDialogComponents());
106                 }
107
108                 // Set angles
109                 _cameraXField.setText(_cameraX);
110                 _cameraYField.setText(_cameraY);
111                 _cameraZField.setText(_cameraZ);
112                 _altitudeFactorField.setText("" + _altFactor);
113                 // Show dialog
114                 _dialog.pack();
115                 _dialog.setVisible(true);
116         }
117
118
119         /**
120          * Make the dialog components to select the export options
121          * @return Component holding gui elements
122          */
123         private Component makeDialogComponents()
124         {
125                 JPanel panel = new JPanel();
126                 panel.setLayout(new BorderLayout());
127                 JLabel introLabel = new JLabel(I18nManager.getText("dialog.exportpov.text"));
128                 introLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4));
129                 panel.add(introLabel, BorderLayout.NORTH);
130                 // OK, Cancel buttons
131                 JPanel buttonPanel = new JPanel();
132                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
133                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
134                 okButton.addActionListener(new ActionListener() {
135                         public void actionPerformed(ActionEvent e)
136                         {
137                                 doExport();
138                                 _dialog.dispose();
139                         }
140                 });
141                 buttonPanel.add(okButton);
142                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
143                 cancelButton.addActionListener(new ActionListener() {
144                         public void actionPerformed(ActionEvent e)
145                         {
146                                 _dialog.dispose();
147                         }
148                 });
149                 buttonPanel.add(cancelButton);
150                 panel.add(buttonPanel, BorderLayout.SOUTH);
151
152                 // central panel
153                 JPanel centralPanel = new JPanel();
154                 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
155
156                 JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font"));
157                 fontLabel.setHorizontalAlignment(SwingConstants.TRAILING);
158                 centralPanel.add(fontLabel);
159                 String defaultFont = Config.getConfigString(Config.KEY_POVRAY_FONT);
160                 if (defaultFont == null || defaultFont.equals("")) {
161                         defaultFont = DEFAULT_FONT_FILE;
162                 }
163                 _fontName = new JTextField(defaultFont, 12);
164                 _fontName.setAlignmentX(Component.LEFT_ALIGNMENT);
165                 _fontName.addKeyListener(new DialogCloser(_dialog));
166                 centralPanel.add(_fontName);
167                 //coordinates of camera
168                 JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax"));
169                 cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING);
170                 centralPanel.add(cameraXLabel);
171                 _cameraXField = new JTextField("" + _cameraX);
172                 centralPanel.add(_cameraXField);
173                 JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray"));
174                 cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING);
175                 centralPanel.add(cameraYLabel);
176                 _cameraYField = new JTextField("" + _cameraY);
177                 centralPanel.add(_cameraYField);
178                 JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz"));
179                 cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING);
180                 centralPanel.add(cameraZLabel);
181                 _cameraZField = new JTextField("" + _cameraZ);
182                 centralPanel.add(_cameraZField);
183                 // Altitude exaggeration
184                 JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudefactor"));
185                 altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING);
186                 centralPanel.add(altitudeCapLabel);
187                 _altitudeFactorField = new JTextField("1.0");
188                 centralPanel.add(_altitudeFactorField);
189
190                 // Radio buttons for style - balls on sticks or tubes
191                 JPanel stylePanel = new JPanel();
192                 stylePanel.setLayout(new GridLayout(0, 2, 10, 4));
193                 JLabel styleLabel = new JLabel(I18nManager.getText("dialog.exportpov.modelstyle"));
194                 styleLabel.setHorizontalAlignment(SwingConstants.TRAILING);
195                 stylePanel.add(styleLabel);
196                 JPanel radioPanel = new JPanel();
197                 radioPanel.setLayout(new BoxLayout(radioPanel, BoxLayout.Y_AXIS));
198                 _ballsAndSticksButton = new JRadioButton(I18nManager.getText("dialog.exportpov.ballsandsticks"));
199                 _ballsAndSticksButton.setSelected(false);
200                 radioPanel.add(_ballsAndSticksButton);
201                 JRadioButton tubesButton = new JRadioButton(I18nManager.getText("dialog.exportpov.tubesandwalls"));
202                 tubesButton.setSelected(true);
203                 radioPanel.add(tubesButton);
204                 ButtonGroup group = new ButtonGroup();
205                 group.add(_ballsAndSticksButton); group.add(tubesButton);
206                 stylePanel.add(radioPanel);
207
208                 // add this grid to the holder panel
209                 JPanel holderPanel = new JPanel();
210                 holderPanel.setLayout(new BorderLayout(5, 5));
211                 JPanel boxPanel = new JPanel();
212                 boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS));
213                 boxPanel.add(centralPanel);
214                 boxPanel.add(stylePanel);
215                 holderPanel.add(boxPanel, BorderLayout.CENTER);
216
217                 // show lines button
218                 JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
219                 showLinesButton.addActionListener(new ActionListener() {
220                         public void actionPerformed(ActionEvent e)
221                         {
222                                 // Need to scale model to find lines
223                                 ThreeDModel model = new ThreeDModel(_track);
224                                 model.scale();
225                                 double[] latLines = model.getLatitudeLines();
226                                 double[] lonLines = model.getLongitudeLines();
227                                 LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
228                                 dialog.showDialog();
229                         }
230                 });
231                 JPanel flowPanel = new JPanel();
232                 flowPanel.setLayout(new FlowLayout());
233                 flowPanel.add(showLinesButton);
234                 holderPanel.add(flowPanel, BorderLayout.EAST);
235                 panel.add(holderPanel, BorderLayout.CENTER);
236                 return panel;
237         }
238
239
240         /**
241          * Select the file and export data to it
242          */
243         private void doExport()
244         {
245                 // Copy camera coordinates
246                 _cameraX = checkCoordinate(_cameraXField.getText());
247                 _cameraY = checkCoordinate(_cameraYField.getText());
248                 _cameraZ = checkCoordinate(_cameraZField.getText());
249
250                 // OK pressed, so choose output file
251                 if (_fileChooser == null)
252                 {
253                         _fileChooser = new JFileChooser();
254                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
255                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.pov", new String[] {"pov"}));
256                         _fileChooser.setAcceptAllFileFilterUsed(false);
257                         // start from directory in config which should be set
258                         final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
259                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
260                 }
261
262                 // Allow choose again if an existing file is selected
263                 boolean chooseAgain = false;
264                 do
265                 {
266                         chooseAgain = false;
267                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
268                         {
269                                 // OK pressed and file chosen
270                                 File file = _fileChooser.getSelectedFile();
271                                 if (!file.getName().toLowerCase().endsWith(".pov"))
272                                 {
273                                         file = new File(file.getAbsolutePath() + ".pov");
274                                 }
275                                 // Check if file exists and if necessary prompt for overwrite
276                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
277                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
278                                                 I18nManager.getText("dialog.save.overwrite.text"),
279                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
280                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
281                                         == JOptionPane.YES_OPTION)
282                                 {
283                                         // Export the file
284                                         if (exportFile(file))
285                                         {
286                                                 // file saved
287                                                 // Store directory in config for later
288                                                 Config.setConfigString(Config.KEY_TRACK_DIR, file.getParentFile().getAbsolutePath());
289                                         }
290                                         else
291                                         {
292                                                 // export failed so need to choose again
293                                                 chooseAgain = true;
294                                         }
295                                 }
296                                 else
297                                 {
298                                         // overwrite cancelled so need to choose again
299                                         chooseAgain = true;
300                                 }
301                         }
302                 } while (chooseAgain);
303         }
304
305
306         /**
307          * Export the track data to the specified file
308          * @param inFile File object to save to
309          * @return true if successful
310          */
311         private boolean exportFile(File inFile)
312         {
313                 FileWriter writer = null;
314                 // find out the line separator for this system
315                 final String lineSeparator = System.getProperty("line.separator");
316                 try
317                 {
318                         // create and scale model
319                         ThreeDModel model = new ThreeDModel(_track);
320                         try
321                         {
322                                 // try to use given altitude cap
323                                 double altFactor = Double.parseDouble(_altitudeFactorField.getText());
324                                 model.setAltitudeFactor(altFactor);
325                         }
326                         catch (NumberFormatException nfe) { // parse failed, reset
327                                 _altitudeFactorField.setText("1.0");
328                         }
329                         model.scale();
330
331                         // Create file and write basics
332                         writer = new FileWriter(inFile);
333                         writeStartOfFile(writer, model.getModelSize(), lineSeparator);
334
335                         // write out lat/long lines using model
336                         writeLatLongLines(writer, model, lineSeparator);
337
338                         // write out points
339                         if (_ballsAndSticksButton.isSelected()) {
340                                 writeDataPointsBallsAndSticks(writer, model, lineSeparator);
341                         }
342                         else {
343                                 writeDataPointsTubesAndWalls(writer, model, lineSeparator);
344                         }
345
346                         // everything worked
347                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
348                                  + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
349                                  + " " + inFile.getAbsolutePath());
350                         return true;
351                 }
352                 catch (IOException ioe)
353                 {
354                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
355                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
356                 }
357                 finally
358                 {
359                         // close file ignoring exceptions
360                         try
361                         {
362                                 writer.close();
363                         }
364                         catch (Exception e) {}
365                 }
366                 return false;
367         }
368
369
370         /**
371          * Write the start of the Pov file, including base plane and lights
372          * @param inWriter Writer to use for writing file
373          * @param inModelSize model size
374          * @param inLineSeparator line separator to use
375          * @throws IOException on file writing error
376          */
377         private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator)
378         throws IOException
379         {
380                 inWriter.write("// Pov file produced by GpsPrune - see http://activityworkshop.net/");
381                 inWriter.write(inLineSeparator);
382                 inWriter.write(inLineSeparator);
383                 // Select font based on user input
384                 String fontPath = _fontName.getText();
385                 if (fontPath == null || fontPath.equals(""))
386                 {
387                         fontPath = DEFAULT_FONT_FILE;
388                 }
389                 else {
390                         Config.setConfigString(Config.KEY_POVRAY_FONT, fontPath);
391                 }
392                 // Set up output
393                 String[] outputLines = {
394                   "global_settings { ambient_light rgb <4, 4, 4> }", "",
395                   "// Background and camera",
396                   "background { color rgb <0, 0, 0> }",
397                   // camera position
398                   "camera {",
399                   "  location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
400                   "  look_at  <0, 0, 0>",
401                   "}", "",
402                 // global declares
403                   "// Global declares",
404                   "#declare lat_line =",
405                   "  cylinder {",
406                   "   <-" + inModelSize + ", 0.1, 0>,",
407                   "   <" + inModelSize + ", 0.1, 0>,",
408                   "   0.1            // Radius",
409                   "   pigment { color rgb <0.5 0.5 0.5> }",
410                   "  }",
411                   "#declare lon_line =",
412                   "  cylinder {",
413                   "   <0, 0.1, -" + inModelSize + ">,",
414                   "   <0, 0.1, " + inModelSize + ">,",
415                   "   0.1            // Radius",
416                   "   pigment { color rgb <0.5 0.5 0.5> }",
417                   "  }",
418                   "#declare point_rod =",
419                   "  cylinder {",
420                   "   <0, 0, 0>,",
421                   "   <0, 1, 0>,",
422                   "   0.15",
423                   "   open",
424                   "   pigment { color rgb <0.5 0.5 0.5> }",
425                   "  }", "",
426                   // MAYBE: Export rods to POV?  How to store in data?
427                   "#declare waypoint_sphere =",
428                   "  sphere {",
429                   "   <0, 0, 0>, 0.4",
430                   "    texture {",
431                   "       pigment {color rgb <0.1 0.1 1.0>}",
432                   "       finish { phong 1 }",
433                   "    }",
434                   "  }",
435                   "#declare track_sphere0 =",
436                   "  sphere {",
437                   "   <0, 0, 0>, 0.3", // size should depend on model size
438                   "   texture {",
439                   "      pigment {color rgb <0.1 0.6 0.1>}", // dark green
440                   "      finish { phong 1 }",
441                   "   }",
442                   " }",
443                   "#declare track_sphere1 =",
444                   "  sphere {",
445                   "   <0, 0, 0>, 0.3", // size should depend on model size
446                   "   texture {",
447                   "      pigment {color rgb <0.4 0.9 0.2>}", // green
448                   "      finish { phong 1 }",
449                   "   }",
450                   " }",
451                   "#declare track_sphere2 =",
452                   "  sphere {",
453                   "   <0, 0, 0>, 0.3", // size should depend on model size
454                   "   texture {",
455                   "      pigment {color rgb <0.7 0.8 0.2>}", // yellow
456                   "      finish { phong 1 }",
457                   "   }",
458                   " }",
459                   "#declare track_sphere3 =",
460                   "  sphere {",
461                   "   <0, 0, 0>, 0.3", // size should depend on model size
462                   "   texture {",
463                   "      pigment {color rgb <0.5 0.8 0.6>}", // greeny
464                   "      finish { phong 1 }",
465                   "   }",
466                   " }",
467                   "#declare track_sphere4 =",
468                   "  sphere {",
469                   "   <0, 0, 0>, 0.3", // size should depend on model size
470                   "   texture {",
471                   "      pigment {color rgb <0.2 0.9 0.9>}", // cyan
472                   "      finish { phong 1 }",
473                   "   }",
474                   " }",
475                   "#declare track_sphere5 =",
476                   "  sphere {",
477                   "   <0, 0, 0>, 0.3", // size should depend on model size
478                   "   texture {",
479                   "      pigment {color rgb <1.0 1.0 1.0>}", // white
480                   "      finish { phong 1 }",
481                   "   }",
482                   " }",
483                   "#declare track_sphere_t =",
484                   "  sphere {",
485                   "   <0, 0, 0>, 0.25", // size should depend on model size
486                   "   texture {",
487                   "      pigment {color rgb <0.6 1.0 0.2>}",
488                   "      finish { phong 1 }",
489                   "   } no_shadow",
490                   " }",
491                   "#declare wall_colour = rgbt <0.5, 0.5, 0.5, 0.3>;", "",
492                   "// Base plane",
493                   "box {",
494                   "   <-" + inModelSize + ", -0.15, -" + inModelSize + ">,  // Near lower left corner",
495                   "   <" + inModelSize + ", 0.15, " + inModelSize + ">   // Far upper right corner",
496                   "   pigment { color rgb <0.5 0.75 0.8> }",
497                   "}", "",
498                 // write cardinals
499                   "// Cardinal letters N,S,E,W",
500                   "text {",
501                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
502                   "  pigment { color rgb <1 1 1> }",
503                   "  translate <0, 0.2, " + inModelSize + ">",
504                   "}",
505                   "text {",
506                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
507                   "  pigment { color rgb <1 1 1> }",
508                   "  translate <0, 0.2, -" + inModelSize + ">",
509                   "}",
510                   "text {",
511                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0",
512                   "  pigment { color rgb <1 1 1> }",
513                   "  translate <" + (inModelSize * 0.97) + ", 0.2, 0>",
514                   "}",
515                   "text {",
516                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0",
517                   "  pigment { color rgb <1 1 1> }",
518                   "  translate <-" + (inModelSize * 1.03) + ", 0.2, 0>",
519                   "}", "",
520                   // MAYBE: Light positions should relate to model size
521                   "// lights",
522                   "light_source { <-1, 9, -4> color rgb <0.5 0.5 0.5>}",
523                   "light_source { <1, 6, -14> color rgb <0.6 0.6 0.6>}",
524                   "light_source { <11, 12, 8> color rgb <0.3 0.3 0.3>}",
525                   "",
526                 };
527                 // write strings to file
528                 int numLines = outputLines.length;
529                 for (int i=0; i<numLines; i++)
530                 {
531                         inWriter.write(outputLines[i]);
532                         inWriter.write(inLineSeparator);
533                 }
534         }
535
536
537         /**
538          * Write out all the lat and long lines to the file
539          * @param inWriter Writer to use for writing file
540          * @param inModel model object for getting lat/long lines
541          * @param inLineSeparator line separator to use
542          * @throws IOException on file writing error
543          */
544         private static void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
545         throws IOException
546         {
547                 inWriter.write("// Latitude and longitude lines:");
548                 inWriter.write(inLineSeparator);
549                 int numlines = inModel.getLatitudeLines().length;
550                 for (int i=0; i<numlines; i++)
551                 {
552                         // write cylinder to file
553                         inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
554                         inWriter.write(inLineSeparator);
555                 }
556                 numlines = inModel.getLongitudeLines().length;
557                 for (int i=0; i<numlines; i++)
558                 {
559                         // write cylinder to file
560                         inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
561                         inWriter.write(inLineSeparator);
562                 }
563                 inWriter.write(inLineSeparator);
564         }
565
566
567         /**
568          * Write out all the data points to the file in the balls-and-sticks style
569          * @param inWriter Writer to use for writing file
570          * @param inModel model object for getting data points
571          * @param inLineSeparator line separator to use
572          * @throws IOException on file writing error
573          */
574         private static void writeDataPointsBallsAndSticks(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
575         throws IOException
576         {
577                 inWriter.write("// Data points:");
578                 inWriter.write(inLineSeparator);
579                 int numPoints = inModel.getNumPoints();
580                 for (int i=0; i<numPoints; i++)
581                 {
582                         // ball (different according to type)
583                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
584                         {
585                                 // waypoint ball
586                                 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
587                                         + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
588                         }
589                         else
590                         {
591                                 // normal track point ball
592                                 inWriter.write("object { track_sphere" + checkHeightCode(inModel.getPointHeightCode(i))
593                                         + " translate <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
594                                         + "," + inModel.getScaledVertValue(i) + "> }");
595                         }
596                         inWriter.write(inLineSeparator);
597                         // vertical rod (if altitude positive)
598                         if (inModel.getScaledAltValue(i) > 0.0)
599                         {
600                                 inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0,"
601                                         + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }");
602                                 inWriter.write(inLineSeparator);
603                         }
604                 }
605                 inWriter.write(inLineSeparator);
606         }
607
608
609         /**
610          * Write out all the data points to the file in the tubes-and-walls style
611          * @param inWriter Writer to use for writing file
612          * @param inModel model object for getting data points
613          * @param inLineSeparator line separator to use
614          * @throws IOException on file writing error
615          */
616         private static void writeDataPointsTubesAndWalls(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
617         throws IOException
618         {
619                 inWriter.write("// Data points:");
620                 inWriter.write(inLineSeparator);
621                 int numPoints = inModel.getNumPoints();
622                 int numTrackPoints = 0;
623                 // Loop over all points and write out waypoints as balls
624                 for (int i=0; i<numPoints; i++)
625                 {
626                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
627                         {
628                                 // waypoint ball
629                                 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
630                                         + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
631                                 // vertical rod (if altitude positive)
632                                 if (inModel.getScaledAltValue(i) > 0.0)
633                                 {
634                                         inWriter.write(inLineSeparator);
635                                         inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0,"
636                                                 + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }");
637                                 }
638                                 inWriter.write(inLineSeparator);
639                         }
640                         else {numTrackPoints++;}
641                 }
642                 inWriter.write(inLineSeparator);
643
644                 // Loop over all the track segments
645                 ArrayList<ModelSegment> segmentList = getSegmentList(inModel);
646                 Iterator<ModelSegment> segmentIterator = segmentList.iterator();
647                 while (segmentIterator.hasNext())
648                 {
649                         ModelSegment segment = segmentIterator.next();
650                         int segLength = segment.getNumTrackPoints();
651
652                         // if the track segment is long enough, do a cubic spline sphere sweep
653                         if (segLength <= 1)
654                         {
655                                 // single point in segment - just draw sphere
656                                 int index = segment.getStartIndex();
657                                 inWriter.write("object { track_sphere_t"
658                                         + " translate <" + inModel.getScaledHorizValue(index) + "," + inModel.getScaledAltValue(index)
659                                         + "," + inModel.getScaledVertValue(index) + "> }");
660                                 // maybe draw some kind of polygon too or rod?
661                         }
662                         else
663                         {
664                                 writeSphereSweep(inWriter, inModel, segment, inLineSeparator);
665                         }
666
667                         // Write wall underneath segment
668                         if (segLength > 1)
669                         {
670                                 writePolygonWall(inWriter, inModel, segment, inLineSeparator);
671                         }
672                 }
673         }
674
675
676         /**
677          * Write out a single sphere sweep using either cubic spline or linear spline
678          * @param inWriter Writer to use for writing file
679          * @param inModel model object for getting data points
680          * @param inSegment model segment to draw
681          * @param inLineSeparator line separator to use
682          * @throws IOException on file writing error
683          */
684         private static void writeSphereSweep(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
685         throws IOException
686         {
687                 // 3d sphere sweep
688                 inWriter.write("// Sphere sweep:");
689                 inWriter.write(inLineSeparator);
690                 String splineType = inSegment.getNumTrackPoints() < 5?"linear_spline":"cubic_spline";
691                 inWriter.write("sphere_sweep { "); inWriter.write(splineType);
692                 inWriter.write(" " + inSegment.getNumTrackPoints() + ",");
693                 inWriter.write(inLineSeparator);
694                 // Loop over all points in this segment and write out sphere sweep
695                 for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++)
696                 {
697                         if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
698                         {
699                                 inWriter.write("  <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
700                                         + "," + inModel.getScaledVertValue(i) + ">, 0.25");
701                                 inWriter.write(inLineSeparator);
702                         }
703                 }
704                 inWriter.write("  tolerance 0.1");
705                 inWriter.write(inLineSeparator);
706                 inWriter.write("  texture { pigment {color rgb <0.6 1.0 0.2>}  finish {phong 1} }");
707                 inWriter.write(inLineSeparator);
708                 inWriter.write("  no_shadow");
709                 inWriter.write(inLineSeparator);
710                 inWriter.write("}");
711                 inWriter.write(inLineSeparator);
712         }
713
714
715         /**
716          * Write out a single polygon-based wall for the tubes-and-walls style
717          * @param inWriter Writer to use for writing file
718          * @param inModel model object for getting data points
719          * @param inSegment model segment to draw
720          * @param inLineSeparator line separator to use
721          * @throws IOException on file writing error
722          */
723         private static void writePolygonWall(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
724         throws IOException
725         {
726                 // wall
727                 inWriter.write(inLineSeparator);
728                 inWriter.write("// wall between sweep and floor:");
729                 inWriter.write(inLineSeparator);
730                 // Loop over all points in this segment again and write out polygons
731                 int prevIndex = -1;
732                 for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++)
733                 {
734                         if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
735                         {
736                                 if (prevIndex >= 0)
737                                 {
738                                         double xDiff = inModel.getScaledHorizValue(i) - inModel.getScaledHorizValue(prevIndex);
739                                         double yDiff = inModel.getScaledVertValue(i) - inModel.getScaledVertValue(prevIndex);
740                                         double dist = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
741                                         if (dist > 0)
742                                         {
743                                                 inWriter.write("polygon {");
744                                                 inWriter.write("  5, <" + inModel.getScaledHorizValue(prevIndex) + ", 0.0, " + inModel.getScaledVertValue(prevIndex) + ">,");
745                                                 inWriter.write(" <" + inModel.getScaledHorizValue(prevIndex) + ", " + inModel.getScaledAltValue(prevIndex) + ", "
746                                                         + inModel.getScaledVertValue(prevIndex) + ">,");
747                                                 inWriter.write(" <" + inModel.getScaledHorizValue(i) + ", " + inModel.getScaledAltValue(i) + ", "
748                                                         + inModel.getScaledVertValue(i) + ">,");
749                                                 inWriter.write(" <" + inModel.getScaledHorizValue(i) + ", 0.0, " + inModel.getScaledVertValue(i) + ">,");
750                                                 inWriter.write(" <" + inModel.getScaledHorizValue(prevIndex) + ", 0.0, " + inModel.getScaledVertValue(prevIndex) + ">");
751                                                 inWriter.write("  pigment { color wall_colour } no_shadow");
752                                                 inWriter.write("}");
753                                                 inWriter.write(inLineSeparator);
754                                         }
755                                 }
756                                 prevIndex = i;
757                         }
758                 }
759         }
760
761
762         /**
763          * @param inCode height code to check
764          * @return validated height code within range 0 to max
765          */
766         private static byte checkHeightCode(byte inCode)
767         {
768                 final byte maxHeightCode = 5;
769                 if (inCode < 0) return 0;
770                 if (inCode > maxHeightCode) return maxHeightCode;
771                 return inCode;
772         }
773
774
775         /**
776          * Check the given coordinate
777          * @param inString String entered by user
778          * @return validated String value
779          */
780         private static String checkCoordinate(String inString)
781         {
782                 double value = 0.0;
783                 try
784                 {
785                         value = Double.parseDouble(inString);
786                 }
787                 catch (Exception e) {} // ignore parse failures
788                 return "" + value;
789         }
790
791         /**
792          * Go through the points making a list of the segment starts and the number of track points in each segment
793          * @param inModel model containing data
794          * @return list of ModelSegment objects
795          */
796         private static ArrayList<ModelSegment> getSegmentList(ThreeDModel inModel)
797         {
798                 ArrayList<ModelSegment> segmentList = new ArrayList<ModelSegment>();
799                 if (inModel != null && inModel.getNumPoints() > 0)
800                 {
801                         ModelSegment currSegment = null;
802                         int numTrackPoints = 0;
803                         for (int i=0; i<inModel.getNumPoints(); i++)
804                         {
805                                 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
806                                 {
807                                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_SEGMENT_START || currSegment == null)
808                                         {
809                                                 // start of segment
810                                                 if (currSegment != null)
811                                                 {
812                                                         currSegment.setEndIndex(i-1);
813                                                         currSegment.setNumTrackPoints(numTrackPoints);
814                                                         segmentList.add(currSegment);
815                                                         numTrackPoints = 0;
816                                                 }
817                                                 currSegment = new ModelSegment(i);
818                                         }
819                                         numTrackPoints++;
820                                 }
821                         }
822                         // Add last segment to list
823                         if (currSegment != null && numTrackPoints > 0)
824                         {
825                                 currSegment.setEndIndex(inModel.getNumPoints()-1);
826                                 currSegment.setNumTrackPoints(numTrackPoints);
827                                 segmentList.add(currSegment);
828                         }
829                 }
830                 return segmentList;
831         }
832 }