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;
24 import tim.prune.I18nManager;
25 import tim.prune.data.Track;
26 import tim.prune.threedee.LineDialog;
27 import tim.prune.threedee.ThreeDModel;
30 * Class to export track information
31 * into a specified Pov file
33 public class PovExporter
35 private JFrame _parentFrame = null;
36 private Track _track = null;
37 private JDialog _dialog = null;
38 private JFileChooser _fileChooser = null;
39 private String _cameraX = null, _cameraY = null, _cameraZ = null;
40 private JTextField _cameraXField = null, _cameraYField = null, _cameraZField = null;
41 private JTextField _fontName = null, _altitudeCapField = null;
42 private int _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
45 private static final double DEFAULT_CAMERA_DISTANCE = 30.0;
46 private static final String DEFAULT_FONT_FILE = "crystal.ttf";
47 // alternative font: DejaVuSans-Bold.ttf
51 * Constructor giving frame and track
52 * @param inParentFrame parent frame
53 * @param inTrack track object to save
55 public PovExporter(JFrame inParentFrame, Track inTrack)
57 _parentFrame = inParentFrame;
59 // Set default camera coordinates
60 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
65 * Set the coordinates for the camera (can be any scale)
66 * @param inX X coordinate of camera
67 * @param inY Y coordinate of camera
68 * @param inZ Z coordinate of camera
70 public void setCameraCoordinates(double inX, double inY, double inZ)
72 // calculate distance from origin
73 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
76 _cameraX = "" + (inX / cameraDist * DEFAULT_CAMERA_DISTANCE);
77 _cameraY = "" + (inY / cameraDist * DEFAULT_CAMERA_DISTANCE);
78 // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system!
79 _cameraZ = "" + (-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE);
85 * @param inAltitudeCap altitude cap to use
87 public void setAltitudeCap(int inAltitudeCap)
89 _altitudeCap = inAltitudeCap;
90 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
92 _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
98 * Show the dialog to select options and export file
100 public void showDialog()
102 // Make dialog window to select angles, colours etc
105 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true);
106 _dialog.setLocationRelativeTo(_parentFrame);
107 _dialog.getContentPane().add(makeDialogComponents());
111 _cameraXField.setText(_cameraX);
112 _cameraYField.setText(_cameraY);
113 _cameraZField.setText(_cameraZ);
114 // Set vertical scale
115 _altitudeCapField.setText("" + _altitudeCap);
123 * Make the dialog components to select the export options
124 * @return Component holding gui elements
126 private Component makeDialogComponents()
128 JPanel panel = new JPanel();
129 panel.setLayout(new BorderLayout());
130 panel.add(new JLabel(I18nManager.getText("dialog.exportpov.text")), BorderLayout.NORTH);
131 // OK, Cancel buttons
132 JPanel buttonPanel = new JPanel();
133 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
134 JButton okButton = new JButton(I18nManager.getText("button.ok"));
135 okButton.addActionListener(new ActionListener() {
136 public void actionPerformed(ActionEvent e)
142 buttonPanel.add(okButton);
143 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
144 cancelButton.addActionListener(new ActionListener() {
145 public void actionPerformed(ActionEvent e)
150 buttonPanel.add(cancelButton);
151 panel.add(buttonPanel, BorderLayout.SOUTH);
154 JPanel centralPanel = new JPanel();
155 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
157 JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font"));
158 fontLabel.setHorizontalAlignment(SwingConstants.TRAILING);
159 centralPanel.add(fontLabel);
160 _fontName = new JTextField(DEFAULT_FONT_FILE, 12);
161 _fontName.setAlignmentX(Component.LEFT_ALIGNMENT);
162 centralPanel.add(_fontName);
163 //coordinates of camera
164 JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax"));
165 cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING);
166 centralPanel.add(cameraXLabel);
167 _cameraXField = new JTextField("" + _cameraX);
168 centralPanel.add(_cameraXField);
169 JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray"));
170 cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING);
171 centralPanel.add(cameraYLabel);
172 _cameraYField = new JTextField("" + _cameraY);
173 centralPanel.add(_cameraYField);
174 JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz"));
175 cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING);
176 centralPanel.add(cameraZLabel);
177 _cameraZField = new JTextField("" + _cameraZ);
178 centralPanel.add(_cameraZField);
180 JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudecap"));
181 altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING);
182 centralPanel.add(altitudeCapLabel);
183 _altitudeCapField = new JTextField("" + _altitudeCap);
184 centralPanel.add(_altitudeCapField);
186 JPanel flowPanel = new JPanel();
187 flowPanel.add(centralPanel);
190 JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
191 showLinesButton.addActionListener(new ActionListener() {
192 public void actionPerformed(ActionEvent e)
194 // Need to scale model to find lines
195 ThreeDModel model = new ThreeDModel(_track);
197 double[] latLines = model.getLatitudeLines();
198 double[] lonLines = model.getLongitudeLines();
199 LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
203 flowPanel.add(showLinesButton);
204 panel.add(flowPanel, BorderLayout.CENTER);
210 * Select the file and export data to it
212 private void doExport()
214 // Copy camera coordinates
215 _cameraX = checkCoordinate(_cameraXField.getText());
216 _cameraY = checkCoordinate(_cameraYField.getText());
217 _cameraZ = checkCoordinate(_cameraZField.getText());
219 // OK pressed, so choose output file
220 if (_fileChooser == null)
221 _fileChooser = new JFileChooser();
222 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
223 _fileChooser.setFileFilter(new FileFilter() {
224 public boolean accept(File f)
226 return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov")));
228 public String getDescription()
230 return I18nManager.getText("dialog.exportpov.filetype");
233 _fileChooser.setAcceptAllFileFilterUsed(false);
235 // Allow choose again if an existing file is selected
236 boolean chooseAgain = false;
240 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
242 // OK pressed and file chosen
243 File file = _fileChooser.getSelectedFile();
244 if (!file.getName().toLowerCase().endsWith(".pov"))
246 file = new File(file.getAbsolutePath() + ".pov");
248 // Check if file exists and if necessary prompt for overwrite
249 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
250 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
251 I18nManager.getText("dialog.save.overwrite.text"),
252 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
253 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
254 == JOptionPane.YES_OPTION)
257 if (exportFile(file))
263 // export failed so need to choose again
269 // overwrite cancelled so need to choose again
273 } while (chooseAgain);
278 * Export the track data to the specified file
279 * @param inFile File object to save to
280 * @return true if successful
282 private boolean exportFile(File inFile)
284 FileWriter writer = null;
285 // find out the line separator for this system
286 String lineSeparator = System.getProperty("line.separator");
289 // create and scale model
290 ThreeDModel model = new ThreeDModel(_track);
293 // try to use given altitude cap
294 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
295 model.setAltitudeCap(_altitudeCap);
297 catch (NumberFormatException nfe) {}
300 // Create file and write basics
301 writer = new FileWriter(inFile);
302 writeStartOfFile(writer, model.getModelSize(), lineSeparator);
304 // write out lat/long lines using model
305 writeLatLongLines(writer, model, lineSeparator);
308 writeDataPoints(writer, model, lineSeparator);
311 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("dialog.save.ok1")
312 + " " + _track.getNumPoints() + " " + I18nManager.getText("dialog.save.ok2")
313 + " " + inFile.getAbsolutePath(),
314 I18nManager.getText("dialog.save.oktitle"), JOptionPane.INFORMATION_MESSAGE);
317 catch (IOException ioe)
319 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(),
320 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
324 // close file ignoring exceptions
329 catch (Exception e) {}
336 * Write the start of the Pov file, including base plane and lights
337 * @param inWriter Writer to use for writing file
338 * @param inModelSize model size
339 * @param inLineSeparator line separator to use
340 * @throws IOException on file writing error
342 private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator)
345 inWriter.write("// Pov file produced by Prune - see http://activityworkshop.net/");
346 inWriter.write(inLineSeparator);
347 inWriter.write(inLineSeparator);
348 // Select font based on user input
349 String fontPath = _fontName.getText();
350 if (fontPath == null || fontPath.equals(""))
352 fontPath = DEFAULT_FONT_FILE;
355 String[] outputLines = {
356 "global_settings { ambient_light rgb <4, 4, 4> }", "",
357 "// Background and camera",
358 "background { color rgb <0, 0, 0> }",
361 " location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
362 " look_at <0, 0, 0>",
365 "// Global declares",
366 "#declare lat_line =",
368 " <-" + inModelSize + ", 0.1, 0>,",
369 " <" + inModelSize + ", 0.1, 0>,",
371 " pigment { color rgb <0.5 0.5 0.5> }",
373 "#declare lon_line =",
375 " <0, 0.1, -" + inModelSize + ">,",
376 " <0, 0.1, " + inModelSize + ">,",
378 " pigment { color rgb <0.5 0.5 0.5> }",
380 "#declare point_rod =",
386 " pigment { color rgb <0.5 0.5 0.5> }",
388 // TODO: Export rods to POV? How to store in data?
389 "#declare waypoint_sphere =",
393 " pigment {color rgb <0.1 0.1 1.0>}",
394 " finish { phong 1 }",
397 "#declare track_sphere0 =",
399 " <0, 0, 0>, 0.3", // size should depend on model size
401 " pigment {color rgb <0.2 1.0 0.2>}",
402 " finish { phong 1 }",
405 "#declare track_sphere1 =",
407 " <0, 0, 0>, 0.3", // size should depend on model size
409 " pigment {color rgb <0.6 1.0 0.2>}",
410 " finish { phong 1 }",
413 "#declare track_sphere2 =",
415 " <0, 0, 0>, 0.3", // size should depend on model size
417 " pigment {color rgb <1.0 1.0 0.1>}",
418 " finish { phong 1 }",
421 "#declare track_sphere3 =",
423 " <0, 0, 0>, 0.3", // size should depend on model size
425 " pigment {color rgb <1.0 1.0 1.0>}",
426 " finish { phong 1 }",
429 "#declare track_sphere4 =",
431 " <0, 0, 0>, 0.3", // size should depend on model size
433 " pigment {color rgb <0.1 1.0 1.0>}",
434 " finish { phong 1 }",
439 " <-" + inModelSize + ", -0.15, -" + inModelSize + ">, // Near lower left corner",
440 " <" + inModelSize + ", 0.15, " + inModelSize + "> // Far upper right corner",
441 " pigment { color rgb <0.5 0.75 0.8> }",
444 "// Cardinal letters N,S,E,W",
446 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
447 " pigment { color rgb <1 1 1> }",
448 " translate <0, 0.2, " + inModelSize + ">",
451 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
452 " pigment { color rgb <1 1 1> }",
453 " translate <0, 0.2, -" + inModelSize + ">",
456 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0",
457 " pigment { color rgb <1 1 1> }",
458 " translate <" + (inModelSize * 0.97) + ", 0.2, 0>",
461 " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0",
462 " pigment { color rgb <1 1 1> }",
463 " translate <-" + (inModelSize * 1.03) + ", 0.2, 0>",
465 // TODO: Light positions should relate to model size
467 "light_source { <-1, 9, -4> color rgb <0.5 0.5 0.5>}",
468 "light_source { <1, 6, -14> color rgb <0.6 0.6 0.6>}",
469 "light_source { <11, 12, 8> color rgb <0.3 0.3 0.3>}",
472 // write strings to file
473 int numLines = outputLines.length;
474 for (int i=0; i<numLines; i++)
476 inWriter.write(outputLines[i]);
477 inWriter.write(inLineSeparator);
483 * Write out all the lat and long lines to the file
484 * @param inWriter Writer to use for writing file
485 * @param inModel model object for getting lat/long lines
486 * @param inLineSeparator line separator to use
487 * @throws IOException on file writing error
489 private void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
492 inWriter.write("// Latitude and longitude lines:");
493 inWriter.write(inLineSeparator);
494 int numlines = inModel.getLatitudeLines().length;
495 for (int i=0; i<numlines; i++)
497 // write cylinder to file
498 inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
499 inWriter.write(inLineSeparator);
501 numlines = inModel.getLongitudeLines().length;
502 for (int i=0; i<numlines; i++)
504 // write cylinder to file
505 inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
506 inWriter.write(inLineSeparator);
508 inWriter.write(inLineSeparator);
513 * Write out all the data points to the file
514 * @param inWriter Writer to use for writing file
515 * @param inModel model object for getting data points
516 * @param inLineSeparator line separator to use
517 * @throws IOException on file writing error
519 private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
522 inWriter.write("// Data points:");
523 inWriter.write(inLineSeparator);
524 int numPoints = inModel.getNumPoints();
525 for (int i=0; i<numPoints; i++)
527 // ball (different according to type)
528 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
531 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
532 + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
536 // normal track point ball
537 inWriter.write("object { track_sphere" + checkHeightCode(inModel.getPointHeightCode(i))
538 + " translate <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i)
539 + "," + inModel.getScaledVertValue(i) + "> }");
541 inWriter.write(inLineSeparator);
542 // vertical rod (if altitude positive)
543 if (inModel.getScaledAltValue(i) > 0.0)
545 inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0,"
546 + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }");
547 inWriter.write(inLineSeparator);
550 inWriter.write(inLineSeparator);
555 * @param inCode height code to check
556 * @return validated height code within range 0 to max
558 private static byte checkHeightCode(byte inCode)
560 final byte maxHeightCode = 4;
561 if (inCode < 0) return 0;
562 if (inCode > maxHeightCode) return maxHeightCode;
568 * Check the given coordinate
569 * @param inString String entered by user
570 * @return validated String value
572 private static String checkCoordinate(String inString)
577 value = Double.parseDouble(inString);
579 catch (Exception e) {} // ignore parse failures