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