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