var esi = esi ? esi : function () {
	var _private = {
	
	  "sel_divider" : "-----" ,
	  "bAlreadySubmitted" : false, //tracks whether form has been successfully submitted at least once 
	
		//from http://www.webmasterworld.com/javascript/3540648.htm
		OBJtoXML: function (obj,d)
		{
			d=(d)?d:0;
			var rString="";
			var pad="";
			
			for(var i=0;i<d;i++)
			{
				pad+=" ";
			}
			
			if(typeof obj==="object")
			{
				if(obj.constructor.toString().indexOf("Array")!== -1)
				{
					for(i=0;i<obj.length;i++)
					{
						rString+=pad+"<item>"+obj[i]+"</item>";
					}
					
					rString=rString.substr(0,rString.length-1);
				}
				else
				{
					for(i in obj)	
					{
					  if (obj.hasOwnProperty(i)) {
  						var val= this.OBJtoXML(obj[i],d+3);
  						
  						if(!val) {
  						  val = "";
  						}
  							
  						rString+=pad+"<"+i+">"+val+((typeof obj[i]==="object")?""+pad:"")+"</"+i+">";
						}
					}
				}
			}
			else if(typeof obj === "string")
			{
				rString=obj;
			}
			else if(obj && obj.toString)
			{
				rString=obj.toString();
			}
			else
			{
				return false;
			}
			return rString;
		},
		
		soapData: function (soapFunction, namespace, data)
		{
			var soap = "<?xml version=\"1.0\" encoding=\"utf-8\"?>";
			soap += "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">";
			soap += "<soap:Body><"+soapFunction+" xmlns=\""+ namespace +"\">";
			soap += this.OBJtoXML(data);
			soap += "</"+soapFunction+"></soap:Body></soap:Envelope>";
			
			return soap;
		},
		   
    get_default_option : function (select_obj)
    {
      var default_label = $(select_obj).attr("default");

      if (!default_label) {
        default_label = "";
      }
      
      return default_label;
    },

    //_private validator methods --------------------------------------------
    
    //make sure valid entry is selected, not a divider
    validate_select_box : function (select_obj)
    {
      var msg = "";
      if ( $(select_obj).val() == _private.sel_divider ) {
        msg = _public.format_error_string( _private.sel_divider + " is not a valid " + _private.get_field_name(select_obj) );
      }
      
      return msg;
    },
    
    //validate_single_num - runs all numeric validators. Sets error state classes / page elements as appropriate
    //  returns empty string if there are no errors, otherwise returns string containing concatenated errors
    //  for this input item.
    validate_single_num: function (input_obj) 
    {			
     	var msg = "";
     	var field_state = _private.check_field(input_obj);
   	
     	if (field_state.is_error) {
     	  msg += field_state.msg; //will contain error message if val was required and is missing
     	}
     	else {
       	if (field_state.has_value) {
       	  //all additional checks go here.        	
        	msg += _private.check_num(input_obj).msg;
        	msg += _private.check_min_max(input_obj, "min").msg;
        	msg += _private.check_min_max(input_obj, "max").msg;
      	}
    	}
    	
    	if (msg) {
    		$(input_obj).addClass("bad_data");
    	}
    	else {
    		$(input_obj).removeClass("bad_data");
    	}
    					
    	return msg;
    },
    
    //parse xml return from server and construct any necessary error strings
    validate_server_response : function (xml) 
    {
      var error = $(xml).find("error_message").text();
      
      if (!error) {
        error = "";
      }
      else {
        error = _public.format_error_string( error );
        
        //look for any secondary error messages and append to string
        var input_arr = $(xml).find("errors").children();
        
        for (var i=0, count=input_arr.length, str; i < count; i++) {
          str = $(input_arr[i]).find("message").text();

          if (str) {
            error += _public.format_error_string ( str );
          }
        }
      }
      
      return error;
    },   
    
    //Validator_response_obj - CLASS defintion, not for direct execution
    //  individual validators (check_num, check_field, etc.) return this object
    //  standard attributes are "is_error," with a val of true or false and
    //  msg, containing any error/results messages that might be passed up 
    //  to the user. Some functions may include additional attributes (see check_field).    
    Validator_response_obj: function (bErr, strMsg)
    {
      this.is_error = ( typeof(bErr) === "boolean" ? bErr : false );
      this.msg = ( typeof(strMsg) === "String" ? strMsg : "" );
    },
    
    //check_field - response object contains the following
    //  has_value: true/false, depending on whether the field has data
    //  is_required: true/false, true unless explicitly marked false with required="no" attribute in source
    //  is_error: true/false, true if both required and empty, otherwise false
    //  msg: error string, if appropriate.
    check_field : function (input_obj)
    {    
      var resp = new this.Validator_response_obj();      
      var strRequired = new String ( $(input_obj).attr("required") ); //must declare string explicitly in case jquery returns undefined 
      var val = $(input_obj).val();
      
      //can't use false as a flag, some browsers report "required=false" by default and I want to default to true
      resp.is_required = ( strRequired.match( /no/i ) ? false : true );
      resp.has_value = ( val === "" ? false : true );

      if (resp.is_required && !resp.has_value) {       
          resp.is_error = true;
          resp.msg = _public.format_error_string( _private.get_field_name(input_obj) + " is a required field" );
      }
      
      return resp;
    },
    
    //check_num - basic numeric validator. simply makes sure we can interpret data as a number
    //  input_obj: Dom element containing the number to check
    //  output: empty string if success, error message otherwise.
    check_num: function (input_obj) 
    {     
     
      var resp = new _private.Validator_response_obj();  
      var val = $(input_obj).val();
      
    	if ( isNaN( _public.parseFloatStrict(val) ) ) {
        resp.is_error = true;
    		resp.msg = _public.format_error_string( _private.get_field_name(input_obj) + ": " + val + " is not a recognizable number." );
    	}

    	return resp;
    },
    
    //check_min_max - 
    //  input_obj: the input control to evaluate
    //  minmax: either "min" or "max" specifying which you'd like to check.
    //
    //  The value in the input box is compared to min="" and max="" params specified in the html source.
    //  These vals may have a prefix of "lt" or "gt" to indicate that you must be less than max or greater than min
    //  source examples, <input ... min="gt0" max="lt1000">, <input ... min="0" max="1000">
    //
    //  returns a Validator_response_obj. obj contains error if min/max checks fail    
    check_min_max : function (input_obj, minmax) 
    {    
      var reg = /^((gt)|(lt))\s?/;
      var resp = new _private.Validator_response_obj();
      
      var val = _public.parseFloatStrict( $(input_obj).val() );
      var limit_str = new String( $(input_obj).attr(minmax) );
      var limit = _public.parseFloatStrict( limit_str.replace(reg, "") );
      
      if ( !isNaN(limit) && !isNaN(val)) // only process with real numbers
      {
             
        var not_equal = limit_str.match(reg);
        
        //compare
        var success;
        var base_error;
        var or_equal = " or equal to ";
                     
        if (minmax == "min") {
          base_error = " must be greater than ";
          
          if (not_equal) {
            success = limit < val;
          }
          else {
            success = limit <= val;
            base_error += or_equal;
          }
        }
        else {
          base_error = " must be less than ";
        
          if (not_equal) {
            success = limit > val;
          }
          else {
            success = limit >= val;
            base_error += or_equal;
          }
        }
        
        //set error code
        if (!success) {
          resp.is_error = true;
          resp.msg += _public.format_error_string( _private.get_field_name(input_obj) + base_error + limit );
        }
        
      }
              
      return resp;
    },
        
    //end _private validator methods	------------------------	
        
    //get_field_name - retrieve name of input field via best possible page source
    get_field_name : function (input_obj)
    {
      var field = $("label[for=" + $(input_obj).attr("id") + "]").text(); //read field name from label
      
      if (field) { //remove trailing colon from label
        field = field.replace(/\:$/, "");
      }

      if (!field) {
        field = $(input_obj).attr("name"); //use name attribute as name
      }
        
      if (!field) {
        field = $(input_obj).attr("id"); // use id as name
      }
      
      return field;    
    },

  	handle_error_block : function (msg, block_id, output_id) 
  	{
  	  var error_block = $("#"+block_id);
  	  
  	  if (msg) { //display msg
        $("#"+output_id).html(msg);
        $(error_block).removeClass("hidden");	 	  
  	  }
  	  else { //hide emtpy block
        $(error_block).addClass("hidden");
  	  }
  	}     
    		
	};//end of _private section
	
	var _public = {
			
		
		//Asks web service for appropriate list of units for each
		//  unit dropdown list, and populates. Unit type is determined
		//  by reading the units="" attribute on the select box. Multiple
		//  unit types may be specified.
		fill_unit_selections : function ()
		{
			var unitDropdowns =  $("select.units");
			var dimensions = {};
			
			for( var i=0; i < unitDropdowns.length; i++)
			{
		        var unitArr = $(unitDropdowns[i]).attr("units").split(" ");
		        
		        for (var j=0, len=unitArr.length; j < len; j++)
		        {
					if ( !dimensions[ unitArr[j] ] ) {
						dimensions[ unitArr[j] ] = [];
					}
		            
		          dimensions[ unitArr[j] ].push( unitDropdowns[i] );
		        }
		    }
					
			for( var key in dimensions )
			{
				_public.fill_drop_lists_with_units( key, dimensions[key] );
			}

		},
		
		fill_drop_lists_with_units : function ( quantity_str, boxes )
		{      
			_public.webservice_call( "units_in_quantity", {	quantity: quantity_str }, 
              function(xml) {
                var names = $(xml).find("string"), //array of <string></string> objects xml. (unit labels requested from server)
                    i, j,           //loop iterators
                    selected,       //adds "selected" state to option, when setting default unit labels
                    raw_label,      //a single unit label string as it comes from server
                    default_label;  //raw text string representing default label
                    
                //HACK: The skip_units array identifies those units that are not handled correctly
                //      in the web service. Bad conversion factors, or something similar. Any unit
                //      strings in the array will be skipped when filling unit selection boxes.
                var skip_units = ["ft^3/lb", "cm^3/g", "R"];
                
                for( i=0; i < boxes.length; i++)
                {
                  //if box already has stuff in it, add divider
                  if ( $(boxes[i]).children().length > 0 ) {
                    $(boxes[i]).append('<option disabled="disabled" value="' + _private.sel_divider + '" > ' + _private.sel_divider + ' </option>');
                  }

                  default_label = _private.get_default_option(boxes[i]);
                  
                  //add one option tag for each label
                  for(j=0; j < names.length; j++)
                  {
                    raw_label = $(names[j]).text();
                    
                    if ($.inArray(raw_label, skip_units) == -1) 
                    {
                        if ( raw_label == default_label) {
                          selected = " selected ";
                        }
                        else {
                          selected = "";
                        }
                          
                        $(boxes[i]).append('<option ' + selected + 'value="' + raw_label + '">' + _public.format_unitlabels( raw_label ) + '</option>');
                    }
                  }                  
                }               
              }
			);
		},
    //bind_events - call from each calc page to setup common event bindings
    //  calc_func: function organizing data, and making server call for specific calculator function
    //  calc_button_id: id of button calc page that triggers the calculation
    //  filter_submit_func: optional function defined by caller. if it returns true, do the calc, otherwise skip
		bind_events : function (calc_func, calc_button_id, filter_submit_func ) 
		{
  		var calc_button = $("#"+calc_button_id);

  		var do_click = function (filter_func, def) {
				var bSubmit;
				
				if ( typeof( filter_func ) === "function" ) {
				  bSubmit = filter_func(); //filter should return true or false (do it/don't do it).
				}
				else {
				  bSubmit = def;
				}

				if ( bSubmit) {
					$(calc_button).click(); 
				}
  		};
  		
  		//validate form fields when focus changes
  		$("input[type=text][validate=num]").blur( function () { _private.validate_single_num(this); } );	
  		
  		//submit request when enter key pressed in input field
  		$("input[type=text]").keypress( function (event) { 
                              		       if (event.which == 13) {
                              		         $(calc_button).click();
                              		         return false; //prevent .net/umbraco from submitting "form" that doesn't exist
                              		       } 
                                       }
      );
  		
  		//process data and send request to web service when button clicked
  		$(calc_button).click ( function () { _public.handle_calc_button( calc_func, calc_button ); } );  	
  		
  		
      //remove calced results whenever user changes inputs.
  		$("input[type=text]").keypress( function (ev) { if (ev.which != 13) _public.clear_results(); } );
  		
  		//submit request when select item changed (wait for real button click if we haven't already submitted once)
  		$("select").change( function () { do_click( filter_submit_func, 
                                                  _private.bAlreadySubmitted 
                                                 ); 
  	                                                   }
  	                                      );  			
  		
  		//attach event handlers to ajax stuff
  	  $(calc_button).ajaxStart( function () { _public.enable_working_message(this); } );
   	  $(calc_button).ajaxStop( function () { _public.disable_working_message(this); } );  
   	  		  			
		},
		
		handle_calc_button : function (calc_func, calc_button_obj)
		{
      //if we know the calc button associated with this call, then only process
      //  the click if the button/calc is not already busy.
      if ( calc_button_obj && !$(calc_button_obj).attr("busy") )
      {
        ///$(calc_button_obj).prevent_default():
        var msg = _public.validate_inputs();   
        
        if ( !msg ) {
          calc_func();
          _private.bAlreadySubmitted = true;
        }                    
        
        _private.handle_error_block( msg, "main_error_block", "main_error_output" );
      }
		},
		
		//built in version of parseFloat truncates strings if they start with numbers but have junk in them
		//  this one makes sure entire string is a number.
		parseFloatStrict : function (numStr) 
		{
      numStr = numStr.toString(); //prevent errors when numStr is not really a string      
      
      //accept: 1; 0.1; .1; 1.; 1.0; 1.0E5; 1.0e-5;
      var match = numStr.match(/^[\-+]?(?:(?:\d*\.?\d+)|(?:\d+\.))(?:[eE][+\-]?\d+)?$/);
      
      if ( match === null ) {
        return NaN;
      }
      else {
        return Number( match[0] ); 
      }
      
		},
		
		format_unitlabels : function ( in_str )
		{
	    //look for ^2, ^3 replace with &sup2; and &sup3;	
		  in_str = in_str.replace(/\^(2|3)/g,"&sup$1;");
	
      //replace asterisks with bullet (dot notation)
		  in_str = in_str.replace(/\*/g, " &bull; ");
		  
		   //add degree symbol before temperatures.
		   //  May need to be more specific in future, 
		   //  if F C or R are used for other things.
		  if( in_str != "Cv" )
			  in_str = in_str.replace(/(F|C|R)/g, "&deg;$1");
		  
		  return in_str;
		},
		
    //format_number - clean up number presentation for display.
    //  precision: number of significant digits when rounding converting to exponential
    //  max_z_after_d: maximum number of zeros immediatly after decimal before triggering exponential
    //  default values when not specified: precision: 6, max_z_after_d: 2.
    //
    //  Default Display Rules: 
    //    values between .001 and 1,000,000 are rounded to 6 sig digits, no scientific notation
    //    Numbers outside this range are converted to scientific notation with six significant digits.
    //    trailing zeros are always removed.
    format_number : function ( num, precision, max_z_after_d )
    {	
      //number check and set defaults. 
      //  precision and max_z_after_d params allow us to change presentation for specific cases.
      var myNum     = _public.parseFloatStrict(num);
      precision     = ( isNaN( precision ) ? 6 : precision );         //significant digits
      max_z_after_d = ( isNaN( max_z_after_d ) ? 2 : max_z_after_d ); //for nums < 1, number of 0 following decimal before using exponential notation.
    
      if (! isNaN(myNum) ){

        myNum = Number(myNum).toPrecision( precision );
        
        //to precision leaves more leading zeros than we want before going exponential,
        //  so check leading zeros and see if we need to force exponential notation.
        
        var z_after_d = myNum.match(/\.(0*)[eE1-9$]/);                     //check for zeros following decimal
        z_after_d = ( z_after_d ? z_after_d[1].toString() : false );       //store all the zeros in a string
        z_after_d = ( z_after_d ? z_after_d.match(/0/g).length : false );  //count the zeros
  
        if (z_after_d && z_after_d > max_z_after_d) {
          myNum = Number(myNum).toExponential( precision - 1 );
        }        
        
        //clean up presentation
        var strNum = myNum.toString();
        
        strNum = strNum.replace( /e/, "E" );
        strNum = strNum.replace( /(\.\d*?)0*(?=(E|$))/, "$1" );
        strNum = strNum.replace( /\.(?=(E|$))/, "");
        
        return strNum;
      }
    },
		
		//unit_value: read a unit/value pair from the DOM and return them in a format
		//  understood by the javascript object to xml parser.
		//  id: string id of the DOM element parent of the unit/value pair
		//  returns a javascript object containing the unit label and the value
		unit_value : function (id)
		{
			var top = $("#"+id);
			var unit_select = $(top).find("select");
			var input_box = $(top).find("input");
			
			var ret =  { "label" : "", "value" : "0.0" }; //initialize default return values
			
			if (unit_select.length == 1) {
			  ret.label = unit_select[0].value;
			}
			
			if (input_box.length == 1 && ! isNaN( _public.parseFloatStrict( input_box[0].value ) ) ) {
			  ret.value = input_box[0].value;
			}
			
			if (unit_select.length > 1 || input_box.length > 1) {  //complain if we find too many selects or inputs
				alert("Could not identify unit_value control pair"); //but just go with defaults if values are missing
	    }
	    
			return ret;
		},
		
		//output_number - read a specific value from xml server response and display on page.
  	//  xml: xml response from server. contains list of return data to be parsed
  	//  key: name of the xml unit/value pair we need
  	//  location_id: id of the HTML element where we'll store/display the numeric output
  	output_number : function(xml, key, location_id) 
  	{
  	    var pair = $(xml).find(key);
  	    var num = $(pair).find("value").text();
  	    
  	    if (num === "") { //for bare numbers (not in unit value) report number at key
  	      num = $(pair).text();
  	    }
  	    
  	    num = _public.format_number(num);
  	    $("#"+location_id).text(num);
  	},		
		
  	//output_server_error - checks server response for error message and sticks
  	//                      it in the specified location, if present.
  	//  xml: the server response
  	//  error_control_block_id: id of of HTML block containing entire error presentation structure
  	//  error_output_id: id of HTML item that will display actual error message.
  	output_server_error : function (xml, error_control_block_id, error_output_id)
  	{	  
  	  _private.handle_error_block( _private.validate_server_response(xml)
  	                              , error_control_block_id
  	                              , error_output_id
  	                            );
  	},
  	
  	//Call when beginning to process ajax requests. Accepts calc button object
  	//  updates text to "working ..." 
  	//  sets busy attribute to "yes". used to prevent simultaneous requests for same calc
  	enable_working_message : function (button_obj)
  	{
  	  if (!button_obj) {
  	    button_obj = $("#btn_calc");
  	  }
  	  
  	  if (button_obj) {
  	    $(button_obj).attr({  "value" : "Working ..." ,
  	                          "busy" : "yes"
  	                       } );
  	  }
   	},
   	
  	//Call when all ajax requests complete. Accepts calc button object
  	//  updates text to "working ..." 
  	//  removes "busy" attribute from button. used to prevent simultaneous requests for same calc
   	disable_working_message : function (button_obj)
   	{
  	  if (!button_obj) {
  	    button_obj = $("#btn_calc");
  	  }
  	    
  	  if (button_obj) {
  	    $(button_obj).attr("value", "Calculate" );
  	    $(button_obj).removeAttr("busy");
  	  }
   	},
		
		webservice_call: function (soapfn, data, successfn)
		{
			$.ajax({
					dataType:    "xml",
					contentType: "text/xml",
					type:	     "post",
					url:	     "/esi_calcs/esi_calcs.asmx",
					data:	     _private.soapData(soapfn, "http://www.eng-software.com/calcs", data),
					success:	 successfn
				});
		},
		
  	//reset output fields when a calc requested, so that if an error
  	//  interrupts the calc, we're not displaying old data.
  	clear_results : function (top)
  	{
  	  if (!top) {
  	    top = "";
  	  }
  	    
  	  $(top).find(".calc_output").text("");
  	},
		
    //validator methods -----------------
    
    //validate_single_num_event - called strictly by bound events such as blur. passes processing on to standard func
    //deprecated - should call bind_events instead. Still used by unit_converter calcs, however
    validate_single_num_event: function () 
    {
    	_private.validate_single_num (this); //called by bound events (blur)
    },
    
    //validate_inputs - called before making a service request. 
    //  returns empty string if everything is good. Otherwise, returns concatenated
    //  string containing all validation error messages.
    validate_inputs : function (top) 
    {
    	if (!top) {
    	  top = "";
    	}
    	
    	var i, count;
    	var result = "";
    	_public.clear_results(top);

    	//validate numbers
    	var list = $(top).find("input:visible[type=text][validate=num]");    	
    	for (i=0, count=list.length; i < count; i++) {
    		result += _private.validate_single_num( list[i] );
    	}
    	
    	//validate select boxes
    	list = $("select");
    	for (i=0, count=list.length; i < count; i++) {
    	  result += _private.validate_select_box( list[i] );
    	}
    
    	return result;
    }, 
    
    
    //checks that all fields in a given calc form have been filled in.
    //  return: true if they have, false if not.
    //  top: DOM element signifying the top of a calc group, if not specified
    //       checks entire page.
    check_fields_complete : function (top)
    {
      if (!top) {
        top = "";
      }
        
      var bSuccess = true;
      var fields = $(top).find("input[type=text]");
        
      for (var i=0, temp, count=fields.length; i < count; i++) {
        temp = _private.check_field(fields[i]);

        if (!temp.has_value && temp.is_required) {
          bSuccess = false;
        }
      }
      
      return bSuccess;
    },
        
    //attach standard prefix/suffix to error strings
    format_error_string : function (strIn)
    {
      //if line doesn not end in punctuation, add period
      var end_punct;
      end_punct = ( strIn.match(/[.!?]$/) ? "" : "." );
      
      return ( "<li>" + strIn + end_punct + "</li>\n" );
    }     	
    //end validator methods -------
		
		
	}; //end of _public section
	
	return _public;
};
