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;
13 import javax.swing.JButton;
14 import javax.swing.JDialog;
15 import javax.swing.JFileChooser;
16 import javax.swing.JFrame;
17 import javax.swing.JLabel;
18 import javax.swing.JOptionPane;
19 import javax.swing.JPanel;
20 import javax.swing.JTextField;
21 import javax.swing.SwingConstants;
22 import javax.swing.filechooser.FileFilter;
25 import tim.prune.I18nManager;
26 import tim.prune.data.Track;
27 import tim.prune.threedee.ThreeDModel;
30 * Class to export track information
31 * into a specified Pov file
33 public class PovExporter
35 private App _app = null;
36 private JFrame _parentFrame = null;
37 private Track _track = null;
38 private JDialog _dialog = null;
39 private JFileChooser _fileChooser = null;
40 private String _cameraX = null, _cameraY = null, _cameraZ = null;
41 private JTextField _cameraXField = null, _cameraYField = null, _cameraZField = null;
42 private JTextField _fontName = null, _altitudeCapField = null;
43 private int _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
46 private static final double DEFAULT_CAMERA_DISTANCE = 30.0;
47 private static final String DEFAULT_FONT_FILE = "crystal.ttf";
48 // alternative font: DejaVuSans-Bold.ttf
52 * Constructor giving App object, frame and track
53 * @param inApp application object to inform of success
54 * @param inParentFrame parent frame
55 * @param inTrack track object to save
57 public PovExporter(App inApp, JFrame inParentFrame, Track inTrack)
60 _parentFrame = inParentFrame;
62 // Set default camera coordinates
63 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
68 * Set the coordinates for the camera (can be any scale)
69 * @param inX X coordinate of camera
70 * @param inY Y coordinate of camera
71 * @param inZ Z coordinate of camera
73 public void setCameraCoordinates(double inX, double inY, double inZ)
75 // calculate distance from origin
76 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
79 _cameraX = "" + (inX / cameraDist * DEFAULT_CAMERA_DISTANCE);
80 _cameraY = "" + (inY / cameraDist * DEFAULT_CAMERA_DISTANCE);
81 // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system!
82 _cameraZ = "" + (-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE);
88 * @param inAltitudeCap altitude cap to use
90 public void setAltitudeCap(int inAltitudeCap)
92 _altitudeCap = inAltitudeCap;
93 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
95 _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
101 * Show the dialog to select options and export file
103 public void showDialog()
105 // Make dialog window to select angles, colours etc
108 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true);
109 _dialog.setLocationRelativeTo(_parentFrame);
110 _dialog.getContentPane().add(makeDialogComponents());
114 _cameraXField.setText(_cameraX);
115 _cameraYField.setText(_cameraY);
116 _cameraZField.setText(_cameraZ);
117 // Set vertical scale
118 _altitudeCapField.setText("" + _altitudeCap);
126 * Make the dialog components to select the export options
127 * @return Component holding gui elements
129 private Component makeDialogComponents()
131 JPanel panel = new JPanel();
132 panel.setLayout(new BorderLayout());
133 panel.add(new JLabel(I18nManager.getText("dialog.exportpov.text")), BorderLayout.NORTH);
134 // OK, Cancel buttons
135 JPanel buttonPanel = new JPanel();
136 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
137 JButton okButton = new JButton(I18nManager.getText("button.ok"));
138 okButton.addActionListener(new ActionListener() {
139 public void actionPerformed(ActionEvent e)
145 buttonPanel.add(okButton);
146 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
147 cancelButton.addActionListener(new ActionListener() {
148 public void actionPerformed(ActionEvent e)
153 buttonPanel.add(cancelButton);
154 panel.add(buttonPanel, BorderLayout.SOUTH);
157 JPanel centralPanel = new JPanel();
158 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
160 JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font"));
161 fontLabel.setHorizontalAlignment(SwingConstants.TRAILING);
162 centralPanel.add(fontLabel);
163 _fontName = new JTextField(DEFAULT_FONT_FILE, 12);
164 _fontName.setAlignmentX(Component.LEFT_ALIGNMENT);
165 centralPanel.add(_fontName);
166 //coordinates of camera
167 JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax"));
168 cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING);
169 centralPanel.add(cameraXLabel);
170 _cameraXField = new JTextField("" + _cameraX);
171 centralPanel.add(_cameraXField);
172 JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray"));
173 cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING);
174 centralPanel.add(cameraYLabel);
175 _cameraYField = new JTextField("" + _cameraY);
176 centralPanel.add(_cameraYField);
177 JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz"));
178 cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING);
179 centralPanel.add(cameraZLabel);
180 _cameraZField = new JTextField("" + _cameraZ);
181 centralPanel.add(_cameraZField);
183 JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudecap"));
184 altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING);
185 centralPanel.add(altitudeCapLabel);
186 _altitudeCapField = new JTextField("" + _altitudeCap);
187 centralPanel.add(_altitudeCapField);
189 JPanel flowPanel = new JPanel();
190 flowPanel.add(centralPanel);
191 panel.add(flowPanel, BorderLayout.CENTER);
197 * Select the file and export data to it
199 private void doExport()
201 // Copy camera coordinates
202 _cameraX = checkCoordinate(_cameraXField.getText());
203 _cameraY = checkCoordinate(_cameraYField.getText());
204 _cameraZ = checkCoordinate(_cameraZField.getText());
206 // OK pressed, so choose output file
207 boolean fileSaved = false;
208 if (_fileChooser == null)
209 _fileChooser = new JFileChooser();
210 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
211 _fileChooser.setFileFilter(new FileFilter() {
212 public boolean accept(File f)
214 return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov")));
216 public String getDescription()
218 return I18nManager.getText("dialog.exportpov.filetype");
221 _fileChooser.setAcceptAllFileFilterUsed(false);
223 // Allow choose again if an existing file is selected
224 boolean chooseAgain = false;
228 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
230 // OK pressed and file chosen
231 File file = _fileChooser.getSelectedFile();
232 if (!file.getName().toLowerCase().endsWith(".pov"))
234 file = new File(file.getAbsolutePath() + ".pov");
236 // Check if file exists and if necessary prompt for overwrite
237 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
238 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
239 I18nManager.getText("dialog.save.overwrite.text"),
240 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
241 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
242 == JOptionPane.YES_OPTION)
245 if (exportFile(file))
251 // export failed so need to choose again
257 // overwrite cancelled so need to choose again
261 } while (chooseAgain);
266 * Export the track data to the specified file
267 * @param inFile File object to save to
268 * @return true if successful
270 private boolean exportFile(File inFile)
272 FileWriter writer = null;
273 // find out the line separator for this system
274 String lineSeparator = System.getProperty("line.separator");
277 // create and scale model
278 ThreeDModel model = new ThreeDModel(_track);
281 // try to use given altitude cap
282 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
283 model.setAltitudeCap(_altitudeCap);
285 catch (NumberFormatException nfe) {}
288 // Create file and write basics
289 writer = new FileWriter(inFile);
290 writeStartOfFile(writer, model.getModelSize(), lineSeparator);
292 // write out lat/long lines using model
293 writeLatLongLines(writer, model, lineSeparator);
296 writeDataPoints(writer, model, lineSeparator);
299 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
300 + " " + _track.getNumPoints() + " " + I18nManager.getText("dialog.save.ok2")
301 + " " + inFile.getAbsolutePath(),
302 I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
305 catch (IOException ioe)
307 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(),
308 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
312 // close file ignoring exceptions
317 catch (Exception e) {}
324 * Write the start of the Pov file, including base plane and lights
325 * @param inWriter Writer to use for writing file
326 * @param inModelSize model size
327 * @param inLineSeparator line separator to use
328 * @throws IOException on file writing error
330 private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator)
333 inWriter.write("// Pov file produced by Prune - see http://activityworkshop.net/");
334 inWriter.write(inLineSeparator);
335 inWriter.write(inLineSeparator);
336 // Select font based on user input
337 String fontPath = _fontName.getText();
338 if (fontPath == null || fontPath.equals(""))
340 fontPath = DEFAULT_FONT_FILE;
343 String[] outputLines = {
344 "global_settings { ambient_light rgb <4, 4, 4> }", "",
345 "// Background and camera",
346 "background { color rgb <0, 0, 0> }",
349 " location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
350 " look_at <0, 0, 0>",
353 "// Global declares",
354 "#declare lat_line =",
356 " <-" + inModelSize + ", 0.1, 0>,",
357 " <" + inModelSize + ", 0.1, 0>,",
359 " pigment { color rgb <0.5 0.5 0.5> }",
361 "#declare lon_line =",
363 " <0, 0.1, -" + inModelSize + ">,",
364 " <0, 0.1, " + inModelSize + ">,",
366 " pigment { color rgb <0.5 0.5 0.5> }",
368 "#declare point_rod =",
374 " pigment { color rgb <0.5 0.5 0.5> }",
376 // TODO: Export rods to POV? How to store in data?
377 "#declare waypoint_sphere =",
381 " pigment {color rgb <0.1 0.1 1.0>}",
382 " finish { phong 1 }",
385 "#declare track_sphere0 =",
387 " <0, 0, 0>, 0.3", // size should depend on model size
389 " pigment {color rgb <0.2 1.0 0.2>}",
390 " finish { phong 1 }",
393 "#declare track_sphere1 =",
395 " <0, 0, 0>, 0.3", // size should depend on model size
397 " pigment {color rgb <0.6 1.0 0.2>}",
398 " finish { phong 1 }",
401 "#declare track_sphere2 =",
403 " <0, 0, 0>, 0.3", // size should depend on model size
405 " pigment {color rgb <1.0 1.0 0.1>}",
406 " finish { phong 1 }",
409 "#declare track_sphere3 =",
411 " <0, 0, 0>, 0.3", // size should depend on model size
413 " pigment {color rgb <1.0 1.0 1.0>}",
414 " finish { phong 1 }",
417 "#declare track_sphere4 =",
419 " <0, 0, 0>, 0.3", // size should depend on model size
421 " pigment {color rgb <0.1 1.0 1.0>}",
422 " finish { phong 1 }",
427 " <-" + inModelSize + ", -0.15, -" + inModelSize + ">, // Near lower left corner",
428 " <" + inModelSize + ", 0.15, " + inModelSize + "> // Far upper right corner",
429 " pigment { color rgb <0.5 0.75 0.8> }",
432 "// Cardinal letters N,S,E,W",
434 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
435 " pigment { color rgb <1 1 1> }",
436 " translate <0, 0.2, " + inModelSize + ">",
439 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
440 " pigment { color rgb <1 1 1> }",
441 " translate <0, 0.2, -" + inModelSize + ">",
444 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0",
445 " pigment { color rgb <1 1 1> }",
446 " translate <" + (inModelSize * 0.97) + ", 0.2, 0>",
449 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0",
450 " pigment { color rgb <1 1 1> }",
451 " translate <-" + (inModelSize * 1.03) + ", 0.2, 0>",
453 // TODO: Light positions should relate to model size
455 "light_source { <-1, 9, -4> color rgb <0.5 0.5 0.5>}",
456 "light_source { <1, 6, -14> color rgb <0.6 0.6 0.6>}",
457 "light_source { <11, 12, 8> color rgb <0.3 0.3 0.3>}",
460 // write strings to file
461 int numLines = outputLines.length;
462 for (int i=0; i<numLines; i++)
464 inWriter.write(outputLines[i]);
465 inWriter.write(inLineSeparator);
471 * Write out all the lat and long lines to the file
472 * @param inWriter Writer to use for writing file
473 * @param inModel model object for getting lat/long lines
474 * @param inLineSeparator line separator to use
475 * @throws IOException on file writing error
477 private void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
480 inWriter.write("// Latitude and longitude lines:");
481 inWriter.write(inLineSeparator);
482 int numlines = inModel.getNumLatitudeLines();
483 for (int i=0; i<numlines; i++)
485 // write cylinder to file
486 inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
487 inWriter.write(inLineSeparator);
489 numlines = inModel.getNumLongitudeLines();
490 for (int i=0; i<numlines; i++)
492 // write cylinder to file
493 inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
494 inWriter.write(inLineSeparator);
496 inWriter.write(inLineSeparator);
501 * Write out all the data points to the file
502 * @param inWriter Writer to use for writing file
503 * @param inModel model object for getting data points
504 * @param inLineSeparator line separator to use
505 * @throws IOException on file writing error
507 private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
510 inWriter.write("// Data points:");
511 inWriter.write(inLineSeparator);
512 int numPoints = inModel.getNumPoints();
513 for (int i=0; i<numPoints; i++)
515 // ball (different according to type)
516 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
519 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
520 + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
524 // normal track point ball
525 inWriter.write("object { track_sphere" + checkHeightCode(inModel.getPointHeightCode(i))
526 + " translate <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
527 + "," + inModel.getScaledVertValue(i) + "> }");
529 inWriter.write(inLineSeparator);
530 // vertical rod (if altitude positive)
531 if (inModel.getScaledAltValue(i) > 0.0)
533 inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0,"
534 + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }");
535 inWriter.write(inLineSeparator);
538 inWriter.write(inLineSeparator);
543 * @param inCode height code to check
544 * @return validated height code within range 0 to max
546 private static byte checkHeightCode(byte inCode)
548 final byte maxHeightCode = 4;
549 if (inCode < 0) return 0;
550 if (inCode > maxHeightCode) return maxHeightCode;
556 * Check the given coordinate
557 * @param inString String entered by user
558 * @return validated String value
560 private static String checkCoordinate(String inString)
565 value = Double.parseDouble(inString);
567 catch (Exception e) {} // ignore parse failures