]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/PovExporter.java
Version 5, May 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.UpdateMessageBroker;
26 import tim.prune.data.Track;
27 import tim.prune.threedee.LineDialog;
28 import tim.prune.threedee.ThreeDModel;
29
30 /**
31  * Class to export track information
32  * into a specified Pov file
33  */
34 public class PovExporter
35 {
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 frame and track
53          * @param inParentFrame parent frame
54          * @param inTrack track object to save
55          */
56         public PovExporter(JFrame inParentFrame, Track inTrack)
57         {
58                 _parentFrame = inParentFrame;
59                 _track = inTrack;
60                 // Set default camera coordinates
61                 _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20";
62         }
63
64
65         /**
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
70          */
71         public void setCameraCoordinates(double inX, double inY, double inZ)
72         {
73                 // calculate distance from origin
74                 double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ);
75                 if (cameraDist > 0.0)
76                 {
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);
81                 }
82         }
83
84
85         /**
86          * @param inAltitudeCap altitude cap to use
87          */
88         public void setAltitudeCap(int inAltitudeCap)
89         {
90                 _altitudeCap = inAltitudeCap;
91                 if (_altitudeCap < ThreeDModel.MINIMUM_ALTITUDE_CAP)
92                 {
93                         _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;
94                 }
95         }
96
97
98         /**
99          * Show the dialog to select options and export file
100          */
101         public void showDialog()
102         {
103                 // Make dialog window to select angles, colours etc
104                 if (_dialog == null)
105                 {
106                         _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.exportpov.title"), true);
107                         _dialog.setLocationRelativeTo(_parentFrame);
108                         _dialog.getContentPane().add(makeDialogComponents());
109                 }
110
111                 // Set angles
112                 _cameraXField.setText(_cameraX);
113                 _cameraYField.setText(_cameraY);
114                 _cameraZField.setText(_cameraZ);
115                 // Set vertical scale
116                 _altitudeCapField.setText("" + _altitudeCap);
117                 // Show dialog
118                 _dialog.pack();
119                 _dialog.show();
120         }
121
122
123         /**
124          * Make the dialog components to select the export options
125          * @return Component holding gui elements
126          */
127         private Component makeDialogComponents()
128         {
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)
138                         {
139                                 doExport();
140                                 _dialog.dispose();
141                         }
142                 });
143                 buttonPanel.add(okButton);
144                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
145                 cancelButton.addActionListener(new ActionListener() {
146                         public void actionPerformed(ActionEvent e)
147                         {
148                                 _dialog.dispose();
149                         }
150                 });
151                 buttonPanel.add(cancelButton);
152                 panel.add(buttonPanel, BorderLayout.SOUTH);
153
154                 // central panel
155                 JPanel centralPanel = new JPanel();
156                 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
157
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);
180                 // Altitude capping
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);
186
187                 JPanel flowPanel = new JPanel();
188                 flowPanel.add(centralPanel);
189
190                 // show lines button
191                 JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
192                 showLinesButton.addActionListener(new ActionListener() {
193                         public void actionPerformed(ActionEvent e)
194                         {
195                                 // Need to scale model to find lines
196                                 ThreeDModel model = new ThreeDModel(_track);
197                                 model.scale();
198                                 double[] latLines = model.getLatitudeLines();
199                                 double[] lonLines = model.getLongitudeLines();
200                                 LineDialog dialog = new LineDialog(_parentFrame, latLines, lonLines);
201                                 dialog.showDialog();
202                         }
203                 });
204                 flowPanel.add(showLinesButton);
205                 panel.add(flowPanel, BorderLayout.CENTER);
206                 return panel;
207         }
208
209
210         /**
211          * Select the file and export data to it
212          */
213         private void doExport()
214         {
215                 // Copy camera coordinates
216                 _cameraX = checkCoordinate(_cameraXField.getText());
217                 _cameraY = checkCoordinate(_cameraYField.getText());
218                 _cameraZ = checkCoordinate(_cameraZField.getText());
219
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)
226                         {
227                                 return (f != null && (f.isDirectory() || f.getName().toLowerCase().endsWith(".pov")));
228                         }
229                         public String getDescription()
230                         {
231                                 return I18nManager.getText("dialog.exportpov.filetype");
232                         }
233                 });
234                 _fileChooser.setAcceptAllFileFilterUsed(false);
235
236                 // Allow choose again if an existing file is selected
237                 boolean chooseAgain = false;
238                 do
239                 {
240                         chooseAgain = false;
241                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
242                         {
243                                 // OK pressed and file chosen
244                                 File file = _fileChooser.getSelectedFile();
245                                 if (!file.getName().toLowerCase().endsWith(".pov"))
246                                 {
247                                         file = new File(file.getAbsolutePath() + ".pov");
248                                 }
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)
256                                 {
257                                         // Export the file
258                                         if (exportFile(file))
259                                         {
260                                                 // file saved
261                                         }
262                                         else
263                                         {
264                                                 // export failed so need to choose again
265                                                 chooseAgain = true;
266                                         }
267                                 }
268                                 else
269                                 {
270                                         // overwrite cancelled so need to choose again
271                                         chooseAgain = true;
272                                 }
273                         }
274                 } while (chooseAgain);
275         }
276
277
278         /**
279          * Export the track data to the specified file
280          * @param inFile File object to save to
281          * @return true if successful
282          */
283         private boolean exportFile(File inFile)
284         {
285                 FileWriter writer = null;
286                 // find out the line separator for this system
287                 String lineSeparator = System.getProperty("line.separator");
288                 try
289                 {
290                         // create and scale model
291                         ThreeDModel model = new ThreeDModel(_track);
292                         try
293                         {
294                                 // try to use given altitude cap
295                                 _altitudeCap = Integer.parseInt(_altitudeCapField.getText());
296                                 model.setAltitudeCap(_altitudeCap);
297                         }
298                         catch (NumberFormatException nfe) {}
299                         model.scale();
300
301                         // Create file and write basics
302                         writer = new FileWriter(inFile);
303                         writeStartOfFile(writer, model.getModelSize(), lineSeparator);
304
305                         // write out lat/long lines using model
306                         writeLatLongLines(writer, model, lineSeparator);
307
308                         // write out points
309                         writeDataPoints(writer, model, lineSeparator);
310
311                         // everything worked
312                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
313                                  + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
314                                  + " " + inFile.getAbsolutePath());
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 }