File: Java3DWindow.java

package info (click to toggle)
gpsprune 10-1
  • links: PTS, VCS
  • area: main
  • in suites: squeeze
  • size: 2,220 kB
  • ctags: 3,013
  • sloc: java: 22,662; sh: 23; makefile: 16; python: 15
file content (591 lines) | stat: -rw-r--r-- 19,315 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
package tim.prune.threedee;

import java.awt.FlowLayout;
import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.GeneralPath;

import javax.media.j3d.AmbientLight;
import javax.media.j3d.Appearance;
import javax.media.j3d.BoundingSphere;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.Canvas3D;
import javax.media.j3d.Font3D;
import javax.media.j3d.FontExtrusion;
import javax.media.j3d.GraphicsConfigTemplate3D;
import javax.media.j3d.Group;
import javax.media.j3d.Material;
import javax.media.j3d.PointLight;
import javax.media.j3d.Shape3D;
import javax.media.j3d.Text3D;
import javax.media.j3d.Transform3D;
import javax.media.j3d.TransformGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.vecmath.Color3f;
import javax.vecmath.Matrix3d;
import javax.vecmath.Point3d;
import javax.vecmath.Point3f;
import javax.vecmath.Vector3d;

import com.sun.j3d.utils.behaviors.vp.OrbitBehavior;
import com.sun.j3d.utils.geometry.Box;
import com.sun.j3d.utils.geometry.Cylinder;
import com.sun.j3d.utils.geometry.Sphere;
import com.sun.j3d.utils.universe.SimpleUniverse;

import tim.prune.FunctionLibrary;
import tim.prune.I18nManager;
import tim.prune.data.Altitude;
import tim.prune.data.Track;


/**
 * Class to hold main window for java3d view of data
 */
public class Java3DWindow implements ThreeDWindow
{
	private Track _track = null;
	private JFrame _parentFrame = null;
	private JFrame _frame = null;
	private ThreeDModel _model = null;
	private OrbitBehavior _orbit = null;
	private int _altitudeCap = ThreeDModel.MINIMUM_ALTITUDE_CAP;

	/** only prompt about big track size once */
	private static boolean TRACK_SIZE_WARNING_GIVEN = false;

	// Constants
	private static final double INITIAL_Y_ROTATION = -25.0;
	private static final double INITIAL_X_ROTATION = 15.0;
	private static final String CARDINALS_FONT = "Arial";
	private static final int MAX_TRACK_SIZE = 2500; // threshold for warning


	/**
	 * Constructor
	 * @param inFrame parent frame
	 */
	public Java3DWindow(JFrame inFrame)
	{
		_parentFrame = inFrame;
	}


	/**
	 * Set the track object
	 * @param inTrack Track object
	 */
	public void setTrack(Track inTrack)
	{
		_track = inTrack;
	}


	/**
	 * Show the window
	 */
	public void show() throws ThreeDException
	{
		// Get the altitude cap to use
		String altitudeUnits = getAltitudeUnitsLabel(_track);
		Object altCapString = JOptionPane.showInputDialog(_parentFrame,
			I18nManager.getText("dialog.3d.altitudecap") + " (" + altitudeUnits + ")",
			I18nManager.getText("dialog.3d.title"),
			JOptionPane.QUESTION_MESSAGE, null, null, "" + _altitudeCap);
		if (altCapString == null) return;
		try
		{
			_altitudeCap = Integer.parseInt(altCapString.toString());
		}
		catch (Exception e) {} // Ignore parse errors

		// Set up the graphics config
		GraphicsConfiguration config = SimpleUniverse.getPreferredConfiguration();
		if (config == null)
		{
			// Config shouldn't be null, but we can try to create a new one as a workaround
			GraphicsConfigTemplate3D gc = new GraphicsConfigTemplate3D();
			gc.setDepthSize(0);
			config = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getBestConfiguration(gc);
		}

		if (config == null)
		{
			// Second attempt also failed, going to have to give up here.
			throw new ThreeDException("Couldn't create graphics config");
		}

		// Check number of points in model isn't too big, and suggest compression
		Object[] buttonTexts = {I18nManager.getText("button.continue"), I18nManager.getText("button.cancel")};
		if (_track.getNumPoints() > MAX_TRACK_SIZE && !TRACK_SIZE_WARNING_GIVEN)
		{
			if (JOptionPane.showOptionDialog(_frame,
					I18nManager.getText("dialog.exportpov.warningtracksize"),
					I18nManager.getText("function.exportpov"), JOptionPane.OK_CANCEL_OPTION,
					JOptionPane.WARNING_MESSAGE, null, buttonTexts, buttonTexts[1])
				== JOptionPane.OK_OPTION)
			{
				// opted to continue, don't show warning again
				TRACK_SIZE_WARNING_GIVEN = true;
			}
			else
			{
				// opted to cancel - show warning again next time
				return;
			}
		}

		Canvas3D canvas = new Canvas3D(config);
		canvas.setSize(400, 300);

		// Create the scene and attach it to the virtual universe
		BranchGroup scene = createSceneGraph();
		SimpleUniverse u = new SimpleUniverse(canvas);

		// This will move the ViewPlatform back a bit so the
		// objects in the scene can be viewed.
		u.getViewingPlatform().setNominalViewingTransform();

		// Add behaviour to rotate using mouse
		_orbit = new OrbitBehavior(canvas, OrbitBehavior.REVERSE_ALL |
								  OrbitBehavior.STOP_ZOOM);
		BoundingSphere bounds = new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);
		_orbit.setSchedulingBounds(bounds);
		u.getViewingPlatform().setViewPlatformBehavior(_orbit);
		u.addBranchGraph(scene);

		// Don't reuse _frame object from last time, because data and/or scale might be different
		// Need to regenerate everything
		_frame = new JFrame(I18nManager.getText("dialog.3d.title"));
		_frame.getContentPane().setLayout(new BorderLayout());
		_frame.getContentPane().add(canvas, BorderLayout.CENTER);
		_frame.setIconImage(_parentFrame.getIconImage());
		// Make panel for render, close buttons
		JPanel panel = new JPanel();
		panel.setLayout(new FlowLayout(FlowLayout.RIGHT));
		// Add callback button for render
		JButton renderButton = new JButton(I18nManager.getText("function.exportpov"));
		renderButton.addActionListener(new ActionListener()
		{
			/** Render button pressed */
			public void actionPerformed(ActionEvent e)
			{
				if (_orbit != null)
				{
					callbackRender();
				}
			}});
		panel.add(renderButton);
		// Display coordinates of lat/long lines of 3d graph in separate dialog
		JButton showLinesButton = new JButton(I18nManager.getText("button.showlines"));
		showLinesButton.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e)
			{
				double[] latLines = _model.getLatitudeLines();
				double[] lonLines = _model.getLongitudeLines();
				LineDialog dialog = new LineDialog(_frame, latLines, lonLines);
				dialog.showDialog();
			}
		});
		panel.add(showLinesButton);
		// Close button
		JButton closeButton = new JButton(I18nManager.getText("button.close"));
		closeButton.addActionListener(new ActionListener()
		{
			/** Close button pressed - clean up */
			public void actionPerformed(ActionEvent e)
			{
				_frame.dispose();
				_frame = null;
				_orbit = null;
			}
		});
		panel.add(closeButton);
		_frame.getContentPane().add(panel, BorderLayout.SOUTH);
		_frame.setSize(500, 350);
		_frame.pack();
		// Add a listener to clean up when window closed
		_frame.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent e)
			{
				dispose();
			}
		});

		// show frame
		_frame.setVisible(true);
		if (_frame.getState() == JFrame.ICONIFIED)
		{
			_frame.setState(JFrame.NORMAL);
		}
	}

	/**
	 * Dispose of the frame and its resources
	 */
	public void dispose()
	{
		if (_frame != null) {
			_frame.dispose();
			_frame = null;
		}
	}

	/**
	 * Create the whole scenery from the given track
	 * @return all objects in the scene
	 */
	private BranchGroup createSceneGraph()
	{
		// Create the root of the branch graph
		BranchGroup objRoot = new BranchGroup();

		// Create the transform group node and initialize it.
		// Enable the TRANSFORM_WRITE capability so it can be spun by the mouse
		TransformGroup objTrans = new TransformGroup();
		objTrans.setCapability(TransformGroup.ALLOW_TRANSFORM_WRITE);

		// Create a translation
		Transform3D shiftz = new Transform3D();
		shiftz.setScale(0.055);
		TransformGroup shiftTrans = new TransformGroup(shiftz);

		objRoot.addChild(shiftTrans);
		Transform3D rotTrans = new Transform3D();
		rotTrans.rotY(Math.toRadians(INITIAL_Y_ROTATION));
		Transform3D rot2 = new Transform3D();
		rot2.rotX(Math.toRadians(INITIAL_X_ROTATION));
		TransformGroup tg2 = new TransformGroup(rot2);
		objTrans.setTransform(rotTrans);
		shiftTrans.addChild(tg2);
		tg2.addChild(objTrans);

		// Base plane
		Appearance planeAppearance = null;
		Box plane = null;
		planeAppearance = new Appearance();
		planeAppearance.setMaterial(new Material(new Color3f(0.1f, 0.2f, 0.2f),
		 new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.3f, 0.4f, 0.4f),
		 new Color3f(0.3f, 0.3f, 0.3f), 0.0f));
		plane = new Box(10f, 0.04f, 10f, planeAppearance);
		objTrans.addChild(plane);

		// N, S, E, W
		GeneralPath bevelPath = new GeneralPath();
		bevelPath.moveTo(0.0f, 0.0f);
		for (int i=0; i<91; i+= 5)
			bevelPath.lineTo((float) (0.1 - 0.1 * Math.cos(Math.toRadians(i))),
			  (float) (0.1 * Math.sin(Math.toRadians(i))));
		for (int i=90; i>0; i-=5)
			bevelPath.lineTo((float) (0.3 + 0.1 * Math.cos(Math.toRadians(i))),
			  (float) (0.1 * Math.sin(Math.toRadians(i))));
		Font3D compassFont = new Font3D(
			new Font(CARDINALS_FONT, Font.PLAIN, 1),
			new FontExtrusion(bevelPath));
		objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.n"), new Point3f(0f, 0f, -10f), compassFont));
		objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.s"), new Point3f(0f, 0f, 10f), compassFont));
		objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.w"), new Point3f(-11f, 0f, 0f), compassFont));
		objTrans.addChild(createCompassPoint(I18nManager.getText("cardinal.e"), new Point3f(10f, 0f, 0f), compassFont));

		// create and scale model
		_model = new ThreeDModel(_track);
		_model.setAltitudeCap(_altitudeCap);
		_model.scale();

		// Lat/Long lines
		objTrans.addChild(createLatLongs(_model));

		// Add points to model
		objTrans.addChild(createDataPoints(_model));

		// Create lights
		BoundingSphere bounds =
		  new BoundingSphere(new Point3d(0.0,0.0,0.0), 100.0);
		AmbientLight aLgt = new AmbientLight(new Color3f(1.0f, 1.0f, 1.0f));
		aLgt.setInfluencingBounds(bounds);
		objTrans.addChild(aLgt);

		PointLight pLgt = new PointLight(new Color3f(1.0f, 1.0f, 1.0f),
		 new Point3f(0f, 0f, 2f),
		 new Point3f(0.25f, 0.05f, 0.0f) );
		pLgt.setInfluencingBounds(bounds);
		objTrans.addChild(pLgt);

		PointLight pl2 = new PointLight(new Color3f(0.8f, 0.9f, 0.4f),
		 new Point3f(6f, 1f, 6f),
		 new Point3f(0.2f, 0.1f, 0.05f) );
		pl2.setInfluencingBounds(bounds);
		objTrans.addChild(pl2);

		PointLight pl3 = new PointLight(new Color3f(0.7f, 0.7f, 0.7f),
		 new Point3f(0.0f, 12f, -2f),
		 new Point3f(0.1f, 0.1f, 0.0f) );
		pl3.setInfluencingBounds(bounds);
		objTrans.addChild(pl3);

		// Have Java 3D perform optimizations on this scene graph.
		objRoot.compile();

		return objRoot;
	}


	/**
	 * Create a text object for compass point, N S E or W
	 * @param text text to display
	 * @param locn position at which to display
	 * @param font 3d font to use
	 * @return Shape3D object
	 */
	private Shape3D createCompassPoint(String inText, Point3f inLocn, Font3D inFont)
	{
		Text3D txt = new Text3D(inFont, inText, inLocn, Text3D.ALIGN_FIRST, Text3D.PATH_RIGHT);
		Material mat = new Material(new Color3f(0.5f, 0.5f, 0.55f),
		 new Color3f(0.05f, 0.05f, 0.1f), new Color3f(0.3f, 0.4f, 0.5f),
		 new Color3f(0.4f, 0.5f, 0.7f), 70.0f);
		mat.setLightingEnable(true);
		Appearance app = new Appearance();
		app.setMaterial(mat);
		Shape3D shape = new Shape3D(txt, app);
		return shape;
	}


	/**
	 * Create all the latitude and longitude lines on the base plane
	 * @param inModel model containing data
	 * @return Group object containing cylinders for lat and long lines
	 */
	private static Group createLatLongs(ThreeDModel inModel)
	{
		Group group = new Group();
		int numlines = inModel.getLatitudeLines().length;
		for (int i=0; i<numlines; i++)
		{
			group.addChild(createLatLine(inModel.getScaledLatitudeLine(i), inModel.getModelSize()));
		}
		numlines = inModel.getLongitudeLines().length;
		for (int i=0; i<numlines; i++)
		{
			group.addChild(createLonLine(inModel.getScaledLongitudeLine(i), inModel.getModelSize()));
		}
		return group;
	}


	/**
	 * Make a single latitude line for the specified latitude
	 * @param inLatitude latitude in scaled units
	 * @param inSize size of model, for length of line
	 * @return Group object containing cylinder for latitude line
	 */
	private static Group createLatLine(double inLatitude, double inSize)
	{
		Cylinder latline = new Cylinder(0.1f, (float) (inSize*2));
		Transform3D horizShift = new Transform3D();
		horizShift.setTranslation(new Vector3d(0.0, 0.0, inLatitude));
		TransformGroup horizTrans = new TransformGroup(horizShift);
		Transform3D zRot = new Transform3D();
		zRot.rotZ(Math.toRadians(90.0));
		TransformGroup zTrans = new TransformGroup(zRot);
		horizTrans.addChild(zTrans);
		zTrans.addChild(latline);
		return horizTrans;
	}


	/**
	 * Make a single longitude line for the specified longitude
	 * @param inLongitude longitude in scaled units
	 * @param inSize size of model, for length of line
	 * @return Group object containing cylinder for longitude line
	 */
	private static Group createLonLine(double inLongitude, double inSize)
	{
		Cylinder lonline = new Cylinder(0.1f, (float) (inSize*2));
		Transform3D horizShift = new Transform3D();
		horizShift.setTranslation(new Vector3d(inLongitude, 0.0, 0.0));
		TransformGroup horizTrans = new TransformGroup(horizShift);
		Transform3D xRot = new Transform3D();
		xRot.rotX(Math.toRadians(90.0));
		TransformGroup xTrans = new TransformGroup(xRot);
		horizTrans.addChild(xTrans);
		xTrans.addChild(lonline);
		return horizTrans;
	}


	/**
	 * Make a Group of the data points to be added
	 * @param inModel model containing data
	 * @return Group object containing spheres, rods etc
	 */
	private static Group createDataPoints(ThreeDModel inModel)
	{
		// Add points to model
		Group group = new Group();
		int numPoints = inModel.getNumPoints();
		for (int i=0; i<numPoints; i++)
		{
			byte pointType = inModel.getPointType(i);
			if (pointType == ThreeDModel.POINT_TYPE_WAYPOINT)
			{
				// Add waypoint
				// Note that x, y and z are horiz, altitude, -vert
				group.addChild(createWaypoint(new Point3d(
					inModel.getScaledHorizValue(i), inModel.getScaledAltValue(i), -inModel.getScaledVertValue(i))));
			}
			else
			{
				// Add colour-coded track point
				// Note that x, y and z are horiz, altitude, -vert
				group.addChild(createTrackpoint(new Point3d(
					inModel.getScaledHorizValue(i), inModel.getScaledAltValue(i), -inModel.getScaledVertValue(i)),
					inModel.getPointHeightCode(i)));
			}
		}
		return group;
	}


	/**
	 * Create a waypoint sphere
	 * @param inPointPos position of point
	 * @return Group object containing sphere
	 */
	private static Group createWaypoint(Point3d inPointPos)
	{
		Material mat = getWaypointMaterial();
		// MAYBE: sort symbol scaling
		Sphere dot = new Sphere(0.35f); // * symbolScaling / 100f);
		return createBall(inPointPos, dot, mat);
	}


	/**
	 * @return a new Material object to define waypoint colour / shine etc
	 */
	private static Material getWaypointMaterial()
	{
		return new Material(new Color3f(0.1f, 0.1f, 0.4f),
			 new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.0f, 0.2f, 0.7f),
			 new Color3f(1.0f, 0.6f, 0.6f), 40.0f);
	}


	private static Group createTrackpoint(Point3d inPointPos, byte inHeightCode)
	{
		Material mat = getTrackpointMaterial(inHeightCode);
		// MAYBE: sort symbol scaling
		Sphere dot = new Sphere(0.2f); // * symbolScaling / 100f);
		return createBall(inPointPos, dot, mat);
	}


	private static Material getTrackpointMaterial(byte inHeightCode)
	{
		// create default material
		Material mat = new Material(new Color3f(0.3f, 0.2f, 0.1f),
			new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.0f, 0.6f, 0.0f),
			new Color3f(1.0f, 0.6f, 0.6f), 70.0f);
		// change colour according to height code
		if (inHeightCode == 1) mat.setDiffuseColor(new Color3f(0.4f, 0.9f, 0.2f));
		if (inHeightCode == 2) mat.setDiffuseColor(new Color3f(0.7f, 0.8f, 0.2f));
		if (inHeightCode == 3) mat.setDiffuseColor(new Color3f(0.5f, 0.85f, 0.95f));
		if (inHeightCode == 4) mat.setDiffuseColor(new Color3f(0.1f, 0.9f, 0.9f));
		if (inHeightCode >= 5) mat.setDiffuseColor(new Color3f(1.0f, 1.0f, 1.0f));
		// return object
		return mat;
	}


	/**
	 * Create a ball at the given point
	 * @param inPosition scaled position of point
	 * @param inSphere sphere object
	 * @param inMaterial material object
	 * @return Group containing sphere
	 */
	private static Group createBall(Point3d inPosition, Sphere inSphere, Material inMaterial)
	{
		Group group = new Group();
		// Create ball and add to group
		Transform3D ballShift = new Transform3D();
		ballShift.setTranslation(new Vector3d(inPosition));
		TransformGroup ballShiftTrans = new TransformGroup(ballShift);
		inMaterial.setLightingEnable(true);
		Appearance ballApp = new Appearance();
		ballApp.setMaterial(inMaterial);
		inSphere.setAppearance(ballApp);
		ballShiftTrans.addChild(inSphere);
		group.addChild(ballShiftTrans);
		// Also create rod for ball to sit on
		Cylinder rod = new Cylinder(0.1f, (float) inPosition.y);
		Material rodMat = new Material(new Color3f(0.2f, 0.2f, 0.2f),
		 new Color3f(0.0f, 0.0f, 0.0f), new Color3f(0.2f, 0.2f, 0.2f),
		 new Color3f(0.05f, 0.05f, 0.05f), 0.4f);
		rodMat.setLightingEnable(true);
		Appearance rodApp = new Appearance();
		rodApp.setMaterial(rodMat);
		rod.setAppearance(rodApp);
		Transform3D rodShift = new Transform3D();
		rodShift.setTranslation(new Vector3d(inPosition.x,
		 inPosition.y/2.0, inPosition.z));
		TransformGroup rodShiftTrans = new TransformGroup(rodShift);
		rodShiftTrans.addChild(rod);
		group.addChild(rodShiftTrans);
		// return the pair
		return group;
	}


	/**
	 * Calculate the angles and call them back to the app
	 */
	private void callbackRender()
	{
		Transform3D trans3d = new Transform3D();
		_orbit.getViewingPlatform().getViewPlatformTransform().getTransform(trans3d);
		Matrix3d matrix = new Matrix3d();
		trans3d.get(matrix);
		Point3d point = new Point3d(0.0, 0.0, 1.0);
		matrix.transform(point);
		// Set up initial rotations
		Transform3D firstTran = new Transform3D();
		firstTran.rotY(Math.toRadians(-INITIAL_Y_ROTATION));
		Transform3D secondTran = new Transform3D();
		secondTran.rotX(Math.toRadians(-INITIAL_X_ROTATION));
		// Apply inverse rotations in reverse order to test point
		Point3d result = new Point3d();
		secondTran.transform(point, result);
		firstTran.transform(result);
		// Callback settings to pov export function
		FunctionLibrary.FUNCTION_POVEXPORT.setCameraCoordinates(result.x, result.y, result.z);
		FunctionLibrary.FUNCTION_POVEXPORT.setAltitudeCap(_altitudeCap);
		FunctionLibrary.FUNCTION_POVEXPORT.begin();
	}


	/**
	 * Get a units label for the altitudes in the given Track
	 * @param inTrack Track object
	 * @return units label for altitude used in Track
	 */
	private static String getAltitudeUnitsLabel(Track inTrack)
	{
		Altitude.Format altitudeFormat = inTrack.getAltitudeRange().getFormat();
		if (altitudeFormat == Altitude.Format.METRES)
			return I18nManager.getText("units.metres.short");
		return I18nManager.getText("units.feet.short");
	}

}