]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/PovExporter.java
Version 2, March 2007
[GpsPrune.git] / tim / prune / save / PovExporter.java
1 package tim.prune.save;
2
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;
9 import java.io.File;
10 import java.io.FileWriter;
11 import java.io.IOException;
12
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;
23
24 import tim.prune.App;
25 import tim.prune.I18nManager;
26 import tim.prune.data.Track;
27 import tim.prune.threedee.ThreeDModel;
28
29 /**
30  * Class to export track information
31  * into a specified Pov file
32  */
33 public class PovExporter
34 {
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;
44
45         // defaults
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
49
50
51         /**
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
56          */
57         public PovExporter(App inApp, JFrame inParentFrame, Track inTrack)
58         {
59                 _app = inApp;
60                 _parentFrame = inParentFrame;
61                 _track = inTrack;
62                 // Set default camera coordinates
63                 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
64         }
65
66
67         /**
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
72          */
73         public void setCameraCoordinates(double inX, double inY, double inZ)
74         {
75                 // calculate distance from origin
76                 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
77                 if (cameraDist > 0.0)
78                 {
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);
83                 }
84         }
85
86
87         /**
88          * @param inAltitudeCap altitude cap to use
89          */
90         public void setAltitudeCap(int inAltitudeCap)
91         {
92                 _altitudeCap = inAltitudeCap;
93                 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
94                 {
95                         _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
96                 }
97         }
98
99
100         /**
101          * Show the dialog to select options and export file
102          */
103         public void showDialog()
104         {
105                 // Make dialog window to select angles, colours etc
106                 if (_dialog == null)
107                 {
108                         _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true);
109                         _dialog.setLocationRelativeTo(_parentFrame);
110                         _dialog.getContentPane().add(makeDialogComponents());
111                 }
112
113                 // Set angles
114                 _cameraXField.setText(_cameraX);
115                 _cameraYField.setText(_cameraY);
116                 _cameraZField.setText(_cameraZ);
117                 // Set vertical scale
118                 _altitudeCapField.setText("" + _altitudeCap);
119                 // Show dialog
120                 _dialog.pack();
121                 _dialog.show();
122         }
123
124
125         /**
126          * Make the dialog components to select the export options
127          * @return Component holding gui elements
128          */
129         private Component makeDialogComponents()
130         {
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)
140                         {
141                                 doExport();
142                                 _dialog.dispose();
143                         }
144                 });
145                 buttonPanel.add(okButton);
146                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
147                 cancelButton.addActionListener(new ActionListener() {
148                         public void actionPerformed(ActionEvent e)
149                         {
150                                 _dialog.dispose();
151                         }
152                 });
153                 buttonPanel.add(cancelButton);
154                 panel.add(buttonPanel, BorderLayout.SOUTH);
155
156                 // central panel
157                 JPanel centralPanel = new JPanel();
158                 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
159
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);
182                 // Altitude capping
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);
188
189                 JPanel flowPanel = new JPanel();
190                 flowPanel.add(centralPanel);
191                 panel.add(flowPanel, BorderLayout.CENTER);
192                 return panel;
193         }
194
195
196         /**
197          * Select the file and export data to it
198          */
199         private void doExport()
200         {
201                 // Copy camera coordinates
202                 _cameraX = checkCoordinate(_cameraXField.getText());
203                 _cameraY = checkCoordinate(_cameraYField.getText());
204                 _cameraZ = checkCoordinate(_cameraZField.getText());
205
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)
213                         {
214                                 return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov")));
215                         }
216                         public String getDescription()
217                         {
218                                 return I18nManager.getText("dialog.exportpov.filetype");
219                         }
220                 });
221                 _fileChooser.setAcceptAllFileFilterUsed(false);
222
223                 // Allow choose again if an existing file is selected
224                 boolean chooseAgain = false;
225                 do
226                 {
227                         chooseAgain = false;
228                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
229                         {
230                                 // OK pressed and file chosen
231                                 File file = _fileChooser.getSelectedFile();
232                                 if (!file.getName().toLowerCase().endsWith(".pov"))
233                                 {
234                                         file = new File(file.getAbsolutePath() + ".pov");
235                                 }
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)
243                                 {
244                                         // Export the file
245                                         if (exportFile(file))
246                                         {
247                                                 fileSaved = true;
248                                         }
249                                         else
250                                         {
251                                                 // export failed so need to choose again
252                                                 chooseAgain = true;
253                                         }
254                                 }
255                                 else
256                                 {
257                                         // overwrite cancelled so need to choose again
258                                         chooseAgain = true;
259                                 }
260                         }
261                 } while (chooseAgain);
262         }
263
264
265         /**
266          * Export the track data to the specified file
267          * @param inFile File object to save to
268          * @return true if successful
269          */
270         private boolean exportFile(File inFile)
271         {
272                 FileWriter writer = null;
273                 // find out the line separator for this system
274                 String lineSeparator = System.getProperty("line.separator");
275                 try
276                 {
277                         // create and scale model
278                         ThreeDModel model = new ThreeDModel(_track);
279                         try
280                         {
281                                 // try to use given altitude cap
282                                 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
283                                 model.setAltitudeCap(_altitudeCap);
284                         }
285                         catch (NumberFormatException nfe) {}
286                         model.scale();
287
288                         // Create file and write basics
289                         writer = new FileWriter(inFile);
290                         writeStartOfFile(writer, model.getModelSize(), lineSeparator);
291
292                         // write out lat/long lines using model
293                         writeLatLongLines(writer, model, lineSeparator);
294
295                         // write out points
296                         writeDataPoints(writer, model, lineSeparator);
297
298                         // everything worked
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);
303                         return true;
304                 }
305                 catch (IOException ioe)
306                 {
307                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(),
308                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
309                 }
310                 finally
311                 {
312                         // close file ignoring exceptions
313                         try
314                         {
315                                 writer.close();
316                         }
317                         catch (Exception e) {}
318                 }
319                 return false;
320         }
321
322
323         /**
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
329          */
330         private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator)
331         throws IOException
332         {
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(""))
339                 {
340                         fontPath = DEFAULT_FONT_FILE;
341                 }
342                 // Set up output
343                 String[] outputLines = {
344                   "global_settings { ambient_light rgb <4, 4, 4> }", "",
345                   "// Background and camera",
346                   "background { color rgb <0, 0, 0> }",
347                   // camera position
348                   "camera {",
349                   "  location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
350                   "  look_at  <0, 0, 0>",
351                   "}", "",
352                 // global declares
353                   "// Global declares",
354                   "#declare lat_line =",
355                   "  cylinder {",
356                   "   <-" + inModelSize + ", 0.1, 0>,",
357                   "   <" + inModelSize + ", 0.1, 0>,",
358                   "   0.1            // Radius",
359                   "   pigment { color rgb <0.5 0.5 0.5> }",
360                   "  }",
361                   "#declare lon_line =",
362                   "  cylinder {",
363                   "   <0, 0.1, -" + inModelSize + ">,",
364                   "   <0, 0.1, " + inModelSize + ">,",
365                   "   0.1            // Radius",
366                   "   pigment { color rgb <0.5 0.5 0.5> }",
367                   "  }",
368                   "#declare point_rod =",
369                   "  cylinder {",
370                   "   <0, 0, 0>,",
371                   "   <0, 1, 0>,",
372                   "   0.15",
373                   "   open",
374                   "   pigment { color rgb <0.5 0.5 0.5> }",
375                   "  }", "",
376                   // TODO: Export rods to POV?  How to store in data?
377                   "#declare waypoint_sphere =",
378                   "  sphere {",
379                   "   <0, 0, 0>, 0.4",
380                   "    texture {",
381                   "       pigment {color rgb <0.1 0.1 1.0>}",
382                   "       finish { phong 1 }",
383                   "    }",
384                   "  }",
385                   "#declare track_sphere0 =",
386                   "  sphere {",
387                   "   <0, 0, 0>, 0.3", // size should depend on model size
388                   "   texture {",
389                   "      pigment {color rgb <0.2 1.0 0.2>}",
390                   "      finish { phong 1 }",
391                   "   }",
392                   " }",
393                   "#declare track_sphere1 =",
394                   "  sphere {",
395                   "   <0, 0, 0>, 0.3", // size should depend on model size
396                   "   texture {",
397                   "      pigment {color rgb <0.6 1.0 0.2>}",
398                   "      finish { phong 1 }",
399                   "   }",
400                   " }",
401                   "#declare track_sphere2 =",
402                   "  sphere {",
403                   "   <0, 0, 0>, 0.3", // size should depend on model size
404                   "   texture {",
405                   "      pigment {color rgb <1.0 1.0 0.1>}",
406                   "      finish { phong 1 }",
407                   "   }",
408                   " }",
409                   "#declare track_sphere3 =",
410                   "  sphere {",
411                   "   <0, 0, 0>, 0.3", // size should depend on model size
412                   "   texture {",
413                   "      pigment {color rgb <1.0 1.0 1.0>}",
414                   "      finish { phong 1 }",
415                   "   }",
416                   " }",
417                   "#declare track_sphere4 =",
418                   "  sphere {",
419                   "   <0, 0, 0>, 0.3", // size should depend on model size
420                   "   texture {",
421                   "      pigment {color rgb <0.1 1.0 1.0>}",
422                   "      finish { phong 1 }",
423                   "   }",
424                   " }", "",
425                   "// Base plane",
426                   "box {",
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> }",
430                   "}", "",
431                 // write cardinals
432                   "// Cardinal letters N,S,E,W",
433                   "text {",
434                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
435                   "  pigment { color rgb <1 1 1> }",
436                   "  translate <0, 0.2, " + inModelSize + ">",
437                   "}",
438                   "text {",
439                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
440                   "  pigment { color rgb <1 1 1> }",
441                   "  translate <0, 0.2, -" + inModelSize + ">",
442                   "}",
443                   "text {",
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>",
447                   "}",
448                   "text {",
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>",
452                   "}", "",
453                   // TODO: Light positions should relate to model size
454                   "// lights",
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>}",
458                   "",
459                 };
460                 // write strings to file
461                 int numLines = outputLines.length;
462                 for (int i=0; i<numLines; i++)
463                 {
464                         inWriter.write(outputLines[i]);
465                         inWriter.write(inLineSeparator);
466                 }
467         }
468
469
470         /**
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
476          */
477         private void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
478         throws IOException
479         {
480                 inWriter.write("// Latitude and longitude lines:");
481                 inWriter.write(inLineSeparator);
482                 int numlines = inModel.getNumLatitudeLines();
483                 for (int i=0; i<numlines; i++)
484                 {
485                         // write cylinder to file
486                         inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
487                         inWriter.write(inLineSeparator);
488                 }
489                 numlines = inModel.getNumLongitudeLines();
490                 for (int i=0; i<numlines; i++)
491                 {
492                         // write cylinder to file
493                         inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
494                         inWriter.write(inLineSeparator);
495                 }
496                 inWriter.write(inLineSeparator);
497         }
498
499
500         /**
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
506          */
507         private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
508         throws IOException
509         {
510                 inWriter.write("// Data points:");
511                 inWriter.write(inLineSeparator);
512                 int numPoints = inModel.getNumPoints();
513                 for (int i=0; i<numPoints; i++)
514                 {
515                         // ball (different according to type)
516                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
517                         {
518                                 // waypoint ball
519                                 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
520                                         + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
521                         }
522                         else
523                         {
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) + "> }");
528                         }
529                         inWriter.write(inLineSeparator);
530                         // vertical rod (if altitude positive)
531                         if (inModel.getScaledAltValue(i) > 0.0)
532                         {
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);
536                         }
537                 }
538                 inWriter.write(inLineSeparator);
539         }
540
541
542         /**
543          * @param inCode height code to check
544          * @return validated height code within range 0 to max
545          */
546         private static byte checkHeightCode(byte inCode)
547         {
548                 final byte maxHeightCode = 4;
549                 if (inCode < 0) return 0;
550                 if (inCode > maxHeightCode) return maxHeightCode;
551                 return inCode;
552         }
553
554
555         /**
556          * Check the given coordinate
557          * @param inString String entered by user
558          * @return validated String value
559          */
560         private static String checkCoordinate(String inString)
561         {
562                 double value = 0.0;
563                 try
564                 {
565                         value = Double.parseDouble(inString);
566                 }
567                 catch (Exception e) {} // ignore parse failures
568                 return "" + value;
569         }
570 }