//////////
//
//	File:		VRMakePano.c
//
//	Contains:	Code for creating a QuickTime VR panoramic movie from a panoramic image.
//
//	Written by:	Tim Monroe
//				Based largely on MakeQTVRPanorama code by Ed Harp (and others?). Cubic projection code
//				based on SampleMakeCubic.cp code by Ken Doyle.
//
//	Copyright:	© 1996-2000 by Apple Computer, Inc., all rights reserved.
//
//	Change History (most recent first):
//
//	   <18>	 	10/25/00	rtm		added VRPano_FlattenMovieForStreaming; fixed VRPano_CreateQTVRMovieVers1x0
//									so it works even if there is no hot spot track
//	   <17>	 	10/18/00	rtm		fixed bug in VRPano_ImportVideoTrack (had a hard-coded number of frames)
//	   <16>	 	09/27/00	rtm		modified VRPano_AddStr255ToAtomContainer so it doesn't add a null byte to
//									the string in the atom container; also added some bullet-proofing
//	   <15>	 	06/14/00	rtm		modified VRPano_ImportVideoTrack as per Ken Doyle's changes in MakeCubicPPC
//	   <14>	 	03/23/00	rtm		added code to handle new cubic file format
//	   <13>	 	03/21/00	rtm		made changes to get things running under CarbonLib
//	   <12>	 	11/24/99	rtm		reworked file opening and saving to avoid using Standard File Package
//									on Macintosh (for Carbonization effort)
//	   <11>	 	02/01/99	rtm		reworked prompt and filename handling to remove "\p" sequences
//	   <10>	 	10/19/98	rtm		added check in VRPano_CreatePanoTrack to make sure we were passed a
//									valid hot spot file spec (otherwise no panorama is created!); added
//									the default movie progress procedure
//	   <9>	 	09/30/98	rtm		tweaked call to AddMovieResource to create single-fork movies;
//									tweaked call to FlattenMovieData to enable FastStart option
//	   <8>	 	08/06/98	rtm		added VRPano_MakeHotSpotVers2x0 (based loosely on code from John Mott)
//									to support version 2.0 hot spot atoms; sketched out preliminary
//									support for 1.0 hot spots, but that's left as an exercise for the reader
//	   <7>	 	08/05/98	rtm		added support for hot spots image tracks (both version 1.0 and 2.0)
//	   <6>	 	08/26/98	rtm		added support for pan/tilt/field-of-view constraint atoms
//	   <5>	 	08/18/98	rtm		added support for version 1.0 panoramic movies
//	   <4>	 	08/16/98	rtm		final clean-up before initial release
//	   <3>	 	08/15/98	rtm		reworked the image file handling using QuickTime graphics importers;
//									added tile-display window for debugging purposes;
//									now we run fine under both MacOS and Windows. Ta da!
//	   <2>	 	01/14/98	rtm		further work; added Endian* macros; got working on MacOS
//	   <1>	 	01/12/98	rtm		first file from CMovieMaker.cp in MakeQTVRPanorama 2.0b
//
//	This file contains functions that convert a panoramic image into a QuickTime VR movie. The image
//	can be a picture (of type 'PICT') or any other kind of image for which QuickTime has a graphics
//	importer component. Here, we can create both version 1.0 and version 2.0 QTVR panoramic movies.
//	We can also create hot spot image tracks and (for version 2.0 only) assemble the various hot spot
//	atoms.
//
//	This file also contains a function that constructs a QuickTime VR movie that uses the new cubic
//	projection. In the following notes, we'll call such movies "cubic movies" or "cubic panoramas".
//
//	VERSION 2.0 FILE FORMAT
//
//	The definitive source of information about creating QTVR 2.0 panoramic movies is Chapter 3 of the
//	book "Virtual Reality Programming With QuickTime VR 2.0". (This information is also available
//	online, at <http://dev.info.apple.com/dev/techsupport/insidemac/qtvr/qtvrapi-2.html>.) Here is
//	a condensed version of the info in that chapter, as pertains to panoramas:
//
//	A panoramic movie is a QuickTime movie that contains at least three tracks: a QTVR track, a panorama
//	track, and a panorama image track. In addition, a QuickTime VR movie must contain some special user data
//	that specifies the QuickTime VR movie controller. A QuickTime VR movie can also contain other kinds of
//	tracks, such as hot spot image tracks and even sound tracks.
//
//	A QuickTime VR movie contains a single "QTVR track", which maintains a list of the nodes in the movie.
//	Each individual sample in the QTVR track's media contains information about a single node, such as the
//	node's type, ID, and name. Since we are creating a single-node movie here, our QTVR track will contain
//	a single media sample. 
//
//	Every media sample in a QTVR track has the same sample description, whose type is QTVRSampleDescription.
//	The data field of that sample description is a "VR world", an atom container whose child atoms specify
//	information about the nodes in the movie, such as the default node ID and the default imaging properties.
//	We'll spend a good bit of time putting things into the VR world.
//
//	A panoramic movie also contains a single "panorama track", which contains information specific to the
//	panorama. A panorama track has a media sample for each media sample in the QTVR track. As a result,
//	our panorama track will have one sample. The QTVRPanoSampleAtom structure defines the media sample data. 
//
//	The actual image data for a panoramic node is contained in a "panorama image track". The individual
//	frames in that track are the diced (and also perhaps compressed) tiles of the original panoramic image.
//	There may also be a "hot spot image track" that contains the diced (and also perhaps compressed) tiles
//	of the hot spot panoramic image.
//
//	So, our general strategy, given a panoramic image, is as follows (though perhaps not in the order listed):
//
//		(1) Create a movie containing a video track whose frames are the compressed tiles of the panoramic
//	        image. Call this movie the "tile movie". Create a similar movie for the hot spot image. Call
//			this movie the "hot spot tile movie".
//		(2) Create a new, empty movie. Call this movie the "QTVR movie".
//		(3) Create a QTVR track and its associated media.
//		(4) Create a VR world atom container; this is stored in the sample description for the QTVR track.
//		(5) Create a node information atom container for each node; this is stored as a media sample
//	        in the QTVR track.
//		(6) Create a panorama track and add it to the movie.
//		(7)	Create a panorama image track by copying the video track from the tile movie to the QTVR movie.
//		(8)	Create a hot spot image track by copying the video track from the hot spot tile movie to the QTVR movie.
//		(9) Set up track references from the QTVR track to the panorama track, and from the panorama track
//	        to the panorama image track and the hot spot image track.
//		(A) Add a user data item that identifies the QTVR movie controller.
//		(B) Flatten the QTVR movie into the final panoramic movie.
//
//
//	VERSION 1.0 FILE FORMAT
//
//	The definitive source of information about creating QTVR 1.0 panoramic movies is Technote 1035,
//	"QuickTime VR 1.0 Panorama Movie File Format" released in March 1996, available online at the address
//	<http://devworld.apple.com/dev/technotes/tn/tn1035.html>. Here is a condensed version of the info
//	in that technote:
//
//	For version 1.0 panoramic movies, the file format is somewhat simpler. A single-node panoramic movie
//	contains a "scene track" and a "panorama track". The scene track is an inactive video track that contains
//	the diced (and also perhaps compressed) tiles of the original panoramic image. A version 1.0 scene track
//	is essentially a version 2.0 panorama image track.
//
//	A panoramic movie also contains a single "panorama track", which contains information specific to the
//	panorama. The panorama track in version 1.0 differs significantly from the panorama track in version 2.0,
//	so don't let the names confuse you. 
//
//	A "panorama track" in version 1.0 contains one media sample for each node in the movie. Every media sample
//	in a panorama track has the same sample description, whose type is PanoramaDescription. This structure
//	contains information about how the panoramic image was diced, along with information about any hot spot
//	and low-resolution scene tracks that might be contained in the movie file.
//
//	Each individual sample in the panorama track's media contains information about a single node,
//	such as the node's default view angles, pan and zoom constraints, and hot spot information.
//	Since we are creating a single-node movie here, our panorama track will contain a single media sample. 
//	The data in a panorama track sample is organized as a sequence of "atom data structures"; each such
//	structure begins with size and type fields, so you can easily read thru the atom data structures by
//	starting at the beginning of the sample data and hopping over each structure. The atoms can appear in
//	any order in the panorama track sample data.
//
//	-> IMPORTANT: The "atom data structures" that comprise a panorama track sample are *not* QTAtoms, and the
//	-> panorama track sample itself is *not* a QTAtomContainer. The version 1.0 panorama file format predates
//	-> QuickTime 2.1, which introduced the atom data routines. As a result, you cannot use the atom data routines
//	-> to read or write version 1.0 panorama track samples.
//
//	A panorama track sample must contain a "panorama header atom", whose structure is PanoSampleHeaderAtom.
//	The sample may contain other atoms as well, such as a string table atom (which contains strings, such as
//	node names) and a hot spot table atom (which lists all of the hot spots in a node). In this sample code,
//	we add a hot spot image track to the VR movie, but we do not (yet) build a hot spot atom table; as a result,
//	our panorama track sample will need to contain only a panorama header atom.
//
//	So, our general strategy, given a panoramic image, is as follows (though perhaps not in the order listed):
//
//		(1) Create a movie containing a video track whose frames are the compressed tiles of the panoramic
//	        image. Call this movie the "tile movie". Create a similar movie for the hot spot image. Call
//			this movie the "hot spot tile movie".
//		(2) Create a new, empty movie. Call this movie the "QTVR movie".
//		(3)	Create a scene track by copying the video track from the tile movie to the QTVR movie.
//		(4)	Create a hot spot image track by copying the video track from the hot spot tile movie to the QTVR movie.
//		(5) Create a panorama track and add it to the movie.
//		(6)	Add a user data item that identifies the QTVR movie controller.
//		(7) Flatten the QTVR movie into the final panoramic movie.
//
//
//	CUBIC PANORAMA FILE FORMAT
//
//	QuickTime VR version X.Y introduces "cubic panoramas", which store the panoramic image as 6 separate images that
//	are (during playback) projected onto the sides of a cube. This allows the user to look straight up and straight
//	down. The file format for cubic panoramas is identical with the Version 2.0 cylindrical file format, with these
//	exceptions:
//
//	The image track (and any associated hot spot and preview tracks) must contain exactly 6 samples (frames); the first
//	sample is the front cube face, the second is the right-hand cube face, and so on; sample 5 is the top of the cube
//	and sample 6 is the bottom of the cube. (You can change this default orientation, but doing so is not currently
//	recommended.) The frames are oriented horizontally, not rotated as with cylindrical panoramas.
//
//	For cubic panoramas, some of the fields in the panorama sample atom should be asigned special values that allow
//	the file to be displayed with the cylindrical engine if the cubic engine is not available. The cubic engine ignores
//	those values, instead using values stored in the new "cubic view atom". See the function VRPano_CreatePanoTrack for
//	the special values you should use.
//
//
//	NOTES:
//
//	*** (1) ***
//	This code is based largely on the existing MakeQTVRPanorama sample code written by Ed Harp (and others?).
//	MakeQTVRPanorama is a full-featured application written in C++ using Metrowerks' PowerPlant, a Mac-only
//	application framework. Here I've uncoupled the central portion of that code, contained in the file CMovieMaker.cp,
//	and converted it into straight C. I have taken the liberty of reworking that code as necessary to make it run
//	also on Windows platforms and to bring it into line with the other QuickTime code samples. So far the biggest
//	changes involve using graphics importers instead of the original PICT-reading code and inserting all those
//	Endian macros.
//
//	*** (2) ***
//	All data in QTAtom structures must be in big-endian format. We use macros like EndianU32_NtoB to convert
//	values into the proper format before inserting them into atoms. See VRPano_CreateVRWorld for some examples.
//
//	*** (3) ***
//	This sample code prompts the user for a panoramic image file (which is required) and a hot spot image file
//	(which is optional). If the user hits the Cancel button while being prompted for a hot spot image file, no
//	hot spot image track will be added to the resulting VR movie file. If the user does select a hot spot image
//	file, this sample code builds the hot spot info atoms (version 2.0) or the hot spot table atom (version 1.0)
//	required to attach names and other information to the individual hot spots. 
//
//////////

//////////
//	   
// header files
//	   
//////////

#include "VRMakePano.h"


//////////
//	   
// global variables
//	   
//////////

UInt32						gVersionToCreate = kQTVRVersion2;			// the version of the file format we create; default to version 2.0

extern Handle 				gValidFileTypes;							// the list of file types that our application can open
extern long					gFirstGITypeIndex;							// the index in gValidFileTypes of the first graphics importer file type


//////////
//
// VRPano_PromptUserForImageFileAndMakeMovie
// Let the user select a panoramic image file, then make a (cylindrical) panoramic movie from it.
//
//////////

void VRPano_PromptUserForImageFileAndMakeMovie (void)
{
	OSType 					myTypeList = kQTFileTypeQuickTimeImage;		// any image file readable by GetGraphicsImporterForFile
	short					myNumTypes = 1;
	FSSpec					myPictSpec;
	FSSpec					myHSPictSpec;
	FSSpec					myTileSpec;
	FSSpec					myDestSpec;
	StringPtr 				myTilePrompt = QTUtils_ConvertCToPascalString(kPanoSaveTilePrompt);
	StringPtr 				myTileFileName = QTUtils_ConvertCToPascalString(kPanoSaveTileFileName);
	StringPtr 				myMoviePrompt = QTUtils_ConvertCToPascalString(kPanoSaveMoviePrompt);
	StringPtr 				myMovieFileName = QTUtils_ConvertCToPascalString(kPanoSaveMovieFileName);
	Boolean					myIsSelected;
	Boolean					myIsReplacing;
	QTFrameFileFilterUPP	myFilterUPP = QTFrame_GetFileFilterUPP((ProcPtr)VRPano_FileFilterFunction);
	OSErr					myErr = noErr;

#if TARGET_OS_MAC
	myNumTypes = 0;
#endif

	// have the user select an image file
	myErr = QTFrame_GetOneFileWithPreview(myNumTypes, (QTFrameTypeListPtr)&myTypeList, &myPictSpec, myFilterUPP);
	if (myErr != noErr)
		goto bail;

	// have the user select a hot spot image file
	myErr = QTFrame_GetOneFileWithPreview(myNumTypes, (QTFrameTypeListPtr)&myTypeList, &myHSPictSpec, myFilterUPP);
	if (myErr != noErr) {
		// we allow the user to cancel, to indicate no hot spot image
		myHSPictSpec.vRefNum = 0;
		myHSPictSpec.parID = 0;
		myHSPictSpec.name[0] = 0;		// set length to 0
	}
		
	// have the user select the name of the new tile movie file
	myErr = QTFrame_PutFile(myTilePrompt, myTileFileName, &myTileSpec, &myIsSelected, &myIsReplacing);
	if (!myIsSelected)
		goto bail;

	// have the user select the name of the new panoramic movie file
	myErr = QTFrame_PutFile(myMoviePrompt, myMovieFileName, &myDestSpec, &myIsSelected, &myIsReplacing);
	if (!myIsSelected)
		goto bail;

	// just do it...
	VRPano_MakePanorama(&myPictSpec, &myHSPictSpec, &myTileSpec, &myDestSpec, kPanoramaWidth, kPanoramaHeight, kCinepakCodecType, codecHighQuality, gVersionToCreate);

	// ...and let the user know we're done
	QTFrame_Beep();
	
	// now clean up after ourselves
	DeleteMovieFile(&myTileSpec);

bail:
	DisposeNavObjectFilterUPP(myFilterUPP);
	free(myTilePrompt);
	free(myTileFileName);
	free(myMoviePrompt);
	free(myMovieFileName);
}


//////////
//
// VRPano_PromptUserForFacesFileAndMakeCubic
// Let the user select a 6-frame movie file, then make a cubic panorama from it.
//
// Currently we do not support adding a hot spot track.
//
//////////

void VRPano_PromptUserForFacesFileAndMakeCubic (void)
{
	OSType 					myTypeList = kQTFileTypeMovie;
	short					myNumTypes = 1;
	FSSpec					myFacesSpec;
	FSSpec					myHSFacesSpec = {0, 0, 0};
	FSSpec					myDestSpec;
	Movie					myMovie = NULL;
	Track					myTrack = NULL;
	Media					myMedia = NULL;
	short					myRefNum = kInvalidFileRefNum;
	StringPtr 				myMoviePrompt = QTUtils_ConvertCToPascalString(kPanoSaveMoviePrompt);
	StringPtr 				myMovieFileName = QTUtils_ConvertCToPascalString(kPanoSaveMovieFileName);
	Boolean					myIsSelected;
	Boolean					myIsReplacing;
	QTFrameFileFilterUPP	myFilterUPP = QTFrame_GetFileFilterUPP((ProcPtr)VRPano_FileFilterFunction);
	OSErr					myErr = noErr;

#if TARGET_OS_MAC
	myNumTypes = 0;
#endif

	// have the user select a 6-frame movie file
	myErr = QTFrame_GetOneFileWithPreview(myNumTypes, (QTFrameTypeListPtr)&myTypeList, &myFacesSpec, myFilterUPP);
	if (myErr != noErr)
		goto bail;

	// verify that the selected movie has a video track with exactly 6 samples
	// (which are the six faces of the cube)
	myErr = OpenMovieFile(&myFacesSpec, &myRefNum, fsRdPerm);
	if (myErr != noErr)
		goto bail;
	
	// now fetch the first movie from the file
	myErr = NewMovieFromFile(&myMovie, myRefNum, NULL, NULL, 0, NULL);
	if (myErr != noErr)
		goto bail;

	myTrack = GetMovieIndTrackType(myMovie, 1, VideoMediaType, movieTrackMediaType);
	myMedia = GetTrackMedia(myTrack);
	if (GetMediaSampleCount(myMedia) != 6) {
#if TARGET_OS_MAC
		QTFrame_ShowWarning("\pMovie doesn't have exactly 6 frames. Exiting.", myErr);
#endif
#if TARGET_OS_WIN32
		QTFrame_ShowCautionAlert(NULL, IDS_NOT_6_SAMPLES, MB_ICONEXCLAMATION, MB_OK, "", "");
#endif
		goto bail;
	}
		
	// have the user select the name of the new cubic movie file
	myErr = QTFrame_PutFile(myMoviePrompt, myMovieFileName, &myDestSpec, &myIsSelected, &myIsReplacing);
	if (!myIsSelected)
		goto bail;

	// just do it...
	VRPano_CreateCubicQTVRMovie(&myDestSpec, &myFacesSpec, &myHSFacesSpec, kPanoramaHeight, kPanoramaWidth);

	// ...and let the user know we're done
	QTFrame_Beep();
	
bail:
	if (myMovie != NULL)
		DisposeMovie(myMovie);
		
	DisposeNavObjectFilterUPP(myFilterUPP);
	free(myMoviePrompt);
	free(myMovieFileName);
}


//////////
//
// VRPano_CreateVRWorld
// Create a VR world atom container and add the basic required atoms to it. Also, create a
// node information atom container and add a node header atom to it. Return both atom containers.
//
// The caller is responsible for disposing of the VR world and the node information atom
// (by calling QTDisposeAtomContainer).
//
// This function assumes that the scene described by the VR world contains a single node whose
// type is specified by the theNodeType parameter.
//
//////////

OSErr VRPano_CreateVRWorld (QTAtomContainer *theVRWorld, QTAtomContainer *theNodeInfo, OSType theNodeType)
{
	QTAtomContainer			myVRWorld = NULL;
	QTAtomContainer			myNodeInfo = NULL;
	QTVRWorldHeaderAtom		myVRWorldHeaderAtom;
	QTAtom					myImagingParentAtom;
	QTAtom					myNodeParentAtom;
	QTAtom					myHSParentAtom;
	QTAtom					myNodeAtom;
	QTVRPanoImagingAtom		myPanoImagingAtom;
	QTVRNodeLocationAtom	myNodeLocationAtom;
	QTVRNodeHeaderAtom		myNodeHeaderAtom;
	UInt32					myIndex;
	OSErr					myErr = noErr;

	//////////
	//
	// create a VR world atom container
	//
	//////////

	myErr = QTNewAtomContainer(&myVRWorld);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// add a VR world header atom to the VR world
	//
	//////////

	myVRWorldHeaderAtom.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
	myVRWorldHeaderAtom.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);

	// insert the scene name string, if we have one; if not, set nameAtomID to 0
	if (false) {
		Str255				myStr = "\pMy Scene";
		QTAtomID			myID;
		
		myErr = VRPano_AddStr255ToAtomContainer(myVRWorld, kParentAtomIsContainer, myStr, &myID);
		myVRWorldHeaderAtom.nameAtomID = EndianU32_NtoB(myID);
	} else
		myVRWorldHeaderAtom.nameAtomID = EndianU32_NtoB(0L);
	
	myVRWorldHeaderAtom.defaultNodeID = EndianU32_NtoB(kDefaultNodeID);
	myVRWorldHeaderAtom.vrWorldFlags = EndianU32_NtoB(0L);
	myVRWorldHeaderAtom.reserved1 = EndianU32_NtoB(0L);
	myVRWorldHeaderAtom.reserved2 = EndianU32_NtoB(0L);

	// add the atom to the atom container (the VR world)
	myErr = QTInsertChild(myVRWorld, kParentAtomIsContainer, kQTVRWorldHeaderAtomType, 1, 1, sizeof(QTVRWorldHeaderAtom), &myVRWorldHeaderAtom, NULL);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add an imaging parent atom to the VR world and insert imaging atoms into it
	//
	// imaging atoms describe the default imaging characteristics for the different types of nodes in the scene;
	// currently, only the panorama imaging atoms are defined, so we'll include those (even in object movies)
	//
	//////////
	
	myErr = QTInsertChild(myVRWorld, kParentAtomIsContainer, kQTVRImagingParentAtomType, 1, 1, 0, NULL, &myImagingParentAtom);
	if (myErr != noErr)
		goto bail;
		
	// fill in the fields of the panorama imaging atom structure
	myPanoImagingAtom.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
	myPanoImagingAtom.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);
	myPanoImagingAtom.correction = EndianU32_NtoB(kQTVRFullCorrection);
	myPanoImagingAtom.imagingValidFlags = EndianU32_NtoB(kQTVRValidCorrection | kQTVRValidQuality | kQTVRValidDirectDraw);
	for (myIndex = 0; myIndex < 6; myIndex++)
		myPanoImagingAtom.imagingProperties[myIndex] = EndianU32_NtoB(0L);
	myPanoImagingAtom.reserved1 = EndianU32_NtoB(0L);
	myPanoImagingAtom.reserved2 = EndianU32_NtoB(0L);
	
	// add a panorama imaging atom for kQTVRMotion state
	myPanoImagingAtom.quality = EndianU32_NtoB(codecLowQuality);
	myPanoImagingAtom.directDraw = EndianU32_NtoB(true);
	myPanoImagingAtom.imagingMode = EndianU32_NtoB(kQTVRMotion);
	myErr = QTInsertChild(myVRWorld, myImagingParentAtom, kQTVRPanoImagingAtomType, 0, 0, sizeof(QTVRPanoImagingAtom), &myPanoImagingAtom, NULL);
	if (myErr != noErr)
		goto bail;
		
	// add a panorama imaging atom for kQTVRStatic state
	myPanoImagingAtom.quality = EndianU32_NtoB(codecHighQuality);
	myPanoImagingAtom.directDraw = EndianU32_NtoB(false);
	myPanoImagingAtom.imagingMode = EndianU32_NtoB(kQTVRStatic);
	myErr = QTInsertChild(myVRWorld, myImagingParentAtom, kQTVRPanoImagingAtomType, 0, 0, sizeof(QTVRPanoImagingAtom), &myPanoImagingAtom, NULL);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add a node parent atom to the VR world and insert node ID atoms into it
	//
	//////////
	
	myErr = QTInsertChild(myVRWorld, kParentAtomIsContainer, kQTVRNodeParentAtomType, 1, 1, 0, NULL, &myNodeParentAtom);
	if (myErr != noErr)
		goto bail;
		
	// add a node ID atom
	myErr = QTInsertChild(myVRWorld, myNodeParentAtom, kQTVRNodeIDAtomType, kDefaultNodeID, 0, 0, NULL, &myNodeAtom);
	if (myErr != noErr)
		goto bail;
	
	// add a single node location atom to the node ID atom
	myNodeLocationAtom.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
	myNodeLocationAtom.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);
	myNodeLocationAtom.nodeType = EndianU32_NtoB(theNodeType);
	myNodeLocationAtom.locationFlags = EndianU32_NtoB(kQTVRSameFile);
	myNodeLocationAtom.locationData = EndianU32_NtoB(0);
	myNodeLocationAtom.reserved1 = EndianU32_NtoB(0);
	myNodeLocationAtom.reserved2 = EndianU32_NtoB(0);
	
	// insert the node location atom into the node ID atom
	myErr = QTInsertChild(myVRWorld, myNodeAtom, kQTVRNodeLocationAtomType, 1, 1, sizeof(QTVRNodeLocationAtom), &myNodeLocationAtom, NULL);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// create a node information atom container and add a node header atom to it
	//
	//////////
	
	myErr = QTNewAtomContainer(&myNodeInfo);
	if (myErr != noErr)
		goto bail;

	myNodeHeaderAtom.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
	myNodeHeaderAtom.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);
	myNodeHeaderAtom.nodeType = EndianU32_NtoB(theNodeType);
	myNodeHeaderAtom.nodeID = EndianU32_NtoB(kDefaultNodeID);
	myNodeHeaderAtom.commentAtomID = EndianU32_NtoB(0L);
	myNodeHeaderAtom.reserved1 = EndianU32_NtoB(0L);
	myNodeHeaderAtom.reserved2 = EndianU32_NtoB(0L);
	
	// insert the node name string into the node info atom container
	if (false) {
		Str255				myStr = "\pMy Node";
		QTAtomID			myID;
		
		myErr = VRPano_AddStr255ToAtomContainer(myNodeInfo, kParentAtomIsContainer, myStr, &myID);
		myNodeHeaderAtom.nameAtomID = EndianU32_NtoB(myID);
	} else
		myNodeHeaderAtom.nameAtomID = EndianU32_NtoB(0L);
	
	// insert the node header atom into the node info atom container
	myErr = QTInsertChild(myNodeInfo, kParentAtomIsContainer, kQTVRNodeHeaderAtomType, 1, 1, sizeof(QTVRNodeHeaderAtom), &myNodeHeaderAtom, NULL);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// add a hot spot parent atom to the node information atom container and insert hot spot atoms into it
	//
	//////////
		
	// insert the hot spot parent atom into the node info atom container
	myErr = QTInsertChild(myNodeInfo, kParentAtomIsContainer, kQTVRHotSpotParentAtomType, 1, 1, 0, NULL, &myHSParentAtom);
	if (myErr != noErr)
		goto bail;
		
	// the following loop adds all possible hot spot atoms to the hot spot parent atom;
	// we do this because we don't know how many hot spots are in the hot spot image or
	// what their color-table indices are; you will want to handle a real hot spot image
	// differently, no doubt....
	
    for (myIndex = 1; myIndex < 255; myIndex++) {
		char		myHSName[100];
		char		myURL[] = "http://www.apple.com";
		
		sprintf(myHSName, "Hot Spot Index %d", myIndex);

		myErr = VRPano_MakeHotSpotVers2x0(myNodeInfo, myHSParentAtom, myURL, myHSName, myIndex);
		if (myErr != noErr)
			goto bail;
    }
	
bail:
	// return the atom containers that we've created and configured here
	*theVRWorld = myVRWorld;
	*theNodeInfo = myNodeInfo;
	
	return(myErr);
}


//////////
//
// VRPano_CreatePanoTrack
// Create a (version 2.0) panorama track.
//
//////////

OSErr VRPano_CreatePanoTrack (Movie theMovie, FSSpec *theSrcFile, FSSpec *theHSSrcFile, Track theQTVRTrack, Track thePanoTrack, Media thePanoMedia, long theWidth, long theHeight)
{
	Track						myImageTrack = NULL;
	Track						myHSImageTrack = NULL;
	TimeValue					myDuration = 7200;
	Movie						mySrcMovie = NULL;
	Movie						myHSSrcMovie = NULL;
	long						myRefIndex = 0;
	long						myHSRefIndex = 0;
	short						mySrcRefNum = 0;
	short						myHSSrcRefNum = 0;
	short						myResID = 0;
	short						myHSResID = 0;
	SampleDescriptionHandle		mySampleDesc = NULL;
	QTAtomContainer				myPanoSample;
	QTVRPanoSampleAtom			myPanoSampleData;
	int							myNumFramesX = 1;
	int							myNumFramesY = kNumTilesInPanoFile;
	double						myTheta;
	OSErr						myErr = noErr;

	//////////
	//
	// create a panorama image track; a reference to it is contained in the pano track
	//
	//////////
	
	// open the source file
	myErr = OpenMovieFile(theSrcFile, &mySrcRefNum, fsRdPerm);
	if (myErr != noErr)
		goto bail;
	
	myErr = NewMovieFromFile(&mySrcMovie, mySrcRefNum, &myResID, 0, 0, 0);
	if (myErr != noErr)
		goto bail;
	
	myErr = VRPano_ImportVideoTrack(mySrcMovie, theMovie, myDuration, &theWidth, &theHeight, &myImageTrack);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// create a hot spot image track, if the user has previously selected a hot spot image;
	// a reference to the hot spot image track is contained in the pano track
	//
	//////////
	
	if (theHSSrcFile->name[0] != 0) {
	
		// open the source file
		myErr = OpenMovieFile(theHSSrcFile, &myHSSrcRefNum, fsRdPerm);
		if (myErr != noErr)
			goto bail;
		
		myErr = NewMovieFromFile(&myHSSrcMovie, myHSSrcRefNum, &myHSResID, 0, 0, 0);
		if (myErr != noErr)
			goto bail;
		
		myErr = VRPano_ImportVideoTrack(myHSSrcMovie, theMovie, myDuration, &theWidth, &theHeight, &myHSImageTrack);
		if (myErr != noErr)
			goto bail;
	}	
	
	//////////
	//
	// create track references from QTVR track to panorama track,
	// and from the panorama track to the panorama image track and the hot spot image track
	//
	//////////
	
	if (thePanoTrack != NULL)
		AddTrackReference(theQTVRTrack, thePanoTrack, kQTVRPanoramaType, NULL);
		
	if (myImageTrack != NULL)
		AddTrackReference(thePanoTrack, myImageTrack, kQTVRImageTrackRefType, &myRefIndex);

	if (myHSImageTrack != NULL)
		AddTrackReference(thePanoTrack, myHSImageTrack, kQTVRHotSpotTrackRefType, &myHSRefIndex);

	//////////
	//
	// add a media sample to the panorama track
	//
	//////////
	
	// create a sample description; this contains no real info, but AddMediaSample requires it
	mySampleDesc = (SampleDescriptionHandle)NewHandleClear(sizeof(SampleDescription));

	myErr = QTNewAtomContainer(&myPanoSample);
	if (myErr != noErr)
		goto bail;
	
	myPanoSampleData.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
	myPanoSampleData.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);
	
	// fill in the track reference indices
	myPanoSampleData.imageRefTrackIndex = EndianU32_NtoB(myRefIndex);
	myPanoSampleData.hotSpotRefTrackIndex = EndianU32_NtoB(myHSRefIndex);

	if (gVersionToCreate == kQTVRVersion2) {
		// we're building a version 2.0 cylindrical panorama
		myPanoSampleData.minPan = 0.0f;
		myPanoSampleData.maxPan = 360.0f;
		myTheta = 180.0 * (atan((theWidth * myNumFramesX) * 3.14159 / (theHeight * myNumFramesY))) / 3.14159;
		myPanoSampleData.minTilt = -myTheta;
		myPanoSampleData.maxTilt = myTheta;
		myPanoSampleData.minFieldOfView = 0.0f;
		myPanoSampleData.maxFieldOfView = myTheta * 2;
		myPanoSampleData.defaultPan = 0.0f;
		myPanoSampleData.defaultTilt = 0.0f;
		myPanoSampleData.defaultFieldOfView = (myTheta * 2) * .8;

		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.minPan);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.maxPan);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.minTilt);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.maxTilt);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.minFieldOfView);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.maxFieldOfView);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.defaultPan);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.defaultTilt);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.defaultFieldOfView);

		myPanoSampleData.imageSizeX = EndianU32_NtoB(theWidth * myNumFramesX);
		myPanoSampleData.imageSizeY = EndianU32_NtoB(theHeight * myNumFramesY);
		myPanoSampleData.imageNumFramesX = EndianU16_NtoB(myNumFramesX);
		myPanoSampleData.imageNumFramesY = EndianU16_NtoB(myNumFramesY);
		
		myPanoSampleData.hotSpotSizeX = EndianU32_NtoB(theWidth * myNumFramesX);
		myPanoSampleData.hotSpotSizeY = EndianU32_NtoB(theHeight * myNumFramesY); 
		myPanoSampleData.hotSpotNumFramesX = EndianU16_NtoB(myNumFramesX);
		myPanoSampleData.hotSpotNumFramesY = EndianU16_NtoB(myNumFramesY);
			
		myPanoSampleData.flags = EndianU32_NtoB(0L);
		myPanoSampleData.panoType = EndianU32_NtoB(0L);
		myPanoSampleData.reserved2 = EndianU32_NtoB(0L);
	} else {
		// we're building a cubic panorama
		
		// note that the following fields in the QTVRPanoSampleAtom structure are not used by the cubic engine;
		// they are set here to values that will allow this panorama to be displayed by the cylindrical engine
		// under QuickTime VR 2.2 (pre-cubic); in particular, the imageNumFramesX field is set to 4, so that the
		// cylinder engine will display the four sides of the cube (albeit somewhat distorted)
		myPanoSampleData.minPan = 0.0f;
		myPanoSampleData.maxPan = 360.0f;
		myPanoSampleData.minTilt = -45.0f;
		myPanoSampleData.maxTilt = 45.0f;
		myPanoSampleData.minFieldOfView = 5.0f;
		myPanoSampleData.maxFieldOfView = 90.0f;
		myPanoSampleData.defaultPan = 0.0f;
		myPanoSampleData.defaultTilt = 0.0f;
		myPanoSampleData.defaultFieldOfView = 45.0f;

		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.minPan);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.maxPan);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.minTilt);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.maxTilt);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.minFieldOfView);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.maxFieldOfView);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.defaultPan);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.defaultTilt);
		VRPano_ConvertFloatToBigEndian(&myPanoSampleData.defaultFieldOfView);

		myPanoSampleData.imageSizeX = EndianU32_NtoB(theWidth * 4);
		myPanoSampleData.imageSizeY = EndianU32_NtoB(theHeight);
		myPanoSampleData.imageNumFramesX = EndianU16_NtoB(4);
		myPanoSampleData.imageNumFramesY = EndianU16_NtoB(1);
		
		myPanoSampleData.hotSpotSizeX = EndianU32_NtoB(theWidth * 4);
		myPanoSampleData.hotSpotSizeY = EndianU32_NtoB(theHeight); 
		myPanoSampleData.hotSpotNumFramesX = EndianU16_NtoB(4);
		myPanoSampleData.hotSpotNumFramesY = EndianU16_NtoB(1);
			
		// set the horizontal flag so that pre-cubic QTVR engine can at least display the four sides of the cube;
		// also, we are using reserved1 to indicate the panorama type
		myPanoSampleData.flags = EndianU32_NtoB(kQTVRPanoFlagHorizontal);
		myPanoSampleData.panoType = EndianU32_NtoB(kQTVRCube);
		myPanoSampleData.reserved2 = EndianU32_NtoB(0L);
	}
	
	// insert the pano sample atom into the pano sample atom container
	myErr = QTInsertChild(myPanoSample, kParentAtomIsContainer, kQTVRPanoSampleDataAtomType, 1, 1, sizeof(QTVRPanoSampleAtom), &myPanoSampleData, NULL);
	if (myErr != noErr)
		goto bail;
	
	// set the values that the cubic engine uses; this atom will be ignored in pre-cubic versions of QTVR
	if (gVersionToCreate == kCubicQTVR) {
		QTVRCubicViewAtom 		myCubicViewAtom;
		
		myCubicViewAtom.minPan = 0.0f;
		myCubicViewAtom.maxPan = 360.0f;
		myCubicViewAtom.minTilt = -90.0f;
		myCubicViewAtom.maxTilt = 90.0f;
		myCubicViewAtom.minFieldOfView = 5.0f;
		myCubicViewAtom.maxFieldOfView = 120.0f;
		
		myCubicViewAtom.defaultPan = 0.0f;
		myCubicViewAtom.defaultTilt = 0.0f;
		myCubicViewAtom.defaultFieldOfView = 45.0f;

		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.minPan);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.maxPan);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.minTilt);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.maxTilt);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.minFieldOfView);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.maxFieldOfView);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.defaultPan);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.defaultTilt);
		VRPano_ConvertFloatToBigEndian(&myCubicViewAtom.defaultFieldOfView);

		myErr = QTInsertChild(myPanoSample, kParentAtomIsContainer, kQTVRCubicViewAtomType, 1, 1, sizeof(QTVRCubicViewAtom), &myCubicViewAtom, NULL);
		if (myErr != noErr)
			goto bail;
	}

	// add pan constraint, tilt constraint, and field-of-view constraint atoms here
	// [left as an exercise for the reader; here's the basic idea:]
	if (false) {
		QTVRAngleRangeAtom		myPanRangeAtom;
		
		myPanRangeAtom.minimumAngle = 0.0;
		myPanRangeAtom.maximumAngle = 270.0;
		VRPano_ConvertFloatToBigEndian(&myPanRangeAtom.minimumAngle);
		VRPano_ConvertFloatToBigEndian(&myPanRangeAtom.maximumAngle);
		
		myErr = QTInsertChild(myPanoSample, kParentAtomIsContainer, kQTVRPanConstraintAtomType, 1, 1, sizeof(QTVRAngleRangeAtom), &myPanRangeAtom, NULL);
	}
		
	// create the media sample
	BeginMediaEdits(thePanoMedia);

	myErr = AddMediaSample(thePanoMedia, (Handle)myPanoSample, 0, GetHandleSize((Handle)myPanoSample), myDuration, (SampleDescriptionHandle)mySampleDesc, 1, 0, NULL);
	if (myErr != noErr)
		goto bail;

	EndMediaEdits(thePanoMedia);

	// add the media to the track
	myErr = InsertMediaIntoTrack(thePanoTrack, 0, 0, myDuration, fixed1);
	
bail:
	if (mySrcMovie != NULL) 
		DisposeMovie(mySrcMovie);
		
	if (mySrcRefNum != 0)
		CloseMovieFile(mySrcRefNum);
		
	if (myHSSrcMovie != NULL) 
		DisposeMovie(myHSSrcMovie);
		
	if (myHSSrcRefNum != 0)
		CloseMovieFile(myHSSrcRefNum);
		
	return(myErr);
}


//////////
//
// VRPano_CreateTileMovie
// Create a QuickTime movie containing tiles from the specified image file.
//
//////////

OSErr VRPano_CreateTileMovie (GraphicsImportComponent theImporter, FSSpec *theTileSpec, CodecType theCodec, CodecQ theQuality, short theDepth)
{
	Rect						myPictRect;
	Rect						myTileRect;
	long						myTileHeight, myTileWidth;
	GWorldPtr					myGWorld = NULL;
	PixMapHandle 				myPixMap = NULL;
	Movie						myMovie = NULL;
	short						myResRefNum = -1;
	short						myResID = movieInDataForkResID;
	Track						myTrack = NULL;
	Media						myMedia = NULL;
	ImageDescriptionHandle		myImageDesc = NULL;
	Handle						myData = NULL;
	Ptr							myDataPtr = NULL;
	long						myFlags = createMovieFileDeleteCurFile | createMovieFileDontCreateResFile;
	long						myFrameSize;
	TimeValue					myDuration = 300;
	UInt16						myIndex;
	OSErr						myErr = noErr;
#if USE_TILE_DISPLAY_WINDOW
	WindowRef					myWindow;
#endif
	
	//////////
	//
	// create a GWorld to draw the uncompressed tiles into
	//
	//////////

	myErr = GraphicsImportGetBoundsRect(theImporter, &myPictRect);
	if (myErr != noErr)
		goto bail;
	
	myTileRect = myPictRect;
	
	myTileHeight = myTileRect.bottom - myTileRect.top;
	myTileWidth = myTileRect.right - myTileRect.left;
	myTileRect.bottom = myTileRect.top + (myTileHeight / kNumTilesInPanoFile);
	myTileHeight = myTileRect.bottom - myTileRect.top;				// since we just changed myTileRect.bottom
	
#if USE_TILE_DISPLAY_WINDOW
	MacOffsetRect(&myTileRect, 20, 100);
	myWindow = NewCWindow(NULL, &myTileRect, "\pUncompressed tiles", true, noGrowDocProc, (WindowPtr)-1L, true, 0);
	MacOffsetRect(&myTileRect, -20, -100);
#endif
	
	myErr = NewGWorld(&myGWorld, theDepth, &myTileRect, NULL, NULL, 0L);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// create a new movie to contain the compressed tiles as video samples
	//
	//////////
	
	// create a movie file for the tile movie
	myErr = CreateMovieFile(theTileSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, myFlags, &myResRefNum, &myMovie);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// create the tile movie track and media
	//
	//////////

	myTrack = NewMovieTrack(myMovie, FixRatio(myTileWidth, 1), FixRatio(myTileHeight, 1), kNoVolume);	
	myMedia = NewTrackMedia(myTrack, VideoMediaType, 600, NULL, 0);
	if ((myTrack == NULL) || (myMedia == NULL))
		goto bail;
	
	//////////
	//
	// dice the picture into pieces, compress them, and add the compressed tiles as video samples to the movie
	//
	//////////

	// create an image description handle; this is resized and filled in below by FCompressImage
	myImageDesc = (ImageDescriptionHandle)NewHandleClear(4);
	if (myImageDesc == NULL)
		goto bail;
	
	BeginMediaEdits(myMedia);
		
	for (myIndex = 0; myIndex < kNumTilesInPanoFile; myIndex++) {
	
		//////////
		//
		// draw the picture into the tile GWorld
		//
		//////////

		
#if USE_TILE_DISPLAY_WINDOW
		// draw the picture into the window we've created
		if (myWindow != NULL) {
			GraphicsImportSetGWorld(theImporter, (CGrafPtr)myWindow, NULL);
			GraphicsImportSetBoundsRect(theImporter, &myPictRect);
			GraphicsImportDraw(theImporter);
		}
#endif

		// draw the picture into the uncompressed tile GWorld
		GraphicsImportSetGWorld(theImporter, myGWorld, NULL);
		GraphicsImportSetBoundsRect(theImporter, &myPictRect);
		GraphicsImportDraw(theImporter);
		
		// offset the picture rectangle for next time thru the loop
		MacOffsetRect(&myPictRect, 0, -myTileHeight);
		
		//////////
		//
		// compress the tile
		//
		//////////

		myPixMap = GetGWorldPixMap(myGWorld);
		LockPixels(myPixMap);

		// find out how big a compressed frame will be
		myErr = GetMaxCompressionSize(myPixMap, &myTileRect, theDepth, theQuality, theCodec, (CompressorComponent)anyCodec, &myFrameSize);
		UnlockPixels(myPixMap);
		if (myErr != noErr)
			goto bail;

		myData = NewHandleClear(myFrameSize);
		if (myData == NULL)
			goto bail;

		HLock(myData);
#if TARGET_CPU_68K
		myDataPtr = StripAddress(*myData);
#else
		myDataPtr = *myData;
#endif		
		// compress the tile
		LockPixels(myPixMap);
		myErr = FCompressImage(myPixMap, &myTileRect, theDepth, theQuality, theCodec, (CompressorComponent)anyCodec, NULL, 0, 0, NULL, NULL, myImageDesc, myDataPtr);
		UnlockPixels(myPixMap);
		if (myErr != noErr)
			goto bail;
			
		//////////
		//
		// add the tile to the movie
		//
		//////////

		myErr = AddMediaSample(myMedia, myData, 0, (**myImageDesc).dataSize, myDuration, (SampleDescriptionHandle)myImageDesc, 1L, 0, NULL);
		if (myErr != noErr)
			goto bail;
		
		DisposeHandle(myData);
		myData = NULL;
	}
	
	EndMediaEdits(myMedia);
	
	// add the media to the track, at time 0
	myErr = InsertMediaIntoTrack(myTrack, 0, 0, GetMediaDuration(myMedia), fixed1);
	if (myErr != noErr)
		goto bail;

	// add the movie resource
	myErr = AddMovieResource(myMovie, myResRefNum, &myResID, NULL);
	
bail:
	if (myGWorld != NULL)
		DisposeGWorld(myGWorld);

	if (myResRefNum != -1)
		CloseMovieFile(myResRefNum);
	
	if (myData != NULL)
		DisposeHandle(myData);
		
	if (myImageDesc != NULL)
		DisposeHandle((Handle)myImageDesc);
	
	if (myMovie != NULL)
		DisposeMovie(myMovie);
	
#if USE_TILE_DISPLAY_WINDOW
	if (myWindow != NULL)
		DisposeWindow(myWindow);
#endif

	return(myErr);
}


//////////
//
// VRPano_CreateQTVRMovieVers1x0
// Create a single-node panoramic QTVR movie from the specified tile movie(s).
//
// NOTE: This function builds a movie that conforms to version 1.0 of the QuickTime VR file format.
//
// The newly-created movie contains references to the original tile movie, not the actual movie data.
// We do this because we assume that the caller will flatten the movie into a third movie, which will
// contain the movie data. Also, the interim file is much smaller than it would be if we copied the data,
// thus saving time and disk space.
//
//////////

OSErr VRPano_CreateQTVRMovieVers1x0 (FSSpec *theMovieSpec, FSSpec *theTileSpec, FSSpec *theHSTileSpec, long theHeight, long theWidth)
{
	PanoramaDescriptionHandle		myPanoDesc = NULL;
	PanoSampleHeaderAtomHandle		myPanoHeader = NULL;
	HotSpotTableAtomHandle			myHotSpotTable = NULL;
	StringTableAtomHandle			myStringTable = NULL;
	short							myResRefNum = -1;
	short							myResID = movieInDataForkResID;
	short							myHSResRefNum = -1;
	short							myTilResRefNum = -1;
	short							myHSTilResRefNum = -1;
	Movie							myMovie = NULL;
	Movie							myTileMovie = NULL;
	Movie							myHSTileMovie = NULL;
	Track							myPanoTrack;
	Media							myPanoMedia;
	Track							myImageTrack;
	Track							myHSImageTrack;
	long							myFlags = createMovieFileDeleteCurFile | createMovieFileDontCreateResFile;
	TimeValue						myDuration = 7200;
	short							myNumFramesX = 1;
	short							myNumFramesY = kNumTilesInPanoFile;
	float							myTheta;
	UInt16							myIndex;
	short							myHeight = theHeight;
	short							myWidth = theWidth;
	Boolean							myGotHS = false;
	ComponentResult					myErr = paramErr;

	if ((theMovieSpec == NULL) || (theHSTileSpec == NULL))
		goto bail;
	
	//////////
	//
	// create a new movie
	//
	//////////

	// create a movie file for the destination movie
	myErr = CreateMovieFile(theMovieSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, myFlags, &myResRefNum, &myMovie);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// copy the video track from the tile movie to the new movie; this is the "scene track"
	//
	//////////

	// open the tile movie file
	myErr = OpenMovieFile(theTileSpec, &myTilResRefNum, fsRdPerm);
	if (myErr != noErr)
		goto bail;
	
	myErr = NewMovieFromFile(&myTileMovie, myTilResRefNum, NULL, 0, 0, 0);
	if (myErr != noErr)
		goto bail;
	
	SetMoviePlayHints(myTileMovie, hintsHighQuality, hintsHighQuality);

	myErr = VRPano_ImportVideoTrack(myTileMovie, myMovie, myDuration, &theWidth, &theHeight, &myImageTrack);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// copy the hot spot image track from the tile movie to the new movie
	//
	//////////
	
	myGotHS = theHSTileSpec->name[0] != 0;
	if (myGotHS) {
		// open the source file
		myErr = OpenMovieFile(theHSTileSpec, &myHSTilResRefNum, fsRdPerm);
		if (myErr != noErr)
			goto bail;
		
		myErr = NewMovieFromFile(&myHSTileMovie, myHSTilResRefNum, &myHSResRefNum, 0, 0, 0);
		if (myErr != noErr)
			goto bail;
		
		myErr = VRPano_ImportVideoTrack(myHSTileMovie, myMovie, myDuration, &theWidth, &theHeight, &myHSImageTrack);
		if (myErr != noErr)
			goto bail;
	}
	
	//////////
	//
	// create a panorama track and add it to the movie
	//
	//////////
	
	// create a panorama track and media
	myPanoTrack = NewMovieTrack(myMovie, FixRatio(myWidth, 1), FixRatio(myHeight, 1), 0);
	myPanoMedia = NewTrackMedia(myPanoTrack, kQTVROldPanoType, kQTVRStandardTimeScale, NULL, 0);
	if ((myPanoTrack == NULL) || (myPanoMedia == NULL))
		goto bail;

	// create a panorama sample description
 	myPanoDesc = (PanoramaDescriptionHandle)NewHandleClear(sizeof(PanoramaDescription));
	if (myPanoDesc == NULL)
		goto bail;

	// fill in the panorama sample description;
	// all the data in this sample description that follows the first 4 long words must be in big-endian format;
	// the first four long words are used by AddMediaSample and must therefore be in native-endian format
	(**myPanoDesc).size = sizeof(PanoramaDescription);
	(**myPanoDesc).type = kPanDescType;
	(**myPanoDesc).reserved1 = 0L;
	(**myPanoDesc).reserved2 = 0L;
	
	(**myPanoDesc).majorVersion = EndianU16_NtoB(0);
	(**myPanoDesc).minorVersion = EndianU16_NtoB(0);
	(**myPanoDesc).sceneTrackID = EndianU32_NtoB(GetTrackID(myImageTrack));
	(**myPanoDesc).loResSceneTrackID = EndianU32_NtoB(0L);		// no lo-res video track
	
	for (myIndex = 1; myIndex < 6; myIndex++) {
		(**myPanoDesc).reserved3[myIndex] = EndianU32_NtoB(0L);
		(**myPanoDesc).reserved4[myIndex] = EndianU32_NtoB(0L);
	}
	
	if (myGotHS) 
		(**myPanoDesc).hotSpotTrackID = EndianU32_NtoB(GetTrackID(myHSImageTrack));
	(**myPanoDesc).loResHotSpotTrackID = EndianU32_NtoB(0L);	// no lo-res hot spot track

	myTheta = 180.0 * (atan((theWidth * myNumFramesX) * 3.14159 / (theHeight * myNumFramesY))) / 3.14159;
	
	(**myPanoDesc).hPanStart = EndianS32_NtoB(0L);
	(**myPanoDesc).hPanEnd = EndianS32_NtoB(360 << 16);
	(**myPanoDesc).vPanTop = EndianS32_NtoB((Fixed)(myTheta * 65536));
	(**myPanoDesc).vPanBottom = EndianS32_NtoB((Fixed)(-myTheta * 65536));
	(**myPanoDesc).minimumZoom = EndianS32_NtoB(0L);
	(**myPanoDesc).maximumZoom = EndianS32_NtoB(0L);
	
	(**myPanoDesc).sceneSizeX = EndianU32_NtoB((long)(theWidth * myNumFramesX));
	(**myPanoDesc).sceneSizeY = EndianU32_NtoB((long)(theHeight * myNumFramesY));
	(**myPanoDesc).numFrames = EndianU32_NtoB((long)myNumFramesY);
	(**myPanoDesc).sceneNumFramesX = EndianU16_NtoB(myNumFramesX);
	(**myPanoDesc).sceneNumFramesY = EndianU16_NtoB(myNumFramesY);
	(**myPanoDesc).sceneColorDepth = EndianU16_NtoB(32);
	
	(**myPanoDesc).hotSpotSizeX = EndianU32_NtoB((long)(theWidth * myNumFramesX));
	(**myPanoDesc).hotSpotSizeY = EndianU32_NtoB((long)(theHeight * myNumFramesY));
	(**myPanoDesc).hotSpotNumFramesX = EndianU16_NtoB(myNumFramesX);
	(**myPanoDesc).hotSpotNumFramesY = EndianU16_NtoB(myNumFramesY);
	(**myPanoDesc).hotSpotColorDepth = EndianU16_NtoB(8);

	// create a panorama header atom;
	// the data in this atom (which is *not* a QTAtom) must be in big-endian format
	myPanoHeader = (PanoSampleHeaderAtomHandle)NewHandleClear(sizeof(PanoSampleHeaderAtom));
	if (myPanoHeader == NULL)
		goto bail;
		
	(**myPanoHeader).size = EndianU32_NtoB(sizeof(PanoSampleHeaderAtom));
	(**myPanoHeader).type = EndianU32_NtoB(kPanHeaderType);
	(**myPanoHeader).nodeID = EndianU32_NtoB(0L);

	// set the default pan, tilt, and zoom
	(**myPanoHeader).defHPan = EndianS32_NtoB(0L);
	(**myPanoHeader).defVPan = EndianS32_NtoB(0L);
	(**myPanoHeader).defZoom = EndianS32_NtoB((Fixed)(1.5 * myTheta * 65536));

	// set constraints for this node; use 0 for the default constraints
	(**myPanoHeader).minHPan = EndianS32_NtoB(0L);
	(**myPanoHeader).minVPan = EndianS32_NtoB(0L);
	(**myPanoHeader).maxHPan = EndianS32_NtoB(0L);
	(**myPanoHeader).maxVPan = EndianS32_NtoB(0L);
	(**myPanoHeader).maxZoom = EndianS32_NtoB(0L);

	(**myPanoHeader).nameStrOffset = EndianU32_NtoB(0L);		// no name or comment
	(**myPanoHeader).commentStrOffset = EndianU32_NtoB(0L);

	// create a string table atom;
	// the data in this atom (which is *not* a QTAtom) must be in big-endian format
	myStringTable = (StringTableAtomHandle)NewHandleClear(sizeof(StringTableAtom));
	if (myStringTable == NULL)
		goto bail;
		
	(**myStringTable).size = EndianU32_NtoB(sizeof(StringTableAtom));
	(**myStringTable).type = EndianU32_NtoB(kStringTableType);
	(**myStringTable).bunchOstrings[0] = EndianU32_NtoB(0);

	// create a hot spot table atom and expand the string table atom to include the
	// hot spot names and comments
	myHotSpotTable = VRPano_MakeHotSpotVers1x0(myStringTable);

	// append the hot spot table atom and the string table atom to the panorama header atom
	// [left as an exercise for the reader]
	
	// add the media sample to the panorama track
	BeginMediaEdits(myPanoMedia);

	myErr = AddMediaSample(myPanoMedia, (Handle)myPanoHeader, 0, GetHandleSize((Handle)myPanoHeader), myDuration, (SampleDescriptionHandle)myPanoDesc, 1, 0, NULL);
	if (myErr != noErr)
		goto bail;

	EndMediaEdits(myPanoMedia);

	// add the media to the track
	myErr = InsertMediaIntoTrack(myPanoTrack, 0, 0, myDuration, fixed1);
	
	//////////
	//
	// add a user data item that identifies the QTVR movie controller
	//
	//////////
	
	myErr = VRPano_SetControllerType(myMovie, kQTVROldPanoType);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add the movie resource to the panorama movie
	//
	//////////
	
	myErr = AddMovieResource(myMovie, myResRefNum, &myResID, NULL);
	
bail:
	if (myPanoDesc != NULL)
		DisposeHandle((Handle)myPanoDesc);
	
	if (myPanoHeader != NULL)
		DisposeHandle((Handle)myPanoHeader);
	
	if (myResRefNum != -1)
		CloseMovieFile(myResRefNum);
	
	if (myHSResRefNum != -1)
		CloseMovieFile(myHSResRefNum);
	
	if (myMovie != NULL)
		DisposeMovie(myMovie);
		
	if (myTilResRefNum != -1)
		CloseMovieFile(myTilResRefNum);
	
	if (myHSTilResRefNum != -1)
		CloseMovieFile(myHSTilResRefNum);
	
	if (myTileMovie != NULL)
		DisposeMovie(myTileMovie);
		
	if (myHSTileMovie != NULL)
		DisposeMovie(myHSTileMovie);
		
	return(myErr);
}


//////////
//
// VRPano_CreateQTVRMovieVers2x0
// Create a single-node panoramic QTVR movie from the specified tile movie(s).
//
// NOTE: This function builds a movie that conforms to version 2.0 of the QuickTime VR file format.
//
// The newly-created movie contains references to the original tile movie, not the actual movie data.
// We do this because we assume that the caller will flatten the movie into a third movie, which will
// contain the movie data. Also, the interim file is much smaller than it would be if we copied the data,
// thus saving time and disk space.
//
//////////

OSErr VRPano_CreateQTVRMovieVers2x0 (FSSpec *theMovieSpec, FSSpec *theTileSpec, FSSpec *theHSTileSpec, long theHeight, long theWidth)
{
	Handle							myHandle = NULL;
	SampleDescriptionHandle			mySampleDesc = NULL;
	QTVRSampleDescriptionHandle		myQTVRDesc = NULL;
	short							myResRefNum = -1;
	Movie							myMovie = NULL;
	Track							myQTVRTrack;
	Media							myQTVRMedia;
	Track							myPanoTrack;
	Media							myPanoMedia;
	long							mySize;
	long							myFlags = createMovieFileDeleteCurFile | createMovieFileDontCreateResFile;
	TimeValue						myDuration = 7200;
	QTAtomContainer					myVRWorld;
	QTAtomContainer					myNodeInfo;
	ComponentResult					myErr = paramErr;

	if (theMovieSpec == NULL)
		goto bail;
	
	//////////
	//
	// create a new movie
	//
	//////////

	// create a movie file for the destination movie
	myErr = CreateMovieFile(theMovieSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, myFlags, &myResRefNum, &myMovie);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// create the QTVR movie track and media
	//
	//////////

	myQTVRTrack = NewMovieTrack(myMovie, FixRatio(theWidth, 1), FixRatio(theHeight, 1), kFullVolume);
	myQTVRMedia = NewTrackMedia(myQTVRTrack, kQTVRQTVRType, kQTVRStandardTimeScale, NULL, 0);
	if ((myQTVRTrack == NULL) || (myQTVRMedia == NULL))
		goto bail;
		
	SetMovieTimeScale(myMovie, kQTVRStandardTimeScale);

	// create a VR world atom container and a node information atom container;
	// remember that the VR world becomes part of the QTVR sample description,
	// and the node information atom container becomes the media sample data
	myErr = VRPano_CreateVRWorld(&myVRWorld, &myNodeInfo, kQTVRPanoramaType);
	if (myErr != noErr)
		goto bail;
		
	if ((myVRWorld == NULL) || (myNodeInfo == NULL))
		goto bail;
	
	// create a QTVR sample description
	mySize = sizeof(QTVRSampleDescription) + GetHandleSize((Handle)myVRWorld) - sizeof(long);
	myQTVRDesc = (QTVRSampleDescriptionHandle)NewHandleClear(mySize);
	if (myQTVRDesc == NULL)
		goto bail;
		
	(**myQTVRDesc).descSize = mySize;
	(**myQTVRDesc).descType = kQTVRQTVRType;
	(**myQTVRDesc).reserved1 = 0;
	(**myQTVRDesc).reserved2 = 0;
	(**myQTVRDesc).dataRefIndex = 0;

	// copy the VR world atom container into the data field of the QTVR sample description
	BlockMove(*((Handle)myVRWorld), &((**myQTVRDesc).data), GetHandleSize((Handle)myVRWorld));
	
	// create the media sample
	BeginMediaEdits(myQTVRMedia);

	myErr = AddMediaSample(myQTVRMedia, (Handle)myNodeInfo, 0, GetHandleSize((Handle)myNodeInfo), myDuration, (SampleDescriptionHandle)myQTVRDesc, 1, 0, NULL);
	if (myErr != noErr)
		goto bail;

	EndMediaEdits(myQTVRMedia);
	
	// add the media to the track
	InsertMediaIntoTrack(myQTVRTrack, 0, 0, myDuration, fixed1);
	
	//////////
	//
	// create a panorama track and add it to the movie
	//
	//////////
	
	// create panorama track and media
	myPanoTrack = NewMovieTrack(myMovie, FixRatio(theWidth, 1), FixRatio(theHeight, 1), kNoVolume);
	myPanoMedia = NewTrackMedia(myPanoTrack, kQTVRPanoramaType, kQTVRStandardTimeScale, NULL, 0);
	if ((myPanoTrack == NULL) || (myPanoMedia == NULL))
		goto bail;
	
	// create the panorama image track and configure the panorama track
	myErr = VRPano_CreatePanoTrack(myMovie, theTileSpec, theHSTileSpec, myQTVRTrack, myPanoTrack, myPanoMedia, theWidth, theHeight);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add a user data item that identifies the QTVR movie controller
	//
	//////////
	
	myErr = VRPano_SetControllerType(myMovie, kQTVRQTVRType);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add the movie resource to the panorama movie
	//
	//////////
	
	myErr = AddMovieResource(myMovie, myResRefNum, NULL, NULL);
	
bail:
	if (mySampleDesc != NULL)
		DisposeHandle((Handle)mySampleDesc);
	
	if (myQTVRDesc != NULL)
		DisposeHandle((Handle)myQTVRDesc);
	
	if (myResRefNum != -1)
		CloseMovieFile(myResRefNum);
	
	if (myVRWorld != NULL)
		QTDisposeAtomContainer(myVRWorld);
		
	if (myNodeInfo != NULL)
		QTDisposeAtomContainer(myNodeInfo);

	if (myMovie != NULL)
		DisposeMovie(myMovie);
		
	return(myErr);
}


//////////
//
// VRPano_CreateCubicQTVRMovie
// Create a single-node cubic panoramic QTVR movie from the specified 6-frame movie.
//
//////////

OSErr VRPano_CreateCubicQTVRMovie (FSSpec *theMovieSpec, FSSpec *theFramesSpec, FSSpec *theHSFramesSpec, long theHeight, long theWidth)
{
	FSSpec							myTempSpec;
	Handle							myHandle = NULL;
	SampleDescriptionHandle			mySampleDesc = NULL;
	QTVRSampleDescriptionHandle		myQTVRDesc = NULL;
	short							myResRefNum = -1;
	Movie							myMovie = NULL;
	Track							myQTVRTrack;
	Media							myQTVRMedia;
	Track							myPanoTrack;
	Media							myPanoMedia;
	long							mySize;
	long							myFlags = createMovieFileDeleteCurFile | createMovieFileDontCreateResFile;
	TimeValue						myDuration = 3600;
	QTAtomContainer					myVRWorld;
	QTAtomContainer					myNodeInfo;
	Movie							myPanoMovie = NULL;
	ComponentResult					myErr = noErr;
	
	//////////
	//
	// create a temporary version of the panorama movie file,
	// located in the same directory as the destination panorama movie file
	//
	//////////
	
	// to create a new file name, we'll just change the last character of the destination movie file name
	// (no doubt you could do a better job here!)
	myTempSpec = *theMovieSpec;
	
	if (myTempSpec.name[myTempSpec.name[0]] == 't')
		myTempSpec.name[myTempSpec.name[0]] = '@';
	else
		myTempSpec.name[myTempSpec.name[0]] = 't';

	//////////
	//
	// create a new movie
	//
	//////////

	// create a movie file for the temporary movie
	myErr = CreateMovieFile(&myTempSpec, FOUR_CHAR_CODE('TVOD'), smCurrentScript, myFlags, &myResRefNum, &myMovie);
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// create the QTVR movie track and media
	//
	//////////

	myQTVRTrack = NewMovieTrack(myMovie, FixRatio(theWidth, 1), FixRatio(theHeight, 1), kFullVolume);
	myQTVRMedia = NewTrackMedia(myQTVRTrack, kQTVRQTVRType, kQTVRStandardTimeScale, NULL, 0);
	if ((myQTVRTrack == NULL) || (myQTVRMedia == NULL))
		goto bail;
		
	SetMovieTimeScale(myMovie, kQTVRStandardTimeScale);

	// create a VR world atom container and a node information atom container;
	// remember that the VR world becomes part of the QTVR sample description,
	// and the node information atom container becomes the media sample data
	myErr = VRPano_CreateVRWorld(&myVRWorld, &myNodeInfo, kQTVRPanoramaType);
	if (myErr != noErr)
		goto bail;
		
	if ((myVRWorld == NULL) || (myNodeInfo == NULL))
		goto bail;
	
	// create a QTVR sample description
	mySize = sizeof(QTVRSampleDescription) + GetHandleSize((Handle)myVRWorld) - sizeof(long);
	myQTVRDesc = (QTVRSampleDescriptionHandle)NewHandleClear(mySize);
	if (myQTVRDesc == NULL)
		goto bail;
		
	(**myQTVRDesc).descSize = mySize;
	(**myQTVRDesc).descType = kQTVRQTVRType;
	(**myQTVRDesc).reserved1 = 0;
	(**myQTVRDesc).reserved2 = 0;
	(**myQTVRDesc).dataRefIndex = 0;

	// copy the VR world atom container into the data field of the QTVR sample description
	BlockMove(*((Handle)myVRWorld), &((**myQTVRDesc).data), GetHandleSize((Handle)myVRWorld));
	
	// create the media sample
	BeginMediaEdits(myQTVRMedia);

	myErr = AddMediaSample(myQTVRMedia, (Handle)myNodeInfo, 0, GetHandleSize((Handle)myNodeInfo), myDuration, (SampleDescriptionHandle)myQTVRDesc, 1, 0, NULL);
	if (myErr != noErr)
		goto bail;

	EndMediaEdits(myQTVRMedia);
	
	// add the media to the track
	InsertMediaIntoTrack(myQTVRTrack, 0, 0, myDuration, fixed1);
	
	//////////
	//
	// create a panorama track and add it to the movie
	//
	//////////
	
	// create panorama track and media
	myPanoTrack = NewMovieTrack(myMovie, FixRatio(theWidth, 1), FixRatio(theHeight, 1), kNoVolume);
	myPanoMedia = NewTrackMedia(myPanoTrack, kQTVRPanoramaType, kQTVRStandardTimeScale, NULL, 0);
	if ((myPanoTrack == NULL) || (myPanoMedia == NULL))
		goto bail;
	
	// create the panorama image track and configure the panorama track
	myErr = VRPano_CreatePanoTrack(myMovie, theFramesSpec, theHSFramesSpec, myQTVRTrack, myPanoTrack, myPanoMedia, theWidth, theHeight);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add a user data item that identifies the QTVR movie controller
	//
	//////////
	
	myErr = VRPano_SetControllerType(myMovie, kQTVRQTVRType);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add the movie resource to the panorama movie
	//
	//////////
	
	myErr = AddMovieResource(myMovie, myResRefNum, NULL, NULL);
	
	//////////
	//
	// create the final, flattened movie
	//
	//////////

	SetMovieProgressProc(myMovie, (MovieProgressUPP)-1, 0L);

	// flatten the temporary file into a new movie file;
	// put the movie resource first so that FastStart is possible
	myErr = VRPano_FlattenMovieForStreaming(myMovie, theMovieSpec);
	if (myErr != noErr)
		myPanoMovie = FlattenMovieData(myMovie, flattenDontInterleaveFlatten | flattenAddMovieToDataFork | flattenForceMovieResourceBeforeMovieData, theMovieSpec, FOUR_CHAR_CODE('TVOD'), smSystemScript, createMovieFileDeleteCurFile | createMovieFileDontCreateResFile);

bail:
	if (mySampleDesc != NULL)
		DisposeHandle((Handle)mySampleDesc);
	
	if (myQTVRDesc != NULL)
		DisposeHandle((Handle)myQTVRDesc);
	
	if (myResRefNum != -1)
		CloseMovieFile(myResRefNum);
	
	if (myVRWorld != NULL)
		QTDisposeAtomContainer(myVRWorld);
		
	if (myNodeInfo != NULL)
		QTDisposeAtomContainer(myNodeInfo);

	if (myMovie != NULL)
		DisposeMovie(myMovie);
		
	if (myPanoMovie != NULL)
		DisposeMovie(myPanoMovie);
		
	DeleteMovieFile(&myTempSpec);
	
	return(myErr);
}


//////////
//
// VRPano_MakePanorama
// Create a single-node QuickTime VR panoramic movie from the specified image file.
//
//////////

OSErr VRPano_MakePanorama (FSSpec *thePictSpec, FSSpec *theHSPictSpec, FSSpec *theTileSpec, FSSpec *theDestSpec, long theWidth, long theHeight, CodecType theCodec, CodecQ theQuality, long theVersion)
{
	long						myPictHeight, myPictWidth;
	GraphicsImportComponent		myImporter = NULL;
	Rect						myRect;
	FSSpec						myTileSpec = *theTileSpec;
	FSSpec						myDestSpec = *theDestSpec;
	FSSpec						myTempSpec;
	FSSpec						myHSTileSpec;
	Movie						myTempMovie = NULL;
	Movie						myPanoMovie = NULL;
	short						myTempResRefNum = -1;
	OSErr						myErr = noErr;

	//////////
	//
	// get a graphics importer for the image file and determine the natural size of the image
	//
	//////////

	myErr = GetGraphicsImporterForFile(thePictSpec, &myImporter);
	if (myErr != noErr)
		goto bail;
	
	myErr = GraphicsImportGetNaturalBounds(myImporter, &myRect);
	if (myErr != noErr)
		goto bail;
	
	myPictHeight = myRect.bottom - myRect.top,
	myPictWidth = myRect.right - myRect.left;

	//////////
	//
	// verify the orientation and dimensions of the picture
	//
	//////////

	// check for correct orientation: the image must be taller than wide;
	// if not, we cannot continue
	if (myPictHeight < myPictWidth)
		goto bail;
		
	// check for correct dimensions: height must be divisible by 96 and width by 4;
	// if not, we'll adjust the dimensions
	if (((myPictHeight % 96) != 0) || ((myPictWidth % 4) != 0)) {
	
		// make sure height is divisible by 96
		if ((myPictHeight % 96) != 0) {
			UInt16			myNewHeight;
			UInt16			myNewWidth;
			double			myScale;
			
			myNewHeight = myPictHeight - (myPictHeight % 96);
			myScale = (double)myNewHeight / (double)myPictHeight;
			myNewWidth = (UInt16)((double)myPictWidth * myScale);
			myNewWidth = myNewWidth - (myNewWidth % 4);
		}
		
		// make sure width is divisible by 4
		if ((myPictWidth % 4) != 0) {
			UInt16			myNewWidth;

			myNewWidth = myPictWidth - (myPictWidth % 4);
		}
	}
	
	//////////
	//
	// dice the panoramic image into compressed tiles and put the tiles into a movie
	//
	//////////

	myErr = VRPano_CreateTileMovie(myImporter, theTileSpec, theCodec, theQuality, 0);
	if (myErr != noErr)
		goto bail;

	CloseComponent(myImporter);
	
	//////////
	//
	// dice the hot spot image into compressed tiles and put the tiles into a movie,
	// if the user has selected a hot spot image file
	//
	//////////
	
	if (theHSPictSpec->name[0] != 0) {
	
		// get a graphics importer for the hot spot image file
		myErr = GetGraphicsImporterForFile(theHSPictSpec, &myImporter);
		if (myErr != noErr)
			goto bail;
	
		// create a file specification for the hot spot tile file
		myHSTileSpec = *theDestSpec;
		
		// to create a new file name, we'll just change the last character of the destination movie file name
		// (no doubt you could do a better job here!)
		if (myHSTileSpec.name[myHSTileSpec.name[0]] == 's')
			myHSTileSpec.name[myHSTileSpec.name[0]] = '#';
		else
			myHSTileSpec.name[myHSTileSpec.name[0]] = 's';

		// a hot spot image track must be compressed with a lossless compressor and must be 8 bits deep
		myErr = VRPano_CreateTileMovie(myImporter, &myHSTileSpec, kGraphicsCodecType, codecLosslessQuality, 8);
		if (myErr != noErr)
			goto bail;
	} else {
	
		// if no hot spot image, clear out myHSTileSpec
		myHSTileSpec.vRefNum = 0;
		myHSTileSpec.parID = 0;
		myHSTileSpec.name[0] = 0;		// set length to 0
	}

	//////////
	//
	// create a temporary version of the panorama movie file,
	// located in the same directory as the destination panorama movie file
	//
	//////////
	
	// to create a new file name, we'll just change the last character of the destination movie file name
	// (no doubt you could do a better job here!)
	myTempSpec = *theDestSpec;
	
	if (myTempSpec.name[myTempSpec.name[0]] == 't')
		myTempSpec.name[myTempSpec.name[0]] = '@';
	else
		myTempSpec.name[myTempSpec.name[0]] = 't';
	
	// create a single node panoramic movie in the temp file
	if (theVersion == kQTVRVersion1)
		myErr = VRPano_CreateQTVRMovieVers1x0(&myTempSpec, theTileSpec, &myHSTileSpec, theHeight, theWidth);
	else if (theVersion == kQTVRVersion2)
		myErr = VRPano_CreateQTVRMovieVers2x0(&myTempSpec, theTileSpec, &myHSTileSpec, theHeight, theWidth);
		
	if (myErr != noErr)
		goto bail;

	//////////
	//
	// create the final, flattened movie
	//
	//////////

	myErr = OpenMovieFile(&myTempSpec, &myTempResRefNum, fsRdPerm);
	if (myErr != noErr)
		goto bail;
				
	myErr = NewMovieFromFile(&myTempMovie, myTempResRefNum, NULL, 0, 0, 0);
	if (myErr != noErr)
		goto bail;
		
	SetMovieProgressProc(myTempMovie, (MovieProgressUPP)-1, 0L);

	// flatten the temporary file into a new movie file;
	// put the movie resource first so that FastStart is possible
	myErr = VRPano_FlattenMovieForStreaming(myTempMovie, &myDestSpec);
	if (myErr != noErr)
		myPanoMovie = FlattenMovieData(myTempMovie, flattenDontInterleaveFlatten | flattenAddMovieToDataFork | flattenForceMovieResourceBeforeMovieData, &myDestSpec, FOUR_CHAR_CODE('TVOD'), smSystemScript, createMovieFileDeleteCurFile | createMovieFileDontCreateResFile);

bail:
	if (myImporter != NULL)
		CloseComponent(myImporter);
	
	if (myPanoMovie != NULL)
		DisposeMovie(myPanoMovie);

	if (myTempMovie != NULL)
		DisposeMovie(myTempMovie);
		
	if (myTempResRefNum != -1)
		CloseMovieFile(myTempResRefNum);
		
	DeleteMovieFile(&myTempSpec);
	DeleteMovieFile(&myHSTileSpec);
					
	return(myErr);
}


//////////
//
// VRPano_ImportVideoTrack
// Copy a video track from one movie (the source) to another (the destination).
//
// We assume that we want the first video track in theSrcMovie.
//
//////////

OSErr VRPano_ImportVideoTrack (Movie theSrcMovie, Movie theDstMovie, TimeValue theDuration, long *theTrackWidth, long *theTrackHeight, Track *theTrack)
{
	Track						mySrcTrack = NULL;
	Media						mySrcMedia = NULL;
	Track						myDstTrack = NULL;
	Media						myDstMedia = NULL;
	Fixed						myWidth, myHeight;
	OSType						myType;
	SampleDescriptionHandle		mySampleDesc = (SampleDescriptionHandle)NewHandle(0);
	Handle						mySampleData = NewHandle(0);
	long						myDataSize;
	short						myIndex;
	short						myNumFrames = 0;
	TimeValue					myMediaTime = 0;
	TimeValue					mySampleDuration;

	ClearMoviesStickyError();
	
	// get the first video track in the source movie
	mySrcTrack = GetMovieIndTrackType(theSrcMovie, 1, VideoMediaType, movieTrackMediaType);
	if (mySrcTrack == NULL)
		return(paramErr);
		
	// count the number of frames in the source movie
	myNumFrames = (short)GetMediaSampleCount(GetTrackMedia(mySrcTrack));
	if (myNumFrames <= 0)
		return(paramErr);
	
	// get the track's media and dimensions
	mySrcMedia = GetTrackMedia(mySrcTrack);
	GetTrackDimensions(mySrcTrack, &myWidth, &myHeight);
	
	// create a destination track
	myDstTrack = NewMovieTrack(theDstMovie, myWidth, myHeight, GetTrackVolume(mySrcTrack));
	if (theTrackWidth != NULL)
		*theTrackWidth = myWidth >> 16;
	if (theTrackHeight != NULL)
		*theTrackHeight = myHeight >> 16;
	if (theTrack != NULL)
		*theTrack = myDstTrack;

	// create a destination media
	GetMediaHandlerDescription(mySrcMedia, &myType, NULL, NULL);
	myDstMedia = NewTrackMedia(myDstTrack, myType, kQTVRStandardTimeScale, 0, 0);

	BeginMediaEdits(myDstMedia);
	
	// extract samples one at a time
	for (myIndex = 0; myIndex < myNumFrames; myIndex++) {
		GetMediaSample(mySrcMedia, mySampleData, 0, &myDataSize, myMediaTime, NULL, &mySampleDuration, mySampleDesc, NULL, 1, NULL, NULL);
		myMediaTime += mySampleDuration;
		AddMediaSample(myDstMedia, mySampleData, 0, myDataSize, theDuration / myNumFrames, mySampleDesc, 1, 0, NULL);
	}

	DisposeHandle((Handle)mySampleDesc);
	DisposeHandle(mySampleData);
	
	EndMediaEdits(myDstMedia);
	
	InsertMediaIntoTrack(myDstTrack, 0, 0, GetMediaDuration(myDstMedia), fixed1);

	// a panorama image track should always be disabled
	SetTrackEnabled(myDstTrack, false);

	return(GetMoviesStickyError());
}


//////////
//
// VRPano_MakeHotSpotVers1x0
// Create a hot spot table atom with atoms for a single hot spot; also, if necessary,
// resize the string table handle theStringTable to contain a name and comment for the
// hot spot.
//
// NOTE: This function builds hot spots that conform to version 1.0 of the QuickTime VR file format.
//
//////////

HotSpotTableAtomHandle VRPano_MakeHotSpotVers1x0 (StringTableAtomHandle theStringTable)
{
#pragma unused(theStringTable)

	HotSpotTableAtomHandle	myHandle = NULL;
	
	// [left as an exercise for the reader]
	
	return(myHandle);
}


//////////
//
// VRPano_MakeHotSpotVers2x0
// Create the necessary atoms inside of theNodeInfo to configure the specified hot spot
// as a URL hot spot linked to the specified URL.
//
// NOTE: This function builds hot spots that conform to version 2.0 of the QuickTime VR file format.
//
//////////

OSErr VRPano_MakeHotSpotVers2x0 (QTAtomContainer theNodeInfo, QTAtom theHSParent, char *theURL, char *theHSName, UInt32 theIndex)
{
    QTAtom					myHSAtom;
    QTVRHotSpotInfoAtom     myHSInfoAtom;
    Str255				    myHSName;
    QTAtomID			    myHSNameID;
    OSErr					myErr = noErr;

	//////////
	//
	// add a hot spot atom to the specified node info atom
	//
	// a hot spot atom contains two children:
	// a hot spot information atom, which contains general info about the hot spot,
	// and (for URL hot spots) a URL hot spot atom
	//
	//////////
	
	// the atom ID should be the same as the hot spot ID, which is an index in a 8-bit color table	
	myErr = QTInsertChild(theNodeInfo, theHSParent, kQTVRHotSpotAtomType, theIndex, 0, 0, NULL, &myHSAtom);
	if (myErr != noErr)
		goto bail;
	
	//////////
	//
	// add a hot spot information atom
	//
	//////////
	
	// fill in the fields of the hot spot information atom data structure
	myHSInfoAtom.majorVersion = EndianU16_NtoB(kQTVRMajorVersion);
	myHSInfoAtom.minorVersion = EndianU16_NtoB(kQTVRMinorVersion);
	myHSInfoAtom.hotSpotType = EndianU32_NtoB(kQTVRHotSpotURLType);

	// the published documentation says that the hot spot name is contained in a string atom
	// that is a sibling of the hot spot atom (that is, a child of the hot spot parent atom);
	// some other documents indicate that a string atom is always a sibling of the atom that
	// contains the reference (in this case, a sibling of the hot spot information atom, and
	// hence a child of the hot spot atom); I'd recommend coding to the latter....
	
	// add the hot spot name atom
    myHSName[0] = strlen(theHSName);
    strcpy((char *)&myHSName[1], theHSName);
	myErr = VRPano_AddStr255ToAtomContainer(theNodeInfo, myHSAtom, myHSName, &myHSNameID);
	if (myErr != noErr)
		goto bail;
	
	myHSInfoAtom.nameAtomID = EndianU32_NtoB(myHSNameID);
	
	// add the hot spot comment atom; 0 means that no comment atom exists
    myHSInfoAtom.commentAtomID = EndianU32_NtoB(0L);
    
    // set the custom cursor IDs; 0 means that no custom cursors exist
    myHSInfoAtom.cursorID[0] = 0;
    myHSInfoAtom.cursorID[1] = 0;
    myHSInfoAtom.cursorID[2] = 0;
	
	// set viewing hints
	myHSInfoAtom.bestPan = 0.0;
	myHSInfoAtom.bestTilt = 0.0;
	myHSInfoAtom.bestFOV = 0.0;

    VRPano_ConvertFloatToBigEndian(&myHSInfoAtom.bestPan);
    VRPano_ConvertFloatToBigEndian(&myHSInfoAtom.bestTilt);
    VRPano_ConvertFloatToBigEndian(&myHSInfoAtom.bestFOV);
    
    myHSInfoAtom.bestViewCenter.x = 0.0;
    myHSInfoAtom.bestViewCenter.y = 0.0;

	// set hot spot bounding rectangle; apparently unused
	myHSInfoAtom.hotSpotRect.top = 0;
	myHSInfoAtom.hotSpotRect.left = 0;
	myHSInfoAtom.hotSpotRect.bottom = 0;
	myHSInfoAtom.hotSpotRect.right = 0;

	myHSInfoAtom.flags = 0L;
	myHSInfoAtom.reserved1 = 0L;
	myHSInfoAtom.reserved2 = 0L;
	
	// insert the hot spot information atom into the hot spot atom
	myErr = QTInsertChild(theNodeInfo, myHSAtom, kQTVRHotSpotInfoAtomType, 1, 0, sizeof(myHSInfoAtom), &myHSInfoAtom, NULL);
	if (myErr != noErr)
		goto bail;
		
	//////////
	//
	// add a URL hot spot atom as a child of the hot spot atom
	//
	//////////
	
	// the atom data is the URL text
	// (not a Pascal or C string, but just the characters themselves)
	myErr = QTInsertChild(theNodeInfo, myHSAtom, kQTVRHotSpotURLType, 1, 0, strlen(theURL), theURL, NULL);

bail:
    return(myErr);
}


//////////
//
// VRPano_SetControllerType
// Set the controller type of the specified movie.
//
// This function adds an item to the movie's user data;
// the updated user data is written to the movie file when the movie is next updated
// (by calling AddMovieResource or UpdateMovieResource).
//
//////////

OSErr VRPano_SetControllerType (Movie theMovie, OSType theType)
{
	UserData		myUserData;
	OSErr			myErr = noErr;

	// make sure we've got a movie
	if (theMovie == NULL)
		return(paramErr);
		
	// get the movie's user data list
	myUserData = GetMovieUserData(theMovie);
	if (myUserData == NULL)
		return(paramErr);
	
	theType = EndianU32_NtoB(theType);
	myErr = SetUserDataItem(myUserData, &theType, sizeof(theType), kQTControllerType, 0);

	return(myErr);
}


//////////
//
// VRPano_AddStr255ToAtomContainer
// Add a Pascal string to the specified atom container; return (through theID) the ID of the new string atom.
//
//////////

OSErr VRPano_AddStr255ToAtomContainer (QTAtomContainer theContainer, QTAtom theParent, Str255 theString, QTAtomID *theID)
{
	OSErr					myErr = noErr;

	if ((theContainer == NULL) || (theParent == 0) || (theID == NULL))
		return(paramErr);
		
	*theID = 0;				// initialize the returned atom ID

	if (theString[0] != 0) {
		QTAtom				myStringAtom;
		UInt16				mySize;
		QTVRStringAtomPtr	myStringAtomPtr = NULL;
		
		mySize = sizeof(QTVRStringAtom) - 4 + theString[0];
		myStringAtomPtr = (QTVRStringAtomPtr)NewPtrClear(mySize);
		
		if (myStringAtomPtr != NULL) {
			myStringAtomPtr->stringUsage = EndianU16_NtoB(1);
			myStringAtomPtr->stringLength = EndianU16_NtoB(theString[0]);
			BlockMove(theString + 1, myStringAtomPtr->theString, theString[0]);
			myErr = QTInsertChild(theContainer, theParent, kQTVRStringAtomType, 0, 0, mySize, (Ptr)myStringAtomPtr, &myStringAtom);
			DisposePtr((Ptr)myStringAtomPtr);
			
			if (myErr == noErr)
				QTGetAtomTypeAndID(theContainer, myStringAtom, NULL, theID);
		}
	}
	
	return(myErr);
}


//////////
//
// VRPano_ConvertFloatToBigEndian
// Convert the specified floating-point number to big-endian format.
//
//////////

void VRPano_ConvertFloatToBigEndian (float *theFloat)
{
	unsigned long		*myLongPtr;
	
	myLongPtr = (unsigned long *)theFloat;
	*myLongPtr = EndianU32_NtoB(*myLongPtr);
}


//////////
//
// VRPano_FileFilterFunction
// Filter files for a file-opening dialog box.
//
//////////

#if TARGET_OS_MAC
PASCAL_RTN Boolean VRPano_FileFilterFunction (AEDesc *theItem, void *theInfo, void *theCallBackUD, NavFilterModes theFilterMode)
{
#pragma unused(theCallBackUD, theFilterMode)
	Boolean					myIsOkay = true;
	NavFileOrFolderInfo		*myInfo = (NavFileOrFolderInfo *)theInfo;
	OSErr					myErr = noErr;
	
	if (gValidFileTypes == NULL)
		QTFrame_BuildFileTypeList();

	if (theItem->descriptorType == typeFSS)
		if (!myInfo->isFolder) {
			myIsOkay = false;
			
			if (gVersionToCreate == kCubicQTVR) {
				if (myInfo->fileAndFolder.fileInfo.finderInfo.fdType == kQTFileTypeMovie)
					myIsOkay = true;
			} else {
				OSType			myType = myInfo->fileAndFolder.fileInfo.finderInfo.fdType;
				OSType			*myTypes = (OSType *)*gValidFileTypes;
				short			myCount;
				short			myIndex;
				
				// see whether the file type is in the list of image file types that QuickTime can open 
				myCount = GetHandleSize(gValidFileTypes) / sizeof(OSType);
				for (myIndex = gFirstGITypeIndex; myIndex < myCount; myIndex++)
					if (myType == myTypes[myIndex])
						myIsOkay = true;
			}
		}

	return(myIsOkay);
}
#endif
#if TARGET_OS_WIN32
PASCAL_RTN Boolean VRPano_FileFilterFunction (CInfoPBPtr thePBPtr)
{
#pragma unused(thePBPtr)
	return(false);
}
#endif


//////////
//
// VRPano_FlattenMovieForStreaming
// Export the specified a QuickTime VR movie, using the QTVR flattener movie export component.
//
//////////

OSErr VRPano_FlattenMovieForStreaming (Movie theMovie, FSSpecPtr theFSSpecPtr)
{
	ComponentDescription		myCompDesc;
	MovieExportComponent		myExporter = NULL;
	long						myFlags = createMovieFileDeleteCurFile | showUserSettingsDialog | movieFileSpecValid;
	ComponentResult				myErr = badComponentType;

	// find and open a movie export component that can flatten a QuickTime VR movie file
	myCompDesc.componentType = MovieExportType;
	myCompDesc.componentSubType = MovieFileType;
	myCompDesc.componentManufacturer = FOUR_CHAR_CODE('vrwe');
	myCompDesc.componentFlags = 0;
	myCompDesc.componentFlagsMask = 0;
	myExporter = OpenComponent(FindNextComponent(NULL, &myCompDesc));
	if (myExporter == NULL)
		goto bail;

	// use the default progress procedure
	SetMovieProgressProc(theMovie, (MovieProgressUPP)-1L, 0);

	// export the movie into a file
	myErr = ConvertMovieToFile(	theMovie,				// the movie to convert
								NULL,					// all tracks in the movie
								theFSSpecPtr,			// the output file
								MovieFileType,			// the output file type
								sigMoviePlayer,			// the output file creator
								smSystemScript,			// the script
								NULL, 					// no resource ID to be returned
								myFlags,				// conversion flags
								myExporter);			// QTVR flattener movie export component

bail:
	// close the movie export component
	if (myExporter != NULL)
		CloseComponent(myExporter);

	return((OSErr)myErr);
}

