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.UpdateMessageBroker;
26 import tim.prune.data.Track;
27 import tim.prune.threedee.LineDialog;
28 import tim.prune.threedee.ThreeDModel;
31 * Class to export track information
32 * into a specified Pov file
34 public class PovExporter
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 frame and track
53 * @param inParentFrame parent frame
54 * @param inTrack track object to save
56 public PovExporter(JFrame inParentFrame, Track inTrack)
58 _parentFrame = inParentFrame;
60 // Set default camera coordinates
61 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
66 * Set the coordinates for the camera (can be any scale)
67 * @param inX X coordinate of camera
68 * @param inY Y coordinate of camera
69 * @param inZ Z coordinate of camera
71 public void setCameraCoordinates(double inX, double inY, double inZ)
73 // calculate distance from origin
74 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
77 _cameraX = "" + (inX / cameraDist * DEFAULT_CAMERA_DISTANCE);
78 _cameraY = "" + (inY / cameraDist * DEFAULT_CAMERA_DISTANCE);
79 // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system!
80 _cameraZ = "" + (-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE);
86 * @param inAltitudeCap altitude cap to use
88 public void setAltitudeCap(int inAltitudeCap)
90 _altitudeCap = inAltitudeCap;
91 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
93 _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
99 * Show the dialog to select options and export file
101 public void showDialog()
103 // Make dialog window to select angles, colours etc
106 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true);
107 _dialog.setLocationRelativeTo(_parentFrame);
108 _dialog.getContentPane().add(makeDialogComponents());
112 _cameraXField.setText(_cameraX);
113 _cameraYField.setText(_cameraY);
114 _cameraZField.setText(_cameraZ);
115 // Set vertical scale
116 _altitudeCapField.setText("" + _altitudeCap);
124 * Make the dialog components to select the export options
125 * @return Component holding gui elements
127 private Component makeDialogComponents()
129 JPanel panel = new JPanel();
130 panel.setLayout(new BorderLayout());
131 panel.add(new JLabel(I18nManager.getText("dialog.exportpov.text")), BorderLayout.NORTH);
132 // OK, Cancel buttons
133 JPanel buttonPanel = new JPanel();
134 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
135 JButton okButton = new JButton(I18nManager.getText("button.ok"));
136 okButton.addActionListener(new ActionListener() {
137 public void actionPerformed(ActionEvent e)
143 buttonPanel.add(okButton);
144 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
145 cancelButton.addActionListener(new ActionListener() {
146 public void actionPerformed(ActionEvent e)
151 buttonPanel.add(cancelButton);
152 panel.add(buttonPanel, BorderLayout.SOUTH);
155 JPanel centralPanel = new JPanel();
156 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
158 JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font"));
159 fontLabel.setHorizontalAlignment(SwingConstants.TRAILING);
160 centralPanel.add(fontLabel);
161 _fontName = new JTextField(DEFAULT_FONT_FILE, 12);
162 _fontName.setAlignmentX(Component.LEFT_ALIGNMENT);
163 centralPanel.add(_fontName);
164 //coordinates of camera
165 JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax"));
166 cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING);
167 centralPanel.add(cameraXLabel);
168 _cameraXField = new JTextField("" + _cameraX);
169 centralPanel.add(_cameraXField);
170 JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray"));
171 cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING);
172 centralPanel.add(cameraYLabel);
173 _cameraYField = new JTextField("" + _cameraY);
174 centralPanel.add(_cameraYField);
175 JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz"));
176 cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING);
177 centralPanel.add(cameraZLabel);
178 _cameraZField = new JTextField("" + _cameraZ);
179 centralPanel.add(_cameraZField);
181 JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudecap"));
182 altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING);
183 centralPanel.add(altitudeCapLabel);
184 _altitudeCapField = new JTextField("" + _altitudeCap);
185 centralPanel.add(_altitudeCapField);
187 JPanel flowPanel = new JPanel();
188 flowPanel.add(centralPanel);
191 JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
192 showLinesButton.addActionListener(new ActionListener() {
193 public void actionPerformed(ActionEvent e)
195 // Need to scale model to find lines
196 ThreeDModel model = new ThreeDModel(_track);
198 double[] latLines = model.getLatitudeLines();
199 double[] lonLines = model.getLongitudeLines();
200 LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
204 flowPanel.add(showLinesButton);
205 panel.add(flowPanel, BorderLayout.CENTER);
211 * Select the file and export data to it
213 private void doExport()
215 // Copy camera coordinates
216 _cameraX = checkCoordinate(_cameraXField.getText());
217 _cameraY = checkCoordinate(_cameraYField.getText());
218 _cameraZ = checkCoordinate(_cameraZField.getText());
220 // OK pressed, so choose output file
221 if (_fileChooser == null)
222 _fileChooser = new JFileChooser();
223 _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
224 _fileChooser.setFileFilter(new FileFilter() {
225 public boolean accept(File f)
227 return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov")));
229 public String getDescription()
231 return I18nManager.getText("dialog.exportpov.filetype");
234 _fileChooser.setAcceptAllFileFilterUsed(false);
236 // Allow choose again if an existing file is selected
237 boolean chooseAgain = false;
241 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
243 // OK pressed and file chosen
244 File file = _fileChooser.getSelectedFile();
245 if (!file.getName().toLowerCase().endsWith(".pov"))
247 file = new File(file.getAbsolutePath() + ".pov");
249 // Check if file exists and if necessary prompt for overwrite
250 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
251 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
252 I18nManager.getText("dialog.save.overwrite.text"),
253 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
254 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
255 == JOptionPane.YES_OPTION)
258 if (exportFile(file))
264 // export failed so need to choose again
270 // overwrite cancelled so need to choose again
274 } while (chooseAgain);
279 * Export the track data to the specified file
280 * @param inFile File object to save to
281 * @return true if successful
283 private boolean exportFile(File inFile)
285 FileWriter writer = null;
286 // find out the line separator for this system
287 String lineSeparator = System.getProperty("line.separator");
290 // create and scale model
291 ThreeDModel model = new ThreeDModel(_track);
294 // try to use given altitude cap
295 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
296 model.setAltitudeCap(_altitudeCap);
298 catch (NumberFormatException nfe) {}
301 // Create file and write basics
302 writer = new FileWriter(inFile);
303 writeStartOfFile(writer, model.getModelSize(), lineSeparator);
305 // write out lat/long lines using model
306 writeLatLongLines(writer, model, lineSeparator);
309 writeDataPoints(writer, model, lineSeparator);
312 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
313 + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
314 + " " + inFile.getAbsolutePath());
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