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