]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/save/SvgExporter.java
Version 11, August 2010
[GpsPrune.git] / tim / prune / save / SvgExporter.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 import java.text.NumberFormat;
13 import java.util.Iterator;
14 import java.util.TreeSet;
15
16 import javax.swing.BorderFactory;
17 import javax.swing.BoxLayout;
18 import javax.swing.JButton;
19 import javax.swing.JCheckBox;
20 import javax.swing.JDialog;
21 import javax.swing.JFileChooser;
22 import javax.swing.JLabel;
23 import javax.swing.JOptionPane;
24 import javax.swing.JPanel;
25 import javax.swing.JTextField;
26 import javax.swing.SwingConstants;
27
28 import tim.prune.App;
29 import tim.prune.I18nManager;
30 import tim.prune.UpdateMessageBroker;
31 import tim.prune.config.Config;
32 import tim.prune.data.Track;
33 import tim.prune.function.Export3dFunction;
34 import tim.prune.load.GenericFileFilter;
35 import tim.prune.threedee.ThreeDModel;
36
37 /**
38  * Class to export a 3d scene of the track to a specified Svg file
39  */
40 public class SvgExporter extends Export3dFunction
41 {
42         private Track _track = null;
43         private JDialog _dialog = null;
44         private JFileChooser _fileChooser = null;
45         private double _phi = 0.0, _theta = 0.0;
46         private JTextField _phiField = null, _thetaField = null;
47         private JTextField _altitudeFactorField = null;
48         private JCheckBox _gradientsCheckbox = null;
49         private static double _scaleFactor = 1.0;
50
51
52         /**
53          * Constructor
54          * @param inApp App object
55          */
56         public SvgExporter(App inApp)
57         {
58                 super(inApp);
59                 _track = inApp.getTrackInfo().getTrack();
60                 // Set default rotation angles
61                 _phi = 30;  _theta = 55;
62         }
63
64         /** Get the name key */
65         public String getNameKey() {
66                 return "function.exportsvg";
67         }
68
69         /**
70          * Set the rotation angles using coordinates for the camera
71          * @param inX X coordinate of camera
72          * @param inY Y coordinate of camera
73          * @param inZ Z coordinate of camera
74          */
75         public void setCameraCoordinates(double inX, double inY, double inZ)
76         {
77                 // Calculate phi and theta based on camera x,y,z
78                 _phi = Math.toDegrees(Math.atan2(inX, inZ));
79                 _theta = Math.toDegrees(Math.atan2(inY, Math.sqrt(inX*inX + inZ*inZ)));
80         }
81
82
83         /**
84          * Show the dialog to select options and export file
85          */
86         public void begin()
87         {
88                 // Make dialog window to select angles, colours etc
89                 if (_dialog == null)
90                 {
91                         _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
92                         _dialog.setLocationRelativeTo(_parentFrame);
93                         _dialog.getContentPane().add(makeDialogComponents());
94                 }
95
96                 // Set angles
97                 NumberFormat threeDP = NumberFormat.getNumberInstance();
98                 threeDP.setMaximumFractionDigits(3);
99                 _phiField.setText(threeDP.format(_phi));
100                 _thetaField.setText(threeDP.format(_theta));
101                 // Set vertical scale
102                 _altitudeFactorField.setText("" + _altFactor);
103                 // Show dialog
104                 _dialog.pack();
105                 _dialog.setVisible(true);
106         }
107
108
109         /**
110          * Make the dialog components to select the export options
111          * @return Component holding gui elements
112          */
113         private Component makeDialogComponents()
114         {
115                 JPanel panel = new JPanel();
116                 panel.setLayout(new BorderLayout());
117                 JLabel introLabel = new JLabel(I18nManager.getText("dialog.exportsvg.text"));
118                 introLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4));
119                 panel.add(introLabel, BorderLayout.NORTH);
120                 // OK, Cancel buttons
121                 JPanel buttonPanel = new JPanel();
122                 buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT));
123                 JButton okButton = new JButton(I18nManager.getText("button.ok"));
124                 okButton.addActionListener(new ActionListener() {
125                         public void actionPerformed(ActionEvent e)
126                         {
127                                 doExport();
128                                 _dialog.dispose();
129                         }
130                 });
131                 buttonPanel.add(okButton);
132                 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
133                 cancelButton.addActionListener(new ActionListener() {
134                         public void actionPerformed(ActionEvent e)
135                         {
136                                 _dialog.dispose();
137                         }
138                 });
139                 buttonPanel.add(cancelButton);
140                 panel.add(buttonPanel, BorderLayout.SOUTH);
141
142                 // central panel
143                 JPanel centralPanel = new JPanel();
144                 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
145
146                 // rotation angles
147                 JLabel phiLabel = new JLabel(I18nManager.getText("dialog.exportsvg.phi"));
148                 phiLabel.setHorizontalAlignment(SwingConstants.TRAILING);
149                 centralPanel.add(phiLabel);
150                 _phiField = new JTextField("" + _phi);
151                 centralPanel.add(_phiField);
152                 JLabel thetaLabel = new JLabel(I18nManager.getText("dialog.exportsvg.theta"));
153                 thetaLabel.setHorizontalAlignment(SwingConstants.TRAILING);
154                 centralPanel.add(thetaLabel);
155                 _thetaField = new JTextField("" + _theta);
156                 centralPanel.add(_thetaField);
157                 // Altitude exaggeration
158                 JLabel altFactorLabel = new JLabel(I18nManager.getText("dialog.3d.altitudefactor"));
159                 altFactorLabel.setHorizontalAlignment(SwingConstants.TRAILING);
160                 centralPanel.add(altFactorLabel);
161                 _altitudeFactorField = new JTextField("" + _altFactor);
162                 centralPanel.add(_altitudeFactorField);
163                 // Checkbox for gradients or not
164                 JLabel gradientsLabel = new JLabel(I18nManager.getText("dialog.exportsvg.gradients"));
165                 gradientsLabel.setHorizontalAlignment(SwingConstants.TRAILING);
166                 centralPanel.add(gradientsLabel);
167                 _gradientsCheckbox = new JCheckBox();
168                 _gradientsCheckbox.setSelected(true);
169                 centralPanel.add(_gradientsCheckbox);
170
171                 // add this grid to the holder panel
172                 JPanel holderPanel = new JPanel();
173                 holderPanel.setLayout(new BorderLayout(5, 5));
174                 JPanel boxPanel = new JPanel();
175                 boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS));
176                 boxPanel.add(centralPanel);
177                 holderPanel.add(boxPanel, BorderLayout.CENTER);
178
179                 panel.add(holderPanel, BorderLayout.CENTER);
180                 return panel;
181         }
182
183
184         /**
185          * Select the file and export data to it
186          */
187         private void doExport()
188         {
189                 // Copy camera coordinates
190                 _phi = checkAngle(_phiField.getText());
191                 _theta = checkAngle(_thetaField.getText());
192
193                 // OK pressed, so choose output file
194                 if (_fileChooser == null)
195                 {
196                         _fileChooser = new JFileChooser();
197                         _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG);
198                         _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"}));
199                         _fileChooser.setAcceptAllFileFilterUsed(false);
200                         // start from directory in config which should be set
201                         final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR);
202                         if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));}
203                 }
204
205                 // Allow choose again if an existing file is selected
206                 boolean chooseAgain = false;
207                 do
208                 {
209                         chooseAgain = false;
210                         if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
211                         {
212                                 // OK pressed and file chosen
213                                 File file = _fileChooser.getSelectedFile();
214                                 if (!file.getName().toLowerCase().endsWith(".svg")) {
215                                         file = new File(file.getAbsolutePath() + ".svg");
216                                 }
217                                 // Check if file exists and if necessary prompt for overwrite
218                                 Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")};
219                                 if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame,
220                                                 I18nManager.getText("dialog.save.overwrite.text"),
221                                                 I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION,
222                                                 JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
223                                         == JOptionPane.YES_OPTION)
224                                 {
225                                         // Export the file
226                                         if (exportFile(file))
227                                         {
228                                                 // file saved - store directory in config for later
229                                                 Config.setConfigString(Config.KEY_TRACK_DIR, file.getParentFile().getAbsolutePath());
230                                         }
231                                         else {
232                                                 // export failed so need to choose again
233                                                 chooseAgain = true;
234                                         }
235                                 }
236                                 else {
237                                         // overwrite cancelled so need to choose again
238                                         chooseAgain = true;
239                                 }
240                         }
241                 } while (chooseAgain);
242         }
243
244
245         /**
246          * Export the track data to the specified file
247          * @param inFile File object to save to
248          * @return true if successful
249          */
250         private boolean exportFile(File inFile)
251         {
252                 FileWriter writer = null;
253                 // find out the line separator for this system
254                 String lineSeparator = System.getProperty("line.separator");
255                 try
256                 {
257                         // create and scale model
258                         ThreeDModel model = new ThreeDModel(_track);
259                         try
260                         {
261                                 // try to use given altitude factor
262                                 _altFactor = Double.parseDouble(_altitudeFactorField.getText());
263                                 model.setAltitudeFactor(_altFactor);
264                         }
265                         catch (NumberFormatException nfe) {}
266                         model.scale();
267                         _scaleFactor = 200 / model.getModelSize();
268
269                         boolean useGradients = _gradientsCheckbox.isSelected();
270
271                         // Create file and write basics
272                         writer = new FileWriter(inFile);
273                         writeStartOfFile(writer, useGradients, lineSeparator);
274                         writeBasePlane(writer, model.getModelSize(), lineSeparator);
275                         // write out cardinal letters NESW
276                         writeCardinals(writer, model.getModelSize(), lineSeparator);
277
278                         // write out points
279                         writeDataPoints(writer, model, useGradients, lineSeparator);
280                         writeEndOfFile(writer, lineSeparator);
281
282                         // everything worked
283                         UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
284                                  + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
285                                  + " " + inFile.getAbsolutePath());
286                         return true;
287                 }
288                 catch (IOException ioe)
289                 {
290                         JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
291                                 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
292                 }
293                 finally
294                 {
295                         // close file ignoring exceptions
296                         try {
297                                 writer.close();
298                         }
299                         catch (Exception e) {}
300                 }
301                 return false;
302         }
303
304
305         /**
306          * Write the start of the Svg file
307          * @param inWriter Writer to use for writing file
308          * @param inUseGradients true to use gradients, false for flat fills
309          * @param inLineSeparator line separator to use
310          * @throws IOException on file writing error
311          */
312         private static void writeStartOfFile(FileWriter inWriter, boolean inUseGradients,
313                 String inLineSeparator)
314         throws IOException
315         {
316                 inWriter.write("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>");
317                 inWriter.write(inLineSeparator);
318                 inWriter.write("<!-- Svg file produced by Prune - see http://activityworkshop.net/ -->");
319                 inWriter.write(inLineSeparator);
320                 inWriter.write("<svg width=\"800\" height=\"700\">");
321                 inWriter.write(inLineSeparator);
322                 if (inUseGradients)
323                 {
324                         final String defs = "<defs>" +
325                                 "<radialGradient id=\"wayfill\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fx=\"0.5\" fy=\"0.5\">" +
326                                 "<stop offset=\"0%\" stop-color=\"#2323aa\"/>" +
327                                 "<stop offset=\"100%\" stop-color=\"#000080\"/>" +
328                                 "</radialGradient>" + inLineSeparator +
329                                 "<radialGradient id=\"trackfill\" cx=\"0.5\" cy=\"0.5\" r=\"0.5\" fx=\"0.5\" fy=\"0.5\">" +
330                                 "<stop offset=\"0%\" stop-color=\"#23aa23\"/>" +
331                                 "<stop offset=\"100%\" stop-color=\"#008000\"/>" +
332                                 "</radialGradient>" +
333                                 "</defs>";
334                     inWriter.write(defs);
335                         inWriter.write(inLineSeparator);
336                 }
337                 inWriter.write("<g inkscape:label=\"Layer 1\" inkscape:groupmode=\"layer\" id=\"layer1\">");
338                 inWriter.write(inLineSeparator);
339         }
340
341         /**
342          * Write the base plane
343          * @param inWriter Writer to use for writing file
344          * @param inModelSize model size
345          * @param inLineSeparator line separator to use
346          * @throws IOException on file writing error
347          */
348         private void writeBasePlane(FileWriter inWriter, double inModelSize, String inLineSeparator)
349         throws IOException
350         {
351                 // Use model size and camera angles to draw path for base rectangle (using 3d transform)
352                 int[] coords1 = convertCoordinates(-inModelSize, -inModelSize, 0);
353                 int[] coords2 = convertCoordinates(inModelSize, -inModelSize, 0);
354                 int[] coords3 = convertCoordinates(inModelSize, inModelSize, 0);
355                 int[] coords4 = convertCoordinates(-inModelSize, inModelSize, 0);
356                 final String corners = "M " + coords1[0] + "," + coords1[1]
357                         + " L " + coords2[0] + "," + coords2[1]
358                         + " L " + coords3[0] + "," + coords3[1]
359                         + " L " + coords4[0] + "," + coords4[1] + " z";
360                 inWriter.write("<path style=\"fill:#446666;stroke:#000000;\" d=\"" + corners + "\" id=\"rect1\" />");
361                 inWriter.write(inLineSeparator);
362         }
363
364         /**
365          * Write the cardinal letters NESW
366          * @param inWriter Writer to use for writing file
367          * @param inModelSize model size
368          * @param inLineSeparator line separator to use
369          * @throws IOException on file writing error
370          */
371         private void writeCardinals(FileWriter inWriter, double inModelSize, String inLineSeparator)
372         throws IOException
373         {
374                 // Use model size and camera angles to calculate positions
375                 int[] coordsN = convertCoordinates(0, inModelSize, 0);
376                 writeCardinal(inWriter, coordsN[0], coordsN[1], "cardinal.n", inLineSeparator);
377                 int[] coordsE = convertCoordinates(inModelSize, 0, 0);
378                 writeCardinal(inWriter, coordsE[0], coordsE[1], "cardinal.e", inLineSeparator);
379                 int[] coordsS = convertCoordinates(0, -inModelSize, 0);
380                 writeCardinal(inWriter, coordsS[0], coordsS[1], "cardinal.s", inLineSeparator);
381                 int[] coordsW = convertCoordinates(-inModelSize, 0, 0);
382                 writeCardinal(inWriter, coordsW[0], coordsW[1], "cardinal.w", inLineSeparator);
383         }
384
385         /**
386          * Write a single cardinal letter
387          * @param inWriter Writer to use for writing file
388          * @param inX x coordinate
389          * @param inY y coordinate
390          * @param inKey key for string to write
391          * @param inLineSeparator line separator to use
392          * @throws IOException on file writing error
393          */
394         private static void writeCardinal(FileWriter inWriter, int inX, int inY, String inKey, String inLineSeparator)
395         throws IOException
396         {
397                 inWriter.write("<text x=\"" + inX + "\" y=\"" + inY + "\" font-size=\"26\" fill=\"black\" " +
398                         "stroke=\"white\" stroke-width=\"0.5\">");
399                 inWriter.write(I18nManager.getText(inKey));
400                 inWriter.write("</text>");
401                 inWriter.write(inLineSeparator);
402         }
403
404         /**
405          * Convert the given 3d coordinates into 2d coordinates by rotating and mapping
406          * @param inX x coordinate (east)
407          * @param inY y coordinate (north)
408          * @param inZ z coordinate (up)
409          * @return 2d coordinates as integer array
410          */
411         private int[] convertCoordinates(double inX, double inY, double inZ)
412         {
413                 // Rotate by phi degrees around vertical axis
414                 final double cosPhi = Math.cos(Math.toRadians(_phi));
415                 final double sinPhi = Math.sin(Math.toRadians(_phi));
416                 final double x2 = inX * cosPhi + inY * sinPhi;
417                 final double y2 = inY * cosPhi - inX * sinPhi;
418                 final double z2 = inZ;
419                 // Rotate by theta degrees around horizontal axis
420                 final double cosTheta = Math.cos(Math.toRadians(_theta));
421                 final double sinTheta = Math.sin(Math.toRadians(_theta));
422                 double x3 = x2;
423                 double y3 = y2 * sinTheta + z2 * cosTheta;
424                 // don't need to calculate z3
425                 // Scale results to sensible scale for svg
426                 x3 = x3 * _scaleFactor + 400;
427                 y3 = -y3 * _scaleFactor + 350;
428                 return new int[] {(int) x3, (int) y3};
429         }
430
431         /**
432          * Finish off the file by closing the tags
433          * @param inWriter Writer to use for writing file
434          * @param inLineSeparator line separator to use
435          * @throws IOException on file writing error
436          */
437         private static void writeEndOfFile(FileWriter inWriter, String inLineSeparator)
438         throws IOException
439         {
440                 inWriter.write(inLineSeparator);
441                 inWriter.write("</g></svg>");
442                 inWriter.write(inLineSeparator);
443         }
444
445         /**
446          * Write out all the data points to the file in the balls-and-sticks style
447          * @param inWriter Writer to use for writing file
448          * @param inModel model object for getting data points
449          * @param inUseGradients true to use gradients, false for flat fills
450          * @param inLineSeparator line separator to use
451          * @throws IOException on file writing error
452          */
453         private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, boolean inUseGradients,
454                 String inLineSeparator)
455         throws IOException
456         {
457                 final int numPoints = inModel.getNumPoints();
458                 TreeSet<SvgFragment> fragments = new TreeSet<SvgFragment>();
459                 for (int i=0; i<numPoints; i++)
460                 {
461                         StringBuilder builder = new StringBuilder();
462                         int[] coords = convertCoordinates(inModel.getScaledHorizValue(i), inModel.getScaledVertValue(i),
463                                 inModel.getScaledAltValue(i));
464                         // vertical rod (if altitude positive)
465                         if (inModel.getScaledAltValue(i) > 0.0)
466                         {
467                                 int[] baseCoords = convertCoordinates(inModel.getScaledHorizValue(i), inModel.getScaledVertValue(i), 0);
468                                 builder.append("<line x1=\"").append(baseCoords[0]).append("\" y1=\"").append(baseCoords[1])
469                                         .append("\" x2=\"").append(coords[0]).append("\" y2=\"").append(coords[1])
470                                         .append("\" stroke=\"gray\" stroke-width=\"3\" />");
471                                 builder.append(inLineSeparator);
472                         }
473                         // ball (different according to type)
474                         if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
475                         {
476                                 // waypoint ball
477                                 builder.append("<circle cx=\"").append(coords[0]).append("\" cy=\"").append(coords[1])
478                                         .append("\" r=\"11\" ").append(inUseGradients?"fill=\"url(#wayfill)\"":"fill=\"blue\"")
479                                         .append(" stroke=\"green\" stroke-width=\"0.2\" />");
480                         }
481                         else
482                         {
483                                 // normal track point ball
484                                 builder.append("<circle cx=\"").append(coords[0]).append("\" cy=\"").append(coords[1])
485                                         .append("\" r=\"7\" ").append(inUseGradients?"fill=\"url(#trackfill)\"":"fill=\"green\"")
486                                         .append(" stroke=\"blue\" stroke-width=\"0.2\" />");
487                         }
488                         builder.append(inLineSeparator);
489                         // add to set
490                         fragments.add(new SvgFragment(builder.toString(), coords[1]));
491                 }
492
493                 // Iterate over the sorted set and write to file
494                 Iterator<SvgFragment> iterator = fragments.iterator();
495                 while (iterator.hasNext()) {
496                         inWriter.write(iterator.next().getFragment());
497                 }
498         }
499
500
501         /**
502          * Check the given angle value
503          * @param inString String entered by user
504          * @return validated value
505          */
506         private static double checkAngle(String inString)
507         {
508                 double value = 0.0;
509                 try {
510                         value = Double.parseDouble(inString);
511                 }
512                 catch (Exception e) {} // ignore parse failures
513                 return value;
514         }
515 }