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