// vc_id = "$Id: locationGateway.js 12680 2009-11-16 21:01:31Z robert $"
// JavaScript Document

var LOCATION_GATEWAY_LOCATION_TYPE = {FIX:1,RAW:2,STATE:3,MLS:4};
    
/**
 * LocationGateway
 * This class represents a location search gateway.
 * The internal impl. is either serviced by msve or gmap,
 * though the interface should hide that fact.
 * This pass of the impl. provides a location search and
 * target facility, with the final destination being a map
 * search that is centered upon the user's location.
 * Other outputs ( non map display ) are envisioned.
 * @constructor
 * @param node id for the map
 * @param url for the location target
 * @param string representing the product that services the request
 * @param optional client key
 * @param optional url for the log
 */
function LocationGateway( node, url, product, key, log ) {
   // attributes
   // StateChangeListener objects.
   this.listeners = new Array( );
   this.name = "location";
   this.state = STATE_CHANGE_STATE_UNINITIALIZED;
   this.status = STATE_CHANGE_STATUS_OK;
   this.STATE_CHANGE_STATUS_UNKNOWN_ADDRESS = 602;
   this.STATE_CHANGE_STATUS_UNAVAILABLE_ADDRESS = 603;
   STATE_CHANGE_STATUS_TEXT[this.STATE_CHANGE_STATUS_UNKNOWN_ADDRESS] = "unknown";
   STATE_CHANGE_STATUS_TEXT[this.STATE_CHANGE_STATUS_UNAVAILABLE_ADDRESS] = "unavailable";

   // LocationGateway attributes
   this.node = node;
   this.url = url;
   this.key = key;
   if ( product == "msve" ) {
       this.server = new LocationGatewayMsve( this );
   } else if ( product == "gmap" ) {
       this.server = new LocationGatewayGmap( this );
   }
   this.fixes = [];
   this.fixesUnfiltered = [];
   this.regions = [{name:"Arizona",arg:"&st=az",abbr:"AZ"},
		   {name:"California",arg:"&st=ca",abbr:"CA"},
		   {name:"Hawaii",arg:"&st=hi",abbr:"HI"},
		   {name:"Idaho",arg:"&st=id",abbr:"ID"},
		   {name:"Montana",arg:"&st=mt",abbr:"MT"},
		   {name:"Nevada",arg:"&st=nv",abbr:"NV"},
		   {name:"Oregon",arg:"&st=or",abbr:"OR"},
		   {name:"Utah",arg:"&st=ut",abbr:"UT"},
		   {name:"Washington",arg:"&st=wa",abbr:"WA"},
		   {name:"Wyoming",arg:"&st=wy",abbr:"WY"}];
   this.regionsIndex = new Object( );
   this.namePattern = /\,\s(\w{2})\s?([\d-]+)?$/;  
   this.countryPattern = /\s(usa?|united\sstates)$/i;
   this.islandPattern = /(Island)(\sAirport)?$/;
   this.location = null;
   this.search = null;
   this.hints = [{pattern:/\bvernon\b/i,replace:"Mount Vernon, WA, USA"},
   		 {pattern:/\bkent\b/i,replace:"Kent, WA, USA"},
		 {pattern:/\bwhidbey\b/i,replace:"Whidbey Island, USA"},
		 {pattern:/\borcas\b/i,replace:"Orcas Island, USA"},
		 {pattern:/\bcamano\b/i,replace:"Camano, WA, USA"}];
   this.track = log;
   this.log = this.track ? new XHR( "Log Location" ) : null;
   this.display = "map";
   
   // LocationGateway methods
   this.initialize = LocationGatewayInitialize;
   this.findLocation = LocationGatewayFindLocation;
   this.retryLocation = LocationGatewayRetryLocation;
   this.callback = LocationGatewayCallback;
   this.targetLocation = LocationGatewayTargetLocation;
   this.targetMls = LocationGatewayTargetMls;
   this.extractState = LocationGatewayExtractState;
   this.getLocations = LocationGatewayGetLocations;
   this.getLocationUrl = LocationGatewayGetLocationUrl;
   this.getLocationHint = LocationGatewayGetLocationHint;
   this.getLocationType = LocationGatewayGetLocationType;
   this.appendCountry = LocationGatewayAppendCountry;
   this.clarifyLocation = LocationGatewayClarifyLocation;
   this.setDisplay = LocationGatewaySetDisplay;

   this.autoZoom = false; // enable dynamic zoom
   this.autoRetry = true; // enable search again
   this.autoClarify = false; // enable programmatic disambiguator
   this.autoCountry = true; // enable auto append country
   this.stateChange = LocationGatewayStateChange;
   this.addListener( this );
}
LocationGateway.prototype = new StateChangeSupport( );

/**
 * initialize the location server
 */
function LocationGatewayInitialize( ) {
    // initialize the known regions index
    for ( i = 0; i < this.regions.length; i++ ) {
	var region = this.regions[i];
	this.regionsIndex[region.name] = region;
	this.regionsIndex[region.abbr] = region;
    }

    this.server.initialize( );
}

/**
 * find a location
 * @param string representing the location
 */
function LocationGatewayFindLocation( location ) {
    this.setState(  STATE_CHANGE_STATE_LOADING );
    this.location = location;
    if ( location && location != "" ) {
	this.search = this.appendCountry( location ) ?
	    location + ", USA" : location;
	this.server.findLocation( this.search );
    } else {
	this.callback( null );
    }
}

/**
 * should we append country to improve search
 * @param location
 * @return true if we should append country
 */
function LocationGatewayAppendCountry( location ) {
    return this.autoCountry && !this.countryPattern.test( location );
}

/**
 * this method represents a programmatic disambiguator.
 * it's job is to attempt to automatically reduce the size 
 * of the internally held list of fixes.
 * this method can be called at any point, though it is 
 * called internally.
 */
function LocationGatewayClarifyLocation( ) {
    if ( this.autoClarify && this.fixes.length > 1 ) {
	// test for ambiguous zip code
	var zips = true;
	for ( i = 0; i < this.fixes.length; i++ ) {
	    if ( this.fixes[i].scheme != LOCATION_FIX_SCHEME.ZIP ) {
		zips = false;
		break;
	    }
	}
	if ( zips ) {
	    // assume 1st fix is best
	    this.fixes.length = 1;
	}
    }
}

/**
 * set the display type
 * @param string representing the display type [ map, list ]
 */
function LocationGatewaySetDisplay( display ) {
    this.display = "list" == display ? "list" : "map";
}

/**
 * server callback method
 * @param a geocode response, proprietary and depends upon map vendor
 */
function LocationGatewayCallback( response ) {
    this.setState(  STATE_CHANGE_STATE_LOADED );

    if ( response ) {
	this.server.callback( response );
    } else {
	this.fixes.length = 0;
	this.fixesUnfiltered.length = 0;
    }

    if ( this.fixesUnfiltered.length == 0 ||
	 this.fixes.length == 0 ) {
	if ( this.fixesUnfiltered.length == 0 ) {
	    this.setStatus( this.STATE_CHANGE_STATUS_UNKNOWN_ADDRESS );
	} else {
	    this.setStatus( this.STATE_CHANGE_STATUS_UNAVAILABLE_ADDRESS );
	}
	// no valid results, conditionally retry
	this.retryLocation( );
    } else {
        this.setStatus( STATE_CHANGE_STATUS_OK );
	this.clarifyLocation( );
	var locations = this.getLocations( );
	if ( locations.length == 1 && locations[0].scheme == LOCATION_FIX_SCHEME.STATE ) {
	    // only one result that is a state, conditionally retry
	    this.retryLocation( );
	} else {
	    this.setState(  STATE_CHANGE_STATE_COMPLETE );
	}
    }
}

/**
 * conditionally retry a location find using a hint
 */
function LocationGatewayRetryLocation( ) {
    var searchAgain = this.autoRetry ? this.getLocationHint( this.search ) : null;
    if ( searchAgain != null ) {
	this.findLocation( searchAgain );
    } else {
	this.setState(  STATE_CHANGE_STATE_COMPLETE ); 
    } 
}

/**
 * look for a search hint.
 * this is to be called when no valid matches are found
 * to attempt to create a search string useful to provide
 * valid matches.
 * most of what is in this method should not be required, 
 * with proper feedback given to msve
 * @param search string
 * @return new search string or null
 */
function LocationGatewayGetLocationHint( search ) {
    var rcode = null;
    
    for ( i = 0; i < this.hints.length; i++ ) {
	if ( this.hints[i].pattern.test( search ) ) {
	    rcode = this.hints[i].replace;
	    // rcode = search.replace( this.hints[i].pattern, this.hints[i].replace );
	    break;
	}
    }
    // protect against circular references
    rcode = search == rcode ? null : rcode;
    return rcode;
}

/**
 * get the location type
 * @param type to find, or null
 * @return [ fix, raw, state ] defaults to fix
 */
function LocationGatewayGetLocationType( type ) {
    var rcode = LOCATION_GATEWAY_LOCATION_TYPE.FIX;
    if ( type &&  ( type == LOCATION_GATEWAY_LOCATION_TYPE.STATE ||
		    type == LOCATION_GATEWAY_LOCATION_TYPE.RAW  ) ) {
	rcode = type;
    } 
    return rcode;
}

/**
 * get the locations
 * @param LOCATION_GATEWAY_LOCATION_TYPE to use 
 *  [ fix, raw, state ] optional defaults to fix
 * @return array of locations, might be empty
 */
function LocationGatewayGetLocations( type ) {
    var collection = null;
    var  locationType = this.getLocationType( type );
    if ( locationType == LOCATION_GATEWAY_LOCATION_TYPE.STATE ) {
	collection = this.regions;
    } else if ( locationType == LOCATION_GATEWAY_LOCATION_TYPE.RAW ) {
	collection = this.fixesUnfiltered;
    } else {
	collection = this.fixes;
    }
    return collection;
}

/**
 * get the location url by index
 * @param polymorphic, fix or index of the fix
 * @param LOCATION_GATEWAY_LOCATION_TYPE to use, valid if index passed 
 *  [ fix, raw, state ] optional defaults to fix
 * @return url or null;
 */
function LocationGatewayGetLocationUrl( index, type ) {
    var href = null;
    var fix = index.Name ? index : this.getLocations( type )[index];
    var locationType = this.getLocationType( type );

    if ( fix ) {
	href = this.url;
	if ( locationType == LOCATION_GATEWAY_LOCATION_TYPE.STATE ) {
	    href += fix.arg;
	} else {
	    href = this.url;
	    href += "&mapExtentNorth=" + fix.bounds.extent.top;
	    href += "&mapExtentSouth=" + fix.bounds.extent.bottom;		
	    href += "&mapExtentEast=" + fix.bounds.extent.right;		
	    href += "&mapExtentWest=" + fix.bounds.extent.left;	
	
	    if( !this.regionsIndex[fix.state] && this.autoZoom ) {
		// dynamic zoom all but state level
		href += "&zoomDynamic=0";
	    }
	   
	    href += "&mapCenterLatitude=" + fix.bounds.center.y;
	    href += "&mapCenterLongitude=" + fix.bounds.center.x;

	    if ( fix.city ) { href += "&cit=" + fix.city; }
	    if ( fix.state ) { href += "&st=" + fix.state; }
	    if ( fix.zip ) { href += "&zip=" + fix.zip5digit( ); }
	    if ( fix.street )  { href += "&address=" + fix.street; }
	    if ( fix.house )  { href += "&hsn=" + fix.house; }

	    if ( this.display == "list" ) {
	      href += "&display=list";
            }

	    /**
	    var st = this.regionsIndex[fix.state];
	    if ( st ) { href += st.arg; } */
	}
    }

    return href;
}

/**
 * target a fix
 * @param index of the fix
 * @param LOCATION_GATEWAY_LOCATION_TYPE to use 
 *  [ fix, raw, state ] optional defaults to fix
 */
function LocationGatewayTargetLocation( index, type ) {
    var href = this.getLocationUrl( index, type );
    if ( href ) {
        location.href = href;
    }
}

/**
 * listener for internal logging of the result.
 * @param StateChangeSupport object
 */
function LocationGatewayStateChange( scs ) {
    if ( scs.complete( ) && scs.log ) {
	var url = scs.track;
	url += "&location=" + scs.location;
	var locations = scs.getLocations( );
	url += "&count=" + locations.length;
	if ( locations.length > 0 ) {
	    var fix = locations[0];
	    url += "&provider=" + fix.provider;
	    url += "&scheme=" + fix.scheme;
	    if ( fix.city ) {
		url += "&city=" + fix.city;
	    }
	    if ( fix.state ) {
		url += "&state=" + fix.state;
	    }
	    if ( fix.zip ) {
		url += "&zip=" + fix.zip5digit( );
	    }
	}
	url += "&display=" + this.display;
	scs.log.setAction( url );
	scs.log.run( null );
    }
}

/**
 * extract the state from the location name
 * @param location name
 * @return state region or null
 */
function LocationGatewayExtractState( location ) {
    var locationName = new String( location );

    var matches = this.namePattern.exec( locationName );
    if ( matches && matches.length >= 2 ) {
	locationName = matches[1];
    }
	    
    return this.regionsIndex[locationName];
}

/**
 * target the mls
 * @param string representing one or more mls numbers,
 *   separated by non word characters
 * @param if true, provide list view
 */
function LocationGatewayTargetMls( mls ) {
    var href = null;

    if ( mls ) {
	mls = mls.replace( /([^\w-]+)/g, "," );
	mls = mls.replace( /(^,|,$)/g, "" );
	if ( mls != "" ) {
	    href = this.url;
	    href += "&searchConstraintType=mls&ln=" + mls;
	    if ( this.display == "list" ) {
	      href += "&display=list";
	    }
	}	
    }

    if ( href ) {
	location.href = href;
    }
}

/**
 * recursively search for a key in an object
 * @param object to search
 * @param key to search for
 * @return the value of that key or null
 */
function LocationGatewayFindKeyInObject( obj, key ) {
    var rcode = null;

    for ( var prop in obj ) {
	if ( prop == key ) {
	    rcode = obj[prop];
	    break;
	} else {
	    var type = typeof obj[prop];
	    if ( type == 'object' ) {
		rcode = LocationGatewayFindKeyInObject( obj[prop], key );
		if ( rcode != null ) { break; }
	    }
	}
    }

    return rcode;
}

/**
 * Class to hold the details of a gmap impl.
 */
function LocationGatewayGmap( parent ) {
    this.parent = parent;
    this.server = null;

    this.initialize = LocationGatewayGmapInitialize;
    this.findLocation = LocationGatewayGmapFindLocation;
    this.callback = LocationGatewayCallbackGmap;
}

/**
 * find a location
 * @param string representing the location
 */
function LocationGatewayGmapInitialize( ) {
    this.server = new GClientGeocoder( );
}

/**
 * find function
 * String location to find
 */
function LocationGatewayGmapFindLocation( location ) {
    var scs = this.parent;
    this.server.getLocations( location,  
			      function( place ) { 
				  scs.callback.call( scs, place ); } );
}

/**
 * server callback method
 * @param a geocode response
 */
function LocationGatewayCallbackGmap( response ) {
    if ( response && response.Status.code == "200" ) {
        this.parent.fixes.length = 0;
	this.parent.fixesUnfiltered.length = 0;
	for ( i = 0; i < response.Placemark.length; i++ ) {
	    var loc = response.Placemark[i];
	    var fix = new LocationFix( );
	    this.parent.fixesUnfiltered[this.parent.fixesUnfiltered.length] = fix;
	    fix.found = true;
	    fix.provider = LOCATION_FIX_PROVIDER.GMAP;
	    
	    fix.id = LocationGatewayFindKeyInObject( loc, "address" );
	    fix.address = LocationGatewayFindKeyInObject( loc, "ThoroughfareName" );
	    if ( !fix.address ) { fix.address = LocationGatewayFindKeyInObject( loc, "AddressLine" ); };
	    fix.city = LocationGatewayFindKeyInObject( loc, "LocalityName" );
	    fix.state = LocationGatewayFindKeyInObject( loc, "AdministrativeAreaName" );
	    fix.zip = LocationGatewayFindKeyInObject( loc, "PostalCodeNumber" );
	    fix.country = LocationGatewayFindKeyInObject( loc, "CountryName" );

	    var coordinates = LocationGatewayFindKeyInObject( loc, "coordinates" );
	    fix.earth.x = coordinates[0];
	    fix.earth.y = coordinates[1];

	    var bounds = LocationGatewayFindKeyInObject( loc, "LatLonBox" );
	    fix.bounds = new Map( fix.id, 
				  new GeoRectangle( bounds.north, bounds.west, 
						    bounds.south, bounds.east ), 
				  fix.earth );
	    
	    var accuracy = LocationGatewayFindKeyInObject( loc, "Accuracy" );
	    switch(accuracy) {
	    case 1:
		fix.scheme = LOCATION_FIX_SCHEME.COUNTRY;
		break;
	    case 2:
		fix.scheme = LOCATION_FIX_SCHEME.STATE;
		break;
	    case 3:
		fix.scheme = LOCATION_FIX_SCHEME.COUNTY;
		break;
	    case 4:
		fix.scheme = LOCATION_FIX_SCHEME.CITY;
		break;
	    case 5:
		fix.scheme = LOCATION_FIX_SCHEME.ZIP;
		break;
	    case 6:
		fix.scheme = LOCATION_FIX_SCHEME.STREET;
		break;
	    case 7:
		fix.scheme = LOCATION_FIX_SCHEME.STREET_INTERSECTION;
		break;
	    case 8:
		fix.scheme = LOCATION_FIX_SCHEME.ADDRESS;
		break;
	    default:
		fix.scheme = LOCATION_FIX_SCHEME.NULL;
		break;
	    }	    

	    if ( fix.scheme == LOCATION_FIX_SCHEME.STREET || 
		 fix.scheme == LOCATION_FIX_SCHEME.STREET_INTERSECTION ) {
		fix.parseAddressAsStreet( );
	    } else if ( fix.scheme == LOCATION_FIX_SCHEME.ADDRESS ) {
		fix.parseAddressAsStreetHouse( );
	    }

	    if ( fix.state == "ID" && fix.city == "Boise City" ) {
		fix.city = "Boise";
	    } else if ( fix.state == "WA" && fix.city == "Sedro-Woolley" ) {
		fix.city = "Sedro Woolley";
	    } else if ( fix.state == "WA" && this.parent.islandPattern.test( fix.address )  ) {
		var city = new String( fix.address );
		fix.city = city.replace( this.parent.islandPattern, "$1" );
		if ( fix.city == "Orcase Island" ) { fix.city = "Orcas Island"; }
		fix.scheme = LOCATION_FIX_SCHEME.CITY;
		fix.zip = null;
	    } else if ( fix.state == "WA" && fix.city == "Camano" ) {
		fix.city = "Camano Island";
	    } else if ( fix.scheme == LOCATION_FIX_SCHEME.COUNTRY &&
			fix.id.indexOf( "Island, Washington" ) > -1 ) {
		fix.state = "WA";
		fix.scheme = LOCATION_FIX_SCHEME.CITY;
		if ( !fix.city ) { fix.city = LocationGatewayFindKeyInObject( loc, "AddressLine" ); }
	    }

	    if ( this.parent.regionsIndex[fix.state] ) {
	      this.parent.fixes[this.parent.fixes.length] = fix;
            }
	}
    } else {
	this.parent.fixes.length = 0;
	this.parent.fixesUnfiltered.length = 0;
    }
}

/**
 * Class to hold the details of a msve impl.
 */
function LocationGatewayMsve( parent ) {
    this.parent = parent;
    this.server = null;
    this.zipPattern = /^(\d{5})\,\s([A-Z]{2})$/;

    this.initialize = LocationGatewayMsveInitialize;
    this.findLocation = LocationGatewayMsveFindLocation;
    this.callback = LocationGatewayCallbackMsve;
}

/**
 * Initialization function
 */
function LocationGatewayMsveInitialize( ) {
    // it is important that we accurately size the 'hidden' map
    // to that of PP3, so that the preferred zoom is properly calculated
    var rightPanel = 417;
    var topPanel = 127;
    var bottomPanel = 23;
    var mapWidth = getWindowWidth( ) - rightPanel;
    var mapHeight = getWindowHeight( ) - topPanel - bottomPanel;
    var mapContainer = document.getElementById( this.parent.node );
    mapContainer.style.width = mapWidth + "px";
    mapContainer.style.height = mapHeight + "px";

    // construct the map
    this.server = new VEMap(this.parent.node);
    var serverOptions = new VEMapOptions();
    
    // set the client token if provided
    if ( this.parent.key && this.parent.key != "" ) {
	this.server.SetClientToken( this.parent.key );
    }

    // load the default map
    serverOptions.LoadBaseTiles = false;
    this.server.HideDashboard( );
    this.server.LoadMap(null, null, null, null, null, null, null, serverOptions);    
}

/**
 * find a location
 * @param string representing the location
 */
function LocationGatewayMsveFindLocation( location ) {
    var scs = this.parent;
    this.server.Find( null, location, VEFindType.Businesses, 
		      null, 0, 10, true, true, false, true, 
		      function( layer, result, place, more, error ) { 
			  scs.callback.call( scs, place ); } );
}

/**
 * server callback method
 * @param a geocode response, an array of VEPlace class objects or null if no results
 */
function LocationGatewayCallbackMsve( response ) {
    if ( response ) {
	this.parent.fixes.length = 0;
	this.parent.fixesUnfiltered.length = 0;

	for ( i = 0; i < response.length; i++ ) {
	    // store the zoom view for the associated zeroth result
	    var zoomLevel = i == 0 ? this.server.GetZoomLevel( ) : null;
	    var loc = response[i];
	    var fix = new LocationFix( );
	    this.parent.fixesUnfiltered[this.parent.fixesUnfiltered.length] = fix;
	    fix.found = true;
	    fix.provider = LOCATION_FIX_PROVIDER.MSVE;
	    
	    fix.id = loc.Name;
	    fix.scheme = LOCATION_FIX_SCHEME.NULL;

	    if ( this.zipPattern.test( fix.id ) ) {
		var matches = this.zipPattern.exec( fix.id );
		if ( matches.length >= 2 ) {
		    fix.zip = matches[1];
		    fix.state = matches[2];
		    fix.scheme = LOCATION_FIX_SCHEME.ZIP;
		}
	    } else {
		var tokens = fix.id.split( ", " );
		if ( tokens.length == 1 ) {
		    fix.scheme = LOCATION_FIX_SCHEME.STATE;
		    fix.state = tokens[0];
		    if ( this.parent.regionsIndex[fix.state] ) {
			fix.state = this.parent.regionsIndex[fix.state].abbr;
		    }
		} else if ( tokens.length == 2 ) {
		    fix.scheme =  LOCATION_FIX_SCHEME.CITY;
		    fix.city = tokens[0];
		    fix.state = tokens[1];
		} else if ( tokens.length == 3 ) {
		    fix.scheme =  LOCATION_FIX_SCHEME.ADDRESS;
		    fix.address = tokens[0];
		    fix.city = tokens[1];
		    var matches = this.parent.namePattern.exec( fix.id );
		    if ( matches && matches.length >= 2 ) {
			fix.state = matches[1];
			fix.zip = matches[2];
		    }
		    // not a good way to break out house and street from address
		}
	    }

	    if ( this.parent.regionsIndex[fix.state] ) {
		this.parent.fixes[this.parent.fixes.length] = fix;
	    }

	    var coordinates = loc.LatLong;
	    fix.earth.x = coordinates.Longitude;
	    fix.earth.y = coordinates.Latitude;

	    var bounds = loc.LatLongRect;
	    fix.bounds = new Map( fix.id, 
				  new GeoRectangle( bounds.TopLeftLatLong.Latitude, 
						    bounds.BottomRightLatLong.Longitude, 
						    bounds.BottomRightLatLong.Latitude, 
						    bounds.TopLeftLatLong.Longitude ), 
				  fix.earth,
				  null,
				  zoomLevel );   
	}
    } else {
	this.parent.fixes.length = 0;
	this.parent.fixesUnfiltered.length = 0;
    }
}
