X-Git-Url: https://gitweb.fperrin.net/?p=GpsPrune.git;a=blobdiff_plain;f=src%2Ftim%2Fprune%2Fsave%2FPovExporter.java;fp=src%2Ftim%2Fprune%2Fsave%2FPovExporter.java;h=d4a6beb87a0482219c5ecedb5620bd2f72992f1c;hp=0000000000000000000000000000000000000000;hb=ce6f2161b8596f7018d6a76bff79bc9e571f35fd;hpb=2d8cb72e84d5cc1089ce77baf1e34ea3ea2f8465 diff --git a/src/tim/prune/save/PovExporter.java b/src/tim/prune/save/PovExporter.java new file mode 100644 index 0000000..d4a6beb --- /dev/null +++ b/src/tim/prune/save/PovExporter.java @@ -0,0 +1,956 @@ +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.util.ArrayList; +import java.util.Iterator; + +import javax.imageio.ImageIO; +import javax.swing.BorderFactory; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.ButtonGroup; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JRadioButton; +import javax.swing.JTextField; +import javax.swing.SwingConstants; + +import tim.prune.App; +import tim.prune.FunctionLibrary; +import tim.prune.I18nManager; +import tim.prune.UpdateMessageBroker; +import tim.prune.config.Config; +import tim.prune.data.NumberUtils; +import tim.prune.data.Track; +import tim.prune.function.Export3dFunction; +import tim.prune.function.srtm.LookupSrtmFunction; +import tim.prune.gui.BaseImageDefinitionPanel; +import tim.prune.gui.DialogCloser; +import tim.prune.gui.TerrainDefinitionPanel; +import tim.prune.gui.map.MapSource; +import tim.prune.gui.map.MapSourceLibrary; +import tim.prune.load.GenericFileFilter; +import tim.prune.threedee.ImageDefinition; +import tim.prune.threedee.TerrainCache; +import tim.prune.threedee.TerrainDefinition; +import tim.prune.threedee.TerrainHelper; +import tim.prune.threedee.ThreeDModel; + +/** + * Class to export a 3d scene of the track to a specified Pov file + */ +public class PovExporter extends Export3dFunction +{ + private Track _track = null; + private JDialog _dialog = null; + private JFileChooser _fileChooser = null; + private String _cameraX = null, _cameraY = null, _cameraZ = null; + private JTextField _cameraXField = null, _cameraYField = null, _cameraZField = null; + private JTextField _fontName = null, _altitudeFactorField = null; + private JRadioButton _ballsAndSticksButton = null; + /** Panel for defining the base image */ + private BaseImageDefinitionPanel _baseImagePanel = null; + /** Component for defining the terrain */ + private TerrainDefinitionPanel _terrainPanel = null; + + // defaults + private static final double DEFAULT_CAMERA_DISTANCE = 30.0; + private static final double MODEL_SCALE_FACTOR = 20.0; + private static final String DEFAULT_FONT_FILE = "crystal.ttf"; + + + /** + * Constructor + * @param inApp App object + */ + public PovExporter(App inApp) + { + super(inApp); + _track = inApp.getTrackInfo().getTrack(); + // Set default camera coordinates + _cameraX = "17"; _cameraY = "13"; _cameraZ = "-20"; + } + + /** Get the name key */ + public String getNameKey() { + return "function.exportpov"; + } + + /** + * Set the coordinates for the camera (can be any scale) + * @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 distance from origin + double cameraDist = Math.sqrt(inX*inX + inY*inY + inZ*inZ); + if (cameraDist > 0.0) + { + _cameraX = NumberUtils.formatNumberUk(inX / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); + _cameraY = NumberUtils.formatNumberUk(inY / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); + // Careful! Need to convert from java3d (right-handed) to povray (left-handed) coordinate system! + _cameraZ = NumberUtils.formatNumberUk(-inZ / cameraDist * DEFAULT_CAMERA_DISTANCE, 5); + } + } + + + /** + * Show the dialog to select options and export file + */ + public void begin() + { + // Make dialog window to select inputs + 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 + _cameraXField.setText(_cameraX); + _cameraYField.setText(_cameraY); + _cameraZField.setText(_cameraZ); + _altitudeFactorField.setText("" + _altFactor); + // Pass terrain and image def parameters (if any) to the panels + if (_terrainDef != null) { + _terrainPanel.initTerrainParameters(_terrainDef); + } + if (_imageDef != null) { + _baseImagePanel.initImageParameters(_imageDef); + } + _baseImagePanel.updateBaseImageDetails(); + // 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(4, 4)); + JLabel introLabel = new JLabel(I18nManager.getText("dialog.exportpov.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) + { + // Need to launch export in new thread + new Thread(new Runnable() { + public void run() + { + doExport(); + _baseImagePanel.getGrouter().clearMapImage(); + } + }).start(); + _dialog.dispose(); + } + }); + buttonPanel.add(okButton); + JButton cancelButton = new JButton(I18nManager.getText("button.cancel")); + cancelButton.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) + { + _baseImagePanel.getGrouter().clearMapImage(); + _dialog.dispose(); + } + }); + buttonPanel.add(cancelButton); + panel.add(buttonPanel, BorderLayout.SOUTH); + + // central panel + JPanel centralPanel = new JPanel(); + centralPanel.setLayout(new GridLayout(0, 2, 10, 4)); + + JLabel fontLabel = new JLabel(I18nManager.getText("dialog.exportpov.font")); + fontLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(fontLabel); + String defaultFont = Config.getConfigString(Config.KEY_POVRAY_FONT); + if (defaultFont == null || defaultFont.equals("")) { + defaultFont = DEFAULT_FONT_FILE; + } + _fontName = new JTextField(defaultFont, 12); + _fontName.setAlignmentX(Component.LEFT_ALIGNMENT); + _fontName.addKeyListener(new DialogCloser(_dialog)); + centralPanel.add(_fontName); + //coordinates of camera + JLabel cameraXLabel = new JLabel(I18nManager.getText("dialog.exportpov.camerax")); + cameraXLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(cameraXLabel); + _cameraXField = new JTextField("" + _cameraX); + centralPanel.add(_cameraXField); + JLabel cameraYLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameray")); + cameraYLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(cameraYLabel); + _cameraYField = new JTextField("" + _cameraY); + centralPanel.add(_cameraYField); + JLabel cameraZLabel = new JLabel(I18nManager.getText("dialog.exportpov.cameraz")); + cameraZLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(cameraZLabel); + _cameraZField = new JTextField("" + _cameraZ); + centralPanel.add(_cameraZField); + // Altitude exaggeration + JLabel altitudeCapLabel = new JLabel(I18nManager.getText("dialog.3d.altitudefactor")); + altitudeCapLabel.setHorizontalAlignment(SwingConstants.TRAILING); + centralPanel.add(altitudeCapLabel); + _altitudeFactorField = new JTextField("1.0"); + centralPanel.add(_altitudeFactorField); + + // Radio buttons for style - balls on sticks or tubes + JPanel stylePanel = new JPanel(); + stylePanel.setLayout(new GridLayout(0, 2, 10, 4)); + JLabel styleLabel = new JLabel(I18nManager.getText("dialog.exportpov.modelstyle")); + styleLabel.setHorizontalAlignment(SwingConstants.TRAILING); + stylePanel.add(styleLabel); + JPanel radioPanel = new JPanel(); + radioPanel.setLayout(new BoxLayout(radioPanel, BoxLayout.Y_AXIS)); + _ballsAndSticksButton = new JRadioButton(I18nManager.getText("dialog.exportpov.ballsandsticks")); + _ballsAndSticksButton.setSelected(false); + radioPanel.add(_ballsAndSticksButton); + JRadioButton tubesButton = new JRadioButton(I18nManager.getText("dialog.exportpov.tubesandwalls")); + tubesButton.setSelected(true); + radioPanel.add(tubesButton); + ButtonGroup group = new ButtonGroup(); + group.add(_ballsAndSticksButton); group.add(tubesButton); + stylePanel.add(radioPanel); + + // Panel for the base image (parent is null because we don't need callback) + _baseImagePanel = new BaseImageDefinitionPanel(null, _dialog, _track); + // Panel for the terrain definition + _terrainPanel = new TerrainDefinitionPanel(); + + // add these panels 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); + boxPanel.add(Box.createVerticalStrut(4)); + boxPanel.add(stylePanel); + boxPanel.add(Box.createVerticalStrut(4)); + boxPanel.add(_terrainPanel); + boxPanel.add(Box.createVerticalStrut(4)); + boxPanel.add(_baseImagePanel); + 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 + _cameraX = checkCoordinate(_cameraXField.getText()); + _cameraY = checkCoordinate(_cameraYField.getText()); + _cameraZ = checkCoordinate(_cameraZField.getText()); + + // OK pressed, so choose output file + if (_fileChooser == null) + { + _fileChooser = new JFileChooser(); + _fileChooser.setDialogType(JFileChooser.SAVE_DIALOG); + _fileChooser.setFileFilter(new GenericFileFilter("filetype.pov", new String[] {"pov"})); + _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 povFile = _fileChooser.getSelectedFile(); + if (!povFile.getName().toLowerCase().endsWith(".pov")) + { + povFile = new File(povFile.getAbsolutePath() + ".pov"); + } + final int nameLen = povFile.getName().length() - 4; + final File imageFile = new File(povFile.getParentFile(), povFile.getName().substring(0, nameLen) + "_base.png"); + final File terrainFile = new File(povFile.getParentFile(), povFile.getName().substring(0, nameLen) + "_terrain.png"); + final boolean imageExists = _baseImagePanel.getImageDefinition().getUseImage() && imageFile.exists(); + final boolean terrainFileExists = _terrainPanel.getUseTerrain() && terrainFile.exists(); + + // Check if files exist and if necessary prompt for overwrite + Object[] buttonTexts = {I18nManager.getText("button.overwrite"), I18nManager.getText("button.cancel")}; + if ((!povFile.exists() && !imageExists && !terrainFileExists) + || 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(s) + if (exportFiles(povFile, imageFile, terrainFile)) + { + // file saved - store directory in config for later + Config.setConfigString(Config.KEY_TRACK_DIR, povFile.getParentFile().getAbsolutePath()); + // also store exaggeration and grid size + Config.setConfigInt(Config.KEY_HEIGHT_EXAGGERATION, (int) (_altFactor * 100)); + if (_terrainPanel.getUseTerrain() && _terrainPanel.getGridSize() > 20) { + Config.setConfigInt(Config.KEY_TERRAIN_GRID_SIZE, _terrainPanel.getGridSize()); + } + } + else + { + // export failed so need to choose again + chooseAgain = true; + } + } + else + { + // overwrite cancelled so need to choose again + chooseAgain = true; + } + } + } while (chooseAgain); + } + + + /** + * Export the data to the specified file(s) + * @param inPovFile File object to save pov file to + * @param inImageFile file object to save image to + * @param inTerrainFile file object to save terrain to + * @return true if successful + */ + private boolean exportFiles(File inPovFile, File inImageFile, File inTerrainFile) + { + FileWriter writer = null; + // find out the line separator for this system + final String lineSeparator = System.getProperty("line.separator"); + try + { + // create and scale model + ThreeDModel model = new ThreeDModel(_track); + model.setModelSize(MODEL_SCALE_FACTOR); + try + { + // try to use given altitude cap + double givenFactor = Double.parseDouble(_altitudeFactorField.getText()); + if (givenFactor > 0.0) _altFactor = givenFactor; + } + catch (NumberFormatException nfe) { // parse failed, reset + _altitudeFactorField.setText("" + _altFactor); + } + model.setAltitudeFactor(_altFactor); + + // Write base image if necessary + ImageDefinition imageDef = _baseImagePanel.getImageDefinition(); + boolean useImage = imageDef.getUseImage(); + if (useImage) + { + // Get base image from grouter + MapSource mapSource = MapSourceLibrary.getSource(imageDef.getSourceIndex()); + MapGrouter grouter = _baseImagePanel.getGrouter(); + GroutedImage baseImage = grouter.getMapImage(_track, mapSource, imageDef.getZoom()); + try + { + useImage = ImageIO.write(baseImage.getImage(), "png", inImageFile); + } + catch (IOException ioe) { + System.err.println("Can't write image: " + ioe.getClass().getName()); + useImage = false; + } + if (!useImage) { + _app.showErrorMessage(getNameKey(), "dialog.exportpov.cannotmakebaseimage"); + } + } + + boolean useTerrain = _terrainPanel.getUseTerrain(); + if (useTerrain) + { + TerrainHelper terrainHelper = new TerrainHelper(_terrainPanel.getGridSize()); + // See if there's a previously saved terrain track we can reuse + TerrainDefinition terrainDef = new TerrainDefinition(_terrainPanel.getUseTerrain(), _terrainPanel.getGridSize()); + Track terrainTrack = TerrainCache.getTerrainTrack(_app.getCurrentDataStatus(), terrainDef); + if (terrainTrack == null) + { + // Construct the terrain track according to these extents and the grid size + terrainTrack = terrainHelper.createGridTrack(_track); + // Get the altitudes from SRTM for all the points in the track + LookupSrtmFunction srtmLookup = (LookupSrtmFunction) FunctionLibrary.FUNCTION_LOOKUP_SRTM; + srtmLookup.begin(terrainTrack); + while (srtmLookup.isRunning()) + { + try { + Thread.sleep(750); // just polling in a wait loop isn't ideal but simple + } + catch (InterruptedException e) {} + } + // Fix the voids + terrainHelper.fixVoids(terrainTrack); + + // Store this back in the cache, maybe we'll need it again + TerrainCache.storeTerrainTrack(terrainTrack, _app.getCurrentDataStatus(), terrainDef); + } + + model.setTerrain(terrainTrack); + model.scale(); + + // Call TerrainHelper to write out the data from the model + terrainHelper.writeHeightMap(model, inTerrainFile); + } + else + { + // No terrain required, so just scale the model as it is + model.scale(); + } + + // Create file and write basics + writer = new FileWriter(inPovFile); + writeStartOfFile(writer, lineSeparator, useImage ? inImageFile : null, useTerrain ? inTerrainFile : null); + + // write out points + if (_ballsAndSticksButton.isSelected()) { + writeDataPointsBallsAndSticks(writer, model, lineSeparator); + } + else { + writeDataPointsTubesAndWalls(writer, model, lineSeparator); + } + + // everything worked + UpdateMessageBroker.informSubscribers(I18nManager.getText("confirm.save.ok1") + + " " + _track.getNumPoints() + " " + I18nManager.getText("confirm.save.ok2") + + " " + inPovFile.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 Pov file, including base plane and lights + * @param inWriter Writer to use for writing file + * @param inLineSeparator line separator to use + * @param inImageFile image file to reference (or null if none) + * @param inTerrainFile terrain file to reference (or null if none) + * @throws IOException on file writing error + */ + private void writeStartOfFile(FileWriter inWriter, String inLineSeparator, File inImageFile, File inTerrainFile) + throws IOException + { + inWriter.write("// Pov file produced by GpsPrune - see https://gpsprune.activityworkshop.net/"); + inWriter.write(inLineSeparator); + inWriter.write("#version 3.6;"); + inWriter.write(inLineSeparator); + inWriter.write(inLineSeparator); + // Select font based on user input + String fontPath = _fontName.getText(); + if (fontPath == null || fontPath.equals("")) + { + fontPath = DEFAULT_FONT_FILE; + } + else { + Config.setConfigString(Config.KEY_POVRAY_FONT, fontPath); + } + + // Make the definition of the base plane depending on whether there's an image or not + final boolean useImage = (inImageFile != null); + final boolean useImageOnBox = useImage && (inTerrainFile == null); + final String boxDefinition = (useImageOnBox ? + " <0, 0, 0>, <1, 1, 0.001>" + inLineSeparator + + " pigment {image_map { png \"" + inImageFile.getName() + "\" map_type 0 interpolate 2 once } }" + inLineSeparator + + " scale 20.0 rotate <90, 0, 0>" + inLineSeparator + + " translate <-10.0, 0, -10.0>" + : " <-10.0, -0.15, -10.0>," + inLineSeparator + + " <10.0, 0.0, 10.0>" + inLineSeparator + + " pigment { color rgb <0.5 0.75 0.8> }"); + // TODO: Maybe could use the same geometry for the imageless case, would simplify code a bit + + // Definition of terrain shape if any + final String terrainDefinition = makeTerrainString(inTerrainFile, inImageFile, inLineSeparator); + + final String[] pointLights = { + "// lights", + "light_source { <-1, 9, -4> color rgb <0.5 0.5 0.5>}", + "light_source { <1, 6, -14> color rgb <0.6 0.6 0.6>}", + "light_source { <11, 12, 8> color rgb <0.3 0.3 0.3>}" + }; + final String[] northwestLight = { + "// lights from NW", + "light_source { <-10, 10, 10> color rgb <1.5 1.5 1.5> parallel }", + }; + final String[] lightsLines = (inTerrainFile == null ? pointLights : northwestLight); + + // Set up output + String[] outputLines = { + "global_settings { ambient_light rgb <4, 4, 4> }", "", + "// Background and camera", + "background { color rgb <0, 0, 0> }", + // camera position + "camera {", + " location <" + _cameraX + ", " + _cameraY + ", " + _cameraZ + ">", + " look_at <0, 0, 0>", + "}", "", + // global declares + "// Global declares", + "#declare point_rod =", + " cylinder {", + " <0, 0, 0>,", + " <0, 1, 0>,", + " 0.15", + " open", + " texture {", + " pigment { color rgb <0.5 0.5 0.5> }", + useImage ? " } no_shadow" : " }", + " }", "", + // MAYBE: Export rods to POV? How to store in data? + "#declare waypoint_sphere =", + " sphere {", + " <0, 0, 0>, 0.4", + " texture {", + " pigment {color rgb <0.1 0.1 1.0>}", + " finish { phong 1 }", + useImage ? " } no_shadow" : " }", + " }", + "#declare track_sphere0 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.1 0.6 0.1>}", // dark green + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere1 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.4 0.9 0.2>}", // green + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere2 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.7 0.8 0.2>}", // yellow + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere3 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.5 0.8 0.6>}", // greeny + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere4 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <0.2 0.9 0.9>}", // cyan + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere5 =", + " sphere {", + " <0, 0, 0>, 0.3", // size should depend on model size + " texture {", + " pigment {color rgb <1.0 1.0 1.0>}", // white + " finish { phong 1 }", + " }", + " }", + "#declare track_sphere_t =", + " sphere {", + " <0, 0, 0>, 0.25", // size should depend on model size + " texture {", + " pigment {color rgb <0.6 1.0 0.2>}", + " finish { phong 1 }", + " } no_shadow", + " }", + "#declare wall_colour = rgbt <0.5, 0.5, 0.5, 0.3>;", "", + "// Base plane", + "box {", + boxDefinition, + "}", "", + // terrain + terrainDefinition, + // write cardinals + "// Cardinal letters N,S,E,W", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.n") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <0, 0.2, 10.0>", + "}", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.s") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <0, 0.2, -10.0>", + "}", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.e") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <9.7, 0.2, 0>", + "}", + "text {", + " ttf \"" + fontPath + "\" \"" + I18nManager.getText("cardinal.w") + "\" 0.3, 0", + " pigment { color rgb <1 1 1> }", + " translate <-10.3, 0.2, 0>", + "}" + }; + + // write strings to file + writeLinesToFile(inWriter, inLineSeparator, outputLines); + writeLinesToFile(inWriter, inLineSeparator, lightsLines); + } + + /** + * Write the given lines to the file + * @param inWriter writer object + * @param inLineSeparator line separator string + * @param lines array of lines to write + * @throws IOException + */ + private void writeLinesToFile(FileWriter inWriter, String inLineSeparator, String[] lines) + throws IOException + { + for (int i=0; i }").append(inLineSeparator); + } + sb.append("\tscale 20.0").append(inLineSeparator) + .append("\ttranslate <-10.0, 0, -10.0>").append(inLineSeparator).append("}"); + return sb.toString(); + } + + /** + * 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 inLineSeparator line separator to use + * @throws IOException on file writing error + */ + private static void writeDataPointsBallsAndSticks(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator) + throws IOException + { + inWriter.write("// Data points:"); + inWriter.write(inLineSeparator); + int numPoints = inModel.getNumPoints(); + for (int i=0; i }"); + } + else + { + // normal track point ball + inWriter.write("object { track_sphere" + checkHeightCode(inModel.getPointHeightCode(i)) + + " translate <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i) + + "," + inModel.getScaledVertValue(i) + "> }"); + } + inWriter.write(inLineSeparator); + // vertical rod (if altitude positive) + if (inModel.getScaledAltValue(i) > 0.0) + { + inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0," + + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }"); + inWriter.write(inLineSeparator); + } + } + inWriter.write(inLineSeparator); + } + + + /** + * Write out all the data points to the file in the tubes-and-walls style + * @param inWriter Writer to use for writing file + * @param inModel model object for getting data points + * @param inLineSeparator line separator to use + * @throws IOException on file writing error + */ + private static void writeDataPointsTubesAndWalls(FileWriter inWriter, ThreeDModel inModel, String inLineSeparator) + throws IOException + { + inWriter.write("// Data points:"); + inWriter.write(inLineSeparator); + int numPoints = inModel.getNumPoints(); + // Loop over all points and write out waypoints as balls + for (int i=0; i }"); + // vertical rod (if altitude positive) + if (inModel.getScaledAltValue(i) > 0.0) + { + inWriter.write(inLineSeparator); + inWriter.write("object { point_rod translate <" + inModel.getScaledHorizValue(i) + ",0," + + inModel.getScaledVertValue(i) + "> scale <1," + inModel.getScaledAltValue(i) + ",1> }"); + } + inWriter.write(inLineSeparator); + } + } + inWriter.write(inLineSeparator); + + // Loop over all the track segments + ArrayList segmentList = getSegmentList(inModel); + Iterator segmentIterator = segmentList.iterator(); + while (segmentIterator.hasNext()) + { + ModelSegment segment = segmentIterator.next(); + int segLength = segment.getNumTrackPoints(); + + // if the track segment is long enough, do a cubic spline sphere sweep + if (segLength <= 1) + { + // single point in segment - just draw sphere + int index = segment.getStartIndex(); + inWriter.write("object { track_sphere_t" + + " translate <" + inModel.getScaledHorizValue(index) + "," + inModel.getScaledAltValue(index) + + "," + inModel.getScaledVertValue(index) + "> }"); + // maybe draw some kind of polygon too or rod? + } + else + { + writeSphereSweep(inWriter, inModel, segment, inLineSeparator); + } + + // Write wall underneath segment + if (segLength > 1) + { + writePolygonWall(inWriter, inModel, segment, inLineSeparator); + } + } + } + + + /** + * Write out a single sphere sweep using either cubic spline or linear spline + * @param inWriter Writer to use for writing file + * @param inModel model object for getting data points + * @param inSegment model segment to draw + * @param inLineSeparator line separator to use + * @throws IOException on file writing error + */ + private static void writeSphereSweep(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator) + throws IOException + { + // 3d sphere sweep + inWriter.write("// Sphere sweep:"); + inWriter.write(inLineSeparator); + String splineType = inSegment.getNumTrackPoints() < 5?"linear_spline":"cubic_spline"; + inWriter.write("sphere_sweep { "); inWriter.write(splineType); + inWriter.write(" " + inSegment.getNumTrackPoints() + ","); + inWriter.write(inLineSeparator); + // Loop over all points in this segment and write out sphere sweep + for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++) + { + if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT) + { + inWriter.write(" <" + inModel.getScaledHorizValue(i) + "," + inModel.getScaledAltValue(i) + + "," + inModel.getScaledVertValue(i) + ">, 0.25"); + inWriter.write(inLineSeparator); + } + } + inWriter.write(" tolerance 0.1"); + inWriter.write(inLineSeparator); + inWriter.write(" texture { pigment {color rgb <0.6 1.0 0.2>} finish {phong 1} }"); + inWriter.write(inLineSeparator); + inWriter.write(" no_shadow"); + inWriter.write(inLineSeparator); + inWriter.write("}"); + inWriter.write(inLineSeparator); + } + + + /** + * Write out a single polygon-based wall for the tubes-and-walls style + * @param inWriter Writer to use for writing file + * @param inModel model object for getting data points + * @param inSegment model segment to draw + * @param inLineSeparator line separator to use + * @throws IOException on file writing error + */ + private static void writePolygonWall(FileWriter inWriter, ThreeDModel inModel, ModelSegment inSegment, String inLineSeparator) + throws IOException + { + // wall + inWriter.write(inLineSeparator); + inWriter.write("// wall between sweep and floor:"); + inWriter.write(inLineSeparator); + // Loop over all points in this segment again and write out polygons + int prevIndex = -1; + for (int i=inSegment.getStartIndex(); i<=inSegment.getEndIndex(); i++) + { + if (inModel.getPointType(i) != ThreeDModel.POINT_TYPE_WAYPOINT) + { + if (prevIndex >= 0) + { + double xDiff = inModel.getScaledHorizValue(i) - inModel.getScaledHorizValue(prevIndex); + double yDiff = inModel.getScaledVertValue(i) - inModel.getScaledVertValue(prevIndex); + double dist = Math.sqrt(xDiff * xDiff + yDiff * yDiff); + if (dist > 0) + { + inWriter.write("polygon {"); + inWriter.write(" 5, <" + inModel.getScaledHorizValue(prevIndex) + ", 0.0, " + inModel.getScaledVertValue(prevIndex) + ">,"); + inWriter.write(" <" + inModel.getScaledHorizValue(prevIndex) + ", " + inModel.getScaledAltValue(prevIndex) + ", " + + inModel.getScaledVertValue(prevIndex) + ">,"); + inWriter.write(" <" + inModel.getScaledHorizValue(i) + ", " + inModel.getScaledAltValue(i) + ", " + + inModel.getScaledVertValue(i) + ">,"); + inWriter.write(" <" + inModel.getScaledHorizValue(i) + ", 0.0, " + inModel.getScaledVertValue(i) + ">,"); + inWriter.write(" <" + inModel.getScaledHorizValue(prevIndex) + ", 0.0, " + inModel.getScaledVertValue(prevIndex) + ">"); + inWriter.write(" pigment { color wall_colour } no_shadow"); + inWriter.write("}"); + inWriter.write(inLineSeparator); + } + } + prevIndex = i; + } + } + } + + + /** + * @param inCode height code to check + * @return validated height code within range 0 to maxHeightCode + */ + private static byte checkHeightCode(byte inCode) + { + final byte maxHeightCode = 5; + if (inCode < 0) return 0; + if (inCode > maxHeightCode) return maxHeightCode; + return inCode; + } + + + /** + * Check the given coordinate + * @param inString String entered by user + * @return validated String value + */ + private static String checkCoordinate(String inString) + { + double value = 0.0; + try + { + value = Double.parseDouble(inString); + } + catch (Exception e) {} // ignore parse failures + return "" + value; + } + + /** + * Go through the points making a list of the segment starts and the number of track points in each segment + * @param inModel model containing data + * @return list of ModelSegment objects + */ + private static ArrayList getSegmentList(ThreeDModel inModel) + { + ArrayList segmentList = new ArrayList(); + if (inModel != null && inModel.getNumPoints() > 0) + { + ModelSegment currSegment = null; + int numTrackPoints = 0; + for (int i=0; i 0) + { + currSegment.setEndIndex(inModel.getNumPoints()-1); + currSegment.setNumTrackPoints(numTrackPoints); + segmentList.add(currSegment); + } + } + return segmentList; + } +}