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); // 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>", "}", "", // MAYBE: Light positions should relate to model size "// 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>}", "", }; // write strings to file int numLines = outputLines.length; 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; } }