package tim.prune.save; import java.awt.BorderLayout; import java.awt.Component; import java.awt.FlowLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.text.NumberFormat; import java.util.Iterator; import java.util.TreeSet; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; import javax.swing.SwingConstants; import tim.prune.App; import tim.prune.I18nManager; import tim.prune.UpdateMessageBroker; import tim.prune.config.Config; import tim.prune.data.Track; import tim.prune.function.Export3dFunction; import tim.prune.gui.DialogCloser; import tim.prune.load.GenericFileFilter; import tim.prune.threedee.ThreeDModel; /** * Class to export a 3d scene of the track to a specified Svg file */ public class SvgExporter extends Export3dFunction { private Track _track = null; private JDialog _dialog = null; private JFileChooser _fileChooser = null; private double _phi = 0.0, _theta = 0.0; private JTextField _phiField = null, _thetaField = null; private JTextField _altitudeFactorField = null; private JCheckBox _gradientsCheckbox = null; private static final double _scaleFactor = 200.0; /** * Constructor * @param inApp App object */ public SvgExporter(App inApp) { super(inApp); _track = inApp.getTrackInfo().getTrack(); // Set default rotation angles _phi = 30; _theta = 55; } /** Get the name key */ public String getNameKey() { return "function.exportsvg"; } /** * Set the rotation angles using coordinates for the camera * @param inX X coordinate of camera * @param inY Y coordinate of camera * @param inZ Z coordinate of camera */ public void setCameraCoordinates(double inX, double inY, double inZ) { // Calculate phi and theta based on camera x,y,z _phi = Math.toDegrees(Math.atan2(inX, inZ)); _theta = Math.toDegrees(Math.atan2(inY, Math.sqrt(inX*inX + inZ*inZ))); } /** * Show the dialog to select options and export file */ public void begin() { // Make dialog window to select input parameters if (_dialog == null) { _dialog = new JDialog(_parentFrame, I18nManager.getText(getNameKey()), true); _dialog.setLocationRelativeTo(_parentFrame); _dialog.getContentPane().add(makeDialogComponents()); } // Get exaggeration factor from config final int exaggFactor = Config.getConfigInt(Config.KEY_HEIGHT_EXAGGERATION); if (exaggFactor > 0) { _altFactor = exaggFactor / 100.0; } // Set angles NumberFormat threeDP = NumberFormat.getNumberInstance(); threeDP.setMaximumFractionDigits(3); _phiField.setText(threeDP.format(_phi)); _thetaField.setText(threeDP.format(_theta)); // Set vertical scale _altitudeFactorField.setText("" + _altFactor); // Show dialog _dialog.pack(); _dialog.setVisible(true); } /** * Make the dialog components to select the export options * @return Component holding gui elements */ private Component makeDialogComponents() { JPanel panel = new JPanel(); panel.setLayout(new BorderLayout()); JLabel introLabel = new JLabel(I18nManager.getText("dialog.exportsvg.text")); introLabel.setBorder(BorderFactory.createEmptyBorder(4, 4, 6, 4)); panel.add(introLabel, BorderLayout.NORTH); // OK, Cancel buttons JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new FlowLayout(FlowLayout.RIGHT)); JButton okButton = new JButton(I18nManager.getText("button.ok")); okButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { doExport(); _dialog.dispose(); } }); buttonPanel.add(okButton); JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); cancelButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { _dialog.dispose(); } }); buttonPanel.add(cancelButton); panel.add(buttonPanel, BorderLayout.SOUTH); // central panel JPanel centralPanel = new JPanel(); centralPanel.setLayout(new GridLayout(0, 2, 10, 4)); // rotation angles JLabel phiLabel = new JLabel(I18nManager.getText("dialog.exportsvg.phi")); phiLabel.setHorizontalAlignment(SwingConstants.TRAILING); centralPanel.add(phiLabel); _phiField = new JTextField("" + _phi); _phiField.addKeyListener(new DialogCloser(_dialog)); centralPanel.add(_phiField); JLabel thetaLabel = new JLabel(I18nManager.getText("dialog.exportsvg.theta")); thetaLabel.setHorizontalAlignment(SwingConstants.TRAILING); centralPanel.add(thetaLabel); _thetaField = new JTextField("" + _theta); centralPanel.add(_thetaField); // Altitude exaggeration JLabel altFactorLabel = new JLabel(I18nManager.getText("dialog.3d.altitudefactor")); altFactorLabel.setHorizontalAlignment(SwingConstants.TRAILING); centralPanel.add(altFactorLabel); _altitudeFactorField = new JTextField("" + _altFactor); centralPanel.add(_altitudeFactorField); // Checkbox for gradients or not JLabel gradientsLabel = new JLabel(I18nManager.getText("dialog.exportsvg.gradients")); gradientsLabel.setHorizontalAlignment(SwingConstants.TRAILING); centralPanel.add(gradientsLabel); _gradientsCheckbox = new JCheckBox(); _gradientsCheckbox.setSelected(true); centralPanel.add(_gradientsCheckbox); // add this grid to the holder panel JPanel holderPanel = new JPanel(); holderPanel.setLayout(new BorderLayout(5, 5)); JPanel boxPanel = new JPanel(); boxPanel.setLayout(new BoxLayout(boxPanel, BoxLayout.Y_AXIS)); boxPanel.add(centralPanel); holderPanel.add(boxPanel, BorderLayout.CENTER); panel.add(holderPanel, BorderLayout.CENTER); return panel; } /** * Select the file and export data to it */ private void doExport() { // Copy camera coordinates _phi = checkAngle(_phiField.getText()); _theta = checkAngle(_thetaField.getText()); // OK pressed, so choose output file if (_fileChooser == null) { _fileChooser = new JFileChooser(); _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); _fileChooser.setFileFilter(new GenericFileFilter("filetype.svg", new String[] {"svg"})); _fileChooser.setAcceptAllFileFilterUsed(false); // start from directory in config which should be set final String configDir = Config.getConfigString(Config.KEY_TRACK_DIR); if (configDir != null) {_fileChooser.setCurrentDirectory(new File(configDir));} } // Allow choose again if an existing file is selected boolean chooseAgain = false; do { chooseAgain = false; if (_fileChooser.showSaveDialog(_parentFrame) == JFileChooser.APPROVE_OPTION) { // OK pressed and file chosen File file = _fileChooser.getSelectedFile(); if (!file.getName().toLowerCase().endsWith(".svg")) { file = new File(file.getAbsolutePath() + ".svg"); } // Check if file exists and if necessary prompt for overwrite Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")}; if (!file.exists() || JOptionPane.showOptionDialog(_parentFrame, I18nManager.getText("dialog.save.overwrite.text"), I18nManager.getText("dialog.save.overwrite.title"), JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1]) == JOptionPane.YES_OPTION) { // Export the file if (exportFile(file)) { // file saved - store directory in config for later Config.setConfigString(Config.KEY_TRACK_DIR, file.getParentFile().getAbsolutePath()); // also store exaggeration Config.setConfigInt(Config.KEY_HEIGHT_EXAGGERATION, (int) (_altFactor * 100)); } else { // export failed so need to choose again chooseAgain = true; } } else { // overwrite cancelled so need to choose again chooseAgain = true; } } } while (chooseAgain); } /** * Export the track data to the specified file * @param inFile File object to save to * @return true if successful */ private boolean exportFile(File inFile) { FileWriter writer = null; // find out the line separator for this system String lineSeparator = System.getProperty("line.separator"); try { // create and scale model ThreeDModel model = new ThreeDModel(_track); try { // try to use given altitude factor _altFactor = Double.parseDouble(_altitudeFactorField.getText()); model.setAltitudeFactor(_altFactor); } catch (NumberFormatException nfe) {} model.scale(); boolean useGradients = _gradientsCheckbox.isSelected(); // Create file and write basics writer = new FileWriter(inFile); writeStartOfFile(writer, useGradients, lineSeparator); writeBasePlane(writer, lineSeparator); // write out cardinal letters NESW writeCardinals(writer, lineSeparator); // write out points writeDataPoints(writer, model, useGradients, lineSeparator); writeEndOfFile(writer, lineSeparator); // everything worked UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1") + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2") + " " + inFile.getAbsolutePath()); return true; } catch (IOException ioe) { JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.save.failed") + " : " + ioe.getMessage(), I18nManager.getText("error.save.dialogtitle"), JOptionPane.ERROR_MESSAGE); } finally { // close file ignoring exceptions try { writer.close(); } catch (Exception e) {} } return false; } /** * Write the start of the Svg file * @param inWriter Writer to use for writing file * @param inUseGradients true to use gradients, false for flat fills * @param inLineSeparator line separator to use * @throws IOException on file writing error */ private static void writeStartOfFile(FileWriter inWriter, boolean inUseGradients, String inLineSeparator) throws IOException { inWriter.write(""); inWriter.write(inLineSeparator); inWriter.write(""); inWriter.write(inLineSeparator); inWriter.write(""); inWriter.write(inLineSeparator); if (inUseGradients) { final String defs = "" + "" + "" + "" + "" + inLineSeparator + "" + "" + "" + "" + ""; inWriter.write(defs); inWriter.write(inLineSeparator); } inWriter.write(""); inWriter.write(inLineSeparator); } /** * Write the base plane * @param inWriter Writer to use for writing file * @param inLineSeparator line separator to use * @throws IOException on file writing error */ private void writeBasePlane(FileWriter inWriter, String inLineSeparator) throws IOException { // Use model size and camera angles to draw path for base rectangle (using 3d transform) int[] coords1 = convertCoordinates(-1.0, -1.0, 0); int[] coords2 = convertCoordinates(1.0, -1.0, 0); int[] coords3 = convertCoordinates(1.0, 1.0, 0); int[] coords4 = convertCoordinates(-1.0, 1.0, 0); final String corners = "M " + coords1[0] + "," + coords1[1] + " L " + coords2[0] + "," + coords2[1] + " L " + coords3[0] + "," + coords3[1] + " L " + coords4[0] + "," + coords4[1] + " z"; inWriter.write(""); inWriter.write(inLineSeparator); } /** * Write the cardinal letters NESW * @param inWriter Writer to use for writing file * @param inLineSeparator line separator to use * @throws IOException on file writing error */ private void writeCardinals(FileWriter inWriter, String inLineSeparator) throws IOException { // Use model size and camera angles to calculate positions int[] coordsN = convertCoordinates(0, 1.0, 0); writeCardinal(inWriter, coordsN[0], coordsN[1], "cardinal.n", inLineSeparator); int[] coordsE = convertCoordinates(1.0, 0, 0); writeCardinal(inWriter, coordsE[0], coordsE[1], "cardinal.e", inLineSeparator); int[] coordsS = convertCoordinates(0, -1.0, 0); writeCardinal(inWriter, coordsS[0], coordsS[1], "cardinal.s", inLineSeparator); int[] coordsW = convertCoordinates(-1.0, 0, 0); writeCardinal(inWriter, coordsW[0], coordsW[1], "cardinal.w", inLineSeparator); } /** * Write a single cardinal letter * @param inWriter Writer to use for writing file * @param inX x coordinate * @param inY y coordinate * @param inKey key for string to write * @param inLineSeparator line separator to use * @throws IOException on file writing error */ private static void writeCardinal(FileWriter inWriter, int inX, int inY, String inKey, String inLineSeparator) throws IOException { inWriter.write(""); inWriter.write(I18nManager.getText(inKey)); inWriter.write(""); inWriter.write(inLineSeparator); } /** * Convert the given 3d coordinates into 2d coordinates by rotating and mapping * @param inX x coordinate (east) * @param inY y coordinate (north) * @param inZ z coordinate (up) * @return 2d coordinates as integer array */ private int[] convertCoordinates(double inX, double inY, double inZ) { // Rotate by phi degrees around vertical axis final double cosPhi = Math.cos(Math.toRadians(_phi)); final double sinPhi = Math.sin(Math.toRadians(_phi)); final double x2 = inX * cosPhi + inY * sinPhi; final double y2 = inY * cosPhi - inX * sinPhi; final double z2 = inZ; // Rotate by theta degrees around horizontal axis final double cosTheta = Math.cos(Math.toRadians(_theta)); final double sinTheta = Math.sin(Math.toRadians(_theta)); double x3 = x2; double y3 = y2 * sinTheta + z2 * cosTheta; // don't need to calculate z3 // Scale results to sensible scale for svg x3 = x3 * _scaleFactor + 400; y3 = -y3 * _scaleFactor + 350; return new int[] {(int) x3, (int) y3}; } /** * Finish off the file by closing the tags * @param inWriter Writer to use for writing file * @param inLineSeparator line separator to use * @throws IOException on file writing error */ private static void writeEndOfFile(FileWriter inWriter, String inLineSeparator) throws IOException { inWriter.write(inLineSeparator); inWriter.write(""); inWriter.write(inLineSeparator); } /** * Write out all the data points to the file in the balls-and-sticks style * @param inWriter Writer to use for writing file * @param inModel model object for getting data points * @param inUseGradients true to use gradients, false for flat fills * @param inLineSeparator line separator to use * @throws IOException on file writing error */ private void writeDataPoints(FileWriter inWriter, ThreeDModel inModel, boolean inUseGradients, String inLineSeparator) throws IOException { final int numPoints = inModel.getNumPoints(); TreeSet fragments = new TreeSet(); for (int i=0; i 0.0) { int[] baseCoords = convertCoordinates(inModel.getScaledHorizValue(i), inModel.getScaledVertValue(i), 0); builder.append(""); builder.append(inLineSeparator); } // ball (different according to type) if (inModel.getPointType(i) == ThreeDModel.POINT_TYPE_WAYPOINT) { // waypoint ball builder.append(""); } else { // normal track point ball builder.append(""); } builder.append(inLineSeparator); // add to set fragments.add(new SvgFragment(builder.toString(), coords[1])); } // Iterate over the sorted set and write to file Iterator iterator = fragments.iterator(); while (iterator.hasNext()) { inWriter.write(iterator.next().getFragment()); } } /** * Check the given angle value * @param inString String entered by user * @return validated value */ private static double checkAngle(String inString) { double value = 0.0; try { value = Double.parseDouble(inString); } catch (Exception e) {} // ignore parse failures return value; } }