]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/PovExporter.java
Version 4, January 2008
[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.I18nManager;
25 import tim.prune.data.Track;
26 import tim.prune.threedee.LineDialog;
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 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;
43
44         // defaults
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
48
49
50         /**
51          * Constructor giving frame and track
52          * @param inParentFrame parent frame
53          * @param inTrack track object to save
54          */
55         public PovExporter(JFrame inParentFrame, Track inTrack)
56         {
57                 _parentFrame = inParentFrame;
58                 _track = inTrack;
59                 // Set default camera coordinates
60                 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
61         }
62
63
64         /**
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
69          */
70         public void setCameraCoordinates(double inX, double inY, double inZ)
71         {
72                 // calculate distance from origin
73                 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
74                 if (cameraDist > 0.0)
75                 {
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);
80                 }
81         }
82
83
84         /**
85          * @param inAltitudeCap altitude cap to use
86          */
87         public void setAltitudeCap(int inAltitudeCap)
88         {
89                 _altitudeCap = inAltitudeCap;
90                 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
91                 {
92                         _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
93                 }
94         }
95
96
97         /**
98          * Show the dialog to select options and export file
99          */
100         public void showDialog()
101         {
102                 // Make dialog window to select angles, colours etc
103                 if (_dialog == null)
104                 {
105                         _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true);
106                         _dialog.setLocationRelativeTo(_parentFrame);
107                         _dialog.getContentPane().add(makeDialogComponents());
108                 }
109
110                 // Set angles
111                 _cameraXField.setText(_cameraX);
112                 _cameraYField.setText(_cameraY);
113                 _cameraZField.setText(_cameraZ);
114                 // Set vertical scale
115                 _altitudeCapField.setText("" + _altitudeCap);
116                 // Show dialog
117                 _dialog.pack();
118                 _dialog.show();
119         }
120
121
122         /**
123          * Make the dialog components to select the export options
124          * @return Component holding gui elements
125          */
126         private Component makeDialogComponents()
127         {
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)
137                         {
138                                 doExport();
139                                 _dialog.dispose();
140                         }
141                 });
142                 buttonPanel.add(okButton);
143                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
144                 cancelButton.addActionListener(new ActionListener() {
145                         public void actionPerformed(ActionEvent e)
146                         {
147                                 _dialog.dispose();
148                         }
149                 });
150                 buttonPanel.add(cancelButton);
151                 panel.add(buttonPanel, BorderLayout.SOUTH);
152
153                 // central panel
154                 JPanel centralPanel = new JPanel();
155                 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
156
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);
179                 // Altitude capping
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);
185
186                 JPanel flowPanel = new JPanel();
187                 flowPanel.add(centralPanel);
188
189                 // show lines button
190                 JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
191                 showLinesButton.addActionListener(new ActionListener() {
192                         public void actionPerformed(ActionEvent e)
193                         {
194                                 // Need to scale model to find lines
195                                 ThreeDModel model = new ThreeDModel(_track);
196                                 model.scale();
197                                 double[] latLines = model.getLatitudeLines();
198                                 double[] lonLines = model.getLongitudeLines();
199                                 LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
200                                 dialog.showDialog();
201                         }
202                 });
203                 flowPanel.add(showLinesButton);
204                 panel.add(flowPanel, BorderLayout.CENTER);
205                 return panel;
206         }
207
208
209         /**
210          * Select the file and export data to it
211          */
212         private void doExport()
213         {
214                 // Copy camera coordinates
215                 _cameraX = checkCoordinate(_cameraXField.getText());
216                 _cameraY = checkCoordinate(_cameraYField.getText());
217                 _cameraZ = checkCoordinate(_cameraZField.getText());
218
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)
225                         {
226                                 return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov")));
227                         }
228                         public String getDescription()
229                         {
230                                 return I18nManager.getText("dialog.exportpov.filetype");
231                         }
232                 });
233                 _fileChooser.setAcceptAllFileFilterUsed(false);
234
235                 // Allow choose again if an existing file is selected
236                 boolean chooseAgain = false;
237                 do
238                 {
239                         chooseAgain = false;
240                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
241                         {
242                                 // OK pressed and file chosen
243                                 File file = _fileChooser.getSelectedFile();
244                                 if (!file.getName().toLowerCase().endsWith(".pov"))
245                                 {
246                                         file = new File(file.getAbsolutePath() + ".pov");
247                                 }
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)
255                                 {
256                                         // Export the file
257                                         if (exportFile(file))
258                                         {
259                                                 // file saved
260                                         }
261                                         else
262                                         {
263                                                 // export failed so need to choose again
264                                                 chooseAgain = true;
265                                         }
266                                 }
267                                 else
268                                 {
269                                         // overwrite cancelled so need to choose again
270                                         chooseAgain = true;
271                                 }
272                         }
273                 } while (chooseAgain);
274         }
275
276
277         /**
278          * Export the track data to the specified file
279          * @param inFile File object to save to
280          * @return true if successful
281          */
282         private boolean exportFile(File inFile)
283         {
284                 FileWriter writer = null;
285                 // find out the line separator for this system
286                 String lineSeparator = System.getProperty("line.separator");
287                 try
288                 {
289                         // create and scale model
290                         ThreeDModel model = new ThreeDModel(_track);
291                         try
292                         {
293                                 // try to use given altitude cap
294                                 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
295                                 model.setAltitudeCap(_altitudeCap);
296                         }
297                         catch (NumberFormatException nfe) {}
298                         model.scale();
299
300                         // Create file and write basics
301                         writer = new FileWriter(inFile);
302                         writeStartOfFile(writer, model.getModelSize(), lineSeparator);
303
304                         // write out lat/long lines using model
305                         writeLatLongLines(writer, model, lineSeparator);
306
307                         // write out points
308                         writeDataPoints(writer, model, lineSeparator);
309
310                         // everything worked
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);
315                         return true;
316                 }
317                 catch (IOException ioe)
318                 {
319                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + ioe.getMessage(),
320                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
321                 }
322                 finally
323                 {
324                         // close file ignoring exceptions
325                         try
326                         {
327                                 writer.close();
328                         }
329                         catch (Exception e) {}
330                 }
331                 return false;
332         }
333
334
335         /**
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
341          */
342         private void writeStartOfFile(FileWriter inWriter, double inModelSize, String inLineSeparator)
343         throws IOException
344         {
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(""))
351                 {
352                         fontPath = DEFAULT_FONT_FILE;
353                 }
354                 // Set up output
355                 String[] outputLines = {
356                   "global_settings { ambient_light rgb <4, 4, 4> }", "",
357                   "// Background and camera",
358                   "background { color rgb <0, 0, 0> }",
359                   // camera position
360                   "camera {",
361                   "  location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">",
362                   "  look_at  <0, 0, 0>",
363                   "}", "",
364                 // global declares
365                   "// Global declares",
366                   "#declare lat_line =",
367                   "  cylinder {",
368                   "   <-" + inModelSize + ", 0.1, 0>,",
369                   "   <" + inModelSize + ", 0.1, 0>,",
370                   "   0.1            // Radius",
371                   "   pigment { color rgb <0.5 0.5 0.5> }",
372                   "  }",
373                   "#declare lon_line =",
374                   "  cylinder {",
375                   "   <0, 0.1, -" + inModelSize + ">,",
376                   "   <0, 0.1, " + inModelSize + ">,",
377                   "   0.1            // Radius",
378                   "   pigment { color rgb <0.5 0.5 0.5> }",
379                   "  }",
380                   "#declare point_rod =",
381                   "  cylinder {",
382                   "   <0, 0, 0>,",
383                   "   <0, 1, 0>,",
384                   "   0.15",
385                   "   open",
386                   "   pigment { color rgb <0.5 0.5 0.5> }",
387                   "  }", "",
388                   // TODO: Export rods to POV?  How to store in data?
389                   "#declare waypoint_sphere =",
390                   "  sphere {",
391                   "   <0, 0, 0>, 0.4",
392                   "    texture {",
393                   "       pigment {color rgb <0.1 0.1 1.0>}",
394                   "       finish { phong 1 }",
395                   "    }",
396                   "  }",
397                   "#declare track_sphere0 =",
398                   "  sphere {",
399                   "   <0, 0, 0>, 0.3", // size should depend on model size
400                   "   texture {",
401                   "      pigment {color rgb <0.2 1.0 0.2>}",
402                   "      finish { phong 1 }",
403                   "   }",
404                   " }",
405                   "#declare track_sphere1 =",
406                   "  sphere {",
407                   "   <0, 0, 0>, 0.3", // size should depend on model size
408                   "   texture {",
409                   "      pigment {color rgb <0.6 1.0 0.2>}",
410                   "      finish { phong 1 }",
411                   "   }",
412                   " }",
413                   "#declare track_sphere2 =",
414                   "  sphere {",
415                   "   <0, 0, 0>, 0.3", // size should depend on model size
416                   "   texture {",
417                   "      pigment {color rgb <1.0 1.0 0.1>}",
418                   "      finish { phong 1 }",
419                   "   }",
420                   " }",
421                   "#declare track_sphere3 =",
422                   "  sphere {",
423                   "   <0, 0, 0>, 0.3", // size should depend on model size
424                   "   texture {",
425                   "      pigment {color rgb <1.0 1.0 1.0>}",
426                   "      finish { phong 1 }",
427                   "   }",
428                   " }",
429                   "#declare track_sphere4 =",
430                   "  sphere {",
431                   "   <0, 0, 0>, 0.3", // size should depend on model size
432                   "   texture {",
433                   "      pigment {color rgb <0.1 1.0 1.0>}",
434                   "      finish { phong 1 }",
435                   "   }",
436                   " }", "",
437                   "// Base plane",
438                   "box {",
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> }",
442                   "}", "",
443                 // write cardinals
444                   "// Cardinal letters N,S,E,W",
445                   "text {",
446                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0",
447                   "  pigment { color rgb <1 1 1> }",
448                   "  translate <0, 0.2, " + inModelSize + ">",
449                   "}",
450                   "text {",
451                   "  ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0",
452                   "  pigment { color rgb <1 1 1> }",
453                   "  translate <0, 0.2, -" + inModelSize + ">",
454                   "}",
455                   "text {",
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>",
459                   "}",
460                   "text {",
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>",
464                   "}", "",
465                   // TODO: Light positions should relate to model size
466                   "// lights",
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>}",
470                   "",
471                 };
472                 // write strings to file
473                 int numLines = outputLines.length;
474                 for (int i=0; i<numLines; i++)
475                 {
476                         inWriter.write(outputLines[i]);
477                         inWriter.write(inLineSeparator);
478                 }
479         }
480
481
482         /**
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
488          */
489         private void writeLatLongLines(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
490         throws IOException
491         {
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++)
496                 {
497                         // write cylinder to file
498                         inWriter.write("object { lat_line translate <0, 0, " + inModel.getScaledLatitudeLine(i) + "> }");
499                         inWriter.write(inLineSeparator);
500                 }
501                 numlines = inModel.getLongitudeLines().length;
502                 for (int i=0; i<numlines; i++)
503                 {
504                         // write cylinder to file
505                         inWriter.write("object { lon_line translate <" + inModel.getScaledLongitudeLine(i) + ", 0, 0> }");
506                         inWriter.write(inLineSeparator);
507                 }
508                 inWriter.write(inLineSeparator);
509         }
510
511
512         /**
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
518          */
519         private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator)
520         throws IOException
521         {
522                 inWriter.write("// Data points:");
523                 inWriter.write(inLineSeparator);
524                 int numPoints = inModel.getNumPoints();
525                 for (int i=0; i<numPoints; i++)
526                 {
527                         // ball (different according to type)
528                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
529                         {
530                                 // waypoint ball
531                                 inWriter.write("object { waypoint_sphere translate <" + inModel.getScaledHorizValue(i)
532                                         + "," + inModel.getScaledAltValue(i) + "," + inModel.getScaledVertValue(i) + "> }");
533                         }
534                         else
535                         {
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) + "> }");
540                         }
541                         inWriter.write(inLineSeparator);
542                         // vertical rod (if altitude positive)
543                         if (inModel.getScaledAltValue(i) > 0.0)
544                         {
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);
548                         }
549                 }
550                 inWriter.write(inLineSeparator);
551         }
552
553
554         /**
555          * @param inCode height code to check
556          * @return validated height code within range 0 to max
557          */
558         private static byte checkHeightCode(byte inCode)
559         {
560                 final byte maxHeightCode = 4;
561                 if (inCode < 0) return 0;
562                 if (inCode > maxHeightCode) return maxHeightCode;
563                 return inCode;
564         }
565
566
567         /**
568          * Check the given coordinate
569          * @param inString String entered by user
570          * @return validated String value
571          */
572         private static String checkCoordinate(String inString)
573         {
574                 double value = 0.0;
575                 try
576                 {
577                         value = Double.parseDouble(inString);
578                 }
579                 catch (Exception e) {} // ignore parse failures
580                 return "" + value;
581         }
582 }