1 package tim.prune.save;
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;
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;
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;
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;
39 * Class to export a 3d scene of the track to a specified Svg file
41 public class SvgExporter extends Export3dFunction
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;
55 * @param inApp App object
57 public SvgExporter(App inApp)
60 _track = inApp.getTrackInfo().getTrack();
61 // Set default rotation angles
62 _phi = 30; _theta = 55;
65 /** Get the name key */
66 public String getNameKey() {
67 return "function.exportsvg";
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
76 public void setCameraCoordinates(double inX, double inY, double inZ)
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)));
85 * Show the dialog to select options and export file
89 // Make dialog window to select input parameters
92 _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true);
93 _dialog.setLocationRelativeTo(_parentFrame);
94 _dialog.getContentPane().add(makeDialogComponents());
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;
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);
111 _dialog.setVisible(true);
116 * Make the dialog components to select the export options
117 * @return Component holding gui elements
119 private Component makeDialogComponents()
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)
137 buttonPanel.add(okButton);
138 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
139 cancelButton.addActionListener(new ActionListener() {
140 public void actionPerformed(ActionEvent e) {
144 buttonPanel.add(cancelButton);
145 panel.add(buttonPanel, BorderLayout.SOUTH);
148 JPanel centralPanel = new JPanel();
149 centralPanel.setLayout(new GridLayout(0, 2, 10, 4));
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);
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);
185 panel.add(holderPanel, BorderLayout.CENTER);
191 * Select the file and export data to it
193 private void doExport()
195 // Copy camera coordinates
196 _phi = checkAngle(_phiField.getText());
197 _theta = checkAngle(_thetaField.getText());
199 // OK pressed, so choose output file
200 if (_fileChooser == null)
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));}
211 // Allow choose again if an existing file is selected
212 boolean chooseAgain = false;
216 if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
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");
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)
232 if (exportFile(file))
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));
240 // export failed so need to choose again
245 // overwrite cancelled so need to choose again
249 } while (chooseAgain);
254 * Export the track data to the specified file
255 * @param inFile File object to save to
256 * @return true if successful
258 private boolean exportFile(File inFile)
260 FileWriter writer = null;
261 // find out the line separator for this system
262 String lineSeparator = System.getProperty("line.separator");
265 // create and scale model
266 ThreeDModel model = new ThreeDModel(_track);
269 // try to use given altitude factor
270 _altFactor = Double.parseDouble(_altitudeFactorField.getText());
271 model.setAltitudeFactor(_altFactor);
273 catch (NumberFormatException nfe) {}
276 boolean useGradients = _gradientsCheckbox.isSelected();
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);
286 writeDataPoints(writer, model, useGradients, lineSeparator);
287 writeEndOfFile(writer, lineSeparator);
290 UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1")
291 + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2")
292 + " " + inFile.getAbsolutePath());
295 catch (IOException ioe)
297 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(),
298 I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE);
302 // close file ignoring exceptions
306 catch (Exception e) {}
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
319 private static void writeStartOfFile(FileWriter inWriter, boolean inUseGradients,
320 String inLineSeparator)
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);
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>" +
341 inWriter.write(defs);
342 inWriter.write(inLineSeparator);
344 inWriter.write("<g inkscape:label=\"Layer 1\" inkscape:groupmode=\"layer\" id=\"layer1\">");
345 inWriter.write(inLineSeparator);
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
354 private void writeBasePlane(FileWriter inWriter, String inLineSeparator)
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);
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
376 private void writeCardinals(FileWriter inWriter, String inLineSeparator)
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);
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
399 private static void writeCardinal(FileWriter inWriter, int inX, int inY, String inKey, String inLineSeparator)
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);
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
416 private int[] convertCoordinates(double inX, double inY, double inZ)
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));
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};
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
442 private static void writeEndOfFile(FileWriter inWriter, String inLineSeparator)
445 inWriter.write(inLineSeparator);
446 inWriter.write("</g></svg>");
447 inWriter.write(inLineSeparator);
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
458 private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, boolean inUseGradients,
459 String inLineSeparator)
462 final int numPoints = inModel.getNumPoints();
463 TreeSet<SvgFragment> fragments = new TreeSet<SvgFragment>();
464 for (int i=0; i<numPoints; i++)
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)
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);
478 // ball (different according to type)
479 if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT)
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\" />");
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\" />");
493 builder.append(inLineSeparator);
495 fragments.add(new SvgFragment(builder.toString(), coords[1]));
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());
507 * Check the given angle value
508 * @param inString String entered by user
509 * @return validated value
511 private static double checkAngle(String inString)
515 value = Double.parseDouble(inString);
517 catch (Exception e) {} // ignore parse failures