1 package tim.prune.save;
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;
10 import java.io.FileWriter;
11 import java.io.IOException;
12 import java.util.ArrayList;
13 import java.util.Iterator;
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;
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;
52 * Class to export a 3d scene of the track to a specified Pov file
54 public class PovExporter extends Export3dFunction
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;
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";
76 * @param inApp App object
78 public PovExporter(App inApp)
81 _track = inApp.getTrackInfo().getTrack();
82 // Set default camera coordinates
83 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
86 /** Get the name key */
87 public String getNameKey() {
88 return "function.exportpov";
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
97 public void setCameraCoordinates(double inX, double inY, double inZ)
99 // calculate distance from origin
100 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
101 if (cameraDist > 0.0)
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);
112 * Show the dialog to select options and export file
116 // Make dialog window to select inputs
119 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
120 _dialog.setLocationRelativeTo(_parentFrame);
121 _dialog.getContentPane().add(makeDialogComponents());
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;
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);
138 if (_imageDef != null) {
139 _baseImagePanel.initImageParameters(_imageDef);
141 _baseImagePanel.updateBaseImageDetails();
144 _dialog.setVisible(true);
149 * Make the dialog components to select the export options
150 * @return Component holding gui elements
152 private Component makeDialogComponents()
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)
166 // Need to launch export in new thread
167 new Thread(new Runnable() {
171 _baseImagePanel.getGrouter().clearMapImage();
177 buttonPanel.add(okButton);
178 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
179 cancelButton.addActionListener(new ActionListener() {
180 public void actionPerformed(ActionEvent e)
182 _baseImagePanel.getGrouter().clearMapImage();
186 buttonPanel.add(cancelButton);
187 panel.add(buttonPanel, BorderLayout.SOUTH);
190 JPanel centralPanel = new JPanel();
191 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
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;
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);
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);
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();
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);
264 panel.add(holderPanel, BorderLayout.CENTER);
270 * Select the file and export data to it
272 private void doExport()
274 // Copy camera coordinates
275 _cameraX = checkCoordinate(_cameraXField.getText());
276 _cameraY = checkCoordinate(_cameraYField.getText());
277 _cameraZ = checkCoordinate(_cameraZField.getText());
279 // OK pressed, so choose output file
280 if (_fileChooser == null)
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));}
291 // Allow choose again if an existing file is selected
292 boolean chooseAgain = false;
296 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
298 // OK pressed and file chosen
299 File povFile = _fileChooser.getSelectedFile();
300 if (!povFile.getName().toLowerCase().endsWith(".pov"))
302 povFile = new File(povFile.getAbsolutePath() + ".pov");
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();
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)
319 // Export the file(s)
320 if (exportFiles(povFile, imageFile, terrainFile))
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());
332 // export failed so need to choose again
338 // overwrite cancelled so need to choose again
342 } while (chooseAgain);
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
353 private boolean exportFiles(File inPovFile, File inImageFile, File inTerrainFile)
355 FileWriter writer = null;
356 // find out the line separator for this system
357 final String lineSeparator = System.getProperty("line.separator");
360 // create and scale model
361 ThreeDModel model = new ThreeDModel(_track);
362 model.setModelSize(MODEL_SCALE_FACTOR);
365 // try to use given altitude cap
366 double givenFactor = Double.parseDouble(_altitudeFactorField.getText());
367 if (givenFactor > 0.0) _altFactor = givenFactor;
369 catch (NumberFormatException nfe) { // parse failed, reset
370 _altitudeFactorField.setText("" + _altFactor);
372 model.setAltitudeFactor(_altFactor);
374 // Write base image if necessary
375 ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
376 boolean useImage = imageDef.getUseImage();
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());
385 useImage = ImageIO.write(baseImage.getImage(), "png", inImageFile);
387 catch (IOException ioe) {
388 System.err.println("Can't write image: " + ioe.getClass().getName());
392 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
396 boolean useTerrain = _terrainPanel.getUseTerrain();
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)
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())
413 Thread.sleep(750); // just polling in a wait loop isn't ideal but simple
415 catch (InterruptedException e) {}
418 terrainHelper.fixVoids(terrainTrack);
420 // Store this back in the cache, maybe we'll need it again
421 TerrainCache.storeTerrainTrack(terrainTrack, _app.getCurrentDataStatus(), terrainDef);
424 model.setTerrain(terrainTrack);
427 // Call TerrainHelper to write out the data from the model
428 terrainHelper.writeHeightMap(model, inTerrainFile);
432 // No terrain required, so just scale the model as it is
436 // Create file and write basics
437 writer = new FileWriter(inPovFile);
438 writeStartOfFile(writer, lineSeparator, useImage ? inImageFile : null, useTerrain ? inTerrainFile : null);
441 if (_ballsAndSticksButton.isSelected()) {
442 writeDataPointsBallsAndSticks(writer, model, lineSeparator);
445 writeDataPointsTubesAndWalls(writer, model, lineSeparator);
449 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
450 + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
451 + " " + inPovFile.getAbsolutePath());
454 catch (IOException ioe)
456 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
457 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
461 // close file ignoring exceptions
466 catch (Exception e) {}
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
480 private void writeStartOfFile(FileWriter inWriter, String inLineSeparator, File inImageFile, File inTerrainFile)
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(""))
492 fontPath = DEFAULT_FONT_FILE;
495 Config.setConfigString(Config.KEY_POVRAY_FONT, fontPath);
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
511 // Definition of terrain shape if any
512 final String terrainDefinition = makeTerrainString(inTerrainFile, inImageFile, inLineSeparator);
514 final String[] pointLights = {
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>}"
520 final String[] northwestLight = {
522 "light_source { <-10, 10, 10> color rgb <1.5 1.5 1.5> parallel }",
524 final String[] lightsLines = (inTerrainFile == null ? pointLights : northwestLight);
527 String[] outputLines = {
528 "global_settings { ambient_light rgb <4, 4, 4> }", "",
529 "// Background and camera",
530 "background { color rgb <0, 0, 0> }",
533 " location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
534 " look_at <0, 0, 0>",
537 "// Global declares",
538 "#declare point_rod =",
545 " pigment { color rgb <0.5 0.5 0.5> }",
546 useImage ? " } no_shadow" : " }",
548 // MAYBE: Export rods to POV? How to store in data?
549 "#declare waypoint_sphere =",
553 " pigment {color rgb <0.1 0.1 1.0>}",
554 " finish { phong 1 }",
555 useImage ? " } no_shadow" : " }",
557 "#declare track_sphere0 =",
559 " <0, 0, 0>, 0.3", // size should depend on model size
561 " pigment {color rgb <0.1 0.6 0.1>}", // dark green
562 " finish { phong 1 }",
565 "#declare track_sphere1 =",
567 " <0, 0, 0>, 0.3", // size should depend on model size
569 " pigment {color rgb <0.4 0.9 0.2>}", // green
570 " finish { phong 1 }",
573 "#declare track_sphere2 =",
575 " <0, 0, 0>, 0.3", // size should depend on model size
577 " pigment {color rgb <0.7 0.8 0.2>}", // yellow
578 " finish { phong 1 }",
581 "#declare track_sphere3 =",
583 " <0, 0, 0>, 0.3", // size should depend on model size
585 " pigment {color rgb <0.5 0.8 0.6>}", // greeny
586 " finish { phong 1 }",
589 "#declare track_sphere4 =",
591 " <0, 0, 0>, 0.3", // size should depend on model size
593 " pigment {color rgb <0.2 0.9 0.9>}", // cyan
594 " finish { phong 1 }",
597 "#declare track_sphere5 =",
599 " <0, 0, 0>, 0.3", // size should depend on model size
601 " pigment {color rgb <1.0 1.0 1.0>}", // white
602 " finish { phong 1 }",
605 "#declare track_sphere_t =",
607 " <0, 0, 0>, 0.25", // size should depend on model size
609 " pigment {color rgb <0.6 1.0 0.2>}",
610 " finish { phong 1 }",
613 "#declare wall_colour = rgbt <0.5, 0.5, 0.5, 0.3>;", "",
621 "// Cardinal letters N,S,E,W",
623 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
624 " pigment { color rgb <1 1 1> }",
625 " translate <0, 0.2, 10.0>",
628 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
629 " pigment { color rgb <1 1 1> }",
630 " translate <0, 0.2, -10.0>",
633 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0",
634 " pigment { color rgb <1 1 1> }",
635 " translate <9.7, 0.2, 0>",
638 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0",
639 " pigment { color rgb <1 1 1> }",
640 " translate <-10.3, 0.2, 0>",
644 // write strings to file
645 writeLinesToFile(inWriter, inLineSeparator, outputLines);
646 writeLinesToFile(inWriter, inLineSeparator, lightsLines);
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
656 private void writeLinesToFile(FileWriter inWriter, String inLineSeparator, String[] lines)
659 for (int i=0; i<lines.length; i++)
661 inWriter.write(lines[i]);
662 inWriter.write(inLineSeparator);
664 inWriter.write(inLineSeparator);
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
674 private static String makeTerrainString(File inTerrainFile, File inImageFile, String inLineSeparator)
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);
686 sb.append("\tpigment {color rgb <0.55 0.7 0.55> }").append(inLineSeparator);
688 sb.append("\tscale 20.0").append(inLineSeparator)
689 .append("\ttranslate <-10.0, 0, -10.0>").append(inLineSeparator).append("}");
690 return sb.toString();
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
700 private static void writeDataPointsBallsAndSticks(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
703 inWriter.write("// Data points:");
704 inWriter.write(inLineSeparator);
705 int numPoints = inModel.getNumPoints();
706 for (int i=0; i<numPoints; i++)
708 // ball (different according to type)
709 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
712 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
713 + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
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) + "> }");
722 inWriter.write(inLineSeparator);
723 // vertical rod (if altitude positive)
724 if (inModel.getScaledAltValue(i) > 0.0)
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);
731 inWriter.write(inLineSeparator);
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
742 private static void writeDataPointsTubesAndWalls(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
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++)
751 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
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)
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> }");
763 inWriter.write(inLineSeparator);
766 inWriter.write(inLineSeparator);
768 // Loop over all the track segments
769 ArrayList<ModelSegment> segmentList = getSegmentList(inModel);
770 Iterator<ModelSegment> segmentIterator = segmentList.iterator();
771 while (segmentIterator.hasNext())
773 ModelSegment segment = segmentIterator.next();
774 int segLength = segment.getNumTrackPoints();
776 // if the track segment is long enough, do a cubic spline sphere sweep
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?
788 writeSphereSweep(inWriter, inModel, segment, inLineSeparator);
791 // Write wall underneath segment
794 writePolygonWall(inWriter, inModel, segment, inLineSeparator);
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
808 private static void writeSphereSweep(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
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++)
821 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
823 inWriter.write(" <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
824 + "," + inModel.getScaledVertValue(i) + ">, 0.25");
825 inWriter.write(inLineSeparator);
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);
835 inWriter.write(inLineSeparator);
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
847 private static void writePolygonWall(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
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
856 for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++)
858 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
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);
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");
877 inWriter.write(inLineSeparator);
887 * @param inCode height code to check
888 * @return validated height code within range 0 to maxHeightCode
890 private static byte checkHeightCode(byte inCode)
892 final byte maxHeightCode = 5;
893 if (inCode < 0) return 0;
894 if (inCode > maxHeightCode) return maxHeightCode;
900 * Check the given coordinate
901 * @param inString String entered by user
902 * @return validated String value
904 private static String checkCoordinate(String inString)
909 value = Double.parseDouble(inString);
911 catch (Exception e) {} // ignore parse failures
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
920 private static ArrayList<ModelSegment> getSegmentList(ThreeDModel inModel)
922 ArrayList<ModelSegment> segmentList = new ArrayList<ModelSegment>();
923 if (inModel != null && inModel.getNumPoints() > 0)
925 ModelSegment currSegment = null;
926 int numTrackPoints = 0;
927 for (int i=0; i<inModel.getNumPoints(); i++)
929 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
931 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_SEGMENT_START || currSegment == null)
934 if (currSegment != null)
936 currSegment.setEndIndex(i-1);
937 currSegment.setNumTrackPoints(numTrackPoints);
938 segmentList.add(currSegment);
941 currSegment = new ModelSegment(i);
946 // Add last segment to list
947 if (currSegment != null && numTrackPoints > 0)
949 currSegment.setEndIndex(inModel.getNumPoints()-1);
950 currSegment.setNumTrackPoints(numTrackPoints);
951 segmentList.add(currSegment);