ExportTerrainCollada.sml

  Download

More scripts: Advanced

Syntax Highlighing:

comments, key words, predefined symbols, class members & methods, functions & classes
            
# ExportTerrainCollada.sml
# Processes a selected DEM and overlapping IMAGE into a 3D model incorporating
# a custom terrain surface and matching image overlay that can be viewed in 3D 
# in Google Earth.  DEM and IMAGE extents do not have to match exactly, and they 
# can have different coordinate reference systems.  The DEM and IMAGE are automatically
# cropped to their common extents if necessary.
# The surface and image texture are output as a COLLADA model packaged in
# a KMZ file (compressed KML).  The COLLADA model is referenced as a placemark
# in the KML file stored within the KMZ.  The placemark includes the WGS84 Geographic
# coordinates of the origin (center point) of the model, its height in meters
# above sea level, and its orientation (angle to north).  This placemark information
# allows Google Earth to correctly locate and orient the terrain model in 3D space.
# The COLLADA model specifies a triangular mesh defining the custom 3D terrain
# derived from the DEM and a mapping to corresponding normalized image 
# coordinates to allow the image (exported as a JPEG, PNG, or TIFF file) to be overlaid as 
# a texture onto the triangular mesh in Google Earth.  The triangular mesh coordinates
# are read from a TIN created from the DEM.  The COLLADA model has projected coordinates 
# derived from the coordinate reference system of the IMAGE (unless it has a 
# non-projected CRS, in which case a location-appropriate UTM CRS is created and used).
# The custom script dialog provides interactive selection of the input DEM and IMAGE
# and the output KMZ file. The elevation units for the DEM (meters or feet) must
# be correctly specified to provide proper vertical scaling for the model.  A
# vertical offset of the model above the Google Earth terrain surface can be
# set to avoid intersection of the model and the Google Earth terrain (portions
# of the model below the Google Earth terrain are not visible).  
# NOTE: MODELS WITH IMAGE TEXTURES LARGER THAN ABOUT 1024 X 1024 OR WITH HIGHLY
# DETAILED TERRAIN MESHES MAY LOAD VERY SLOWLY IN GOOGLE EARTH AND DEGRADE GOOGLE 
# EARTH PERFORMANCE.
# Randy Smith, MicroImages, Inc.
# 28 February 2011
class RVC_RASTER DEM, IMAGE;
class RVC_OBJITEM demObjItem, imgObjItem;
class STRING prompt$, err$;
class RVC_GEOREFERENCE georefDEM, georefIMG;
class SR_COORDREFSYS crsDEM, crsIMG;
class POINT2D demCellSizesPt, imgCellSizesPt;
numeric demCellSize, imgCellSize;
numeric err;
numeric i, j, k, m;		# loop counters
numeric tempfile1exists, tempfile2exists, tempfile3exists;
class STRING zUnit$;		# length unit for DEM elevation values
numeric zScaleDEM;
numeric zOffset;
class STRING kmzfilename$;
class GUI_DLG dlgwin;
class GUI_CTRL_COMBOBOX elevunitsCombo, formatCombo;
class GUI_CTRL_PUSHBUTTON imgBtn, kmzBtn;
class GUI_CTRL_EDIT_STRING demText, imgText, kmzText;
class STRING outFormat$;
numeric canceled;
#############  STATUS DIALOG COMPONENTS  #####################
class STATUSDIALOG statusD;		# status dialog to show process status
class STATUSCONTEXT statusC;		# context for the status dialog
############################################################################
#######								
#######      USER-DEFINED FUNCTIONS AND PROCEDURES
# procedure automatically called when the dialog window is initialized
proc onInitDialog()
	{
	# disable the OK button
	dlgwin.SetOkEnabled(0);
	# add elevation units to the DEM Elevation Units combobox
	elevunitsCombo = dlgwin.GetCtrlByID("elevunitsCombo");
	elevunitsCombo.AddItem("meters", "meters");
	elevunitsCombo.AddItem("feet", "feet");
	elevunitsCombo.SetSelectedItemIndex(0);	# set meters as default unit shown
	# add formats to the Image Format combobox
	formatCombo = dlgwin.GetCtrlByID("formatCombo");
	formatCombo.AddItem("JPEG", "JPEG");
	formatCombo.AddItem("PNG", "PNG");
	formatCombo.AddItem("TIFF", "TIFF");
	formatCombo.SetSelectedItemIndex(0);	# set JPEG as the default format
	# get handles for pushbuttons to enable them
	imgBtn = dlgwin.GetCtrlByID("imgBtn");
	kmzBtn = dlgwin.GetCtrlByID("kmzBtn");
	# get handles for the edittext controls to show the names of the input rasters in the dialog
	demText = dlgwin.GetCtrlByID("demText");
	imgText = dlgwin.GetCtrlByID("imgText");
	kmzText = dlgwin.GetCtrlByID("kmzText");
	}
# procedure called when DEM button is pressed on the dialog window
# Get ObjItem for input DEM and write its display string to its edittext
proc getDEM()
	{
	prompt$ = "Choose input DEM raster:";
	DlgGetObject(prompt$, "Raster", demObjItem, "ExistingOnly");
	demText.SetValueStr(demObjItem.GetDisplayString() );
	imgBtn.SetEnabled(1);
	}
# procedure called when IMAGE button is pressed on the dialog window
# Get ObjItem for input IMAGE and write its display string to its edittext
proc getIMAGE()
	{
	prompt$ = "Choose input IMAGE raster:";
	DlgGetObject(prompt$, "Raster", imgObjItem, "ExistingOnly");
	imgText.SetValueStr(imgObjItem.GetDisplayString() );
	kmzBtn.SetEnabled(1);
	}
# procedure called when KMZ button is pressed on the dialog window
# Get filename/location for the output KMZ file.
proc getKMZ()
	{
	prompt$ = "Choose an output KMZ file to package the image/terrain models:";
	kmzfilename$ = GetOutputFileName(_context.ScriptDir, prompt$, ".kmz");
	kmzText.SetValueStr(kmzfilename$);
	printf("Full path to KMZ file: %s\n", kmzfilename$);
	dlgwin.SetOkEnabled(1);
	}
# procedure called when OK button on the dialog window is pressed.
# Get values for maximum image tile size, DEM elevation units, and Z offset
proc onDlgOK()
	{ 
	zUnit$ = elevunitsCombo.GetValueStr();
	zOffset = dlgwin.GetCtrlByID("zOffsetNum").GetValueNum();
	outFormat$ = formatCombo.GetValueStr();
	# get scale factor to convert DEM elevation units to meters
	zScaleDEM = GetUnitConvDist(zUnit$, "meters");
	printf("DEM elevation unit = %s\n", zUnit$);
	printf("Scale factor from DEM elevation units to meters = %.4f\n", zScaleDEM);
	printf("Offset of terrain model above true elevation = %d m\n", zOffset);
	}
# procedure called when Cancel button on the dialog window is pressed.
# Exit script.
proc onDlgCancel()
	{
	dlgwin.Close(1);
	canceled = 1;
	}
############################
# error checking procedure for image pipeline routines
proc ReportError(numeric linenum, numeric err) 
	{
	printf("FAILED -line: %d, err: %d\n", linenum, err);
	PopupError(err);
	}
#############################
# function to get and check the georeference and coordinate reference system of the input DEM and IMAGE
func checkGeoref(
	class RVC_RASTER RAST, 
	class STRING name$, 
	var class RVC_GEOREFERENCE georef, 
	var class SR_COORDREFSYS crs,
	var class STRING e$)
	{
	if (RAST.GetDefaultGeoref(georef) )
		{
		crs = georef.GetCoordRefSys();
		if (crs.IsDefined() )
			{
			if (crs.IsLocal() <> 1)
				{
#				printf("%s Coordinate Reference System = %s\n", name$, crs.Name);
				return 1;
				}
			else
				{
				e$ = sprintf("Selected %s has a local arbitrary coordinate reference system.  Exiting now.\n", name$);
				return 0;
				}
			}
		else
			{
			e$ = sprintf("Selected %s has an undefined coordinate reference system.  Exiting now.\n", name$);
			return 0;
			}
		}
	else
		{
		e$ = sprintf("Selected %s is not georeferenced.  Exiting now.\n", name$);
		return 0;
		}
	}		# end checkGeoref()
######################################
# function to return the smaller of two values
func smaller(numeric a, numeric b)
	{
	if (a < b) then 
		return a;
	else return b;
	}
###############################################
# function to create a UTM CRS for raster that has lat / lon CRS
func class SR_COORDREFSYS makeUTMcrs(class RVC_RASTER RAST, class RVC_GEOREFERENCE inGeoref)
	{
	local class RECT3D extentsObj;
	local class POINT2D center;
	local class STRING demCRSunit$;
	local class TRANS2D_MAPGEN transObjToMap;
	inGeoref.GetTransParm(transObjToMap,  0, inGeoref.GetCalibModel() );
	RAST.GetExtents(extentsObj);
	center = extentsObj.center;
	center = transObjToMap.ConvertPoint2DFwd(center);
	printf("Image center: longitude = %.6f, latitude = %.6f\n", center.x, center.y); 
	# variables for setting up UTM CRS
	local class SR_COORDREFSYS utmCRS;			# the CRS to create
	local class SR_COORDSYS utmCoordSys;		# coordinate system
	local class SR_COORDOPDEF projectDef;		# projection parameters and method
	local class SR_COORDOPMETHOD method;
	local class SR_DATUM datum;
	datum = inGeoref.GetCoordRefSys().Datum;		# get datum from the input raster
	utmCoordSys.Assign("Projected2D_EN_m");		# assign projected coordinate system
	method.Assign("1909");					# assign using numeric ID for Transverse Mercator projection
	projectDef.Method = method;		# assign method to projection
	# assign projection parameters that are the same for all UTM zones
	projectDef.SetParmValueNum("LatitudeOfNaturalOrigin", 0);
	projectDef.SetParmValueNum("ScaleFactorAtNaturalOrigin", 0.9996);
	projectDef.SetParmValueNum("FalseEasting", 500000);
	# assign false northing based on whether north or south of equator
	if ( center.y < 0) then
		projectDef.SetParmValueNum("FalseNorthing", 10000);
	else
		projectDef.SetParmValueNum("FalseNorthing", 0);
	# assign longitude of natural origin (central meridian of UTM zone) based on longitude
	local numeric zoneNum, centMerid;		# UTM zone and longitude of central meridian
	zoneNum = ceil( (180 + center.x) / 6 );
	printf("UTM zone number = %d\n", zoneNum);
	centMerid = (zoneNum * 6) - 183;
	projectDef.SetParmValueNum("LongitudeOfNaturalOrigin", centMerid);
	# create defined UTM coordinate reference system
	utmCRS.Create(utmCoordSys, datum, projectDef);
	printf("CRS for reprojection: %s\n", utmCRS.name);
	printf("Central meridian = %d\n", centMerid); 
	return utmCRS;
	}	# end makeUTMcrs()
######################################################
# function to resample a raster to a specified CRS and cell size
func class RVC_OBJITEM resampleRast(
	class RVC_OBJITEM objItemS, 		# objItem of input raster
	class RVC_OBJITEM objItemT,		# objItem of output raster
	class SR_COORDREFSYS crs,		# CRS to resample to
	numeric cellsize	)
	{ 
	local class IMAGE_PIPELINE_SOURCE_RVC sourceRVC(objItemS);
	err = sourceRVC.Initialize();
	if (err < 0) ReportError(_context.CurrentLineNum, err);
	local class IMAGE_PIPELINE_FILTER_RESAMPLE resample(sourceRVC, crs, cellsize, cellsize, "CubicConvolution");
	err = resample.Initialize();
	if (err < 0) ReportError(_context.CurrentLineNum, err);
	local class IMAGE_PIPELINE_TARGET_RVC targetRVC(resample, objItemT);
	targetRVC.SetCompression("DPCM", 0);
	err = targetRVC.Initialize();
	if (err < 0) ReportError(_context.CurrentLineNum, err);
	targetRVC.Process();
	objItemT = targetRVC.GetObjItem();
	return objItemT;
	} # end resampleRast()
#########################################################
# function to crop a raster 
func class RVC_OBJITEM cropImage(
	class RVC_OBJITEM objItemS,		# objItem of input raster
	class RECT intersect,					# common image extents RECT to use to crop input raster
	class RVC_OBJITEM objItemT)		# objItem of output
	{
	local class IMAGE_PIPELINE_SOURCE_RVC sourceRVC(objItemS);
	err = sourceRVC.Initialize();
	if (err < 0) ReportError(_context.CurrentLineNum, err);
	local class RECT sourceRect;
	sourceRect.pt1.x = 0;		sourceRect.pt1.y = 0;
	sourceRect.pt2.x = sourceRVC.GetTotalColumns();
	sourceRect.pt2.y = sourceRVC.GetTotalRows(); 
	if (sourceRect.ContainsRect(intersect) )
		{
		print("Intersect extents are fully contained within DEM extents.");
		local class IMAGE_PIPELINE_FILTER_CROP crop(sourceRVC, intersect);
		err = crop.Initialize();
		if (err < 0) ReportError(_context.CurrentLineNum, err);
		local class IMAGE_PIPELINE_TARGET_RVC targetRVC(crop, objItemT);
		err = targetRVC.Initialize();
		if (err < 0) ReportError(_context.CurrentLineNum, err);
		targetRVC.Process();
		objItemT = targetRVC.GetObjItem();
		return objItemT;
		}
	else
		{
		print("Intersect extents not fully contained within DEM extents. Cannot crop DEM.");
		}
	} # end cropImage()
################################################################
# function to determine if three points are in counter-clockwise order
# using image coordinates (origin in upper left, positive downward);
# returns value < 0 if counter-clockwise, value > 0 if clockwise.
func ccwImageCoords(class POINT2D p1, class POINT2d p2, class POINT2d p3)
	{
	return (p2.x - p1.x)*(p3.y - p1.y) - (p2.y - p1.y)*(p3.x - p1.x);
	}
############################################################################
######################                               
######################   		MAIN PROGRAM   		  
######################
clear();	# clear the console window                               
class STRING dlgxml$;
dlgxml$ = '<?xml version="1.0"?>
<!DOCTYPE root SYSTEM "smlforms.dtd">
<root>
	<dialog id="dlgid" Title="Export DEM and IMAGE to KML/COLLADA" OnOK="onDlgOK()" OnCancel="onDlgCancel()">
		<groupbox Name=" Select Inputs " HorizResize="Expand" ExtraBorder="3">
			<pane Orientation="horizontal">
				<pushbutton id="demBtn" Name=" DEM... " OnPressed="getDEM()"/>
				<edittext id="demText" Width="40" ReadOnly="true"/>
			</pane>
			<pane Orientation="horizontal">
				<pushbutton id="imgBtn" Name=" IMAGE... " Enabled="false" OnPressed="getIMAGE()"/>
				<edittext id="imgText" Width="40" ReadOnly="true"/>
			</pane>
		</groupbox>
		<groupbox Name=" Select Output File " HorizResize="Expand" ExtraBorder="3">
			<pane Orientation="horizontal">
				<pushbutton id="kmzBtn" Name = " KMZ... " Enabled="false" OnPressed="getKMZ()"/>
				<edittext id="kmzText" Width="40" ReadOnly="true"/>
			</pane>
		</groupbox>
		<pane Orientation="horizontal">
			<label>DEM elevation units: </label>
			<combobox id="elevunitsCombo" HorizResize="Fixed"/>
			<label>Image Format: </label>
			<combobox id="formatCombo" HorizResize="Fixed"/>
		</pane>
		<pane Orientation="horizontal">
			<label>Offset of model above true elevation: </label>
			<editnumber id="zOffsetNum" Width="4" Precision="0" Default="20" MinVal="0" HorizResize="Fixed"/>
			<label> meters</label>
		</pane>
	</dialog>
</root>';
##############################################
### parse XML text for the dialog into memory; 
### return an error code (number < 0 ) if there are syntax errors
class XMLDOC dlgdoc;
err = dlgdoc.Parse(dlgxml$);
if ( err < 0 ) {
	PopupError( err ); 	# Popup an error dialog. "Details" button shows syntax errors.
	Exit();
	}
################################################
# get the dialog element from the parsed XML document and
# show error message if the dialog element can't be found
class XMLNODE dlgnode;
dlgnode = dlgdoc.GetElementByID("dlgid");
if ( dlgnode == 0 ) {
	PopupMessage("Could not find dialog node in XML document");
	Exit();
	}
################################################
# Set the XML dialog element as the source for the GUI_DLG class 
# instance we are using for the dialog window.
err = dlgwin.SetXMLNode(dlgnode);
if ( err < 0 ) {
	PopupError( err ); 	# Popup an error dialog. 
	Exit();
	}
dlgwin.SetOnInitDialog(onInitDialog);
#############################
# open the dialog window
dlgwin.DoModal();
if (canceled) then Exit();
####################
# Open the input rasters using the selected ObjItems
DEM.Open(demObjItem, "Read");
IMAGE.Open(imgObjItem, "Read");
# check DEM and IMAGE georeference and CRS
if ( checkGeoref(DEM, "DEM", georefDEM, crsDEM, err$ ) <> 1 or  checkGeoref(IMAGE, "IMAGE", georefIMG, crsIMG, err$) <> 1 )
	{
	PopupMessage(err$);
	Exit();
	}
printf("DEM Coordinate Reference System = %s\n", crsDEM.Name);
printf("IMAGE Coordinate Reference System = %s\n", crsIMG.Name);
#########################
# Set up KMZ file and ZIP file structure
class FILEPATH kmzFilepath(kmzfilename$);
class FILEPATH dirFilepath(kmzfilename$);
dirFilepath.StripToFolder();
class STRING dir$;
dir$ = dirFilepath;
printf("Directory containing KMZ file: %s\n", dir$);
# ZIP file structure to use for KMZ 
class ZIPFILE kmz;
########################################
# get cell sizes for the DEM and IMAGE
DEM.ComputeScaleFromGeoref(georefDEM, demCellSizesPt, 1);
printf("Cell sizes of DEM: col = %.1f m, lin = %.1f m\n", demCellSizesPt.x, demCellSizesPt.y);
demCellSize = smaller(demCellSizesPt.x, demCellSizesPt.y);
printf("Smaller cell size of DEM: %.1f\n", demCellSize);
IMAGE.ComputeScaleFromGeoref(georefIMG, imgCellSizesPt, 1);
printf("Cell sizes of IMAGE: col = %.1f m, lin = %.1f m\n", imgCellSizesPt.x, imgCellSizesPt.y);
imgCellSize = smaller(imgCellSizesPt.x, imgCellSizesPt.y);
printf("Smaller cell size of IMAGE: %.1f\n", imgCellSize);
##########################################################
### Create status dialog and its context
##########################################################
statusD.Create();
statusC = statusD.CreateContext();
### check IMAGE for projected CRS; if not projected, it is geographic (lat / lon);
### reproject to a UTM CRS with zone appropriate for its location
class SR_COORDREFSYS crsIMGp;
if (crsIMG.IsProjected() <> 1) 
	{
	tempfile1exists = 1;
	class RVC_OBJECT tempfile1;
	tempfile1.MakeTempFile(1);		# set to delete on close
	class RVC_OBJITEM imgpObjItem;			# for image converted to projected CRS
	class RVC_DESCRIPTOR imgpDescript;
	imgpDescript.SetName("IMAGE");
	imgpDescript.SetDescription("");
	imgpObjItem.CreateNew(tempfile1.GetObjItem(), "RASTER", imgpDescript);
	# compute a UTM CRS appropriate to center point and reproject inage raster to UTM
	print("Reprojecting IMAGE with Geographic CRS to UTM.");
	statusC.Message = "Reprojecting IMAGE with Geographic CRS to UTM.";
	# call function to create a UTM CRS, computing the zone from the center of the image extents
	crsIMGp = makeUTMcrs(IMAGE, georefIMG);
	# call user-defined procedure to resample raster to specified CRS and cell size 
	imgpObjItem = resampleRast(IMAGE.GetObjItem(), imgpObjItem, crsIMGp, imgCellSize);
	# close the original IMAGE raster and open the resampled raster as IMAGE
	IMAGE.Close(); 
	IMAGE.Open(imgpObjItem, "Read");
	georefIMG.OpenLastUsed(IMAGE);
	crsIMG = crsIMGp;
	}
else
	print("IMAGE has a projected CRS, no reprojection necessary.");
#############################################
### if necessary, reproject DEM to match CRS of IMAGE
if (crsDEM.GetID("MicroImages") <> crsIMG.GetID("MicroImages") )
	{
	tempfile2exists = 1;
	class RVC_OBJECT tempfile2;
	tempfile2.MakeTempFile(1);		# set to delete on close
	class RVC_OBJITEM dempObjItem;
	class RVC_DESCRIPTOR dempDescript;
	class SR_COORDREFSYS crsDEMp;
	dempDescript.SetName("DEMp");
	dempDescript.SetDescription("");
	dempObjItem.CreateNew(tempfile2.GetObjItem(), "RASTER", dempDescript);
	print("Reprojecting DEM to match CRS of IMAGE.");
	statusC.Message = "Reprojecting DEM to match CRS of IMAGE.";
	# call user-defined procedure to resample raster to specified CRS and cell size
	dempObjItem = resampleRast(demObjItem, dempObjItem, crsIMG, demCellSize);
	# close the original DEM raster and open the resampled raster as DEM
	DEM.Close();
	DEM.Open(dempObjItem, "Read");
	georefDEM.OpenLastUsed(DEM);
	}
else
	print("DEM and IMAGE have the same CRS, DEM does not need to be reprojected.");
#################################################
# get extents of IMAGE and DEM in map coordinates
class RECT3D extentsIMG, extentsDEM;		# get extents of each raster in object coordinates
class RECT extentsRECTimg, extentsRECTdem;		# ConvertRectFwd method below returns a RECT, not RECT3D
IMAGE.GetExtents(extentsIMG);
DEM.GetExtents(extentsDEM);
# get coordinate transformations from image to map coordinates
class TRANS2D_MAPGEN transIMGobjToMap, transDEMobjToMap;
georefIMG.GetTransParm(transIMGobjToMap, 0, georefIMG.GetCalibModel() );
georefDEM.GetTransParm(transDEMobjToMap, 0, georefDEM.GetCalibModel() );
# convert extents to map coordinates
extentsRECTimg =  transIMGobjToMap.ConvertRectFwd(extentsIMG);
extentsRECTdem = transDEMobjToMap.ConvertRectFwd(extentsDEM);
printf("IMAGE extents: x1 = %.1f, y1 = %.1f, x2 = %.1f, y2 = %.1f\n", extentsRECTimg.pt1.x, extentsRECTimg.pt1.y, extentsRECTimg.pt2.x, extentsRECTimg.pt2.y);
printf("DEM extents: x1 = %.1f, y1 = %.1f, x2 = %.1f, y2 = %.1f\n", extentsRECTdem.pt1.x, extentsRECTdem.pt1.y, extentsRECTdem.pt2.x, extentsRECTdem.pt2.y);
# check if extents of DEM and IMAGE are different
if (extentsRECTimg.pt1 != extentsRECTdem.pt1 or extentsRECTimg.pt2 != extentsRECTdem.pt2)
	{
	# check that extents overlap
	if (extentsRECTimg.Overlaps(extentsRECTdem) )
		{
		# find intersect of the two extents RECTs in map coordinates
		class RECT intersect; # new extents RECT
		intersect.pt1 = extentsRECTimg.pt1;		intersect.pt2 = extentsRECTimg.pt2;		# set to same extents as IMAGE
		intersect.Intersect(extentsRECTdem);	 # intersect with DEM extents
		printf("common extents: x1 = %.1f, y1 = %.1f, x2 = %.1f, y2 = %.1f\n", intersect.pt1.x, intersect.pt1.y, intersect.pt2.x, intersect.pt2.y);
		class RVC_OBJECT tempfile3;
		tempfile3.MakeTempFile(1);		# set to delete on close
		tempfile3exists = 1;
		numeric cropIMAGE = 1;		# flags to indicate whether to crop, set to 1 by default
		numeric cropDEM = 1;
		# check for full containment cases where don't need to crop one of the inputs
		if (extentsRECTdem.ContainsRect(extentsRECTimg) ) then
			cropIMAGE = 0;		# if DEM contains IMAGE, don't need to crop IMAGE
		else if (extentsRECTimg.ContainsRect(extentsRECTdem) ) then
			cropDEM = 0;		# if IMAGE contains DEM, don't need to crop DEM
		class RVC_DESCRIPTOR iDescript;
		if (cropDEM)
			{
			class RVC_OBJITEM demiObjItem;
			iDescript.SetName("DEM");		iDescript.SetDescription("");
			demiObjItem.CreateNew(tempfile3.GetObjItem(), "RASTER", iDescript);
			print("Cropping the DEM to the common extents.");
			statusC.Message = "Cropping the DEM to the common extents.";
			# convert intersect RECT to object coordinates and adjust to integer values
			intersect = transDEMobjToMap.ConvertRectInv(intersect);
			intersect.pt1.x = ceil(intersect.pt1.x);
			intersect.pt1.y = ceil(intersect.pt1.y);
			intersect.pt2.x = floor(intersect.pt2.x);
			intersect.pt2.y = floor(intersect.pt2.y);
			demiObjItem = cropImage(DEM.GetObjItem(), intersect, demiObjItem);
			DEM.Close();
			DEM.Open(demiObjItem, "Read");
			georefDEM.OpenLastUsed(DEM);
			georefDEM.GetTransParm(transDEMobjToMap, 0, georefDEM.GetCalibModel() );
			}
		if (cropIMAGE)
			{
			class RVC_OBJITEM imgiObjItem;
			iDescript.SetName("IMAGE");	iDescript.SetDescription("");
			imgiObjItem.CreateNew(tempfile3.GetObjItem(), "RASTER", iDescript);
			print("Cropping the IMAGE to the common extents.");
			statusC.Message = "Cropping the IMAGE to the common extents.";
			# convert intersect RECT to object coordinates and adjust to integer values
			intersect = transIMGobjToMap.ConvertRectInv(intersect);
			intersect.pt1.x = ceil(intersect.pt1.x);
			intersect.pt1.y = ceil(intersect.pt1.y);
			intersect.pt2.x = floor(intersect.pt2.x);
			intersect.pt2.y = floor(intersect.pt2.y);
			imgiObjItem = cropImage(IMAGE.GetObjItem(), intersect, imgiObjItem);
			IMAGE.Close();
			IMAGE.Open(imgiObjItem, "Read");
			georefIMG.OpenLastUsed(IMAGE);
			georefIMG.GetTransParm(transIMGobjToMap, 0, georefIMG.GetCalibModel() );
			}
		if (tempfile2exists) then tempfile2.Close();
		}
	else
		{
		print("IMAGE and DEM do not overlap. Exiting now.");
		SetStatusMessage("IMAGE and DEM do not overlap. Exiting now.");
		Exit();
		}
	}
else		# DEM and IMAGE have the same extents
	print("DEM and IMAGE have the same extents, no cropping necessary.");
printf("Final DEM dimensions: %d columns, %d lines\n", DEM.$Info.NumCols, DEM.$Info.NumLins);
printf("Final IMAGE dimensions: %d columns, %d lines\n", IMAGE.$Info.NumCols, IMAGE.$Info.NumLins);
#################################
### get unit in DEM's projected coordinate reference system
### and set conversion factors between this unit and meters
class STRING unit$;
class SR_COORDAXIS axisX;
numeric unitConvToMeters;
numeric metersConvToUnit;
crsDEM = georefDEM.GetCoordRefSys();
axisX = crsDEM.CoordSys.GetAxis();
unit$ = axisX.Unit.GetName();
unitConvToMeters = GetUnitConvDist(unit$, "meter"); 
printf("Map unit = %s, conversion factor to meters = %.4f\n", unit$, unitConvToMeters);
metersConvToUnit = GetUnitConvDist("meter", unit$);
# set up a WGS84 / Geographic CRS for lat/lon coordinates to position terrain models in Google Earth
class SR_COORDREFSYS geoCRS;
geoCRS.Assign("Geographic2D_WGS84_Deg");
printf("Geographic CRS for Google Earth = %s\n", geoCRS.Name);
# set up coordinate transformation from DEM CRS to geoCRS
class TRANS2D_MAPGEN transToGeo;
transToGeo.InputCoordRefSys = crsDEM;
transToGeo.OutputCoordRefSys = geoCRS;
#	Find minimum values for the DEM map coordinates and elevation to use to set coordinate offsets
# in the COLLADA models and to position them in the KMZ file
numeric minX, minY, minZ, minZm;
minX = extentsDEM.pt1.x;			minY = extentsDEM.pt1.y;
minZ = GlobalMin(DEM);					# minimum elevation value in DEM elevation units
minZm = minZ * zScaleDEM;				# minimum elevation in meters to set origin in Google Earth
printf("Minimum DEM z value in native elevation units = %.1f\n", minZ);
printf("Minimum DEM z value in neters = %.1f\n", minZm);
######################################
# string with COLLADA file template
class STRING dae$ = '<?xml version="1.0" encoding="utf-8"?>
   <COLLADA xmlns="http://www.collada.org/2005/11/COLLADASchema" version="1.4.1">
   <asset>
      <contributor>
         <authoring_tool>TNTmips SML Script</authoring_tool>
      </contributor>
      <created></created>
      <modified></modified>
      <unit name="meters" meter="1.0"/>
      <up_axis>Z_UP</up_axis>
   </asset>
   <library_images>
      <image id="terrain-image" name="terrain-image">
         <init_from></init_from>
      </image>
   </library_images>
   <library_materials>
      <material id="terrainID" name="terrain">
         <instance_effect url="#terrain-effect"/>
      </material>
   </library_materials>
   <library_effects>
      <effect id="terrain-effect" name="terrain-effect">
         <profile_COMMON>
            <newparam sid="terrain-image-surface">
               <surface type="2D">
                  <init_from>terrain-image</init_from>
               </surface>
            </newparam>
            <newparam sid="terrain-image-sampler">
               <sampler2D>
                  <source>terrain-image-surface</source>
               </sampler2D>
            </newparam>
            <technique sid="COMMON">
               <phong>
                  <emission>
                     <color>0.000000 0.000000 0.000000 1</color>
                  </emission>
                  <ambient>
                     <color>0.000000 0.000000 0.000000 1</color>
                  </ambient>
                  <diffuse>
                     <texture texture="terrain-image-sampler" texcoord="UVSET0"/>
                  </diffuse>
                  <specular>
                     <color>0.330000 0.330000 0.330000 1</color>
                  </specular>
                  <shininess>
                     <float>20.000000</float>
                  </shininess>
                  <reflectivity>
                     <float>0.100000</float>
                  </reflectivity>
                  <transparent>
                     <color>1 1 1 1</color>
                  </transparent>
                  <transparency>
                     <float>0.000000</float>
                  </transparency>
               </phong>
            </technique>
            <extra>
               <technique profile="GOOGLEEARTH">
                  <double_sided>1</double_sided>
               </technique>
            </extra>
         </profile_COMMON>
      </effect>
   </library_effects>
   <library_geometries>
      <geometry id="mesh1-geometry" name="mesh1-geometry">
         <mesh>
            <source id="mesh1-geometry-position">
               <float_array id="mesh1-geometry-position-array" count="">
               		 </float_array>
               <technique_common>
                  <accessor source="#mesh1-geometry-position-array" count="" stride="3">
                     <param name="X" type="float"/>
                     <param name="Y" type="float"/>
                     <param name="Z" type="float"/>
                  </accessor>
               </technique_common>
            </source>
            <source id="mesh1-geometry-normal">
               <float_array id="mesh1-geometry-normal-array" count="3">-0.000000 -1.000000 -0.000000 </float_array>
               <technique_common>
                  <accessor source="#mesh1-geometry-normal-array" count="1" stride="3">
                     <param name="X" type="float"/>
                     <param name="Y" type="float"/>
                     <param name="Z" type="float"/>
                  </accessor>
               </technique_common>
            </source>
            <source id="mesh1-geometry-uv">
               <float_array id="mesh1-geometry-uv-array" count="">
               		 </float_array>
               <technique_common>
                  <accessor source="#mesh1-geometry-uv-array" count="" stride="2">
                     <param name="S" type="float"/>
                     <param name="T" type="float"/>
                  </accessor>
               </technique_common>
            </source>
            <vertices id="mesh1-geometry-vertex">
               <input semantic="POSITION" source="#mesh1-geometry-position"/>
            </vertices>
            <triangles material="terrain" count="">
               <input semantic="VERTEX" source="#mesh1-geometry-vertex" offset="0"/>
               <input semantic="NORMAL" source="#mesh1-geometry-normal" offset="1"/>
               <input semantic="TEXCOORD" source="#mesh1-geometry-uv" offset="2" set="0"/>
               <p> </p>
            </triangles>
         </mesh>
      </geometry>
   </library_geometries>
   <library_visual_scenes>
      <visual_scene id="TerrainScene" name="TerrainScene">
         <node id="Model" name="Model">
            <node id="mesh1" name="mesh1">
               <instance_geometry url="#mesh1-geometry">
                  <bind_material>
                     <technique_common>
                        <instance_material symbol="terrain" target="#terrainID">
                           <bind_vertex_input semantic="UVSET0" input_semantic="TEXCOORD" input_set="0"/>
                        </instance_material>
                     </technique_common>
                  </bind_material>
               </instance_geometry>
            </node>
         </node>
      </visual_scene>
   </library_visual_scenes>
   <scene>
      <instance_visual_scene url="#TerrainScene"/>
   </scene>
</COLLADA>';
######################################
### string with KML file template
class STRING kmldoc$;
class STRING kmlfile$ = dir$ + "/" + "doc.kml";		# filepath to doc.kml
class FILEPATH kmlFilepath(kmlfile$);
kmldoc$ = '<?xml version="1.0" encoding="UTF-8"?>
<kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:kml="http://www.opengis.net/kml/2.2" xmlns:atom="http://www.w3.org/2005/Atom">
<Folder>
	<name></name>
	<open>0</open>
</Folder>
</kml>';
# variables for modifying the KML file template
class XMLDOC kmldoc;
class XMLNODE kmlRootNode;
class XMLNODE folder, document, placemark, model;
class XMLNODE node, node1, node2, node3;
#########################################
# parse the KML file template to reference and position the COLLADA models
err = kmldoc.Parse(kmldoc$);
kmlRootNode = kmldoc.GetRootNode();	# get root node 
folder = kmlRootNode.FindChild("Folder");		# get folder node that will hold a Document for each cross-section
# set the name of the folder to show in the Google Earth layer list
node = folder.FindChild("name");
node.SetText(kmzfilename$);
###############################################################
#################  make a JEPG file from the drape raster 
class RVC_DESCRIPTOR imgDescriptor;
imgDescriptor = IMAGE.GetDescriptor();
class STRING imgName$ = kmzFilepath.GetNameOnly();
class STRING ext$;		# file extension for image file
class STRING img$ = dir$ + "/" + imgName$;
class FILEPATH imgFilepath(img$);		# filepath for texture image, lacking extension
statusC.Message = "Making image file for model texture.";
# PIPELINE SOURCE
class IMAGE_PIPELINE_SOURCE_RVC sourceIMG(IMAGE.GetObjItem() );
err = sourceIMG.Initialize();
if ( err < 0) ReportError(_context.CurrentLineNum, err);
# PIPELINE TARGET
class IMAGE_PIPELINE_TARGET target;		# generic pipeline target, to be assigned specific type
switch (outFormat$)
	{
	case "JPEG":
		ext$ = ".jpg";
		imgFilepath.SetExtension(ext$);
		class IMAGE_PIPELINE_TARGET_JPEG targetJPG(sourceIMG, imgFilepath, 75, "none");
		target = targetJPG;
		break;
	case "PNG":	
		ext$ = ".png";
		imgFilepath.SetExtension(ext$);
		class IMAGE_PIPELINE_TARGET_PNG targetPNG(sourceIMG, imgFilepath, "none");
		target = targetPNG;
		break;
	case "TIFF":
		ext$ = ".tif";
		imgFilepath.SetExtension(ext$);
		class IMAGE_PIPELINE_TARGET_TIFF_SETTINGS tiff_settings;
		tiff_settings.SetCompression("JPEG_DCT"); 
		class IMAGE_PIPELINE_TARGET_TIFF targetTIFF(sourceIMG, imgFilepath, "none");
		targetTIFF.SetParms(tiff_settings);
		target = targetTIFF;
		break;
	}
# Initialize the pipeline target
err = target.Initialize();
if ( err < 0) ReportError(_context.CurrentLineNum, err);
# Execute the pipeline
err = target.Process(1);		# show status bar in status dialog
if ( err < 0) ReportError(_context.CurrentLineNum, err);
# add the JPEG file to the KMZ (ZIP) file
kmz.AddFile(kmzFilepath, imgFilepath, "files/" + imgName$ + ext$);
# delete the source JPEG file
DeleteFile(imgFilepath);
###########################################################
################  make TIN from DEM to generate COLLADA meshes
class RVC_OBJECT tempfile;
tempfile.MakeTempFile(1);		# set to delete on close
class FILEPATH tempfilePath(tempfile.GetObjItem().GetFilePath());
class RVC_TIN demTIN;
class RVC_GEOREFERENCE georefTIN;
CreateTIN(demTIN, tempfilePath, "TIN");
numeric zTolerance = 1;
numeric maxNodes = 100000;
numeric zDelta = 1;
numeric shortEdge = 3;
numeric longEdge = 50000;
print("\nMaking TIN from DEM...");
statusC.Message = "Making TIN from DEM";
RasterToTINIterative2(demTIN, DEM, zTolerance, maxNodes, zDelta, shortEdge, longEdge);
demTIN.OpenAttached("Read");		# RasterToTINInterative2 closes the newly-created TIN, so must open it
demTIN.GetDefaultGeoref(georefTIN);
class TRANS2D_MAPGEN transTinObjToMap;
georefTIN.GetTransParm(transTinObjToMap, 0, georefTIN.GetCalibModel() );
# set origin point for the COLLADA model at the center of the TIN
class RECT3D extentsTIN;
demTIN.GetExtents(extentsTIN);
class POINT3D originPt;
originPt = extentsTIN.center;
originPt = transTinObjToMap.ConvertPoint3DFwd(originPt);		# convert to projected map coordinates
printf("Origin point for tile in projected coordinates: x = %.1f, y = %.1f\n", originPt.x, originPt.y);
# find angle to north at the origin point to orient the COLLADA model in Google Earth
numeric angleToNorth = georefTIN.GetCoordRefSys().ComputeAngleToNorth(originPt);
# reproject origin point to WGS84 / Geographic CRS for placing model in Google Earth
originPt = transToGeo.ConvertPoint3DFwd(originPt); 
originPt.z = minZm;
printf("Origin point for tile in WGS84/Geographic coordinates: x = %.8f, y = %.8f\n", originPt.x, originPt.y);
printf("Origin point z value (meters): %.1f\n", originPt.z);
printf("Origin point z value in DEM elevation units: %.1f\n", originPt.z / zScaleDEM); 
#############################################################
############## process TIN to COLLADA meshes
class POINT3D pt;
class STRING xyzString, uvString;
numeric s, t;
numeric numPts = demTIN.$Info.NumNodes;
numeric numTriangles = demTIN.$Info.NumTriangles;
numeric k;
# TIN object coordinates are natively offset to a local origin at lower left corner, and may be scaled as well.
# For TIN created from DEM, node in lower left corner cell is at middle of cell and gets TIN object
# coordinates 0.5, 0.5.  Origin of TIN object coordinates is at lower left corner of the lower left cell of the DEM.
# Get scales for TIN x and y coordinates (as a POINT2D) from its georeference
class POINT2D mapScalesTIN;
demTIN.ComputeScaleFromGeoref(georefTIN, mapScalesTIN, 0);
numeric zScaleTIN = demTIN.GetZScale();	# get Z scale and offset from TIN object info
numeric zOffsetTIN = demTIN.GetZOffset();
printf("TIN result: number of nodes = %d, number of triangles = %d\n", numPts, numTriangles);
printf("TIN scales: x = %.4f, %.4f, %.4f\n", mapScalesTIN.x, mapScalesTIN.y, zScaleTIN);
class POINT2D ptList[numPts];		# array of POINT2Ds to hold drape image coordinates for each node;
statusC.Message = sprintf("Processing %d TIN nodes...", numPts);
statusC.BarInit(numPts, 0);
# loop through TIN nodes to get x, y, and z values and write to xyzString, find corresponding normalized
# image coordinates and write them to uvString.  
for k = 1 to numPts
	{
	statusC.BarIncrement(1, 0);
	# get x, y, and z values (object coordinates) from Node table. 
	pt.x = demTIN.Node[ k ].NODE.x;
	pt.y = demTIN.Node[ k ].NODE.y;
	pt.z = demTIN.Node[ k ].NODE.z * zScaleTIN  + zOffsetTIN;		# pt elevation in DEM's elevation units
	pt.z = pt.z * zScaleDEM - originPt.z;			# rescale elevations to meters and subtract origin point elevation
	# apply scale factors from georeference to TIN x and y object coordinates 
	# so they have same scale as map coordinates and add to xyzString for COLLADA model
	class POINT2D meshPt;
	meshPt.x = (pt.x - extentsTIN.center.x) * mapScalesTIN.x;
	meshPt.y = (pt.y - extentsTIN.center.y) * mapScalesTIN.y;
	xyzString += sprintf("%.2f %.2f %.1f ", meshPt.x, meshPt.y, pt.z); 
	#	printf("xyzString = %s\n", xyzString);
	# get map coordinates of the TIN node from its raw (unscaled) object coordinates using previously-obtained
	# object-to-map coordinate transformation
	pt = transTinObjToMap.ConvertPoint3DFwd(pt); 
	# find the image coordinates of the point
	pt = transIMGobjToMap.ConvertPoint3DInv(pt);
	# add image coordinates to point list (for determining node order for triangles in later step)
	ptList[ k ] = pt; 
	# convert image coordinates to texture coordinates (normalized to 0 to 1 range, origin in lower left)
	s = pt.x / (IMAGE.$Info.NumCols);
	t = 1 - (pt.y / IMAGE.$Info.NumLins);		# inverted due to lower left origin
	uvString += sprintf("%.6f %.6f ", s, t);
	} # end for k = 1 to numPts
# loop through TIN triangles to get the node index for each triangle vertex and reorder to
# counter-clockwise order if necessary
array numeric triNodeIndexArray[3];
array numeric tempArray[3];
numeric testCCW;
class STRING pString;
statusC.Message = sprintf("Processing %d TIN triangles...", numTriangles);
statusC.BarInit(numTriangles, 0);
for k = 1 to numTriangles
	{
	statusC.BarIncrement(1, 0);
	tempArray[1] = demTIN.Triangle[ k ].TRIANGLE.Node1;
	tempArray[2] = demTIN.Triangle[ k ].TRIANGLE.Node2;
	tempArray[3] = demTIN.Triangle[ k ].TRIANGLE.Node3;
	testCCW = ccwImageCoords(ptList[ tempArray[1] ], ptList[ tempArray[2] ], ptList[ tempArray[3] ]); 
	triNodeIndexArray[1] = tempArray[1];
	if (testCCW > 0)	# if nodes not in counterclockwise order, reorder nodes 2 and 3
		{
		triNodeIndexArray[2] = tempArray[3];
		triNodeIndexArray[3] = tempArray[2];
		}
	else
		{
		triNodeIndexArray[2] = tempArray[2];
		triNodeIndexArray[3] = tempArray[3];
		}
	# loop through the nodes for the current triangle to write index positions to the "primitive" string (pString)
	# for the COLLADA file.  For each triangle need three values, in order: index for vertex, index for normals,
	# index for texture. All indices are 0-based; index for vertex and texture are equal to each other and are
	# node index - 1. 
	for m = 1 to 3
		{
		pString += sprintf("%d %d %d ", triNodeIndexArray[ m ] - 1, 0, triNodeIndexArray[ m ] - 1);
		}
	} # end for k = 1 to numTriangles
# close objects and temporary files that are no longer needed
demTIN.Close();
DEM.Close();
IMAGE.Close();
tempfile.Close();
#######################################
# get current date and time for inclusion in the COLLADA file
class DATETIME dt;
dt.SetCurrent();
class STRING dt$, format$;
format$ = "%Y-%m-%dT%H:%M:%SZ";
dt$ = dt.GetString(format$);
printf("formatted date and time: %s\n", dt$);
##############################################
###########    make the COLLADA file
print("Making COLLADA file.");
### parse the COLLADA template 
class XMLDOC daedoc;
err = daedoc.Parse(dae$);
class XMLNODE colladaRootNode;
colladaRootNode = daedoc.GetRootNode();
node = colladaRootNode.FindChild("asset");
node1 = node.FindChild("created");
node1.SetText(dt$);
node1 = node.FindChild("modified");
node1.SetText(dt$);
node1 = node.FindChild("unit");
node1.SetAttribute("name", unit$);
node1.SetAttribute("meter", sprintf("%.4f", unitConvToMeters) );
### set the output JPEG file as the library image
node = daedoc.GetElementByID("terrain-image");
node1 = node.FindChild("init_from");
node1.SetText(imgFilepath.GetName() );
### set the position mesh count and array
numeric arraycount = numPts * 3;
node = daedoc.GetElementByID("mesh1-geometry-position-array");
node.SetAttribute("count", NumToStr(arraycount) );
node.SetText(xyzString);
### set the count for the position array accessor
node1 = node.GetParent();
node2 = node1.FindChild("technique_common");
node3 = node2.FindChild("accessor");
node3.SetAttribute("count", NumToStr(numPts) );
### set the texture mesh count and array
arraycount = numPts * 2;
node = daedoc.GetElementByID("mesh1-geometry-uv-array");
node.SetAttribute("count", NumToStr(arraycount) );
node.SetText(uvString);
### set the count for the uv array accessor
node1 = node.GetParent();
node2 = node1.FindChild("technique_common");
node3 = node2.FindChild("accessor");
node3.SetAttribute("count", NumToStr(numPts) );
### set the material and primitive for the triangles element
node = daedoc.GetElementByID("mesh1-geometry");
node1 = node.FindChild("mesh");
node2 = node1.FindChild("triangles");
node2.SetAttribute("count", NumToStr(numTriangles) );
node3 = node2.FindChild("p");
node3.SetText(pString);
########################
# write out the COLLADA file
class STRING daefile$ = dir$ + "/" + imgName$ + ".dae";
printf("\nDAE filepath = %s\n", daefile$);
daedoc.Write(daefile$);
class FILEPATH daeFilepath(daefile$);
########################
# add COLLADA file to the KMZ
kmz.AddFile(kmzFilepath, daeFilepath, "files/" + imgName$ + ".dae"); 
# delete the source DAE file
DeleteFile(daefile$);
####################################################
#####
#####	add "document" to the doc.kml file to reference and place this model
document = folder.NewChild("Document");
node = document.NewChild("name");
node.SetText(imgName$);
placemark = document.NewChild("Placemark");
# create name element for placemark
placemark.NewChild("name", imgName$ );
# create style element for placemark
node = placemark.NewChild("Style");
node.SetAttribute("id", "default");
# create model element for placemark
model = placemark.NewChild("Model");
model.SetAttribute("id", "model");
# create altitude mode element for model
model.NewChild("altitudeMode", "absolute");
# create location element for model
node = model.NewChild("Location");
node.NewChild("longitude", sprintf("%.8f", originPt.x) );
node.NewChild("latitude", sprintf("%.8f", originPt.y) );
node.NewChild("altitude", sprintf("%.1f", originPt.z + zOffset) );
# create orientation element for model
node = model.NewChild("Orientation");
node.NewChild("heading", NumToStr(angleToNorth) );
node.NewChild("tilt", "0");
node.NewChild("roll", "0");
# create link element for model
node = model.NewChild("Link");
node.NewChild("href", "files/" + imgName$ + ".dae");
# create ResourceMap element for model
node = model.NewChild("ResourceMap");
node1 = node.NewChild("Alias");
node1.NewChild("targetHref", "files/" + imgName$ +".jpg");
node1.NewChild("sourceHref", "files/" + imgName$ +".jpg"); 
############## Write the KML structure to a file and add to KMZ
kmldoc.Write(kmlfile$);
kmz.AddFile(kmzFilepath, kmlFilepath, "doc.kml");
DeleteFile(kmlfile$);
if (tempfile3exists) then tempfile3.Close();
print("End.");