]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/PovExporter.java
1b0edfbab07ed76297650040a5fc151e72a5d26d
[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.BoxLayout;
16 import javax.swing.ButtonGroup;
17 import javax.swing.JButton;
18 import javax.swing.JDialog;
19 import javax.swing.JFileChooser;
20 import javax.swing.JLabel;
21 import javax.swing.JOptionPane;
22 import javax.swing.JPanel;
23 import javax.swing.JRadioButton;
24 import javax.swing.JTextField;
25 import javax.swing.SwingConstants;
26
27 import tim.prune.App;
28 import tim.prune.GenericFunction;
29 import tim.prune.I18nManager;
30 import tim.prune.UpdateMessageBroker;
31 import tim.prune.config.Config;
32 import tim.prune.data.Track;
33 import tim.prune.load.GenericFileFilter;
34 import tim.prune.threedee.LineDialog;
35 import tim.prune.threedee.ThreeDModel;
36
37 /**
38  * Class to export track information
39  * into a specified Pov file
40  */
41 public class PovExporter extends GenericFunction
42 {
43         private Track _track = null;
44         private JDialog _dialog = null;
45         private JFileChooser _fileChooser = null;
46         private String _cameraX = null, _cameraY = null, _cameraZ = null;
47         private JTextField _cameraXField = null, _cameraYField = null, _cameraZField = null;
48         private JTextField _fontName = null, _altitudeCapField = null;
49         private int _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
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 giving frame and track
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 = "" + (inX / cameraDist * DEFAULT_CAMERA_DISTANCE);
87                         _cameraY = "" + (inY / cameraDist * DEFAULT_CAMERA_DISTANCE);
88                         // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system!
89                         _cameraZ = "" + (-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE);
90                 }
91         }
92
93
94         /**
95          * @param inAltitudeCap altitude cap to use
96          */
97         public void setAltitudeCap(int inAltitudeCap)
98         {
99                 _altitudeCap = inAltitudeCap;
100                 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
101                 {
102                         _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
103                 }
104         }
105
106
107         /**
108          * Show the dialog to select options and export file
109          */
110         public void begin()
111         {
112                 // Make dialog window to select angles, colours etc
113                 if (_dialog == null)
114                 {
115                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
116                         _dialog.setLocationRelativeTo(_parentFrame);
117                         _dialog.getContentPane().add(makeDialogComponents());
118                 }
119
120                 // Set angles
121                 _cameraXField.setText(_cameraX);
122                 _cameraYField.setText(_cameraY);
123                 _cameraZField.setText(_cameraZ);
124                 // Set vertical scale
125                 _altitudeCapField.setText("" + _altitudeCap);
126                 // Show dialog
127                 _dialog.pack();
128                 _dialog.setVisible(true);
129         }
130
131
132         /**
133          * Make the dialog components to select the export options
134          * @return Component holding gui elements
135          */
136         private Component makeDialogComponents()
137         {
138                 JPanel panel = new JPanel();
139                 panel.setLayout(new BorderLayout());
140                 panel.add(new JLabel(I18nManager.getText("dialog.exportpov.text")), BorderLayout.NORTH);
141                 // OK, Cancel buttons
142                 JPanel buttonPanel = new JPanel();
143                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
144                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
145                 okButton.addActionListener(new ActionListener() {
146                         public void actionPerformed(ActionEvent e)
147                         {
148                                 doExport();
149                                 _dialog.dispose();
150                         }
151                 });
152                 buttonPanel.add(okButton);
153                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
154                 cancelButton.addActionListener(new ActionListener() {
155                         public void actionPerformed(ActionEvent e)
156                         {
157                                 _dialog.dispose();
158                         }
159                 });
160                 buttonPanel.add(cancelButton);
161                 panel.add(buttonPanel, BorderLayout.SOUTH);
162
163                 // central panel
164                 JPanel centralPanel = new JPanel();
165                 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
166
167                 JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font"));
168                 fontLabel.setHorizontalAlignment(SwingConstants.TRAILING);
169                 centralPanel.add(fontLabel);
170                 String defaultFont = Config.getConfigString(Config.KEY_POVRAY_FONT);
171                 if (defaultFont == null || defaultFont.equals("")) {
172                         defaultFont = DEFAULT_FONT_FILE;
173                 }
174                 _fontName = new JTextField(defaultFont, 12);
175                 _fontName.setAlignmentX(Component.LEFT_ALIGNMENT);
176                 centralPanel.add(_fontName);
177                 //coordinates of camera
178                 JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax"));
179                 cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING);
180                 centralPanel.add(cameraXLabel);
181                 _cameraXField = new JTextField("" + _cameraX);
182                 centralPanel.add(_cameraXField);
183                 JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray"));
184                 cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING);
185                 centralPanel.add(cameraYLabel);
186                 _cameraYField = new JTextField("" + _cameraY);
187                 centralPanel.add(_cameraYField);
188                 JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz"));
189                 cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING);
190                 centralPanel.add(cameraZLabel);
191                 _cameraZField = new JTextField("" + _cameraZ);
192                 centralPanel.add(_cameraZField);
193                 // Altitude capping
194                 JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudecap"));
195                 altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING);
196                 centralPanel.add(altitudeCapLabel);
197                 _altitudeCapField = new JTextField("" + _altitudeCap);
198                 centralPanel.add(_altitudeCapField);
199
200                 // Radio buttons for style - balls on sticks or tubes
201                 JPanel stylePanel = new JPanel();
202                 stylePanel.setLayout(new GridLayout(0, 2, 10, 4));
203                 JLabel styleLabel = new JLabel(I18nManager.getText("dialog.exportpov.modelstyle"));
204                 styleLabel.setHorizontalAlignment(SwingConstants.TRAILING);
205                 stylePanel.add(styleLabel);
206                 JPanel radioPanel = new JPanel();
207                 radioPanel.setLayout(new BoxLayout(radioPanel, BoxLayout.Y_AXIS));
208                 _ballsAndSticksButton = new JRadioButton(I18nManager.getText("dialog.exportpov.ballsandsticks"));
209                 _ballsAndSticksButton.setSelected(false);
210                 radioPanel.add(_ballsAndSticksButton);
211                 JRadioButton tubesButton = new JRadioButton(I18nManager.getText("dialog.exportpov.tubesandwalls"));
212                 tubesButton.setSelected(true);
213                 radioPanel.add(tubesButton);
214                 ButtonGroup group = new ButtonGroup();
215                 group.add(_ballsAndSticksButton); group.add(tubesButton);
216                 stylePanel.add(radioPanel);
217
218                 // add this grid to the holder panel
219                 JPanel holderPanel = new JPanel();
220                 holderPanel.setLayout(new BorderLayout(5, 5));
221                 JPanel boxPanel = new JPanel();
222                 boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS));
223                 boxPanel.add(centralPanel);
224                 boxPanel.add(stylePanel);
225                 holderPanel.add(boxPanel, BorderLayout.CENTER);
226
227                 // show lines button
228                 JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
229                 showLinesButton.addActionListener(new ActionListener() {
230                         public void actionPerformed(ActionEvent e)
231                         {
232                                 // Need to scale model to find lines
233                                 ThreeDModel model = new ThreeDModel(_track);
234                                 model.scale();
235                                 double[] latLines = model.getLatitudeLines();
236                                 double[] lonLines = model.getLongitudeLines();
237                                 LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
238                                 dialog.showDialog();
239                         }
240                 });
241                 JPanel flowPanel = new JPanel();
242                 flowPanel.setLayout(new FlowLayout());
243                 flowPanel.add(showLinesButton);
244                 holderPanel.add(flowPanel, BorderLayout.EAST);
245                 panel.add(holderPanel, BorderLayout.CENTER);
246                 return panel;
247         }
248
249
250         /**
251          * Select the file and export data to it
252          */
253         private void doExport()
254         {
255                 // Copy camera coordinates
256                 _cameraX = checkCoordinate(_cameraXField.getText());
257                 _cameraY = checkCoordinate(_cameraYField.getText());
258                 _cameraZ = checkCoordinate(_cameraZField.getText());
259
260                 // OK pressed, so choose output file
261                 if (_fileChooser == null)
262                 {
263                         _fileChooser = new JFileChooser();
264                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
265                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.pov", new String[] {"pov"}));
266                         _fileChooser.setAcceptAllFileFilterUsed(false);
267                         // start from directory in config which should be set
268                         final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
269                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
270                 }
271
272                 // Allow choose again if an existing file is selected
273                 boolean chooseAgain = false;
274                 do
275                 {
276                         chooseAgain = false;
277                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
278                         {
279                                 // OK pressed and file chosen
280                                 File file = _fileChooser.getSelectedFile();
281                                 if (!file.getName().toLowerCase().endsWith(".pov"))
282                                 {
283                                         file = new File(file.getAbsolutePath() + ".pov");
284                                 }
285                                 // Check if file exists and if necessary prompt for overwrite
286                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
287                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
288                                                 I18nManager.getText("dialog.save.overwrite.text"),
289                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
290                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
291                                         == JOptionPane.YES_OPTION)
292                                 {
293                                         // Export the file
294                                         if (exportFile(file))
295                                         {
296                                                 // file saved
297                                                 // Store directory in config for later
298                                                 Config.setConfigString(Config.KEY_TRACK_DIR, file.getParentFile().getAbsolutePath());
299                                         }
300                                         else
301                                         {
302                                                 // export failed so need to choose again
303                                                 chooseAgain = true;
304                                         }
305                                 }
306                                 else
307                                 {
308                                         // overwrite cancelled so need to choose again
309                                         chooseAgain = true;
310                                 }
311                         }
312                 } while (chooseAgain);
313         }
314
315
316         /**
317          * Export the track data to the specified file
318          * @param inFile File object to save to
319          * @return true if successful
320          */
321         private boolean exportFile(File inFile)
322         {
323                 FileWriter writer = null;
324                 // find out the line separator for this system
325                 String lineSeparator = System.getProperty("line.separator");
326                 try
327                 {
328                         // create and scale model
329                         ThreeDModel model = new ThreeDModel(_track);
330                         try
331                         {
332                                 // try to use given altitude cap
333                                 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
334                                 model.setAltitudeCap(_altitudeCap);
335                         }
336                         catch (NumberFormatException nfe) {}
337                         model.scale();
338
339                         // Create file and write basics
340                         writer = new FileWriter(inFile);
341                         writeStartOfFile(writer, model.getModelSize(), lineSeparator);
342
343                         // write out lat/long lines using model
344                         writeLatLongLines(writer, model, lineSeparator);
345
346                         // write out points
347                         if (_ballsAndSticksButton.isSelected()) {
348                                 writeDataPointsBallsAndSticks(writer, model, lineSeparator);
349                         }
350                         else {
351                                 writeDataPointsTubesAndWalls(writer, model, lineSeparator);
352                         }
353
354                         // everything worked
355                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
356                                  + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
357                                  + " " + inFile.getAbsolutePath());
358                         return true;
359                 }
360                 catch (IOException ioe)
361                 {
362                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
363                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
364                 }
365                 finally
366                 {
367                         // close file ignoring exceptions
368                         try
369                         {
370                                 writer.close();
371                         }
372                         catch (Exception e) {}
373                 }
374                 return false;
375         }
376
377
378         /**
379          * Write the start of the Pov file, including base plane and lights
380          * @param inWriter Writer to use for writing file
381          * @param inModelSize model size
382          * @param inLineSeparator line separator to use
383          * @throws IOException on file writing error
384          */
385         private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator)
386         throws IOException
387         {
388                 inWriter.write("// Pov file produced by Prune - see http://activityworkshop.net/");
389                 inWriter.write(inLineSeparator);
390                 inWriter.write(inLineSeparator);
391                 // Select font based on user input
392                 String fontPath = _fontName.getText();
393                 if (fontPath == null || fontPath.equals(""))
394                 {
395                         fontPath = DEFAULT_FONT_FILE;
396                 }
397                 else {
398                         Config.setConfigString(Config.KEY_POVRAY_FONT, fontPath);
399                 }
400                 // Set up output
401                 String[] outputLines = {
402                   "global_settings { ambient_light rgb <4, 4, 4> }", "",
403                   "// Background and camera",
404                   "background { color rgb <0, 0, 0> }",
405                   // camera position
406                   "camera {",
407                   "  location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
408                   "  look_at  <0, 0, 0>",
409                   "}", "",
410                 // global declares
411                   "// Global declares",
412                   "#declare lat_line =",
413                   "  cylinder {",
414                   "   <-" + inModelSize + ", 0.1, 0>,",
415                   "   <" + inModelSize + ", 0.1, 0>,",
416                   "   0.1            // Radius",
417                   "   pigment { color rgb <0.5 0.5 0.5> }",
418                   "  }",
419                   "#declare lon_line =",
420                   "  cylinder {",
421                   "   <0, 0.1, -" + inModelSize + ">,",
422                   "   <0, 0.1, " + inModelSize + ">,",
423                   "   0.1            // Radius",
424                   "   pigment { color rgb <0.5 0.5 0.5> }",
425                   "  }",
426                   "#declare point_rod =",
427                   "  cylinder {",
428                   "   <0, 0, 0>,",
429                   "   <0, 1, 0>,",
430                   "   0.15",
431                   "   open",
432                   "   pigment { color rgb <0.5 0.5 0.5> }",
433                   "  }", "",
434                   // MAYBE: Export rods to POV?  How to store in data?
435                   "#declare waypoint_sphere =",
436                   "  sphere {",
437                   "   <0, 0, 0>, 0.4",
438                   "    texture {",
439                   "       pigment {color rgb <0.1 0.1 1.0>}",
440                   "       finish { phong 1 }",
441                   "    }",
442                   "  }",
443                   "#declare track_sphere0 =",
444                   "  sphere {",
445                   "   <0, 0, 0>, 0.3", // size should depend on model size
446                   "   texture {",
447                   "      pigment {color rgb <0.2 1.0 0.2>}",
448                   "      finish { phong 1 }",
449                   "   }",
450                   " }",
451                   "#declare track_sphere1 =",
452                   "  sphere {",
453                   "   <0, 0, 0>, 0.3", // size should depend on model size
454                   "   texture {",
455                   "      pigment {color rgb <0.6 1.0 0.2>}",
456                   "      finish { phong 1 }",
457                   "   }",
458                   " }",
459                   "#declare track_sphere2 =",
460                   "  sphere {",
461                   "   <0, 0, 0>, 0.3", // size should depend on model size
462                   "   texture {",
463                   "      pigment {color rgb <1.0 1.0 0.1>}",
464                   "      finish { phong 1 }",
465                   "   }",
466                   " }",
467                   "#declare track_sphere3 =",
468                   "  sphere {",
469                   "   <0, 0, 0>, 0.3", // size should depend on model size
470                   "   texture {",
471                   "      pigment {color rgb <1.0 1.0 1.0>}",
472                   "      finish { phong 1 }",
473                   "   }",
474                   " }",
475                   "#declare track_sphere4 =",
476                   "  sphere {",
477                   "   <0, 0, 0>, 0.3", // size should depend on model size
478                   "   texture {",
479                   "      pigment {color rgb <0.1 1.0 1.0>}",
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 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 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 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 = 4;
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 }