GPSphoto2.sml

  Download

More scripts: Advanced

Syntax Highlighing:

comments, key words, predefined symbols, class members & methods, functions & classes
            
#### GPSphoto2.sml
### Requires version 2006:73 of the TNT products
### Brett Colombe
### MicroImages, Inc.
###	Software Engineer
###	19 January 2007
# 2.0 - Major changes to use new GPSDBASE class
#######################################################
# Script inputs: GPS log file(s) and JPEG images with EXIF headers.
#
# This Script gets from the EXIF header the date and time each picture was taken
# and compares them with the GPS log to assign geographic coordinates for the image.
# User chooses whether to interpolate coordinates based on image time or use closest GPS coordinates.
# Each image location is added as a point to the user-selected vector object.
#
# As images are added, the EXIF fields for Date and Time are read and displayed in the listbox.
# As logs are added, each log file is parsed and the Date, Time, and Lat/Long/Elev are displayed in the listbox.
# User sets the constraints options and selects his output vector
# Coordinates for the images are calculated by:
# Search through the list of coordinates from the gps log files for the two closest log points
# Using the time of the image and these two coordinates, new coordinates are then interpolated for the image
# Constraints are checked, then Date, Time, Lat, Lon, Elev and Path of image are added to table, with records sorted by time
# Table is stored in Point Database for the vector, with each record attached to a vector point
# Vector may be displayed in a Layout group for hyperlinking the images by selecting File by Attribute and choosing the image Path in the table
##################################
# Images Tab:
# User has option to add a directory of images or single images, images are then displayed in a listbox
# Save As - allows user to select the vector outputed by script
# Compute Image Coordinates - calculates the coordinates of the images and adds these to the table
# Display GPS Table - to display the table
# Change Directory of Images - if user moves images to a different directory, allows user to select this other directory, 
#			searches for image names in the new directory matching those in the table, then these images' paths are changed to the new directory
##################################
# GPS Log Tab:
# User has option to add a directory of log files or single log files
# Log file path/name added to first list box
# Total list of coordinates added to second list box
# Set the offset time for camera if camera time is different than gps log time
# All log files must be MicroImages standard log format or NMEA standard, see script below for details
##################################
# Options Tab:
# Choose to interpolate new coordinates or simply select closest log point's coordinates
# Set the maximum allowed difference in time between image and closest log point
########## Global Class Declarations ##############################
class GPSDBASE gpsdbase; ## NEW CLASS
class XMLDOC dlgdoc, logdoc, latlondoc, transferdoc, exifdoc;			# class instance for the XML document
class XMLNODE gpsdlgnode, lognode, latlonnode, transfernode, exifnode;	# class instance for the node in the XML
class GUI_DLG gpsdialog, logdialog, latlondialog, transferdialog, exifdialog;	# class instance for the GUI dialog
class DATABASE dbase; # class instance for database
class DBTABLEINFO table; # class instance for GPS table
class STRINGLIST imageListName; # string lists containing listbox image name
class STRINGLIST imageListNameAttached; # string lists containing image name for found coordinates
class STRINGLIST coordStringList;
class STRING imageStringList[];
class DATETIME imageListTime[];
class GPSDATA imageListData[];
numeric Assigned[];
#####
class GRE_GROUP gp;                  # spatial group for display.
class GRE_VIEW view;                 # view that displays the group.
class XmForm pcwin;           # parent form for dialog window.
class PointTool myPt;  # class for tool used to return a 3D point.
class RASTER rasterIn;
class GRE_LAYER_RASTER rasterInLayer;
class POINT2D ptrastmap;       # point location in raster map coordinates.
class Point2D ptGeog;				# point location in WGS84 / Geographic coordinates.
class SR_COORDREFSYS coordrefsysRast;
class SR_COORDREFSYS coordrefsysGeog;
class TransParm transViewToRastMap;
class TransParm transRastMapToGeog;
class MAPPROJ rastmapproj;            # Coordinate system / projection parameters.
#####
class SR_COORDREFSYS crs; # coordinate reference system class
crs.Assign("Geographic2D_WGS84_Deg"); # set crs for vector
class EXIF exifhandle; # declare EXIF class handle for getting/writing exif values
string xml$;			# string containing the XML text with the dialog specification
numeric err;			# value returned by the class method that reads and parses
							         # the XML text 
numeric ret;			# value returned by the class method that opens dialog
string filename$, obj$, desc$; # database and project file name
string IMAGEtablename$="Images"; # image table name
string tabledesc$="Table created by GPS.sml script to hold GPS coordinates and attached images"; # gps table description
vector GPSVector; # gps vector object 
####################### Procedures ###############################
###############################################
########## Procedures for GPS Logs ############
###############################################
proc DisplayEXIF() { # Procedure to create dialog to display EXIF information about a photo
	local numeric errexif;
	local string xmlexif$;
 
	### Create string variable with XML specification of dialog
	xmlexif$ = '<?xml version="1.0"?>
	<root>
		<dialog id = "exifdlg" Title = "Exif Information" OnOpen="PopulateEXIF()">
			<listbox id="exifbox" Height="25" Width="50" Enabled="1"/>
		</dialog>
	</root>';
	### parse XML text for the dialog into memory; 
	### return an error code (number < 0 ) if there are syntax errorsi
	errexif = exifdoc.Parse(xmlexif$);
	if ( errexif < 0 ) {
		PopupError( errexif ); 	# 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
	exifnode = exifdoc.GetElementByID("exifdlg");
	if ( exifnode == 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.
	exifdialog.SetXMLNode(exifnode);
	ret = exifdialog.DoModal();
	} # end DisplayEXIF
#############################################
proc PopulateEXIF() { # Get detailed information about EXIF tags and add to dialog
	local numeric index; # selected item index
	class GUI_CTRL_LISTBOX list, exifbox;
	class STRINGLIST keyStrings;
	exifbox = exifdialog.GetCtrlByID("exifbox");
	exifbox.DeleteAllItems();
	list = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	index = list.GetSelectedItemIndex(); # get selected item index
	local string image$ = imageListName.GetString(index); # grab input name
	if(image$=="")
		{
		PopupMessage("Please Select an Image.");
		}
	else 
		{
		local string output$="";
		output$+=sprintf("EXIF Header Information for image: %s\n -----------------------------\n");
		# GetEXIFTags takes filename of image as input
		# returns hash with key names and values
		exifhandle.Open(image$);
		# Get key list from hash, returns as string list
		keyStrings = exifhandle.GetKeyList();
		local numeric value,i;
		local string value$;
		# print all keys and values to text file
		for i=0 to keyStrings.GetNumItems()-1 {
			output$= sprintf("Key: %i\t%s\nValue: %s\n", i, keyStrings[i], exifhandle.GetDatumStr(keyStrings[i]));
			# Exif.Photo.Flash
			# bit0: 0 flash didn't fire, 1 flash fired
			# bit12: 00 no strobe return detection function, 01 reserved, 10 strobe return light not detected, 11 strobe return ligth deteced
			# bit34: 00 unknown, 01 Compulsory flash firing, 10 Compulsory flash suppression, 11 auto mode
			# bit5: 0 flash function present, 1 no flash function
			# bit6: 0 no red eye reduction mode or unknown, 1 red eye reduction supported
			if (keyStrings[i]=="Exif.Photo.Flash")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.Flash"));
				if (value>=64) # red eye mode
					{
					value=value-64;
					output$+= "\tRed Eye Mode - Red eye reduction supported";
					}
				else
					output$+= "\tRed Eye Mode - No red eye reduction mode or unknown";
				if(value>=32) # flash function
					{
					value=value-32;
					output$+= "\tFlash Function - No flash function";
					}
				else
					output$+= "\tFlash Function - Flash function present";
				if (value>=16) # flash mode
					{
					value=value-16;
					if (value>=8)
						{
						value=value-8;
						output$+= "\tFlash Mode - Auto mode\n";
						}
					else
						output$+= "\tFlash Mode - Compulsory flash suprression\n";
					}
				else
					{
					if (value>=8)
						{
						value=value-8;
						output$+="\tFlash Mode - Compulsory flash firing";
						}
					else
						output$+="\tFlash Mode - Unknown mode";
					}
				if (value>=4) # flash return
					{
					value=value-4;
					if (value>=2)
						{
						value=value-2;
						output$+=  "\t Flash Return - Strobe return light detected";
						}
					else
						output$+= "\tFlash Return - Strobe return light not detected";
					}
				else
					{
					if (value>=2)
						{
						value=value-2;
						output$+= "\tFlash Return - Reserved";
						}
					else
						output$+= "\tFlash Return - No strobe return detection function";
					}
				if (value>=1) # flash fired
					output$+= "\tFlash Fired - Flash did fire\n";
				else
					output$+= "\tFlash Fired - Flash did not fire\n";
				} # end Exif.Photo.Flash
			# Exif.Photo.ExifVersion
			if(keyStrings[i]=="Exif.Photo.ExifVersion")
				{
				value$=exifhandle.GetDatumStr("Exif.Photo.ExifVersion");
				output$+= sprintf("\tExif Version: %i%i.%i%i\n", StrToNum(GetToken(value$, " ",1))-48, StrToNum(GetToken(value$, " ",2))-48, StrToNum(GetToken(value$, " ",3))-48, StrToNum(GetToken(value$, " ",4))-48);
				}
			# Exif.Photo.FlashpixVersion
			if(keyStrings[i]=="Exif.Photo.FlashpixVersion")
				{
				value$=exifhandle.GetDatumStr("Exif.Photo.FlashpixVersion");
				output$+= sprintf("\tFlashpix Version: %i%i.%i%i\n", StrToNum(GetToken(value$, " ",1))-48, StrToNum(GetToken(value$, " ",2))-48, StrToNum(GetToken(value$, " ",3))-48, StrToNum(GetToken(value$, " ",4))-48);
				}
			# Exif.Iop.InteroperabilityVersion
			if(keyStrings[i]=="Exif.Iop.InteroperabilityVersion")
				{
				value$=exifhandle.GetDatumStr("Exif.Iop.InteroperabilityVersion");
				output$+= sprintf("\tInteroperability Version: %i%i.%i%i\n", StrToNum(GetToken(value$, " ",1))-48, StrToNum(GetToken(value$, " ",2))-48, StrToNum(GetToken(value$, " ",3))-48, StrToNum(GetToken(value$, " ",4))-48);
				}
			# Exif.Image.Orientation
			if(keyStrings[i]=="Exif.Image.Orientation" || keyStrings[i]=="Exif.Thumbnail.Orientation")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Image.Orientation"));
				switch(value)
					{
			      case 1: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","top","left-hand side"); break;
			      case 2: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","top","right-hand side"); break;
   			   case 3: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","bottom","right-hand side"); break;
  				   case 4: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","bottom","left-hand side"); break;
			      case 5: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","left-hand side","top"); break;
  				   case 6: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","right-hand side","top"); break;
					case 7: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","right-hand side","bottom"); break;
					case 8: output$+= sprintf("\tOrientation: The 0th row is at the visual %s of the image, and the 0th column is the visual %s.\n","left-hand side","bottom"); break;
					}
				}
			# Exif.Image.ResolutionUnit
			if(keyStrings[i]=="Exif.Image.ResolutionUnit" || keyStrings[i]=="Exif.Thumbnail.ResolutionUnit" || keyStrings[i]=="Exif.Photo.FocalPlaneResolutionUnit")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Image.ResolutionUnit"));
				switch(value)
					{
					case 2: output$+= sprintf("\tUnit for Resolution: Inches\n"); break;
					case 3: output$+= sprintf("\tUnit for Resolution: Centimeters\n"); break;
					}
				}
			# Exif.Image.YCbCrPositioning
			if(keyStrings[i]=="Exif.Image.YCbCrPositioning" || keyStrings[i]=="Exif.Thumbnail.YCbCrPositioning")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Image.YCbCrPositioning"));
				switch(value)
					{
					case 1: output$+= sprintf("\tPosition of Chrominance components in relation to Luminance components: centered.\n"); break;
					case 2: output$+= sprintf("\tPosition of Chrominance components in relation to Luminance components: co-sited.\n"); break;
					}
				}
			# Exif.Image.XResolution / YResolution
			if(keyStrings[i]=="Exif.Image.XResolution" || keyStrings[i]=="Exif.Image.YResolution" || keyStrings[i]=="Exif.Photo.FocalPlaneXResolution" || keyStrings[i]=="Exif.Photo.FocalPlaneYResolution" || keyStrings[i]=="Exif.Thumbnail.XResolution" || keyStrings[i]=="Exif.Thumbnail.YResolution")
				output$+= sprintf("\tNumber of pixles per resolution unit.\n");
			# Exif.Iop.InteroperabilityIndex
			if(keyStrings[i]=="Exif.Iop.InteroperabilityIndex")
				{
				value$=exifhandle.GetDatumStr("Exif.Iop.InteroperabilityIndex");
				switch(value$)
					{
					case "R98": output$+= sprintf("\tIndicates a file conforming to R98 file specification of Recommended Exif Interoperability Rules (ExifR98) or to DCF basic file stipulated by Design Rule for Camera File System.\n"); break;
					case "THM": output$+= sprintf("\tIndicates a file conforming to DCF thumbnail file stipulated by Design rule for Camera File System.\n"); break;
					}
				}
			# Exif.Photo.ColorSpace
			if (keyStrings[i]=="Exif.Photo.ColorSpace")
				{
				value$=exifhandle.GetDatumStr("Exif.Photo.ColorSpace");
				if (value$=="1")
					output$+= sprintf("\tsRGB (=1) is used to define the color space based on the PC monitor conditions and environment.\n");
				else
					output$+= sprintf("\tUncalibrated.\n"); 
				}
			# Exif.Photo.ExposureMode
			if(keyStrings[i]=="Exif.Photo.ExposureMode")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.ExposureMode"));
				switch(value)
					{
					case 0: output$+= sprintf("\tAuto exposure.\n"); break;
					case 1: output$+= sprintf("\tManual exposure.\n"); break;
					case 2: output$+= sprintf("\tAuto bracket.\n"); break;
					}
				}
			# Exif.Photo.FileSource
			if(keyStrings[i]=="Exif.Photo.FileSource")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.FileSource"));
				if(value==3)
					output$+= sprintf("\tImage recorded on a DSC.\n");
				}
			# Exif.Photo.SceneType
			if(keyStrings[i]=="Exif.Photo.SceneType")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.SceneType"));
				if(value==1)
					output$+= sprintf("\tA directly photographed image.\n");
				}
			# Exif.Photo.ComponentsConfiguration
			if(keyStrings[i]=="Exif.Photo.ComponentsConfiguration")
				{
				value$=exifhandle.GetDatumStr("Exif.Photo.ComponentsConfiguration");
				local string line$="";
				local numeric k;
				for k=1 to 4
					{
					value=StrToNum(GetToken(value$," ",k));
					switch(value)
		 				{
						case 0: line$=line$ + sprintf("%i=%s ",value ,"does not exist"); break;
			 			case 1: line$=line$ + sprintf("%i=%s ",value ,"Y"); break;
						case 2: line$=line$ + sprintf("%i=%s ",value ,"Cb"); break;
						case 3: line$=line$ + sprintf("%i=%s ",value ,"Cr"); break;
		 				case 4: line$=line$ + sprintf("%i=%s ",value ,"R"); break;
						case 5: line$=line$ + sprintf("%i=%s ",value ,"G"); break;
						case 6: line$=line$ + sprintf("%i=%s ",value ,"B"); break;
						}
					}
				output$+= sprintf("\tChannels of each component: %s.\n",line$);
				}
			# Exif.Photo.MeteringMode
			if (keyStrings[i]=="Exif.Photo.MeteringMode")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.MeteringMode"));
				switch(value)
					{
		       	case 0: output$+= sprintf("\tMetering Mode: %s.\n","Unknown"); break;
		       	case 1: output$+= sprintf("\tMetering Mode: %s.\n","Average"); break;
		       	case 2: output$+= sprintf("\tMetering Mode: %s.\n","Center Weighted Average"); break;
		       	case 3: output$+= sprintf("\tMetering Mode: %s.\n","Spot"); break;
		       	case 4: output$+= sprintf("\tMetering Mode: %s.\n","Multi Spot"); break;
		       	case 5: output$+= sprintf("\tMetering Mode: %s.\n","Pattern"); break;
		       	case 6: output$+= sprintf("\tMetering Mode: %s.\n","Partial"); break;
		       	case 255: output$+= sprintf("\tMetering Mode: %s.\n","Other"); break;
		 		 	}
				}
			# Exif.Photo.SceneCaptureType
			if(keyStrings[i]=="Exif.Photo.SceneCaptureType")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.SceneCaptureType"));
				switch(value)
					{
		       	case 0: output$+= sprintf("\tScene Type: %s.\n","Standard"); break;
  			     	case 1: output$+= sprintf("\tScene Type: %s.\n","Landscape"); break;
   		    	case 2: output$+= sprintf("\tScene Type: %s.\n","Portrait"); break;
		       	case 3: output$+= sprintf("\tScene Type: %s.\n","Night Scene"); break;
		 		 	}
				}
			# Exif.Photo.SensingMethod
			if(keyStrings[i]=="Exif.Photo.SensingMethod")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.SensingMethod"));
				switch(value)
					{
		       	case 1: output$+= sprintf("\tImage Sensor Type: %s.\n","Not defined"); break;
		       	case 2: output$+= sprintf("\tImage Sensor Type: %s.\n","One-chip color area sensor"); break;
		       	case 3: output$+= sprintf("\tImage Sensor Type: %s.\n","Two-chip color area sensor"); break;
   		    	case 4: output$+= sprintf("\tImage Sensor Type: %s.\n","Three-chip color area sensor"); break;
   		    	case 5: output$+= sprintf("\tImage Sensor Type: %s.\n","Color sequential area sensor"); break;
		       	case 7: output$+= sprintf("\tImage Sensor Type: %s.\n","Trilinear sensor"); break;
		       	case 8: output$+= sprintf("\tImage Sensor Type: %s.\n","Color sequential linear sensor"); break;
		 		 	}
				}
			# Exif.Photo.WhiteBalance
			if(keyStrings[i]=="Exif.Photo.WhiteBalance")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.WhiteBalance"));
				switch(value)
					{
					case 0: output$+= sprintf("\tWhite Balance: %s\n", "Auto"); break;
					case 1: output$+= sprintf("\tWhite Balance: %s\n", "Manual"); break;
					}
				}
			# Exif.Thumbnail.Compression
			if (keyStrings[i]=="Exif.Thumbnail.Compression")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Thumbnail.Compression"));
				switch(value)
					{
					case 1: output$+= sprintf("\t%s\n", "Uncompressed"); break;
					case 6: output$+= sprintf("\t%s\n", "JPEG compression"); break;
					}
				}
			# Exif.Thumbnail.JPEGInterchangeFormat
			if (keyStrings[i]=="Exif.Thumbnail.JPEGInterchangeFormat")
				output$+= sprintf("\tThe offset to the start byte (SOI) of JPEG compressed thumbnail data.\n");
			# Exif.Photo.ExposureProgram
			if (keyStrings[i]=="Exif.Photo.ExposureProgram")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.ExposureProgram"));
				switch(value)
					{
		       	case 0: output$+= sprintf("\tExposure Program Class: %s.\n","Not defined"); break;
		       	case 1: output$+= sprintf("\tExposure Program Class: %s.\n","Manual"); break;
		       	case 2: output$+= sprintf("\tExposure Program Class: %s.\n","Normal Program"); break;
		       	case 3: output$+= sprintf("\tExposure Program Class: %s.\n","Aperture priority"); break;
		       	case 4: output$+= sprintf("\tExposure Program Class: %s.\n","Shutter priority"); break;
		       	case 5: output$+= sprintf("\tExposure Program Class: %s.\n","Creative program (biased toward depth of field)"); break;
		       	case 6: output$+= sprintf("\tExposure Program Class: %s.\n","Action program (biased toward fast shutter speed"); break;
		       	case 7: output$+= sprintf("\tExposure Program Class: %s.\n","Portrait mode (for closeup photos with the background out of focus)"); break;
		       	case 8: output$+= sprintf("\tExposure Program Class: %s.\n","Landscape mode (for landscape photos with the background in focus"); break;
		 		 	}
				}
			# Exif.Photo.LightSource
			if (keyStrings[i]=="Exif.Photo.LightSource")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Photo.LightSource"));
				switch(value)
					{
		       	case 0: output$+= sprintf("\tLight Source: %s.\n","Unknown"); break;
		       	case 1: output$+= sprintf("\tLight Source: %s.\n","Daylight"); break;
		       	case 2: output$+= sprintf("\tLight Source: %s.\n","Fluorescent"); break;
		       	case 3: output$+= sprintf("\tLight Source: %s.\n","Tungsten (incandescent light)"); break;
 		      	case 4: output$+= sprintf("\tLight Source: %s.\n","Flash"); break;
 		      	case 9: output$+= sprintf("\tLight Source: %s.\n","Fine weather"); break;
 		      	case 10: output$+= sprintf("\tLight Source: %s.\n","Cloudy weather"); break;
 		      	case 11: output$+= sprintf("\tLight Source: %s.\n","Shade"); break;
 		      	case 12: output$+= sprintf("\tLight Source: %s.\n","Daylight fluorescent (D 5700 - 7100K)"); break;
  			     	case 13: output$+= sprintf("\tLight Source: %s.\n","Day white fluorescent (N 4600 - 5400K)"); break;
		       	case 14: output$+= sprintf("\tLight Source: %s.\n","Cool white fluorescent (W 3900 - 4500K)"); break;
		       	case 15: output$+= sprintf("\tLight Source: %s.\n","White fluorescent (WW 3200 - 3700K)"); break;
		       	case 17: output$+= sprintf("\tLight Source: %s.\n","Standard light A"); break;
		       	case 18: output$+= sprintf("\tLight Source: %s.\n","Standard light B"); break;
		       	case 19: output$+= sprintf("\tLight Source: %s.\n","Standard light C"); break;
		       	case 20: output$+= sprintf("\tLight Source: %s.\n","D55"); break;
		  	     	case 21: output$+= sprintf("\tLight Source: %s.\n","D65"); break;
		       	case 22: output$+= sprintf("\tLight Source: %s.\n","D75"); break;
		       	case 23: output$+= sprintf("\tLight Source: %s.\n","D50"); break;
		       	case 24: output$+= sprintf("\tLight Source: %s.\n","ISO studio tungsten"); break;
		       	case 255: output$+= sprintf("\tLight Source: %s.\n","Other light source"); break;
		 		 	}
				}
			# Exif.Thumbnail.PhotometricInterpretation
			if (keyStrings[i]=="Exif.Image.PhotometricInterpretation" || keyStrings[i]=="Exif.Thumbnail.PhotometricInterpretation")
				{
				value=StrToNum(exifhandle.GetDatumStr("Exif.Thumbnail.PhotometricInterpretation"));
				switch(value)
					{
					case 2: output$+= sprintf("\tPixel Composition: %s\n", "RGB"); break;
					case 6: output$+= sprintf("\tPixel Composition: %s\n", "YCbCr"); break;
					}
				}
			#########################################################
			exifbox.AddItem(output$);
			} # end else
		} # end for
	} # end proc PopulateExif
############################
func DecimalTime(numeric hour, numeric min, numeric second) { # function to convert time to decimal value, returns decimal time
	if (hour<0) # check if hour is negative value for camera offset time
		{
		min=min*-1; # set minutes to negative
		second=second*-1; # set seconds to negative
		}
	local numeric time=hour + (min/60) + (second/3600); # convert to decimal value
	return time;
} # end func DecimalTime
############################
# Procedure to get the options set by user and return the values;
# Values are returned to the variables defined as procedure parameters
proc GetOptions(var numeric cameraoffset, var string method$, var numeric timeoffset) { 
	local string offsetTime$ = gpsdialog.GetCtrlValueStr("cameraoffset"); #get camera offset
	# parse and convert to decimal time
	cameraoffset=DecimalTime(StrToNum(GetToken(offsetTime$,":",1)), StrToNum(GetToken(offsetTime$,":",2)), StrToNum(GetToken(offsetTime$,":",3)));
	method$=gpsdialog.GetCtrlValueStr("method"); # get selected method: Interpolate or Closest
	offsetTime$=gpsdialog.GetCtrlValueStr("timeoffset"); # get max allowed difference in time for gps
	# parse and convert to decimal time
	timeoffset=DecimalTime(StrToNum(GetToken(offsetTime$,":",1)), StrToNum(GetToken(offsetTime$,":",2)), StrToNum(GetToken(offsetTime$,":",3)));
	timeoffset = timeoffset *3600;
	cameraoffset = cameraoffset*3600;
} # end proc GetOptions()
############################
proc ComputeCoordinates(numeric imageNum, numeric computedflag, numeric errorreportflag) {
	# same as AttachImages, but doesn't use Table
	# procedure to compute coordinates for images
	class POINT3D leftXYZ, rightXYZ, newXYZ;
	local string report$;
	local numeric i, listcount = 0; 
	local string method$; # options
	local numeric cameraoffset, timeoffset = 0; # options
	local string inputname$; # image time and name
	class DATETIME inputtime;
	class GPSDATA inputdata;
	if(imageNum==-1){
		imageListNameAttached.Clear();
	}
	GetOptions(cameraoffset, method$, timeoffset);
	# timeoffset=max allowed difference in time
	local numeric startcount, endcount;
	if (imageNum == -1) {
		endcount = imageListName.GetNumItems()-1;
		startcount=0;
		}
	else
		{
		endcount = imageNum;
		startcount=imageNum;
		}
	for listcount=startcount to endcount # for all images in image list box
		{
		inputname$ = imageListName.GetString(listcount); # grab input name
		inputtime = imageListTime[inputname$]; # grab input date and time
		inputdata = imageListData[inputname$];
		## check for existing coords
		local string imageName$=inputname$;
		class POINT3D coord;
		class STRINGLIST keys;
		local numeric keyindex = 0;
		local numeric valid=0;
		exifhandle.Open(imageName$);
		keys = exifhandle.GetKeyList();
		for keyindex=0 to keys.GetNumItems()-1{
			if (keys[keyindex]=="Exif.GPSInfo.GPSLongitude")
				valid++;
			if (keys[keyindex]=="Exif.GPSInfo.GPSLongitudeRef")
				valid++;
			if (keys[keyindex]=="Exif.GPSInfo.GPSLatitude")
				valid++;
			if (keys[keyindex]=="Exif.GPSInfo.GPSLatitudeRef")
				valid++;
			if (keys[keyindex]=="Exif.GPSInfo.GPSAltitude")
				valid++;
			if (keys[keyindex]=="Exif.GPSInfo.GPSAltitudeRef")
				valid++;
			}
		if (valid==6 && computedflag==0 && (Assigned[inputname$]!=1 || IsNull(Assigned[inputname$])))
			{
			string xcoord$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLongitude");
			string xcoordref$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLongitudeRef");
			string ycoord$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLatitude");
			string ycoordref$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLatitudeRef");
			string zcoord$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSAltitude");
			string zcoordref$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSAltitudeRef");
			coord.x = (StrToNum(GetToken(GetToken(xcoord$, " ", 1),"/",1)) / StrToNum(GetToken(GetToken(xcoord$, " ", 1),"/",2))) 
							+ ((StrToNum(GetToken(GetToken(xcoord$, " ", 2),"/",1)) / StrToNum(GetToken(GetToken(xcoord$, " ", 2),"/",2)))
							+ (StrToNum(GetToken(GetToken(xcoord$, " ", 3),"/",1)) / StrToNum(GetToken(GetToken(xcoord$, " ", 3),"/",2)))/60)/60;
			if (xcoordref$=="W") then coord.x=coord.x*-1;
			coord.y = (StrToNum(GetToken(GetToken(ycoord$, " ", 1),"/",1)) / StrToNum(GetToken(GetToken(ycoord$, " ", 1),"/",2))) 
							+ ((StrToNum(GetToken(GetToken(ycoord$, " ", 2),"/",1)) / StrToNum(GetToken(GetToken(ycoord$, " ", 2),"/",2)))
							+ (StrToNum(GetToken(GetToken(ycoord$, " ", 3),"/",1)) / StrToNum(GetToken(GetToken(ycoord$, " ", 3),"/",2)))/60)/60;
			if (ycoordref$=="S") then coord.y=coord.y*-1;
			coord.z = StrToNum(GetToken(zcoord$,"/",1))/ StrToNum(GetToken(zcoord$,"/",2));
			if (zcoordref$=="1") then coord.z=coord.z*-1;
			imageListData[imageName$].Position.x = coord.x;
			imageListData[imageName$].Position.y = coord.y;
			imageListData[imageName$].Position.z = coord.z;
			Assigned[imageName$]=1;
			} # end if
		else if ( (Assigned[inputname$]!=1 || IsNull(Assigned[inputname$])) || computedflag==1)
			{
			Assigned[inputname$]=0;
			imageListData[inputname$].Position.x = 0;
			imageListData[inputname$].Position.y = 0;
			imageListData[inputname$].Position.z = 0;
			# compute coordinates using GPSDBASE class method
			class GPSDATA data;
			gpsdbase.Compute(inputtime, data, method$, timeoffset);
			imageListData[inputname$] = data;
			if (!IsNull(data.Position.x)  && !IsNull(data.Position.y)  && !IsNull(data.Position.z))
				{
				imageListNameAttached.AddToEnd(inputname$);
				imageListData[inputname$] = data;
				}
		else if (errorreportflag == 1) # image coordinates not computed
			report$=report$ + sprintf("Failed to compute coordinates for image '%s' \n", inputname$);
		} # end if Assigned 
		else
			{
			imageListNameAttached.AddToEnd(inputname$);
			imageListData[inputname$] = inputdata;
			}
		} # end for listcount
		if (report$ != "") # report$ contains images with no coordinates
			{
			report$=report$ + sprintf("These Images fall outside log range.");
			PopupMessage(report$);
			}
	} # end proc ComputeCoordinates
#################################################
func GetEXIF(string filename$) {
	## Function to read EXIF header from image "filename$"
	## Adds string containing date and time (YYYYMMDD HH:MM:SS) to imageListTime string list
	## returns 0 if it cannot find a header, 1 if successful
	class DATETIME datetime;
	exifhandle.Open(filename$);
	# key "Exif.Image.DateTime" contains date/time value (YYYY:MM:DD HH:MM:SS)
	# parse string and add to EXIFDateTime$ string as YYYYMMDD HH:MM:SS
	string exifDateTime$ = exifhandle.GetDatumStr("Exif.Image.DateTime"); #pass the name of a Key to GetDatumStr and returns the value as a string
	string exifDateTime2$ = exifhandle.GetDatumStr("Exif.Photo.DateTimeOriginal");
	if (exifDateTime$ != "") {
		datetime.SetDateYYYYMMDD(StrToNum(sprintf("%s%s%s",exifDateTime$.substr(0,4),exifDateTime$.substr(5,2),exifDateTime$.substr(8,2))));
		datetime.SetTime(StrToNum(exifDateTime$.substr(11,2)), StrToNum(exifDateTime$.substr(14,2)), StrToNum(exifDateTime$.substr(17,2)));
		}
	else if (exifDateTime2$ != "") {
		datetime.SetDateYYYYMMDD(StrToNum(sprintf("%s%s%s",exifDateTime2$.substr(0,4),exifDateTime2$.substr(5,2),exifDateTime2$.substr(8,2))));
		datetime.SetTime(StrToNum(exifDateTime2$.substr(11,2)), StrToNum(exifDateTime2$.substr(14,2)), StrToNum(exifDateTime2$.substr(17,2)));
		}
	else
		return 0;
	gpsdialog.SetCtrlValueStr( "status", "Added: " + FileNameGetName(filename$) + "." + FileNameGetExt(filename$));
	imageListTime[filename$] = datetime; # add time to string list
	return 1; # return 1 as successful
	}# end func GetEXIF
##################################
proc UpdateImages(numeric imageNum, numeric computedflag, numeric errorreportflag) {
		class GUI_CTRL_LISTBOX imagelist;
		local string imageString$, imageName$;
		local numeric i=0;
		imagelist = gpsdialog.GetCtrlByID("imagebox");
		imagelist.DeleteAllItems(); # clear list box
		imageStringList.Clear();
		imageListName.RemoveDuplicates();
		ComputeCoordinates(imageNum, computedflag, errorreportflag); # recompute coordiantes for images
		for i=0 to imageListName.GetNumItems()-1{
			imageName$=imageListName[i];
			if(Assigned[imageName$]==1) # assigned coordinates
				{
				imageListNameAttached.AddToEnd(imageName$);
				imageListNameAttached.RemoveDuplicates();
				}
			print(imageListData[imageName$].Position.x);
			if(IsNull(imageListData[imageName$].Position.x) && IsNull(imageListData[imageName$].Position.y) && IsNull(imageListData[imageName$].Position.z)) # computed coordinates
				imageString$ = sprintf("*** %s.%s   %i %02i:%02i:%02i  x: %.7f  y: %.7f  z: %.7f ", FileNameGetName(imageName$), FileNameGetExt(imageName$), imageListTime[imageName$].GetDateYYYYMMDD(), imageListTime[imageName$].GetHour(), imageListTime[imageName$].GetMin(), imageListTime[imageName$].GetSec(), imageListData[imageName$].Position.x, imageListData[imageName$].Position.y, imageListData[imageName$].Position.z);
			else
				imageString$ = sprintf("%s.%s   %i %02i:%02i:%02i  x: %.7f  y: %.7f  z: %.7f ", FileNameGetName(imageName$), FileNameGetExt(imageName$), imageListTime[imageName$].GetDateYYYYMMDD(), imageListTime[imageName$].GetHour(), imageListTime[imageName$].GetMin(), imageListTime[imageName$].GetSec(), imageListData[imageName$].Position.x, imageListData[imageName$].Position.y, imageListData[imageName$].Position.z);
			imagelist.AddItem(imageString$);
			imageStringList[imageName$] = imageString$;
	     	} # end for
	}# end proc UpdateImages()
##################################
proc RemoveAll() { # procedure to remove all GPS coordinates in listbox
	class GUI_CTRL_LISTBOX coordlist, loglist;
	coordlist = gpsdialog.GetCtrlByID("coordlistbox"); # get control for listbox
	loglist = gpsdialog.GetCtrlByID("loglistbox");
		if( PopupYesNo("Remove All GPS coordinates?")==1) # confirm remove all coordinates
			{
			coordlist.DeleteAllItems(); # clear coord list box
			loglist.DeleteAllItems(); # clear log list box
			coordStringList.Clear();
			gpsdbase.RemoveAllLogs();
			gpsdialog.SetCtrlValueStr( "status", "Removed All Logs");
			}
	} # end proc RemoveAll()
#############################
proc AddGPS(string logname$) {
# procedure to add GPS coordinates to listbox
# called each time a log is removed or added to list
	class GUI_CTRL_LISTBOX loglist,coordlist;
	local numeric count=0;
	class FILE logfile;
	class DATETIME datetime;
	class GPSDATA data;
	numeric cameraoffset, timeoffset;
	string method$;
	GetOptions(cameraoffset, method$, timeoffset); # returns: method$, constraints$ cameraoffset, timeoffset
	gpsdbase.SetOffset(cameraoffset);
	coordlist = gpsdialog.GetCtrlByID("coordlistbox");
	coordlist.DeleteAllItems(); # clear coord listbox
	coordStringList.Clear();
	if(logname$!="")
		gpsdbase.ReadLog(logname$);
	for count=0 to gpsdbase.GetNumPoints()-1 {
		gpsdbase.GetPoint(count, datetime, data);
		datetime = gpsdbase.ApplyOffset(datetime);
		local string coord$ = sprintf("%i %02i:%02i:%02i , X: %.6f , Y: %.6f , Z: %.6f", datetime.GetDateYYYYMMDD(), datetime.GetHour(), datetime.GetMin(), datetime.GetSec(), data.Position.x, data.Position.y, data.Position.z); 
		coordlist.AddItem(coord$);
		coordStringList.AddToEnd(coord$);
		}
	UpdateImages(-1, 0, 1);
  	} # end proc AddGPS()
#############################
proc RemoveGPS() { # procedure to remove GPS coordinates in listbox
	class GUI_CTRL_LISTBOX list;
	local numeric index; # selected item index
	list = gpsdialog.GetCtrlByID("coordlistbox"); # get control for list box
	index = list.GetSelectedItemIndex(); # get selected item index
	list.DeleteItemIndex(index); # remove from listbox
	gpsdbase.RemovePoint(index);
	coordStringList.Remove(index);
	} # end proc RemoveGPS()
#############################
proc AddLogDirectory() {
# Procedure to add a directory of logfiles to current list of log files
# Checks if valid log file before adding
# Correct format:
# date(YYYYMMDD),time(HHMMSS),XPos(deg),YPos(deg),Elev(m),XVel(m/s),YVel(m/s),ZVel(m/s),Head(deg),Speed(m/s),DataSrc,NumSat
	class GUI_CTRL_LISTBOX list; # class for listbox control
	class FILEPATH filepath;
	class STRINGLIST filenames;
	local string defaultpath$,logpath$, line$;
	local numeric records, count, i=0; # format=flag to check for valid format, 1=valid, 0=invalid
	local string report$; # string holding any errors with adding log files
	defaultpath$ = _context.ScriptDir; # get directory containing gps logs
	filepath.SetName( GetDirectory( defaultpath$, "Please select the directory containing the GPS log files" ) );
	filenames = filepath.GetFileList( "*.gps"); # get file list in directory
	list = gpsdialog.GetCtrlByID("loglistbox"); # get control for listbox holding logfile list
	for count=0 to filenames.GetNumItems()-1 # for all files in directory
		{
		logpath$ = sprintf( "%s/%s.%s", filepath.GetPath(), FileNameGetName(filenames[count]), FileNameGetExt(filenames[count]));
		if(FileNameGetName(filenames[count])!="") # if user did not cancel selection dialog
			{
			records = gpsdbase.GetNumPoints();
			AddGPS(logpath$);
			if(gpsdbase.GetNumPoints() > records)
				list.AddItem(logpath$);
			else # if not valid
				report$=report$ + sprintf("Could not add '%s.%s'\n",FileNameGetName(logpath$), FileNameGetExt(logpath$));
			} # end if
		} # end for
	filenames = filepath.GetFileList( "*.log"); # get file list in directory
	list = gpsdialog.GetCtrlByID("loglistbox"); # get control for listbox holding logfile list
	for count=0 to filenames.GetNumItems()-1 # for all files in directory
		{
		logpath$ = sprintf( "%s/%s.%s", filepath.GetPath(), FileNameGetName(filenames[count]), FileNameGetExt(filenames[count]));
		if(FileNameGetName(filenames[count])!="") # if user did not cancel selection dialog
			{
			records = gpsdbase.GetNumPoints();
			AddGPS(logpath$);
			if(gpsdbase.GetNumPoints() > records)
				list.AddItem(logpath$);
			else # end if not valid
				report$=report$ + sprintf("Could not add '%s.%s'\n",FileNameGetName(logpath$), FileNameGetExt(logpath$));
			} # end if
		} # end for
	if(report$ != "") # print error report
		{
		report$=report$ + sprintf("These gps log files are not standard format and could not be added.");
		PopupMessage(report$);
		}
	gpsdialog.SetCtrlValueStr( "status", "Added: " + filepath.GetPath());
	} # end proc AddLogDirectory()
##############################
proc AddLog() {
# Add a single selected log file to list of log files
	class GUI_CTRL_LISTBOX list;
	local numeric records=0;
	local string line$, message$;
	local string prompt$ = "Select GPS Log";
	local string logname$=GetInputFileName("", prompt$, ".gps .log"); # User selected log file
	local string logpath$ = sprintf( "%s/%s.%s", FileNameGetPath(logname$), FileNameGetName(logname$), FileNameGetExt(logname$));
	list = gpsdialog.GetCtrlByID("loglistbox");
	if(FileNameGetName(logname$)!="") # if user did not cancel dialog
		{
		records = gpsdbase.GetNumPoints();
		AddGPS(logpath$);
		if(gpsdbase.GetNumPoints() > records)
			{
			list.AddItem(logpath$);
			gpsdialog.SetCtrlValueStr( "status", "Added: " + FileNameGetName(logname$) + "." + FileNameGetExt(logname$));
			}
		else # if not valid
			{ # print error report
			message$=sprintf("Could not add '%s.%s', log file not standard format.",FileNameGetName(logname$), FileNameGetExt(logname$));
			PopupMessage(message$); # pop up message
			gpsdialog.SetCtrlValueStr( "status", "Could not add: " + FileNameGetName(logname$) + "." + FileNameGetExt(logname$));
			}
		} # end if
	} # end proc
#############################
proc RemoveLog() { # procedure to remove a log from the list
	class GUI_CTRL_LISTBOX list;
	local numeric records;
	list = gpsdialog.GetCtrlByID("loglistbox"); # get control for list box
	local numeric index=list.GetSelectedItemIndex(); # get selected item index
	local string logname$; 
	gpsdbase.GetLog(index-1, logname$);
	gpsdialog.SetCtrlValueStr( "status", "Removed: " + FileNameGetName(logname$) + "." + FileNameGetExt(logname$));
	list.DeleteItemIndex(index); # remove from listbox
	gpsdbase.RemoveLog(logname$);
	AddGPS(""); # redisplay new list of coordinates
	} # end proc RemoveLog()
#############################
proc CreateTable() { # procedure to create new GPS table
	# DatabaseCreate
	if (ObjectExists(filename$, obj$, "Vector") == 0) # check if vector exists
		{
		CreateVector(GPSVector, filename$, obj$, desc$, "3DVector"); # create vector
		CreateImpliedGeoref(GPSVector, crs);
		}
	if (ObjectExists(filename$, obj$, "Vector") == 1) # check if vector exists
		OpenVector(GPSVector, filename$, obj$); # open vector
	dbase= OpenVectorPointDatabase(GPSVector); # open point dbase
	if ( TableExists(dbase,IMAGEtablename$) == 0) # table does not exist
		{
		table=TableCreate(dbase, IMAGEtablename$, tabledesc$); # create table, add time, x, y, z, image fields
		TableAddFieldString(table, "Date Time", 17, 17);
		TableAddFieldFloat(table, "Long (deg)",9,6);
		TableAddFieldFloat(table, "Lat (deg)",9,6);
		TableAddFieldFloat(table, "Elev (m)",9,6);
		TableAddFieldString(table, "Image",200,100);
		}
  } # end proc CreateTable()
############################
proc Save() { # procedure to select GPS log file
	string prompt$ = "Select Vector Object";
	GetOutputObject("Vector", "NewOrExisting", prompt$, filename$, obj$, desc$); # User selected vector object
	# filename$ = rvc file
	# obj$ = vector object name
	# desc$ = vector object description
	# write file name to text box
	local string dlgtext$ = sprintf( "%s %s/%s.%s", FileNameGetName(obj$), FileNameGetPath(filename$), FileNameGetName(filename$), FileNameGetExt(filename$));
	gpsdialog.SetCtrlValueStr( "filetext", dlgtext$ );
	# enable Display of database, Attaching to Database
#	gpsdialog.GetCtrlbyID("displaytable").SetEnabled(1);
	gpsdialog.GetCtrlbyID("attach").SetEnabled(1);
	gpsdialog.GetCtrlbyID("changedirectory").SetEnabled(1);
	CreateTable(); # create GPS table
	}	# end proc Save()
#######################################
######### View Display Procedures ###############
#######################################
proc DisplayTable() { # procedure to display table in database editor
	class DBEDITOR dbedit;
	class DBEDITORTABLE tabview;
	dbase= OpenVectorPointDatabase(GPSVector); # open point dbase
	dbedit = DBEditorCreate(dbase); # create dbeditor handle
	tabview = DBEditorOpenTabularView(dbedit, IMAGEtablename$); # open table in tabular view
	} # end proc DisplayTable()
#######################################
###############################################
########## Procedures for Images #######################
###############################################
############################
proc AddImageDirectory() { # procedure to select input image folder and add to image list
	class FILEPATH filepath;
	class STRINGLIST filenames;
	local string defaultpath$ = _context.ScriptDir; # get directory containing images
	local numeric hasEXIF=0; # flag to determine if jpg has valid EXIF header
	local numeric count=0;
	local string report$; # string containing error report
	local string imagePath$, imageTime$;
	filepath.SetName( GetDirectory( defaultpath$, "Please select the directory containing the JPEG files" ) );
	filenames = filepath.GetFileList( "*.jpg" ); # get list of files in directory
	for count=0 to filenames.GetNumItems()-1 # for all images in directory
	{ 
		imagePath$ = sprintf( "%s/%s.%s", filepath.GetPath(), FileNameGetName(filenames[count]), FileNameGetExt(filenames[count]));
		if (Assigned[imagePath$]==1)
			break;
		if (filenames[count]!="") # if user has not cancelled dialog
			{
			hasEXIF=GetEXIF(imagePath$); # get EXIF header, returns 1 if valid, 0 if invalid
			if (hasEXIF == 1) # has EXIF
				{
				imageListName.AddToEnd(imagePath$); # add name to string list
				imageListData[imagePath$].Position.x=0; imageListData[imagePath$].Position.y=0; imageListData[imagePath$].Position.z=0;
				}
			if (hasEXIF == 0) # has no EXIF
				{
				report$=report$ + sprintf("Could not add image '%s.%s'.\n",FileNameGetName(filenames[count]),FileNameGetExt(filenames[count]));
				}
			} # end if
	} # end for
	if(report$ != "") # print error report
		{
		report$=report$ + sprintf("These Images contained no EXIF header and could not be added.");
		PopupMessage(report$);
		}
	UpdateImages(-1,0,0);
	gpsdialog.SetCtrlValueStr( "status", "Added: " + filepath.GetPath());
	} # end proc AddImageDirectory
############################
proc AddImage() { # procedure to select single input image and add to image list
	local string prompt$ = "Select Image";
	local string imageName$=GetInputFileName("", prompt$, ".jpg"); # User selected Image
	local string imagePath$ = sprintf( "%s/%s.%s", FileNameGetPath(imageName$), FileNameGetName(imageName$), FileNameGetExt(imageName$));
	local numeric hasEXIF=0;
	local string imageTime$, message$;
	if(Assigned[imagePath$]==1)
		break;
	if(imageName$!="")# if user did not cancel dialog
		{
		hasEXIF=GetEXIF(imagePath$); # get exif header
		if(hasEXIF == 1) # has exif
			{
			imageListName.AddToEnd(imagePath$); # add name to string list
			local numeric NumItems = imageListName.GetNumItems();
			imageListName.RemoveDuplicates();
			if(imageListName.GetNumItems() == NumItems)
				UpdateImages(imageListName.GetNumItems()-1,0,0);
			}
		if(hasEXIF == 0) # has no exif
			{
			message$=sprintf("Could not add image '%s.%s', image has no EXIF header.",FileNameGetName(imageName$),FileNameGetExt(imageName$));
			PopupMessage(message$);# report error
			}
		} # end if imageName
	} # end proc AddImage
#######################################################################
## Procedures related to choosing image coordinates from a view of a
## georeferenced raster using a point tool.
# Procedure called when point tool is placed in the view.
proc OnToolSet () {
	clear();
	# transform point coordinates from view coordinates returned by point tool to raster map coordinates
	ptrastmap = TransPoint2D(myPt.Point, transViewToRastMap);
	# transform point coordinates from raster map to WGS84 / Geographic
	ptGeog = TransPoint2D(ptrastmap, transRastMapToGeog);
	local string coords$ = sprintf("x = %5.5f, y = %5.5f",ptGeog.x, ptGeog.y);
	ViewSetMessage(view,coords$);		# update status line in view
	}
# Procedure called when right mouse button is pressed
proc OnToolApply () {
	class GUI_CTRL_LISTBOX list;
	list = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	local numeric index = list.GetSelectedItemIndex(); # get selected item index
	local string imageName$ = imageListName.GetString(index); # grab input name
	imageListData[imageName$].Position.x = ptGeog.x;
	imageListData[imageName$].Position.y = ptGeog.y;
	imageListData[imageName$].Position.z = 0;
	Assigned[imageName$]=1;
	UpdateImages(index,0,1);
	}
# Procedure called when Close button on view window is pressed.
proc OnClose() {
	DialogClose(pcwin);
	DestroyWidget(pcwin);
	CloseRaster(rasterIn);
	}
# Procedure called when view window widget is destroyed.
proc OnDestroy() {
       Exit();
	}
# Procedure called by Select Coordinates from Georeferenced Raster entry on the gpsdialog Image menu.
# Prompts to choose raster, creates a view window and displays the raster, and creates a point tool
# for obtaining map coordinates from the raster.
proc SelectPoint() {
	GetInputRaster(rasterIn);
	gp = GroupCreate();
	# Create dialog window.
	pcwin = CreateFormDialog("Find Coordinates");
	WidgetAddCallback(pcwin.Shell.PopdownCallback, OnClose);
	WidgetAddCallback(pcwin.DestroyCallback, OnDestroy);
	# Create pushbutton item for Close.
	class PushButtonItem btnItemClose;
	btnItemClose = CreatePushButtonItem(" Close ",OnClose);
	# Create button row for Close button.
	class XmForm btnrow;
	btnrow = CreateButtonRow(pcwin,btnItemClose);
	btnrow.BottomWidget = pcwin;
	btnrow.RightWidget = pcwin;
	btnrow.LeftWidget = pcwin;
	# Create view in pcwin form to display input raster and vector.
	# A view has its own XmForm widget accessed as a class member "Form".
	# It is automatically attached to the parent form at the top.
	view = GroupCreateView(gp,pcwin,"",380,380,
              "NoLegendView,NoScalePosLine,DestroyOnClose");
	view.Form.LeftWidget = pcwin;
	view.Form.RightWidget = pcwin;
	view.Form.BottomWidget = btnrow;
	# Add point tool to view.
	myPt = ViewCreatePointTool(view,"Point Tool","point_select","standard");
	ToolAddCallback(myPt.PositionSetCallback,OnToolSet);
	ToolAddCallback(myPt.ActivateCallback,OnToolApply);
	myPt.DialogPosition = "RightCenter";
	ViewAddToolIcons(view);
	DialogOpen(pcwin);	# open the view
	# add selected raster to the view and redraw
	rasterInLayer = GroupQuickAddRasterVar(gp,rasterIn);
	ViewRedrawFull(view);
	# get the coordinate reference system from the raster layer and
	# get TRANSPARM from view to raster map coordinates
	coordrefsysRast = rasterInLayer.MapRegion.CoordRefSys;		
	transViewToRastMap = ViewGetTransMapToView(view, coordrefsysRast, 1); 	
	# set up a TRANSPARM from raster map coordinates to WGS84 / Geographic
	# to match GPS log coordinates
	coordrefsysGeog.Assign("Geographic2D_WGS84_Deg");
	transRastMapToGeog.InputCoordRefSys = coordrefsysRast;
	transRastMapToGeog.OutputCoordRefSys = coordrefsysGeog;
	ViewActivateTool(view,myPt);
	ViewSetMessage(view,"Left-click to move point.  Right-click to assign coordinates.");
	} # end proc SelectPoint
#############################
proc FillLog(){ # procedure to fill log selection dialog
	class GUI_CTRL_LISTBOX coordlist;
	coordlist = logdialog.GetCtrlByID("coordlistbox");
	coordlist.DeleteAllItems();
	local numeric i=0;
	for i=0 to coordStringList.GetNumItems()-1
	coordlist.AddItem(coordStringList[i]);
	} # end proc FillLog()
#############################
proc SelectLog(){ # procedure to select log record from dialog
	local numeric imageindex, coordindex; # selected item index
	class GUI_CTRL_LISTBOX imagelist, coordlist;
	class POINT3D coord;
	class DATETIME datetime;
	class GPSDATA data;
	imagelist = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	coordlist = logdialog.GetCtrlByID("coordlistbox");
	imageindex = imagelist.GetSelectedItemIndex(); # get selected item index
	local string imageName$ = imageListName.GetString(imageindex); # grab input name
	coordindex = coordlist.GetSelectedItemIndex(); # get selected item index
	gpsdbase.GetPoint(coordindex, datetime, data);
	imageListData[imageName$].Position.x = data.Position.x;
	imageListData[imageName$].Position.x = data.Position.y;
	imageListData[imageName$].Position.x = data.Position.z;
	Assigned[imageName$]=1;
	UpdateImages(imageindex,0,1);
	} # end proc SelectLog()
######################################
proc AssignLogImage() { # procedure to select log record and assign lat/lon to image
	local numeric errlog;
	local string xmllog$;
 
	### Create string variable with XML specification of dialog
	xmllog$ = '<?xml version="1.0"?>
	<root>
		<dialog id = "logdlg" Title = "Select GPS Log Record" OnOpen="FillLog()" OnOK="SelectLog()">
		<groupbox Name="Coordinate List: " ExtraBorder="4">
			<pane Orientation="horizontal">
				<listbox id="coordlistbox" Height="7" Width="43" Enabled="1"/>
			</pane>
		</groupbox>
		</dialog>
	</root>';
	### parse XML text for the dialog into memory; 
	### return an error code (number < 0 ) if there are syntax errorsi
	errlog = logdoc.Parse(xmllog$);
	if ( errlog < 0 ) {
		PopupError( errlog ); 	# 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
	lognode = logdoc.GetElementByID("logdlg");
	if ( lognode == 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.
	logdialog.SetXMLNode(lognode);
	ret = logdialog.DoModal();
	} # end proc AssignLogImage()
#############################
proc FillImage() { # procedure to fill log selection dialog
	class GUI_CTRL_LISTBOX imagelist;
	local string imageName$;
	local numeric i=0;
	imagelist = transferdialog.GetCtrlByID("imagebox");
	imagelist.DeleteAllItems();
	for i=0 to imageListNameAttached.GetNumItems()-1{
		imageName$=imageListNameAttached[i];
		imagelist.AddItem(imageStringList[imageName$]);
		}
	} # end proc FillLog()
#############################
proc SelectImage() { # procedure to select log record from dialog
	local numeric imageindex, coordindex; #selected item index
	class POINT3D coord;
	class GUI_CTRL_LISTBOX imagelist, transferlist;
	imagelist = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	transferlist = transferdialog.GetCtrlByID("imagebox");
	imageindex = imagelist.GetSelectedItemIndex(); # get selected item index
	local string imageName$ = imageListName.GetString(imageindex); # grab input name
	imageindex = transferlist.GetSelectedItemIndex();
	local string transferName$ = imageListNameAttached.GetString(imageindex);
	imageListData[imageName$] = imageListData[transferName$];
	Assigned[imageName$]=1;
	UpdateImages(imageindex,0,1);
	} # end proc SelectLog()
#############################
proc AssignTransferImage() { # procedure to select log record and assign lat/lon to image
	local numeric errtransfer;
	local string xmltransfer$;
 
	### Create string variable with XML specification of dialog
	xmltransfer$ = '<?xml version="1.0"?>
	<root>
		<dialog id = "transferdlg" Title = "Select Image" OnOpen="FillImage()" OnOK="SelectImage()">
		<groupbox Name="Image to Transfer Coordiantes From: " ExtraBorder="4">
			<pane Orientation="horizontal">
				<listbox id="imagebox" Height="7" Width="43" Enabled="1"/>
			</pane>
		</groupbox>
		</dialog>
	</root>';
	### parse XML text for the dialog into memory; 
	### return an error code (number < 0 ) if there are syntax errorsi
	errtransfer = transferdoc.Parse(xmltransfer$);
	if ( errtransfer < 0 ) {
		PopupError( errtransfer ); 	# 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
	transfernode = transferdoc.GetElementByID("transferdlg");
	if ( transfernode == 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.
	transferdialog.SetXMLNode(transfernode);
	ret = transferdialog.DoModal();
	} # end proc AssignTransferImage()
#############################
proc SetLatLon() { # procedure to select log record from dialog
	local numeric imageindex; # selected item index
	local numeric xcoord, ycoord=0;
	class GUI_CTRL_LISTBOX imagelist;
	imagelist = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	imageindex = imagelist.GetSelectedItemIndex(); # get selected item index
	local string imageName$ = imageListName.GetString(imageindex); # grab input name
	xcoord = latlondialog.GetCtrlValueNum("xcoorddeg") + (latlondialog.GetCtrlValueNum("xcoordmin") + (latlondialog.GetCtrlValueNum("xcoordsec")/60))/60;
	ycoord = latlondialog.GetCtrlValueNum("ycoorddeg") + (latlondialog.GetCtrlValueNum("ycoordmin") + (latlondialog.GetCtrlValueNum("ycoordsec")/60))/60;
	imageListData[imageName$].Position.x = xcoord;
	imageListData[imageName$].Position.y = ycoord;
	imageListData[imageName$].Position.z = latlondialog.GetCtrlValueNum("zcoord");
	Assigned[imageName$]=1;
	UpdateImages(imageindex,0,1);
	} # end proc SetLatLon
#############################
proc AssignLatLonImage() { # procedure to select log record and assign lat/lon to image
	local numeric errlatlon;
	local string xmllatlon$;
 
	### Create string variable with XML specification of dialog
	xmllatlon$ = '<?xml version="1.0"?>
	<root>
		<dialog id = "latlondlg" Title = "Set Latitude/Longitude" OnOK="SetLatLon()">
		<groupbox Name="Enter X,Y,Z: " ExtraBorder="4">
			<pane Orientation="horizontal">
				<label WidthGroup="labels">Latitude: </label>
				<editnumber id="ycoorddeg" Justify="Right" Default="0" MinVal="-90" MaxVal="90" Width="3" Precision="0"/>
				<editnumber id="ycoordmin" Justify="Right" Default="0" MinVal="0" MaxVal="60" Width="3" Precision="0"/>
				<editnumber id="ycoordsec" Justify="Right" Default="0" MinVal="0" MaxVal="60" Widht="8" Precision="5"/>
			</pane>
			<pane Orientation="horizontal">
				<label WidthGroup="labels">Longitude: </label>
				<editnumber id="xcoorddeg" Justify="Right" Default="0" MinVal="-180" MaxVal="180" Width="3" Precision="0"/>
				<editnumber id="xcoordmin" Justify="Right" Default="0" MinVal="0" MaxVal="60" Width="3" Precision="0"/>
				<editnumber id="xcoordsec" Justify="Right" Default="0" MinVal="0" MaxVal="60" Widht="8" Precision="5"/>
			</pane>
			<pane Orientation="horizontal">
				<label WidthGroup="labels">Elevation: </label>
				<editnumber id="zcoord" Justify="Right" Default="0" Width="20" Precision="5"/>
			</pane>
		</groupbox>
		</dialog>
	</root>';
	### parse XML text for the dialog into memory; 
	### return an error code (number < 0 ) if there are syntax errorsi
	errlatlon = latlondoc.Parse(xmllatlon$);
	if ( errlatlon < 0 ) {
		PopupError( errlatlon ); 	# 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
	latlonnode = latlondoc.GetElementByID("latlondlg");
	if ( latlonnode == 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.
	latlondialog.SetXMLNode(latlonnode);
	ret = latlondialog.DoModal();
	} # end proc AssignLogImage()
#############################
proc AssignExistingImage() { # procedure to use coordinates already existing in image
	local numeric index, keyindex, valid = 0; # selected item index
	class GUI_CTRL_LISTBOX list;
	class POINT3D coord;
	class STRINGLIST keys;
	list = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	index = list.GetSelectedItemIndex(); # get selected item index
	local string imageName$ = imageListName.GetString(index); # grab input name
	exifhandle.Open(imageName$);
	keys = exifhandle.GetKeyList();
	for keyindex=0 to keys.GetNumItems()-1{
		if(keys[keyindex]=="Exif.GPSInfo.GPSLongitude")
			valid++;
		if(keys[keyindex]=="Exif.GPSInfo.GPSLongitudeRef")
			valid++;
		if(keys[keyindex]=="Exif.GPSInfo.GPSLatitude")
			valid++;
		if(keys[keyindex]=="Exif.GPSInfo.GPSLatitudeRef")
			valid++;
		if(keys[keyindex]=="Exif.GPSInfo.GPSAltitude")
			valid++;
		if(keys[keyindex]=="Exif.GPSInfo.GPSAltitudeRef")
			valid++;
		}
	if(valid==6){
		string xcoord$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLongitude");
		string xcoordref$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLongitudeRef");
		string ycoord$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLatitude");
		string ycoordref$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSLatitudeRef");
		string zcoord$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSAltitude");
		string zcoordref$ = exifhandle.GetDatumStr("Exif.GPSInfo.GPSAltitudeRef");
		coord.x = (StrToNum(GetToken(GetToken(xcoord$, " ", 1),"/",1)) / StrToNum(GetToken(GetToken(xcoord$, " ", 1),"/",2))) 
							+ ((StrToNum(GetToken(GetToken(xcoord$, " ", 2),"/",1)) / StrToNum(GetToken(GetToken(xcoord$, " ", 2),"/",2)))
							+ (StrToNum(GetToken(GetToken(xcoord$, " ", 3),"/",1)) / StrToNum(GetToken(GetToken(xcoord$, " ", 3),"/",2)))/60)/60;
		if (xcoordref$=="W") then coord.x=coord.x*-1;
		coord.y = (StrToNum(GetToken(GetToken(ycoord$, " ", 1),"/",1)) / StrToNum(GetToken(GetToken(ycoord$, " ", 1),"/",2))) 
							+ ((StrToNum(GetToken(GetToken(ycoord$, " ", 2),"/",1)) / StrToNum(GetToken(GetToken(ycoord$, " ", 2),"/",2)))
							+ (StrToNum(GetToken(GetToken(ycoord$, " ", 3),"/",1)) / StrToNum(GetToken(GetToken(ycoord$, " ", 3),"/",2)))/60)/60;
		if (ycoordref$=="S") then coord.y=coord.y*-1;
		coord.z = StrToNum(GetToken(zcoord$,"/",1))/ StrToNum(GetToken(zcoord$,"/",2));
		if (zcoordref$=="1") then coord.z=coord.z*-1;
		imageListData[imageName$].Position.x = coord.x;
		imageListData[imageName$].Position.y = coord.y;
		imageListData[imageName$].Position.z = coord.z;
		Assigned[imageName$]=1;
		UpdateImages(index, 0, 1);
		}
		else
		{
		PopupMessage("Image does not contain all necessary GPS tags.");
		}
	} # end proc AssignExistingImage
#############################
proc PopulateImageMenu() {
	class GUI_CTRL_MENUBUTTON menu;
	menu = gpsdialog.GetCtrlByID("imagemenu");
	menu.AddItem("  Use Computed Coordinates from Logs...", "ComputedCoords");
	menu.AddItem("  Select Coordinates from Georeferenced Raster... ", "SelectPoint");
	menu.AddItem("  Select Coordinates in a Log Record... ", "AssignLogImage");
	menu.AddItem("  Manually Enter Lat/Lon Coordinates... ", "AssignLatLonImage");
	menu.AddItem("  Transfer Coordinates from Other Image... ", "AssignTransferImage");
	menu.AddItem("  Use Pre-Existing EXIF Coordinates... ", "AssignExistingImage");
	} # end proc PopulateImageMenu
proc SelectImageMenu() {
	class GUI_CTRL_MENUBUTTON menu;
	class GUI_CTRL_LISTBOX list;
	menu = gpsdialog.GetCtrlByID("imagemenu");
	list = gpsdialog.GetCtrlByID("imagebox");
	local numeric index = list.GetSelectedItemIndex();
	if(menu.GetValue()=="ComputedCoords") UpdateImages(index,1,1);
	if(menu.GetValue()=="SelectPoint") SelectPoint();
	if(menu.GetValue()=="AssignLogImage") AssignLogImage();
	if(menu.GetValue()=="AssignLatLonImage") AssignLatLonImage();
	if(menu.GetValue()=="AssignTransferImage") AssignTransferImage();
	if(menu.GetValue()=="AssignExistingImage") AssignExistingImage();
	} # end proc SelectImageMenu
#############################
proc ViewImage() { # procedure to open selected image in default OS viewer
	local numeric index; # selected item index
	class GUI_CTRL_LISTBOX list;
	list = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
	index = list.GetSelectedItemIndex(); # get selected item index
	local string image$ = imageListName.GetString(index); # grab input name
	RunAssociatedApplication(image$);
	} # end proc ViewImage()
#############################
proc RemoveImage() { # procedure to remove image in listbox
	class GUI_CTRL_LISTBOX list;
	local numeric index; # selected item index
		list = gpsdialog.GetCtrlByID("imagebox"); # get control for list box
		index = list.GetSelectedItemIndex(); # get selected item index
		list.DeleteItemIndex(index); # remove from listbox
		local string imageName$ = imageListName[index];
		gpsdialog.SetCtrlValueStr( "status", "Removed: " + FileNameGetName(imageName$) + "." + FileNameGetExt(imageName$) + " - " + imageListTime[imageName$]);
		imageListData[imageName$].Position.x = 0; imageListData[imageName$].Position.y = 0; imageListData[imageName$].Position.z = 0;
		Assigned[imageName$]=0;
		imageListName.Remove(index); # remove from stringlist
		local numeric i;
		for i=0 to imageListNameAttached.GetNumItems()-1{
			if(imageListNameAttached[i]==imageName$) imageListNameAttached.Remove(i);
			}
		imageStringList[imageName$] = "";
	} # end proc RemoveImage()
#############################
proc RemoveAllImages() { # procedure to remove all images in listbox
	class GUI_CTRL_LISTBOX list;
	list = gpsdialog.GetCtrlByID("imagebox"); # get control for listbox
	if( PopupYesNo("Remove All Images?")==1) # confirm remove all images
		{
		list.DeleteAllItems(); # clear list box
		imageListName.Clear(); # clear string list
		imageListNameAttached.Clear();
		imageStringList.Clear();
		imageListTime.Clear(); # clear string list
		imageListData.Clear();
		Assigned.Clear();
		gpsdialog.SetCtrlValueStr( "status", "Removed All Images");
		}
	} # end proc RemoveAll()
############################
proc ChangeDirectory() {
# Procedure to change directory of all images in table
# User selects directory to change the image path to
# Image name read in from each record of the table
# Image name then compared to the list of images in the directory
# If the image name is equal to a file in the directory,
# then the path of the image in the table is changed to the new directory
	class FILEPATH filepath;
	class STRINGLIST filenames;
	dbase= OpenVectorPointDatabase(GPSVector); # get dbase
	table=DatabaseGetTableInfo(dbase, IMAGEtablename$); # get table class
	local numeric count,recordNum;
	local string imageName$, imagePath$, imageCurrent$, imageExt$;
	local numeric numberOfRecords=table.NumRecords;
	local string defaultpath$ = _context.ScriptDir; # get directory containing gps logs
	filepath.SetName( GetDirectory( defaultpath$, "Please select the directory containing the images" ) );
	filenames = filepath.GetFileList( "*.jpg" );# get file list in directory
	for recordNum=1 to numberOfRecords # for all records
		{
		# get image path, name and extension
		imagePath$=TableReadFieldStr(table, "Image", recordNum);
		imageCurrent$=FileNameGetName(imagePath$);
		imageExt$=FileNameGetExt(imagePath$);
		for count=0 to filenames.GetNumItems()-1 # for all files in directory
			{
			imageName$=FileNameGetName(filenames[count]); #get name of file
			if(imageCurrent$==imageName$)# compare both names
				{
				count=filenames.GetNumItems()-1;
				# if image name in directory equal to record, then change path in table to this directory
				imagePath$ = sprintf( "%s/%s.%s", filepath.GetPath(), imageName$, imageExt$);
				}
			}
		# change image path
		TableWriteField(table, recordNum, "Image", imagePath$);
		}
	}# end proc ChangeDirectory()
############################
proc AttachPoints() { 
# procedure to attach table records to points in vector
# call each time new record added
	local array numeric recordarray[100]; # record array
	local array numeric elementarray[1]; # element array
	local array numeric writearray[1]; # records to write array
	local numeric numberOfRecords, numberOfElements, elementnum, recordnum,x,y,z,i;
	dbase= OpenVectorPointDatabase(GPSVector); # get dbase
	table=DatabaseGetTableInfo(dbase, IMAGEtablename$); # get table class
	elementnum=1; # set to first element
	numberOfRecords=table.NumRecords;
	# clear all current attachments:
	for i=1 to numberOfRecords # add all records to record array
		recordarray[i]=i;
	for i=1 to numberOfRecords # for every element in vector 
		{
		elementarray[1]=i;
		 # remove all records from each element
		TableRemoveAttachment(table, elementarray[1], recordarray, numberOfRecords);
		}
	for recordnum=1 to numberOfRecords # for all records
		{
		x=TableReadFieldNum(table, "Long (deg)", recordnum); # read in coordinates
		y=TableReadFieldNum(table, "Lat (deg)", recordnum);
		z=TableReadFieldNum(table, "Elev (m)", recordnum);
		VectorChangePoint(GPSVector, elementnum, x, y, z); # change vector point to coordinates of record
		writearray[1]=recordnum; # add current record to array of records to write
		TableWriteAttachment(table, elementnum, writearray, 1, "point"); # make attachment
		if(TableReadFieldStr(table, "Date Time",recordnum) != TableReadFieldStr(table, "Date Time", recordnum+1))
			elementnum++; # if not duplicate records, move to next point
		}
	} # end proc AttachPoints()
############################
proc ShiftRecords(numeric recordnum){ 
# procedure to create new record and shift all records forward 1 starting at recordnum
# designed to handle TableNewRecord only adding record to end of table,
# but want to preserve a certain record order number
	class POINT3D coordCurrent,coordNext; # point3d class to hold current and next record coordinates
	dbase= OpenVectorPointDatabase(GPSVector); # open point dbase
	table=DatabaseGetTableInfo(dbase, IMAGEtablename$); # get table class
	local numeric count=table.NumRecords;
	local string datetimeCurrent$, datetimeNext$;
	local string imageCurrent$, imageNext$;
	local numeric i=0;
	datetimeCurrent$=TableReadFieldStr(table, "Date Time", recordnum); # read current record coordinates
	coordCurrent.x=TableReadFieldNum(table, "Long (deg)", recordnum);
	coordCurrent.y=TableReadFieldNum(table, "Lat (deg)", recordnum);
	coordCurrent.z=TableReadFieldNum(table, "Elev (m)", recordnum);
	imageCurrent$=TableReadFieldStr(table, "Image", recordnum);
	 TableNewRecord(table); # create new blank record at end of table to shift last record into
	 for i=recordnum to count
		{
		datetimeNext$=TableReadFieldStr(table, "Date Time", i+1); # read next record info
		coordNext.x=TableReadFieldNum(table, "Long (deg)", i+1); 
		coordNext.y=TableReadFieldNum(table, "Lat (deg)", i+1);
		coordNext.z=TableReadFieldNum(table, "Elev (m)", i+1);
		imageNext$=TableReadFieldStr(table, "Image", i+1);
		# write previous record information to next record, shifting the record forward 1
		TableWriteRecord(table, i+1, datetimeCurrent$, coordCurrent.x, coordCurrent.y, coordCurrent.z, imageCurrent$);
		datetimeCurrent$=datetimeNext$; # Old next record becomes new current record
		coordCurrent.x=coordNext.x;
		coordCurrent.y=coordNext.y;
		coordCurrent.z=coordNext.z;
		imageCurrent$=imageNext$; 
		} # end for
 
	} # end proc ShiftRecords
############################
func NewRecord(string inputtime$, numeric x, numeric y, numeric z, string inputname$) {
# function to add a new record to a table
# preserves correct record order based on time
# also checks for duplicate records
	class DATETIME datetimeInput, datetimeCurrent;
	local numeric count=1;
	local string currentTime$, currentName$, date$, time$;
	local numeric tInput, tCurrent;
	dbase= OpenVectorPointDatabase(GPSVector); # open point dbase
	table= DatabaseGetTableInfo(dbase, IMAGEtablename$); # open table
	date$=inputtime$.substr(0,8); # parse date for input
	datetimeInput.SetDateYYYYMMDD(StrToNum(date$));
	time$=inputtime$.substr(9,8); # parse time for input
	datetimeInput.SetTime(StrToNum(time$.substr(0,2)),StrToNum(time$.substr(3,2)),StrToNum(time$.substr(6,2)));
	tInput=DecimalTime(datetimeInput.GetHour(),datetimeInput.GetMin(),datetimeInput.GetSec()); # get decimal time for input
	if (table.NumRecords >= 1) # if table not empty
		{
		for count=1 to table.NumRecords
			{
			currentTime$=TableReadFieldStr(table, "Date Time", count); # get current record time
			currentName$=TableReadFieldStr(table, "Image", count); # get current record name
			date$=currentTime$.substr(0,8); # parse date for current record
			datetimeCurrent.SetDateYYYYMMDD(StrToNum(date$));
			time$=currentTime$.substr(9,8); # parse time for current record
			datetimeCurrent.SetTime(StrToNum(time$.substr(0,2)),StrToNum(time$.substr(3,2)),StrToNum(time$.substr(6,2)));
			tCurrent=DecimalTime(datetimeCurrent.GetHour(),datetimeCurrent.GetMin(),datetimeCurrent.GetSec()); # get decimal time for current record
			# check if day has rolled over
			# adjusts by 24*(difference in days)
			if(datetimeInput.GetDateYYYYMMDD() != datetimeCurrent.GetDateYYYYMMDD())
				tCurrent=tCurrent+24*(datetimeCurrent.GetDateYYYYMMDD()-datetimeInput.GetDateYYYYMMDD());
			if(currentName$ == inputname$) # if image already attached, do not create new record
				{
				TableWriteRecord(table, count, inputtime$, x, y, z, inputname$); # write to current record
				return 0;
				}
			if(tInput < tCurrent) # if input time is less than current record's time
				{
				ShiftRecords(count); # shift records forward 1 to make room for new record
				TableWriteRecord(table, count, inputtime$, x, y, z, inputname$); # write to newly freed record
				VectorAddPoint(GPSVector,x,y,z); # add blank vector point
				return 0;
				}
			if(tInput>tCurrent && count==table.NumRecords) # if has reached end of table
				{
				TableNewRecord(table, inputtime$, x, y, z, inputname$); # add new record at end of table
				VectorAddPoint(GPSVector,x,y,z); # add blank vector point
				return 0;
				}
			} # end for
		} # end if
	if (table.NumRecords==0) # if table empty
		{
		TableNewRecord(table, inputtime$, x, y, z, inputname$); # add new record
		VectorAddPoint(GPSVector,x,y,z); # add blank vector point
		}
	return 1;
	} # end proc NewRecord
############################
proc AttachImages() {
	local numeric listcount;
	local string inputtime$, inputname$; #image time and name
	for listcount=0 to imageListNameAttached.GetNumItems()-1 # for all images in image list box
		{
		inputname$ = imageListNameAttached.GetString(listcount); # grab input name
		inputtime$ = sprintf("%i %i:%i:%i",imageListTime[inputname$].GetDateYYYYMMDD(), imageListTime[inputname$].GetHour(),  imageListTime[inputname$].GetMin(),  imageListTime[inputname$].GetSec()); # grab input date and time
		NewRecord(inputtime$,imageListData[inputname$].Position.x, imageListData[inputname$].Position.y, imageListData[inputname$].Position.z, inputname$);
		}
	AttachPoints(); # attach records to points in vector
	VectorValidate(GPSVector);
	CloseVector(GPSVector);
	# enable Display Table icon button
	gpsdialog.GetCtrlbyID("displaytable").SetEnabled(1);
	} # end proc AttachImages
############################
proc WriteEXIF(){ 
# write to exif metadata
	class POINT3D coord; # point3d class to hold current and next record coordinates
	local string imagePathOriginal$, imageTime$;
	local numeric i=0;
	local numeric BackupFlag = PopupYesNo("Would you like to create backup copies of the images?");
	 for i=0 to imageListNameAttached.GetNumItems()-1
		{ # get values to write
		imagePathOriginal$=imageListNameAttached.GetString(i); # grab input name
		coord.x =imageListData[imagePathOriginal$].Position.x;
		coord.y =imageListData[imagePathOriginal$].Position.y;
		coord.z =imageListData[imagePathOriginal$].Position.z;
		if(BackupFlag){
			local string imagePath$ = sprintf( "%s/%s%s.%s", FileNameGetPath(imagePathOriginal$), FileNameGetName(imagePathOriginal$), "-SML",FileNameGetExt(imagePathOriginal$));
			CopyFile(imagePathOriginal$, imagePath$);
			exifhandle.Open(imagePath$); # open jpeg to write
			}
		else
		exifhandle.Open(imagePathOriginal$);
		# writeexif:
		# Set Long Reference: indicates if West or East longitude
		if (coord.x < 0)
			exifhandle.AddDatumStr("Exif.GPSInfo.GPSLongitudeRef","W");
		if (coord.x >= 0)
			exifhandle.AddDatumStr("Exif.GPSInfo.GPSLongitudeRef", "E");
		# Set Lat Reference: indicates if South or North Latitude
		if (coord.y < 0)
			exifhandle.AddDatumStr("Exif.GPSInfo.GPSLatitudeRef","S");
		if (coord.y >= 0)
			exifhandle.AddDatumStr("Exif.GPSInfo.GPSLatitudeRef", "N");
		# Set altitude used as reference altitude
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSAltitudeRef", "0");
		# EXIF tags take decimal values as a fraction
		# Must convert altitude to a fraction string to write
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSVersionID", "2 2 0 0");
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSMapDatum", "WGS-84");
		local string Date$ = sprintf("%i:%i:%i", imageListTime[imagePathOriginal$].GetYear(), imageListTime[imagePathOriginal$].GetMonth(), imageListTime[imagePathOriginal$].GetDayOfMonth());
		local string Time$ = sprintf("%s/1 %s/1 %s/1", imageTime$.substr(9,2), imageTime$.substr(12,2), imageTime$.substr(15,2));
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSDateStamp", Date$);
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSTimeStamp", Time$);
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSAltitude", sprintf("%i/1000",round(coord.z*1000)));
		coord.x = abs(coord.x);
		coord.y = abs(coord.y);
		numeric LatDeg = floor(coord.y);
		numeric LatMin = floor((coord.y - LatDeg)*60);
		numeric LatSec = floor( ((((coord.y - LatDeg)*60) - LatMin) *60)*10 );
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSLatitude", sprintf("%i/1 %i/1 %i/10",LatDeg,LatMin,LatSec));
		numeric LonDeg = floor(coord.x);
		numeric LonMin = floor((coord.x - LonDeg)*60);
		numeric LonSec = floor( ((((coord.x - LonDeg)*60) - LonMin) *60)*10 );
		exifhandle.AddDatumStr("Exif.GPSInfo.GPSLongitude", sprintf("%i/1 %i/1 %i/10",LonDeg,LonMin,LonSec));
		exifhandle.Write();
		} # for
		local string report$;
		if(BackupFlag) 
 			report$=sprintf("%i image(s) saved. Images written to new files appended with -SML to preserve originals.", imageListNameAttached.GetNumItems());
		else
			report$=sprintf("%i image(s) saved.",imageListNameAttached.GetNumItems());
		PopupMessage(report$);
		gpsdialog.SetCtrlValueStr( "status", NumToStr(imageListNameAttached.GetNumItems()) + " images' EXIF tags saved.");
	} # end proc WriteEXIF
#############################
proc ApplyChanges() {
	AddGPS("");
	}
#############################
proc OpenScript() { # procedure called when starting
# set default values
	gpsdialog.SetCtrlValueStr("cameraoffset", "+000:00:00");
	gpsdialog.SetCtrlValueStr("timeoffset", "00:05:00");
	PopulateImageMenu();
	}
#############################
proc ExitScript() { # procedure called when exiting
	print("Closing...");
	CloseVector(GPSVector);
	# delete lock file
	local string lokfile$ = FileNameGetPath(filename$) + "/" + FileNameGetName(filename$) + "_rvc.lok";
	if(fexists(lokfile$)==1) # check for lok file and delete
		{
		DeleteFile(lokfile$);
		print("Deleted ",lokfile$);
		}
	gpsdialog.Close(0);
	}
#################################################
############## Main Program #####################
#################################################
clear();
$warnings 3;
# create string variable with XML specification for control dialog
xml$='<?xml version="1.0"?>
<root>
	<dialog id="gps" Title="GPSphoto" VertResize="Relative" Buttons="" OnOpen="OpenScript()" OnClose="ExitScript()">
		<book>
	<page Name="Images">
		<groupbox Name="Select Images" ExtraBorder="4" VertResize="Fixed">
			<pane Orientation="horizontal" HorizResize="Fixed" ChildSpacing="6">
				<menubutton id="imagemenu" Icon="EDIT_CONTROLS" ToolTip=" Image Options... " OnSelection="SelectImageMenu()"/>
				<pushbutton Icon="RVCOBJ_DISP_SIM3D" ToolTip=" View Image " OnPressed="ViewImage()"/>
				<pushbutton Icon="RVCOBJ_METADATA" ToolTIp=" EXIF Info " OnPressed="DisplayEXIF()"/>
				<label></label>
				<pushbutton Icon="CONTROL_ADD_CYAN" ToolTip=" Add Image " OnPressed="AddImage()"/>
				<pushbutton Icon="CONTROL_SUBTRACT_CYAN" ToolTip=" Remove Image " OnPressed="RemoveImage()"/>
				<pushbutton Icon="FOLDER_NEW" ToolTip=" Add Image Directory " OnPressed="AddImageDirectory()"/>
				<pushbutton Icon="CONTROL_SUBTRACT_ALL_CYAN" ToolTip=" Remove All " OnPressed="RemoveAllImages()"/>
			</pane>
			<listbox id="imagebox" Height="14" Width="50" Enabled="1"/>
		</groupbox>
	</page>
	<page Name="GPS Log">
	   <groupbox Name="GPS Logs: " ExtraBorder="4">
			<pane Orientation="horizontal" HorizResize="Fixed" ChildSpacing="6">
				<pushbutton Icon="CONTROL_ADD_CYAN" ToolTip=" Add Log " OnPressed="AddLog()"/>
				<pushbutton Icon="CONTROL_SUBTRACT_CYAN" ToolTip=" Remove Log " OnPressed="RemoveLog()"/>
				<pushbutton Icon="FOLDER_NEW" ToolTip=" Add Log Directory " OnPressed="AddLogDirectory()"/>
			</pane>
			<pane Orientation="horizontal">
				<listbox id="loglistbox" Height="4" Enable="1"/>
			</pane>
	   </groupbox>
		<groupbox Name="Coordinate List: " ExtraBorder="4">
			<pane Orientation="horizontal" HorizResize="Fixed" ChildSpacing="6">
				<pushbutton Icon="CONTROL_SUBTRACT_CYAN" ToolTip=" Remove Coordinates " OnPressed="RemoveGPS()"/>
				<pushbutton Icon="CONTROL_SUBTRACT_ALL_CYAN" ToolTip=" Remove All Coordinates " OnPressed="RemoveAll()"/>
			</pane>
			<pane Orientation="horizontal">
				<listbox id="coordlistbox" Height="5" Width="43" Enabled="1"/>
			</pane>
		</groupbox>
		<pane Orientation="horizontal" HorizAlignt="Left">
				<label>Set Offset Time for Camera +/- (HHH:MM:SS)</label>
				<edittext id="cameraoffset" WidthGroup="boxes" Justify="Right" MaxLength="10"/>
				<pushbutton Icon="EDIT_APPLY_RED" ToolTip=" Apply Changes " OnPressed="ApplyChanges()"/>
		</pane>
	</page>
	<page Name="Options">
		<groupbox Name="Attaching Options" ExtraBorder="4" VertResize="Fixed">
			<pane Orientation="vertical">
				<groupbox Name="Select Method of Assigning Coordinates" ExtraBorder="4">
					<radiogroup id="method">
						<item Value="Interpolate" Name="Interpolate Coordinates"/>
						<item Value="Closest" Name="Use Closest Coordinates"/>
					</radiogroup>
				</groupbox>
				<groupbox Name="Select Constraints" ExtraBorder="4">
				<pane Orientation="horizontal">
					<label WidthGroup="labels">Set Max Difference in Time for GPS (HH:MM:SS)</label>
					<edittext id="timeoffset" WidthGroup="boxes" Justify="Right" MaxLength="9"/>
				</pane>
				</groupbox>
			</pane>
		</groupbox>
		<pane Orienation="horizontal" VertResize="Fixed" HorizAlign="Center">
			<pushbutton Name="   Apply Changes   " OnPressed="ApplyChanges()"/>
		</pane>
	</page>
	</book>
	   <groupbox Name="Output Vector: " ExtraBorder="4" VertResize="Fixed">
			<pane Orientation="horizontal" ChildSpacing="6">
				<pushbutton Icon="RVCOBJ_VECTOR" ToolTip="Select Vector..." WidthGroup="Buttons" OnPressed="Save()"/>
				<edittext id="filetext" Width="25" ReadOnly="true"/> 
				<pushbutton id="attach" Icon="FILE_SAVE" ToolTip=" Save Vector " Enabled="0" OnPressed="AttachImages()"/>
				<pushbutton id="displaytable" Icon="RVCOBJ_DB_TABLE_INTERNAL" ToolTip=" Display GPS Table " Enabled="0" OnPressed="DisplayTable()"/>
				<pushbutton id="changedirectory" Icon="FOLDER_NEW" ToolTip=" Change Directory of Images in Table " Enabled="0" OnPressed="ChangeDirectory()"/>
		   </pane>
	   </groupbox>
		<pane Orientation="horizontal" ChildSpacing="6" HorizAlign="Right" >
			<edittext id="status" ReadOnly="true"/>
			<pushbutton id="writeexif" Icon="GRE_LAYER_SKETCH" ToolTip=" Write EXIF " Enabled="1" HorizResize="Fixed" OnPressed="WriteEXIF()"/>
			<pushbutton id="close" Name="   Cancel   " HorizResize="Fixed" OnPressed="ExitScript()"/>
		</pane>
	</dialog>
</root>';
### parse XML text for the dialog into memory; 
### return an error code (number < 0 ) if there are syntax errorsi
err = dlgdoc.Parse(xml$);
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
gpsdlgnode = dlgdoc.GetElementByID("gps");
if ( gpsdlgnode == 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.
gpsdialog.SetXMLNode(gpsdlgnode);
ret = gpsdialog.DoModal();
if ( ret == -1 ) {	# exit script if Cancel button on dialog is pressed
	Exit();
	}