]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/PovExporter.java
e360878006443a82da87f60e7bf38594ae7bcac5
[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                 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.2 1.0 0.2>}",
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.6 1.0 0.2>}",
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 <1.0 1.0 0.1>}",
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 <1.0 1.0 1.0>}",
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.1 1.0 1.0>}",
472                   "      finish { phong 1 }",
473                   "   }",
474                   " }",
475                   "#declare track_sphere_t =",
476                   "  sphere {",
477                   "   <0, 0, 0>, 0.25", // size should depend on model size
478                   "   texture {",
479                   "      pigment {color rgb <0.6 1.0 0.2>}",
480                   "      finish { phong 1 }",
481                   "   } no_shadow",
482                   " }",
483                   "#declare wall_colour = rgbt <0.5, 0.5, 0.5, 0.3>;", "",
484                   "// Base plane",
485                   "box {",
486                   "   <-" + inModelSize + ", -0.15, -" + inModelSize + ">,  // Near lower left corner",
487                   "   <" + inModelSize + ", 0.15, " + inModelSize + ">   // Far upper right corner",
488                   "   pigment { color rgb <0.5 0.75 0.8> }",
489                   "}", "",
490                 // write cardinals
491                   "// Cardinal letters N,S,E,W",
492                   "text {",
493                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
494                   "  pigment { color rgb <1 1 1> }",
495                   "  translate <0, 0.2, " + inModelSize + ">",
496                   "}",
497                   "text {",
498                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
499                   "  pigment { color rgb <1 1 1> }",
500                   "  translate <0, 0.2, -" + inModelSize + ">",
501                   "}",
502                   "text {",
503                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0",
504                   "  pigment { color rgb <1 1 1> }",
505                   "  translate <" + (inModelSize * 0.97) + ", 0.2, 0>",
506                   "}",
507                   "text {",
508                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0",
509                   "  pigment { color rgb <1 1 1> }",
510                   "  translate <-" + (inModelSize * 1.03) + ", 0.2, 0>",
511                   "}", "",
512                   // MAYBE: Light positions should relate to model size
513                   "// lights",
514                   "light_source { <-1, 9, -4> color rgb <0.5 0.5 0.5>}",
515                   "light_source { <1, 6, -14> color rgb <0.6 0.6 0.6>}",
516                   "light_source { <11, 12, 8> color rgb <0.3 0.3 0.3>}",
517                   "",
518                 };
519                 // write strings to file
520                 int numLines = outputLines.length;
521                 for (int i=0; i<numLines; i++)
522                 {
523                         inWriter.write(outputLines[i]);
524                         inWriter.write(inLineSeparator);
525                 }
526         }
527
528
529         /**
530          * Write out all the lat and long lines to the file
531          * @param inWriter Writer to use for writing file
532          * @param inModel model object for getting lat/long lines
533          * @param inLineSeparator line separator to use
534          * @throws IOException on file writing error
535          */
536         private void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
537         throws IOException
538         {
539                 inWriter.write("// Latitude and longitude lines:");
540                 inWriter.write(inLineSeparator);
541                 int numlines = inModel.getLatitudeLines().length;
542                 for (int i=0; i<numlines; i++)
543                 {
544                         // write cylinder to file
545                         inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
546                         inWriter.write(inLineSeparator);
547                 }
548                 numlines = inModel.getLongitudeLines().length;
549                 for (int i=0; i<numlines; i++)
550                 {
551                         // write cylinder to file
552                         inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
553                         inWriter.write(inLineSeparator);
554                 }
555                 inWriter.write(inLineSeparator);
556         }
557
558
559         /**
560          * Write out all the data points to the file in the balls-and-sticks style
561          * @param inWriter Writer to use for writing file
562          * @param inModel model object for getting data points
563          * @param inLineSeparator line separator to use
564          * @throws IOException on file writing error
565          */
566         private void writeDataPointsBallsAndSticks(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
567         throws IOException
568         {
569                 inWriter.write("// Data points:");
570                 inWriter.write(inLineSeparator);
571                 int numPoints = inModel.getNumPoints();
572                 for (int i=0; i<numPoints; i++)
573                 {
574                         // ball (different according to type)
575                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
576                         {
577                                 // waypoint ball
578                                 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
579                                         + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
580                         }
581                         else
582                         {
583                                 // normal track point ball
584                                 inWriter.write("object { track_sphere" + checkHeightCode(inModel.getPointHeightCode(i))
585                                         + " translate <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
586                                         + "," + inModel.getScaledVertValue(i) + "> }");
587                         }
588                         inWriter.write(inLineSeparator);
589                         // vertical rod (if altitude positive)
590                         if (inModel.getScaledAltValue(i) > 0.0)
591                         {
592                                 inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0,"
593                                         + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }");
594                                 inWriter.write(inLineSeparator);
595                         }
596                 }
597                 inWriter.write(inLineSeparator);
598         }
599
600
601         /**
602          * Write out all the data points to the file in the tubes-and-walls style
603          * @param inWriter Writer to use for writing file
604          * @param inModel model object for getting data points
605          * @param inLineSeparator line separator to use
606          * @throws IOException on file writing error
607          */
608         private void writeDataPointsTubesAndWalls(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
609         throws IOException
610         {
611                 inWriter.write("// Data points:");
612                 inWriter.write(inLineSeparator);
613                 int numPoints = inModel.getNumPoints();
614                 int numTrackPoints = 0;
615                 // Loop over all points and write out waypoints as balls
616                 for (int i=0; i<numPoints; i++)
617                 {
618                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
619                         {
620                                 // waypoint ball
621                                 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
622                                         + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
623                                 // vertical rod (if altitude positive)
624                                 if (inModel.getScaledAltValue(i) > 0.0)
625                                 {
626                                         inWriter.write(inLineSeparator);
627                                         inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0,"
628                                                 + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }");
629                                 }
630                                 inWriter.write(inLineSeparator);
631                         }
632                         else {numTrackPoints++;}
633                 }
634                 inWriter.write(inLineSeparator);
635
636                 // Loop over all the track segments
637                 ArrayList<ModelSegment> segmentList = getSegmentList(inModel);
638                 Iterator<ModelSegment> segmentIterator = segmentList.iterator();
639                 while (segmentIterator.hasNext())
640                 {
641                         ModelSegment segment = segmentIterator.next();
642                         int segLength = segment.getNumTrackPoints();
643
644                         // if the track segment is long enough, do a cubic spline sphere sweep
645                         if (segLength <= 1)
646                         {
647                                 // single point in segment - just draw sphere
648                                 int index = segment.getStartIndex();
649                                 inWriter.write("object { track_sphere_t"
650                                         + " translate <" + inModel.getScaledHorizValue(index) + "," + inModel.getScaledAltValue(index)
651                                         + "," + inModel.getScaledVertValue(index) + "> }");
652                                 // maybe draw some kind of polygon too or rod?
653                         }
654                         else
655                         {
656                                 writeSphereSweep(inWriter, inModel, segment, inLineSeparator);
657                         }
658
659                         // Write wall underneath segment
660                         if (segLength > 1)
661                         {
662                                 writePolygonWall(inWriter, inModel, segment, inLineSeparator);
663                         }
664                 }
665         }
666
667
668         /**
669          * Write out a single sphere sweep using either cubic spline or linear spline
670          * @param inWriter Writer to use for writing file
671          * @param inModel model object for getting data points
672          * @param inSegment model segment to draw
673          * @param inLineSeparator line separator to use
674          * @throws IOException on file writing error
675          */
676         private static void writeSphereSweep(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
677         throws IOException
678         {
679                 // 3d sphere sweep
680                 inWriter.write("// Sphere sweep:");
681                 inWriter.write(inLineSeparator);
682                 String splineType = inSegment.getNumTrackPoints() < 5?"linear_spline":"cubic_spline";
683                 inWriter.write("sphere_sweep { "); inWriter.write(splineType);
684                 inWriter.write(" " + inSegment.getNumTrackPoints() + ",");
685                 inWriter.write(inLineSeparator);
686                 // Loop over all points in this segment and write out sphere sweep
687                 for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++)
688                 {
689                         if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
690                         {
691                                 inWriter.write("  <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
692                                         + "," + inModel.getScaledVertValue(i) + ">, 0.25");
693                                 inWriter.write(inLineSeparator);
694                         }
695                 }
696                 inWriter.write("  tolerance 0.1");
697                 inWriter.write(inLineSeparator);
698                 inWriter.write("  texture { pigment {color rgb <0.6 1.0 0.2>}  finish {phong 1} }");
699                 inWriter.write(inLineSeparator);
700                 inWriter.write("  no_shadow");
701                 inWriter.write(inLineSeparator);
702                 inWriter.write("}");
703                 inWriter.write(inLineSeparator);
704         }
705
706
707         /**
708          * Write out a single polygon-based wall for the tubes-and-walls style
709          * @param inWriter Writer to use for writing file
710          * @param inModel model object for getting data points
711          * @param inSegment model segment to draw
712          * @param inLineSeparator line separator to use
713          * @throws IOException on file writing error
714          */
715         private static void writePolygonWall(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
716         throws IOException
717         {
718                 // wall
719                 inWriter.write(inLineSeparator);
720                 inWriter.write("// wall between sweep and floor:");
721                 inWriter.write(inLineSeparator);
722                 // Loop over all points in this segment again and write out polygons
723                 int prevIndex = -1;
724                 for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++)
725                 {
726                         if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
727                         {
728                                 if (prevIndex >= 0)
729                                 {
730                                         double xDiff = inModel.getScaledHorizValue(i) - inModel.getScaledHorizValue(prevIndex);
731                                         double yDiff = inModel.getScaledVertValue(i) - inModel.getScaledVertValue(prevIndex);
732                                         double dist = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
733                                         if (dist > 0)
734                                         {
735                                                 inWriter.write("polygon {");
736                                                 inWriter.write("  5, <" + inModel.getScaledHorizValue(prevIndex) + ", 0.0, " + inModel.getScaledVertValue(prevIndex) + ">,");
737                                                 inWriter.write(" <" + inModel.getScaledHorizValue(prevIndex) + ", " + inModel.getScaledAltValue(prevIndex) + ", "
738                                                         + inModel.getScaledVertValue(prevIndex) + ">,");
739                                                 inWriter.write(" <" + inModel.getScaledHorizValue(i) + ", " + inModel.getScaledAltValue(i) + ", "
740                                                         + inModel.getScaledVertValue(i) + ">,");
741                                                 inWriter.write(" <" + inModel.getScaledHorizValue(i) + ", 0.0, " + inModel.getScaledVertValue(i) + ">,");
742                                                 inWriter.write(" <" + inModel.getScaledHorizValue(prevIndex) + ", 0.0, " + inModel.getScaledVertValue(prevIndex) + ">");
743                                                 inWriter.write("  pigment { color wall_colour } no_shadow");
744                                                 inWriter.write("}");
745                                                 inWriter.write(inLineSeparator);
746                                         }
747                                 }
748                                 prevIndex = i;
749                         }
750                 }
751         }
752
753
754         /**
755          * @param inCode height code to check
756          * @return validated height code within range 0 to max
757          */
758         private static byte checkHeightCode(byte inCode)
759         {
760                 final byte maxHeightCode = 4;
761                 if (inCode < 0) return 0;
762                 if (inCode > maxHeightCode) return maxHeightCode;
763                 return inCode;
764         }
765
766
767         /**
768          * Check the given coordinate
769          * @param inString String entered by user
770          * @return validated String value
771          */
772         private static String checkCoordinate(String inString)
773         {
774                 double value = 0.0;
775                 try
776                 {
777                         value = Double.parseDouble(inString);
778                 }
779                 catch (Exception e) {} // ignore parse failures
780                 return "" + value;
781         }
782
783         /**
784          * Go through the points making a list of the segment starts and the number of track points in each segment
785          * @param inModel model containing data
786          * @return list of ModelSegment objects
787          */
788         private static ArrayList<ModelSegment> getSegmentList(ThreeDModel inModel)
789         {
790                 ArrayList<ModelSegment> segmentList = new ArrayList<ModelSegment>();
791                 if (inModel != null && inModel.getNumPoints() > 0)
792                 {
793                         ModelSegment currSegment = null;
794                         int numTrackPoints = 0;
795                         for (int i=0; i<inModel.getNumPoints(); i++)
796                         {
797                                 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
798                                 {
799                                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_SEGMENT_START || currSegment == null)
800                                         {
801                                                 // start of segment
802                                                 if (currSegment != null)
803                                                 {
804                                                         currSegment.setEndIndex(i-1);
805                                                         currSegment.setNumTrackPoints(numTrackPoints);
806                                                         segmentList.add(currSegment);
807                                                         numTrackPoints = 0;
808                                                 }
809                                                 currSegment = new ModelSegment(i);
810                                         }
811                                         numTrackPoints++;
812                                 }
813                         }
814                         // Add last segment to list
815                         if (currSegment != null && numTrackPoints > 0)
816                         {
817                                 currSegment.setEndIndex(inModel.getNumPoints()-1);
818                                 currSegment.setNumTrackPoints(numTrackPoints);
819                                 segmentList.add(currSegment);
820                         }
821                 }
822                 return segmentList;
823         }
824 }