GPSphoto.sml

  Download

More scripts: Advanced

Syntax Highlighing:

comments, key words, predefined symbols, class members & methods, functions & classes
            
#### GPSphoto.sml
### Requires version 2006:72 of the TNT products.
### Brett Colombe
### Microimages, Inc.
### Software Engineer
### 25 October 2006
#Coordinates now displayed next to image in image list and updated as new logs added.
#Changes to offset time now reflected in time displayed under log list
#Added View Image option
#Added 4 options for assigning different coordinates to images
#Changed 9/25/06: New EXIF class added to SML, changed script to use this new class
#		Now have ability to write to EXIF tags
#######################################################
# 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
# All log files must be MicroImages standard log format, 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
# Set the maximum allowed difference in distance between image and closest log point
# Choose if both time and distance constraints must be satisfied or if only 1 must be satisfied
# Set the offset time for camera if camera time is different than gps log time
########## Global Class Declarations ##############################
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 logStringList, coordStringList; # string lists containing gps log names and coordinates read in from logs
class STRING imageListTime[];
class STRING imageListTimeAttached[];
class STRING imageStringAttached[];
#####
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 TransParm transViewToRastMap;
class MAPPROJ rastmapproj;            # Coordinate system / projection parameters.
#####
numeric xcoords[];
numeric ycoords[];
numeric zcoords[];
numeric Assigned[];
class EXIF exifhandle; # declare EXIF class handle for getting/writing exif values
class SR_COORDREFSYS crs; # coordinate reference system class
crs.Assign("Geographic2D_WGS84_Deg"); # set crs for vector
########## Global Variable Declarations##############################
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();
}
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$);
}#for
}
}#end proc DisplayExif
############################
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
############################
proc GetOptions(var numeric cameraoffset, var string method$, var numeric timeoffset, var numeric gpsoffset, var string constraints$) { # procedure to get the options set by user and return the values
	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)));
	gpsoffset=gpsdialog.GetCtrlValueNum("gpsoffset"); # get max allowed difference in gps coordinates distance
	constraints$=gpsdialog.GetCtrlValueStr("constraints"); # get option for checking constraints
}# end proc GetOptions()
############################
proc SearchCoords(string inputtime$, var numeric tLeft, var numeric tRight, var numeric tInput, var numeric leftCoord, var numeric rightCoord) {
# procedure to search through coordinates
# finds closest time less than input time (tLeft)
# finds closest time greater than input time (tRight)
# if both tLeft and tRight are greater than inputtime, then Image's time is less than all times in list
# if both tLeft and tRight are less than inputtime, then Image's time is greater than all times in list
# if both tLeft and tRight are equal to inputtime, then Image's time is equal to a time in the list
# if tLeft less than inputtime and tRight greater than inputtime, then can interpolate new coordinates for Image's time
class DATETIME datetimeLeft, datetimeRight, datetimeInput, datetimeCurrent;
local string date$, time$;
local numeric count = coordStringList.GetNumItems()-1;
local numeric i = 0;
local numeric tCurrent=0;
# tLeft, tRight, tCurrent, tInput decimal times for those records
# leftCoord, rightCoord record number of current closest left and right times
# leftCoord and rightCoord start at record 0
	leftCoord=0;
	rightCoord=0;
	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
	#tInput=tInput - cameraoffset; # apply camera time offset
	date$=coordStringList.GetString(leftCoord).substr(0,8); # parse date for left cord
	datetimeLeft.SetDateYYYYMMDD(StrToNum(date$));
	time$=coordStringList.GetString(leftCoord).substr(9,8); # parse time for left coord
	datetimeLeft.SetTime(StrToNum(time$.substr(0,2)),StrToNum(time$.substr(3,2)),StrToNum(time$.substr(6,2)));
	tLeft=DecimalTime(datetimeLeft.GetHour(),datetimeLeft.GetMin(),datetimeLeft.GetSec()); # get decimal time
	datetimeRight=datetimeLeft; # right date and time same as left for first record
	tRight=tLeft; # decimal times equal for first record
	# check if day has rolled over
	# adjusts by 24*(difference in days)
	if(datetimeInput.GetDateYYYYMMDD() != datetimeLeft.GetDateYYYYMMDD())
		tLeft=tLeft+24*(datetimeLeft.GetDateJulian()-datetimeInput.GetDateJulian());
	if(datetimeInput.GetDateYYYYMMDD() != datetimeRight.GetDateYYYYMMDD())
		tRight=tRight+24*(datetimeRight.GetDateJulian()-datetimeInput.GetDateJulian());
for i=0 to count # for all items in coordinate list
	{
	date$=coordStringList.GetString(i).substr(0,8); # parse date for current record
	datetimeCurrent.SetDateYYYYMMDD(StrToNum(date$));
	time$=coordStringList.GetString(i).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
	# check if day has rolled over
	# adjusts by 24*(difference in days)
	if(datetimeInput.GetDateYYYYMMDD() != datetimeCurrent.GetDateYYYYMMDD())
		tCurrent=tCurrent+24*(datetimeCurrent.GetDateJulian()-datetimeInput.GetDateJulian());
   #print(tInput, tCurrent, tRight);
	# check for new closest left
	if( tCurrent <= tInput )
		{
		leftCoord=i;
		tLeft=tCurrent;
		datetimeLeft=datetimeCurrent;
		}
	# check for new closest right
	if( tCurrent >= tInput && ( (tRight-tInput) > (tCurrent-tInput) || (tRight-tInput) < 0 ) )
		{
		rightCoord=i;
		tRight=tCurrent;
		datetimeRight=datetimeCurrent;
		}
	}# for
}# end proc SearchCoords
############################
proc ComputeCoordinates(numeric imageNum, numeric computedflag) {
#same as AttachImages, but doesn't use Table
# procedure to compute where images should be attached
# calls SearchCoords to determine closest leftCoord and RightCoord
class POINT3D leftXYZ, rightXYZ, newXYZ;
local string report$;
local numeric i, listcount, attached, scale=0; 
local numeric tLeft, tRight, tInput, leftCoord, rightCoord=0; #values returned by searchcoords
local string method$, constraints$; #options
local numeric cameraoffset, timeoffset, gpsoffset=0; #options
local string inputtime$, inputname$; #image time and name
if(imageNum==-1){
	imageListNameAttached.Clear();
	imageListTimeAttached.Clear();
}
GetOptions(cameraoffset, method$, timeoffset, gpsoffset, constraints$); # returns: method$, constraints$ cameraoffset, timeoffset, gpsoffset
# timeoffset=max allowed difference in time
# gpsoffset=max allowed difference in distance
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
	{
	attached=0; # mark image as current unattached
	inputname$ = imageListName.GetString(listcount); # grab input name
	inputtime$ = imageListTime[inputname$]; # grab input date and time
	##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") 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") coord.y=coord.y*-1;
	coord.z = StrToNum(GetToken(zcoord$,"/",1))/ StrToNum(GetToken(zcoord$,"/",2));
	if(zcoordref$=="1") coord.z=coord.z*-1;
	xcoords[imageName$] = coord.x;
	ycoords[imageName$] = coord.y;
	zcoords[imageName$] = coord.z;
	Assigned[imageName$]=1;
	}
	##
	else if( (Assigned[inputname$]!=1 || IsNull(Assigned[inputname$])) || computedflag==1){
	Assigned[inputname$]=0;
	xcoords[inputname$]=0;
	ycoords[inputname$]=0;
	zcoords[inputname$]=0;
	SearchCoords( inputtime$, tLeft, tRight, tInput, leftCoord, rightCoord); # return leftCoord, rightCoord
	# get x,y,z of leftCoord
	leftXYZ.x=StrToNum(GetToken(coordStringList.GetString(leftCoord),",",2).slice(3));
	leftXYZ.y=StrToNum(GetToken(coordStringList.GetString(leftCoord),",",3).slice(3));
	leftXYZ.z=StrToNum(GetToken(coordStringList.GetString(leftCoord),",",4).slice(3));
	# get x, y, z of RightCoord
	rightXYZ.x=StrToNum(GetToken(coordStringList.GetString(rightCoord),",",2).slice(3));
	rightXYZ.y=StrToNum(GetToken(coordStringList.GetString(rightCoord),",",3).slice(3));
	rightXYZ.z=StrToNum(GetToken(coordStringList.GetString(rightCoord),",",4).slice(3));
	# if(both times greater than input) attach to right cord
	# check if falls within timeoffset
	if(tLeft > tInput && abs(tRight-tInput) <= timeoffset)
		{
		#NewRecord(inputtime$, rightXYZ.x, rightXYZ.y, rightXYZ.z, inputname$);
		xcoords[inputname$]=rightXYZ.x;
		ycoords[inputname$]=rightXYZ.y;
		zcoords[inputname$]=rightXYZ.z;
		attached=1;
		}
	# if(both times less than input) attach to left cord
	# check if falls within timeoffset
	if(tRight < tInput && abs(tInput-tLeft) <= timeoffset)
		{
		#NewRecord(inputtime$, leftXYZ.x, leftXYZ.y, leftXYZ.z, inputname$);
		xcoords[inputname$]=leftXYZ.x;
		ycoords[inputname$]=leftXYZ.y;
		zcoords[inputname$]=leftXYZ.z;
		attached=1;
		}
	# if(both times equal input) attach to either cord
	if(tRight==tInput && tLeft==tInput)
		{
		#NewRecord(inputtime$, rightXYZ.x, rightXYZ.y, rightXYZ.z, inputname$);
		xcoords[inputname$]=rightXYZ.x;
		ycoords[inputname$]=rightXYZ.y;
		zcoords[inputname$]=rightXYZ.z;
		attached=1;
		}
	# if(tleft < tInput && tRight > tInput) interpolate
	if(tLeft < tInput && tRight > tInput)
		{
		scale=( (tInput-tLeft) / (tRight-tLeft) ); 
		# linear interpolate new coordinates
		# e.g. if Current x is 10, Previous x is 5, inputtime gives a scale of .2
		# then new x value is: 5 + ( (10-5) * .2) = 5 + (5 * .2) = 5 + 1 = 6
		newXYZ.x=leftXYZ.x + ((rightXYZ.x-leftXYZ.x) * scale); 
		newXYZ.y=leftXYZ.y + ((rightXYZ.y-leftXYZ.y) * scale);
		newXYZ.z=leftXYZ.z + ((rightXYZ.z-leftXYZ.z) * scale);
		# print(newXYZ.x, newXYZ.y, newXYZ.z);
		# check if distance and time fall within constraints
		local numeric ORcheck = (constraints$=="OR" && ( (abs(tInput-tLeft) <= timeoffset || abs(tRight-tInput) <= timeoffset ) || ( (abs(newXYZ.x-leftXYZ.x)^2 + abs(newXYZ.y-leftXYZ.y)^2)^.5 <= gpsoffset || (abs(newXYZ.x-rightXYZ.x)^2 + abs(newXYZ.y-rightXYZ.y)^2)^.5 <= gpsoffset)));
		local numeric ANDcheck = (constraints$=="AND" && ( (abs(tInput-tLeft) <= timeoffset || abs(tRight-tInput) <= timeoffset ) && ( (abs(newXYZ.x-leftXYZ.x)^2 + abs(newXYZ.y-leftXYZ.y)^2)^.5 <= gpsoffset || (abs(newXYZ.x-rightXYZ.x)^2 + abs(newXYZ.y-rightXYZ.y)^2)^.5 <= gpsoffset))); 
		if( ORcheck==1 || ANDcheck==1 )#if valid
			{
			if(method$=="Interpolate")
				{
				#NewRecord(inputtime$, newXYZ.x, newXYZ.y, newXYZ.z, inputname$);
				xcoords[inputname$]=newXYZ.x;
				ycoords[inputname$]=newXYZ.y;
				zcoords[inputname$]=newXYZ.z;
				}
			if(method$=="Closest")
				{
				if( tInput-tLeft <= tRight-tInput ) #closer to left
					{
					#NewRecord(inputtime$, leftXYZ.x, leftXYZ.y, leftXYZ.z, inputname$);
					xcoords[inputname$]=leftXYZ.x;
					ycoords[inputname$]=leftXYZ.y;
					zcoords[inputname$]=leftXYZ.z;
					}
				if( tInput-tLeft > tRight-tInput ) #closer to right
					{
					#NewRecord(inputtime$, rightXYZ.x, rightXYZ.y, rightXYZ.z, inputname$);
					xcoords[inputname$]=rightXYZ.x;
					ycoords[inputname$]=rightXYZ.y;
					zcoords[inputname$]=rightXYZ.z;
					}
				}# closest
			attached=1;
			}# if within gpsoffset
		}# if falls between times
	if(attached==1)
		{
			imageListNameAttached.AddToEnd(inputname$);
			imageListTimeAttached[inputname$]=inputtime$;
		}
	if(attached==0) # image not attached
		report$=report$ + sprintf("Failed to compute coordinates for image '%s' \n", inputname$);
	}#if Assigned 
	else
		{
		imageListNameAttached.AddToEnd(inputname$);
		imageListTimeAttached[inputname$]=inputtime$;
		}
	}# for
if(report$ != "") # report$ contains images not attached
	{
	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
	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
	local string EXIFDateTime$=sprintf("%s%s%s %s",exifDateTime$.substr(0,4),exifDateTime$.substr(5,2),exifDateTime$.substr(8,2),exifDateTime$.substr(11,8));
	# if no value for key "Exif.Image.DateTime" then check key "Exif.Photo,DateTimeOriginal"
	if (GetToken(EXIFDateTime$, ",", 1) == " " || GetToken(EXIFDateTime$, "," ,1) == "")
		{
		exifDateTime$ = exifhandle.GetDatumStr("Exif.Photo.DateTimeOriginal");
		EXIFDateTime$=sprintf("%s%s%s %s",exifDateTime$.substr(0,4),exifDateTime$.substr(5,2),exifDateTime$.substr(8,2),exifDateTime$.substr(11,8));
		}
	# if no value for either key, return 0 as unsuccessful
	if (GetToken(EXIFDateTime$, ",", 1) == " " || GetToken(EXIFDateTime$, "," ,1) == "")
		return 0;
	gpsdialog.SetCtrlValueStr( "status", "Added: " + FileNameGetName(filename$) + "." + FileNameGetExt(filename$) + " - " + EXIFDateTime$  );
	print("Added: ",EXIFDateTime$, filename$);
	imageListTime[filename$]=EXIFDateTime$; # add time to string list
		return 1; # return 1 as successful
}# end func GetEXIF
##################################
proc UpdateImages(numeric imageNum, numeric computedflag) {
		class GUI_CTRL_LISTBOX imagelist;
		local string imageString$, imageName$;
		local numeric i=0;
		imagelist = gpsdialog.GetCtrlByID("imagebox");
		imagelist.DeleteAllItems(); # clear list box
		imageListName.RemoveDuplicates();
		ComputeCoordinates(imageNum, computedflag); #recompute coordiantes for images
		for i=0 to imageListName.GetNumItems()-1{
			imageName$=imageListName[i];
			if(Assigned[imageName$]==1)#assigned coordinates
				{
				imageString$ = sprintf("%s.%s   %s  x: %.7f  y: %.7f  z: %.7f ", FileNameGetName(imageName$), FileNameGetExt(imageName$), imageListTime[imageName$], xcoords[imageName$], ycoords[imageName$], zcoords[imageName$]);
				imageListNameAttached.AddToEnd(imageName$);
				imageListNameAttached.RemoveDuplicates();
				imageListTimeAttached[imageName$]=imageListTime[imageName$];
				imageStringAttached[imageName$]=imageString$;
				}
			else
				{
					if(xcoords[imageName$]==0 && ycoords[imageName$]==0 && zcoords[imageName$]==0)#computed coordinates
						imageString$ = sprintf("*** %s.%s   %s  x: %.7f  y: %.7f  z: %.7f ", FileNameGetName(imageName$), FileNameGetExt(imageName$), imageListTime[imageName$], xcoords[imageName$], ycoords[imageName$], zcoords[imageName$]);
					else{
						imageString$ = sprintf("%s.%s   %s  x: %.7f  y: %.7f  z: %.7f ", FileNameGetName(imageName$), FileNameGetExt(imageName$), imageListTime[imageName$], xcoords[imageName$], ycoords[imageName$], zcoords[imageName$]);
						imageStringAttached[imageName$]=imageString$;
						}
				}
			imagelist.AddItem(imageString$);
	     	}#for
	}#end proc UpdateImages()
##################################
proc ParseLogNMEA(var numeric records, class STRINGLIST slist, string logname$) {
class DATETIME datetime;
class TIMEINTERVAL offset;
local string line$, date$, timeToken$, time$, type$;
local numeric x,y,z=0;
local numeric xdeg, ydeg, zdge, xmin, ymin, zmin;
records=0;
local numeric cameraoffset, timeoffset, gpsoffset;
local string method$, constraints$;
GetOptions(cameraoffset, method$, timeoffset, gpsoffset, constraints$); # returns: method$, constraints$ cameraoffset, timeoffset, gpsoffset
	class FILE logfile;
	logfile = fopen(logname$); # open logfile
	date$ = FileNameGetName(logname$).substr(2,8);
	while (!feof(logfile)) # while not reached end of file
		{
		line$ = fgetline$(logfile); # get next line in log file
		type$ = GetToken(line$,",",1); # parse first entry of line
		if (type$=="$GPGGA") #fix data
			{
			records++;
			datetime.SetDateYYYYMMDD(StrToNum(date$));
			datetime.ConvertToLocal();
			datetime.SetDateYYYYMMDD(StrToNum(date$));
			timeToken$ = GetToken(line$,",",2); # parse second entry of line (time)
			datetime.SetTime(StrToNum(timeToken$.substr(0,2)), StrToNum(timeToken$.substr(2,2)), StrToNum(timeToken$.substr(4,2)));
			offset.Value = cameraoffset*3600;
			datetime=datetime+offset;
			datetime.ConvertToLocal();
			y = StrToNum(GetToken(line$,",",3)); # parse third entry of line (XPos)
			ydeg = floor(y/100);
			ymin = (y - (ydeg * 100))/60;
			y = ydeg + ymin;
			if(GetToken(line$, ",", 4) == "S") y=y*-1;
			x = StrToNum(GetToken(line$,",",5));
			xdeg = floor(x/100);
			xmin = (x - (xdeg * 100))/60;
			x = xdeg + xmin;
			if(GetToken(line$, ",", 6) == "W") x=x*-1;
			z = StrToNum(GetToken(line$,",",10));
			line$ = sprintf("%04i%02i%02i %02i:%02i:%02i , X: %.8f , Y: %.8f , Z: %.8f", datetime.GetYear(), datetime.GetMonth(), datetime.GetDayOfMonth(), datetime.GetHour(), datetime.GetMin(), datetime.GetSec(), x, y, z);
			slist.AddToEnd(line$); # add date, time, x, y, z to string list
			}# if
		}# while
	fclose(logfile); # close logfile
}
##################################
proc ParseLogMI(var numeric records, class STRINGLIST slist, string logname$) {
# Procedure to open log file "logname$" and parse the results
# adds string containing Time(YYYYMMDD HH:MM:SS), X, Y, Z to stringlist
# format of standard MI log file:
# 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
local string line$, date$, timeToken$, time$;
local numeric x,y,z=0;
local numeric i, format=0;
records=0;
	class FILE logfile;
	logfile = fopen(logname$); # open logfile
	while(!feof(logfile) && i<=5)
		{
		i++;
		line$ = fgetline$(logfile);
		if(GetToken(line$,",",1)=="#date(YYYYMMDD)" && GetToken(line$,",",2)=="time(HHMMSS)" && GetToken(line$,",",3)=="XPos(deg)" && GetToken(line$,",",4)=="YPos(deg)" && GetToken(line$,",",5)=="Elev(m)")
			{
			format=1;
			i=6;
			}
		}
	if(format==1)
	{
	while (!feof(logfile)) # while not reached end of file
		{
		line$ = fgetline$(logfile); # get next line in log file
		date$ = GetToken(line$,",",1); # parse first entry of line (date)
		if (StrToNum(date$) > 1)
			{
			records++;
			timeToken$ = GetToken(line$,",",2); # parse second entry of line (time)
			time$ = sprintf("%s:%s:%s", timeToken$.substr(0,2), timeToken$.substr(2,2), timeToken$.substr(4,2)); # change HHMMSS to HH:MM:SS
			x = StrToNum(GetToken(line$,",",3)); # parse third entry of line (XPos)
			y = StrToNum(GetToken(line$,",",4)); # parse fourth entry of line (YPos)
			z = StrToNum(GetToken(line$,",",5)); # parse fifth entry of line (Elev)
			line$ = sprintf("%s %s , X: %.6f , Y: %.6f , Z: %.6f", date$, time$, x, y, z);
			slist.AddToEnd(line$); # add date, time, x, y, z to string list
			}# if
		}# while
	}
	fclose(logfile); # close logfile
}# end proc ParseLog
##################################
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(); # clear coord string list
			logStringList.Clear(); # clear log string list
			gpsdialog.SetCtrlValueStr( "status", "Removed All Logs");
			}
	} # end proc RemoveAll()
#############################
proc AddGPS(string logname$, var numeric records) {
# 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;
	coordlist = gpsdialog.GetCtrlByID("coordlistbox");
	coordlist.DeleteAllItems(); # clear coord listbox
	coordStringList.Clear(); # clear coord string list
	for count=0 to logStringList.GetNumItems()-1 # for all logs in list
		{
		if(FileNameGetExt(logStringList[count])=="gps")
			ParseLogMI(records, coordStringList, logStringList[count]); # add coordinates to coordStringList
		if(FileNameGetExt(logStringList[count])=="log")
			ParseLogNMEA(records, coordStringList, logStringList[count]);
		}
	if(logname$!="")
	{
	logfile=fopen(logname$);
		if(FileNameGetExt(logname$)=="gps")
			ParseLogMI(records, coordStringList, logname$); # add coordinates to coordStringList
		if(FileNameGetExt(logname$)=="log")
			ParseLogNMEA(records, coordStringList,logname$);
	}
	coordStringList.Sort(); # sort coordinates by time
	for count=0 to coordStringList.GetNumItems()-1
		coordlist.AddItem(coordStringList[count]); # add all coordinates in coordStringList to coord list box
	UpdateImages(-1, 0);
  	} # 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
	local string coord$ = coordStringList[index];
	if(coord$ != "") gpsdialog.SetCtrlValueStr( "status", "Removed: " + coord$);
	coordStringList.Remove(index); # remove from stringlist
	} # 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
			{
			AddGPS(logpath$, records);
			if(records>0)
			{
			list.AddItem(logpath$);
			logStringList.AddToEnd(logpath$);
			}
			if(records==0)# if not valid
				report$=report$ + sprintf("Could not add '%s.%s'\n",FileNameGetName(logpath$), FileNameGetExt(logpath$));
			}# if
		}# 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
			{
			AddGPS(logpath$, records);
			if(records>0)
			{
			list.AddItem(logpath$);
			logStringList.AddToEnd(logpath$);
			}
			if(records==0)# if not valid
				report$=report$ + sprintf("Could not add '%s.%s'\n",FileNameGetName(logpath$), FileNameGetExt(logpath$));
			}# if
		}# 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
		{
		AddGPS(logname$, records);
		if(records>0)
			{
			list.AddItem(logpath$);
			logStringList.AddToEnd(logpath$);
			gpsdialog.SetCtrlValueStr( "status", "Added: " + FileNameGetName(logname$) + "." + FileNameGetExt(logname$));
			}
		if(records==0)# 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$));
			}
		}# 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$ = logStringList[index];
	gpsdialog.SetCtrlValueStr( "status", "Removed: " + FileNameGetName(logname$) + "." + FileNameGetExt(logname$));
	list.DeleteItemIndex(index); # remove from listbox
	logStringList.Remove(index); # remove from string list
	AddGPS("",records); # 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
				xcoords[imagePath$]=0; ycoords[imagePath$]=0; zcoords[imagePath$]=0;
				}
			if(hasEXIF == 0) # has no EXIF
				{
				report$=report$ + sprintf("Could not add image '%s.%s'.\n",FileNameGetName(filenames[count]),FileNameGetExt(filenames[count]));
				}
			}# if
	}# 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);
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);
		}
	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 proc AddImage
###########################
#############################
#Procedures to bring up view window to select coords:
proc OnToolSet () {
       clear();
       ptrastmap = TransPoint2D(myPt.Point,transViewToRastMap);
       local string coords$ = sprintf("x = %5.5f, y = %5.5f",ptrastmap.x,ptrastmap.y);
       ViewSetMessage(view,coords$);
}
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
		xcoords[imageName$] = ptrastmap.x;
		ycoords[imageName$] = ptrastmap.y;
		zcoords[imageName$] = 0;
		Assigned[imageName$]=1;
		UpdateImages(index,0);
}
proc OnClose() {
       DialogClose(pcwin);
       DestroyWidget(pcwin);
       CloseRaster(rasterIn);
}
proc OnDestroy() {
       Exit();
}
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);
rasterInLayer = GroupQuickAddRasterVar(gp,rasterIn);
ViewRedrawFull(view);
class MAPPROJ rastmapproj = rasterInLayer.Projection;
rastmapproj.SetSystemLatLon();
transViewToRastMap = ViewGetTransMapToView(view,rastmapproj,1);
ViewActivateTool(view,myPt);
ViewSetMessage(view,"Left-click to move point.  Right-click to assign coordinates.");
}
#############################
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;
	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
	coord.x=StrToNum(GetToken(coordStringList.GetString(coordindex),",",2).slice(3));
	coord.y=StrToNum(GetToken(coordStringList.GetString(coordindex),",",3).slice(3));
	coord.z=StrToNum(GetToken(coordStringList.GetString(coordindex),",",4).slice(3));
	xcoords[imageName$] = coord.x;
	ycoords[imageName$] = coord.y;
	zcoords[imageName$] = coord.z;
	Assigned[imageName$]=1;
	UpdateImages(imageindex,0);
}#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(imageStringAttached[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);
	coord.x=xcoords[transferName$];
	coord.y=ycoords[transferName$];
	coord.z=zcoords[transferName$];
	xcoords[imageName$] = coord.x;
	ycoords[imageName$] = coord.y;
	zcoords[imageName$] = coord.z;
	Assigned[imageName$]=1;
	UpdateImages(imageindex,0);
}#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;
	xcoords[imageName$] = xcoord;
	ycoords[imageName$] = ycoord;
	zcoords[imageName$] = latlondialog.GetCtrlValueNum("zcoord");
	Assigned[imageName$]=1;
	UpdateImages(imageindex,0);
}#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") 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") coord.y=coord.y*-1;
	coord.z = StrToNum(GetToken(zcoord$,"/",1))/ StrToNum(GetToken(zcoord$,"/",2));
	if(zcoordref$=="1") coord.z=coord.z*-1;
	xcoords[imageName$] = coord.x;
	ycoords[imageName$] = coord.y;
	zcoords[imageName$] = coord.z;
	Assigned[imageName$]=1;
	UpdateImages(index, 0);
	}
	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");
}
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);
	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();
}
#############################
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$]);
		xcoords[imageName$]=0; ycoords[imageName$]=0; zcoords[imageName$]=0;
		Assigned[imageName$]=0;
		imageListTime[imageName$]="";
		imageListTimeAttached[imageName$]="";
		imageStringAttached[imageName$]="";
		imageListName.Remove(index); # remove from stringlist
		local numeric i;
		for i=0 to imageListNameAttached.GetNumItems()-1{
			if(imageListNameAttached[i]==imageName$) imageListNameAttached.Remove(i);
			}
	} # 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
			imageListTime.Clear(); # clear string list
			imageListName.Clear(); # clear string list
			xcoords.Clear(); ycoords.Clear(); zcoords.Clear();
			Assigned.Clear();
			imageListTimeAttached.Clear();
			imageStringAttached.Clear();
			imageListNameAttached.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
	##numberOfElements=NumVectorPoints(GPSVector);
	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$; 
	}# 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;
			}
		}# for
	}# 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() {
#ComputeCoordinates();
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$ = imageListTimeAttached[inputname$]; # grab input date and time
	NewRecord(inputtime$,xcoords[inputname$],ycoords[inputname$],zcoords[inputname$], inputname$);
	}
AttachPoints(); # attach records to points in vector
VectorValidate(GPSVector);
CloseVector(GPSVector);
} # end proc AttachImages
############################
proc WriteEXIF(){ 
# write to exif metadata
#ComputeCoordinates();
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
	imageTime$=imageListTimeAttached[imagePathOriginal$];
	coord.x=xcoords[imagePathOriginal$];
	coord.y=ycoords[imagePathOriginal$];
	coord.z=zcoords[imagePathOriginal$];
	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("%s:%s:%s", imageTime$.substr(0,4), imageTime$.substr(4,2), imageTime$.substr(6,2));
	#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() {
	local numeric records;
	AddGPS("", records);
}
#############################
proc OpenScript() { # procedure called when starting
# set default values
gpsdialog.SetCtrlValueStr("cameraoffset", "+000:00:00");
gpsdialog.SetCtrlValueStr("timeoffset", "00:05:00");
gpsdialog.SetCtrlValueNum("gpsoffset", .5);
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>
				<pane Orientation="horizontal">
					<label WidthGroup="labels">Set Max Difference in Distance for GPS (degrees)</label>
					<editnumber id="gpsoffset" WidthGroup="boxes" Justify="Right"/>
				</pane>
					<radiogroup id="constraints">
						<item Value="AND" Name="Check if falls within BOTH time AND distance"/>
						<item Value="OR" Name="Check if falls within EITHER time OR distance"/>
					</radiogroup>
				</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();
	}