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