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