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.TerrainHelper;
47 import tim.prune.threedee.ThreeDModel;
50 * Class to export a 3d scene of the track to a specified Pov file
52 public class PovExporter extends Export3dFunction
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;
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";
74 * @param inApp App object
76 public PovExporter(App inApp)
79 _track = inApp.getTrackInfo().getTrack();
80 // Set default camera coordinates
81 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
84 /** Get the name key */
85 public String getNameKey() {
86 return "function.exportpov";
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
95 public void setCameraCoordinates(double inX, double inY, double inZ)
97 // calculate distance from origin
98 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
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);
110 * Show the dialog to select options and export file
114 // Make dialog window to select inputs
117 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
118 _dialog.setLocationRelativeTo(_parentFrame);
119 _dialog.getContentPane().add(makeDialogComponents());
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;
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);
136 if (_imageDef != null) {
137 _baseImagePanel.initImageParameters(_imageDef);
139 _baseImagePanel.updateBaseImageDetails();
142 _dialog.setVisible(true);
147 * Make the dialog components to select the export options
148 * @return Component holding gui elements
150 private Component makeDialogComponents()
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)
164 // Need to launch export in new thread
165 new Thread(new Runnable() {
169 _baseImagePanel.getGrouter().clearMapImage();
175 buttonPanel.add(okButton);
176 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
177 cancelButton.addActionListener(new ActionListener() {
178 public void actionPerformed(ActionEvent e)
180 _baseImagePanel.getGrouter().clearMapImage();
184 buttonPanel.add(cancelButton);
185 panel.add(buttonPanel, BorderLayout.SOUTH);
188 JPanel centralPanel = new JPanel();
189 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
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;
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);
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);
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();
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);
262 panel.add(holderPanel, BorderLayout.CENTER);
268 * Select the file and export data to it
270 private void doExport()
272 // Copy camera coordinates
273 _cameraX = checkCoordinate(_cameraXField.getText());
274 _cameraY = checkCoordinate(_cameraYField.getText());
275 _cameraZ = checkCoordinate(_cameraZField.getText());
277 // OK pressed, so choose output file
278 if (_fileChooser == null)
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));}
289 // Allow choose again if an existing file is selected
290 boolean chooseAgain = false;
294 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
296 // OK pressed and file chosen
297 File povFile = _fileChooser.getSelectedFile();
298 if (!povFile.getName().toLowerCase().endsWith(".pov"))
300 povFile = new File(povFile.getAbsolutePath() + ".pov");
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();
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)
317 // Export the file(s)
318 if (exportFiles(povFile, imageFile, terrainFile))
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));
327 // export failed so need to choose again
333 // overwrite cancelled so need to choose again
337 } while (chooseAgain);
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
348 private boolean exportFiles(File inPovFile, File inImageFile, File inTerrainFile)
350 FileWriter writer = null;
351 // find out the line separator for this system
352 final String lineSeparator = System.getProperty("line.separator");
355 // create and scale model
356 ThreeDModel model = new ThreeDModel(_track);
357 model.setModelSize(MODEL_SCALE_FACTOR);
360 // try to use given altitude cap
361 double givenFactor = Double.parseDouble(_altitudeFactorField.getText());
362 if (givenFactor > 0.0) _altFactor = givenFactor;
364 catch (NumberFormatException nfe) { // parse failed, reset
365 _altitudeFactorField.setText("" + _altFactor);
367 model.setAltitudeFactor(_altFactor);
369 // Write base image if necessary
370 ImageDefinition imageDef = _baseImagePanel.getImageDefinition();
371 boolean useImage = imageDef.getUseImage();
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());
380 useImage = ImageIO.write(baseImage.getImage(), "png", inImageFile);
382 catch (IOException ioe) {
383 System.err.println("Can't write image: " + ioe.getClass().getName());
387 _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage");
391 boolean useTerrain = _terrainPanel.getUseTerrain();
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())
402 Thread.sleep(750); // just polling in a wait loop isn't ideal but simple
404 catch (InterruptedException e) {}
407 terrainHelper.fixVoids(terrainTrack);
409 model.setTerrain(terrainTrack);
412 // Call TerrainHelper to write out the data from the model
413 terrainHelper.writeHeightMap(model, inTerrainFile);
417 // No terrain required, so just scale the model as it is
421 // Create file and write basics
422 writer = new FileWriter(inPovFile);
423 writeStartOfFile(writer, lineSeparator, useImage ? inImageFile : null, useTerrain ? inTerrainFile : null);
426 if (_ballsAndSticksButton.isSelected()) {
427 writeDataPointsBallsAndSticks(writer, model, lineSeparator);
430 writeDataPointsTubesAndWalls(writer, model, lineSeparator);
434 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
435 + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
436 + " " + inPovFile.getAbsolutePath());
439 catch (IOException ioe)
441 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
442 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
446 // close file ignoring exceptions
451 catch (Exception e) {}
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
465 private void writeStartOfFile(FileWriter inWriter, String inLineSeparator, File inImageFile, File inTerrainFile)
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(""))
477 fontPath = DEFAULT_FONT_FILE;
480 Config.setConfigString(Config.KEY_POVRAY_FONT, fontPath);
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
496 // Definition of terrain shape if any
497 final String terrainDefinition = makeTerrainString(inTerrainFile, inImageFile, inLineSeparator);
500 String[] outputLines = {
501 "global_settings { ambient_light rgb <4, 4, 4> }", "",
502 "// Background and camera",
503 "background { color rgb <0, 0, 0> }",
506 " location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
507 " look_at <0, 0, 0>",
510 "// Global declares",
511 "#declare point_rod =",
518 " pigment { color rgb <0.5 0.5 0.5> }",
519 useImage ? " } no_shadow" : " }",
521 // MAYBE: Export rods to POV? How to store in data?
522 "#declare waypoint_sphere =",
526 " pigment {color rgb <0.1 0.1 1.0>}",
527 " finish { phong 1 }",
528 useImage ? " } no_shadow" : " }",
530 "#declare track_sphere0 =",
532 " <0, 0, 0>, 0.3", // size should depend on model size
534 " pigment {color rgb <0.1 0.6 0.1>}", // dark green
535 " finish { phong 1 }",
538 "#declare track_sphere1 =",
540 " <0, 0, 0>, 0.3", // size should depend on model size
542 " pigment {color rgb <0.4 0.9 0.2>}", // green
543 " finish { phong 1 }",
546 "#declare track_sphere2 =",
548 " <0, 0, 0>, 0.3", // size should depend on model size
550 " pigment {color rgb <0.7 0.8 0.2>}", // yellow
551 " finish { phong 1 }",
554 "#declare track_sphere3 =",
556 " <0, 0, 0>, 0.3", // size should depend on model size
558 " pigment {color rgb <0.5 0.8 0.6>}", // greeny
559 " finish { phong 1 }",
562 "#declare track_sphere4 =",
564 " <0, 0, 0>, 0.3", // size should depend on model size
566 " pigment {color rgb <0.2 0.9 0.9>}", // cyan
567 " finish { phong 1 }",
570 "#declare track_sphere5 =",
572 " <0, 0, 0>, 0.3", // size should depend on model size
574 " pigment {color rgb <1.0 1.0 1.0>}", // white
575 " finish { phong 1 }",
578 "#declare track_sphere_t =",
580 " <0, 0, 0>, 0.25", // size should depend on model size
582 " pigment {color rgb <0.6 1.0 0.2>}",
583 " finish { phong 1 }",
586 "#declare wall_colour = rgbt <0.5, 0.5, 0.5, 0.3>;", "",
594 "// Cardinal letters N,S,E,W",
596 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
597 " pigment { color rgb <1 1 1> }",
598 " translate <0, 0.2, 10.0>",
601 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
602 " pigment { color rgb <1 1 1> }",
603 " translate <0, 0.2, -10.0>",
606 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0",
607 " pigment { color rgb <1 1 1> }",
608 " translate <9.7, 0.2, 0>",
611 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0",
612 " pigment { color rgb <1 1 1> }",
613 " translate <-10.3, 0.2, 0>",
615 // MAYBE: Light positions should relate to model size
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>}",
622 // write strings to file
623 int numLines = outputLines.length;
624 for (int i=0; i<numLines; i++)
626 inWriter.write(outputLines[i]);
627 inWriter.write(inLineSeparator);
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
638 private static String makeTerrainString(File inTerrainFile, File inImageFile, String inLineSeparator)
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);
650 sb.append("\tpigment {color rgb <0.55 0.7 0.55> }").append(inLineSeparator);
652 sb.append("\tscale 20.0").append(inLineSeparator)
653 .append("\ttranslate <-10.0, 0, -10.0>").append(inLineSeparator).append("}");
654 return sb.toString();
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
664 private static void writeDataPointsBallsAndSticks(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
667 inWriter.write("// Data points:");
668 inWriter.write(inLineSeparator);
669 int numPoints = inModel.getNumPoints();
670 for (int i=0; i<numPoints; i++)
672 // ball (different according to type)
673 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
676 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
677 + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
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) + "> }");
686 inWriter.write(inLineSeparator);
687 // vertical rod (if altitude positive)
688 if (inModel.getScaledAltValue(i) > 0.0)
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);
695 inWriter.write(inLineSeparator);
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
706 private static void writeDataPointsTubesAndWalls(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
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++)
715 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
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)
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> }");
727 inWriter.write(inLineSeparator);
730 inWriter.write(inLineSeparator);
732 // Loop over all the track segments
733 ArrayList<ModelSegment> segmentList = getSegmentList(inModel);
734 Iterator<ModelSegment> segmentIterator = segmentList.iterator();
735 while (segmentIterator.hasNext())
737 ModelSegment segment = segmentIterator.next();
738 int segLength = segment.getNumTrackPoints();
740 // if the track segment is long enough, do a cubic spline sphere sweep
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?
752 writeSphereSweep(inWriter, inModel, segment, inLineSeparator);
755 // Write wall underneath segment
758 writePolygonWall(inWriter, inModel, segment, inLineSeparator);
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
772 private static void writeSphereSweep(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
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++)
785 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
787 inWriter.write(" <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
788 + "," + inModel.getScaledVertValue(i) + ">, 0.25");
789 inWriter.write(inLineSeparator);
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);
799 inWriter.write(inLineSeparator);
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
811 private static void writePolygonWall(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator)
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
820 for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++)
822 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
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);
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");
841 inWriter.write(inLineSeparator);
851 * @param inCode height code to check
852 * @return validated height code within range 0 to max
854 private static byte checkHeightCode(byte inCode)
856 final byte maxHeightCode = 5;
857 if (inCode < 0) return 0;
858 if (inCode > maxHeightCode) return maxHeightCode;
864 * Check the given coordinate
865 * @param inString String entered by user
866 * @return validated String value
868 private static String checkCoordinate(String inString)
873 value = Double.parseDouble(inString);
875 catch (Exception e) {} // ignore parse failures
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
884 private static ArrayList<ModelSegment> getSegmentList(ThreeDModel inModel)
886 ArrayList<ModelSegment> segmentList = new ArrayList<ModelSegment>();
887 if (inModel != null && inModel.getNumPoints() > 0)
889 ModelSegment currSegment = null;
890 int numTrackPoints = 0;
891 for (int i=0; i<inModel.getNumPoints(); i++)
893 if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT)
895 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_SEGMENT_START || currSegment == null)
898 if (currSegment != null)
900 currSegment.setEndIndex(i-1);
901 currSegment.setNumTrackPoints(numTrackPoints);
902 segmentList.add(currSegment);
905 currSegment = new ModelSegment(i);
910 // Add last segment to list
911 if (currSegment != null && numTrackPoints > 0)
913 currSegment.setEndIndex(inModel.getNumPoints()-1);
914 currSegment.setNumTrackPoints(numTrackPoints);
915 segmentList.add(currSegment);