//Author:   lorraine.kilby
//Created:  10/7/2009 4:35:41 PM

        
var Locator = {

    InitialZoom: 4,
    InitialCentre: new MapDS.LatLng( -28, 135),
    
    Strings: {
        InvalidSearch: 'Please enter a Street Address, Town/Sburb or Postcode.',
        InvalidSuburb: 'Invalid Suburb',
        InvalidPostcode: 'Invalid Postcode',
        MultipleAddressDialogTitle: 'Multiple Address Matches Found',
        MultipleAddressPrompt: 'Choose an address',
        Searching: 'Searching...',
        ItemsFound: '{0} match{1} found'
    },
    
    Images: {
        Loading: './images/loading-circle-black.gif'
    },
    
    ElementIDs: {
        POIResults: 'poi',
        MultipleAddressDialog: 'multipleAddressDialog',
        Sidebar: 'sidebar',
        Map: 'map',
        MultipleAddresses: 'ddlMultipleAddresses',
        ItemsFound: 'itemsfound',
        Poi:'poi',
        Search: 'search',
        SearchStreet: 'txtStreet',
        SearchSuburb: 'txtSuburb',
        SearchState: 'ddlState',
        SearchPostcode: 'txtPostcode',
        AdvancedSearchTrigger: 'chkAdvancedSearch',
        AdvancedSearch: 'search-advanced',
        SearchButton: 'btnFindAddress',
        ResetButton: 'btnReset',
        ExceptionDialog: 'exception-dialog',
        ResponseDialog: 'responseTimeDialog',
        DrivingDirections: 'drivingDirections'
    },
    
    /// This method is used to validate the search fields. The following function has code
    /// to perform validation on the standard address fields. Use it as a guide for your
    /// custom search fields.
    validateSearchFields: function() {
        var haveValidSuburb = false;
        var haveValidPostcode = false;
        
        var txtSuburb = $( this.getElementID( this.ElementIDs.SearchSuburb ) );
        var txtPostcode = $( this.getElementID( this.ElementIDs.SearchPostcode ) );
        
        if ( txtSuburb.attr("value") != '' && txtSuburb.attr("value").toLowerCase() != txtSuburb.attr("title").toLowerCase() ) {
            haveValidSuburb = true;
            
        } else if ( txtPostcode.attr("value") != '' && 
                txtPostcode.attr("value").toLowerCase() != txtPostcode.attr("title").toLowerCase() && 
                this.isNumeric( txtPostcode.attr("value") ) ) {
            haveValidPostcode = true;
            
        }
        
        //check if an invalid postcode has been entered (non numeric)
        if ( txtPostcode.attr("value") != '' && 
                txtPostcode.attr("value").toLowerCase() != txtPostcode.attr("title").toLowerCase() && 
                !this.isNumeric( txtPostcode.attr("value") ) ) {
                  this.displayException( this.Strings.InvalidPostcode );
                  return false;
                }
        
        if ( !haveValidSuburb && !haveValidPostcode ) {
            this.displayException( this.Strings.InvalidSearch );
            return false;
        }
        
        return true;
    },
    
    /// Reset the search fields back to default values.
    /// Here we set the textfields to the value in the title attribute and then call the blur method to
    /// trigger jQuery hint code.
    resetSearchFields: function() {
       $( this.getElementID( this.ElementIDs.Search ) + ' input[title!=""]').attr("value","").blur();
       $( this.getElementID( this.ElementIDs.Search ) + ' input[type="checkbox"]').attr("checked","");
       $( this.getElementID( this.ElementIDs.SearchState ) ).attr( "selectedIndex", 0 );
    },
    
    
    /// Builds a JSON representation of the address to be geocoded. This method is 
    /// automatically called, it should return an object that contains the following
    /// properties:
    ///     sb = suburb
    ///     st = state
    ///     pc = postcode
    /// if the values are all required by the Geocode handler.
    buildAddressSearch: function() {
        
        // load suburb information
        var txtSuburb = $( this.getElementID( this.ElementIDs.SearchSuburb ) );
        var suburb = txtSuburb.attr("value");
        if( txtSuburb.attr("value").toLowerCase() == txtSuburb.attr("title").toLowerCase() ) {
            suburb = '';
        }
        
        // load postcode information
        var txtPostcode = $( this.getElementID( this.ElementIDs.SearchPostcode ) );
        var postcode = txtPostcode.attr("value");
        if( txtPostcode.attr("value").toLowerCase() == txtPostcode.attr("title").toLowerCase() ) {
            postcode = '';
        }
        
        // load postcode information
        var txtStreet = $( this.getElementID( this.ElementIDs.SearchStreet ) );
        var street = txtStreet.attr("value");
        if( txtStreet.attr("value").toLowerCase() == txtStreet.attr("title").toLowerCase() ) {
            street = '';
        }
     
        // load selected state value
        var state = '';
        var ddlState = $( this.getElementID( this.ElementIDs.SearchState ) );
        if( ddlState.attr("selectedIndex") != 0 ) {
            state = ddlState.attr("value");
        }
        
        return { 
            sb: suburb, 
            st: state, 
            pc: postcode,
            street: street
        };
    },
    
    /// Builds a JSON representation of the search filters available.
    /// MUST return either an object or null. Last line should always be "return null;"
    /// instead of removing, return your object before this line. A return is immediate
    /// and the rest of the code within the method will not execute.
    getSearchFiltersJSON: function() {
        return null;
    },
    
    /// This method is called from within findNearest. The purpose of this function is to allow
    /// for control of how the marker is created and how the extra POI information is displayed.
    processPOI: function(id,poi) {
        //Logger.debug(poi);
        var _SELF = this;
        var size = new MapDS.Size( 26, 26 );  
        var offset = new MapDS.Pixel( -13, -13 );  
        var icn = new MapDS.Icon( './images/markers/{0}.png'.format( id ), size, offset );  
        var mkr = new MapDS.Marker( new MapDS.LatLng( poi.Position.Latitude, poi.Position.Longitude ), icn );
        mkr.setPopupContent( '<div class="popup-content"><img src="images/logo.png" alt="Fuji Xerox Logo" title="Fuji Xerox Logo"/><h4>{0}</h4><p>{1}{2}{3}{4}</p>'.format( 
            poi.Name, 
            ( ( poi.Address != '' && !_SELF.isUndefined( poi.Address ) ) ? poi.Address + ', ' : '' ), 
            ( ( poi.Suburb != '' && !_SELF.isUndefined( poi.Suburb ) ) ? poi.Suburb + ', ' : '' ),
            ( ( poi.State != '' && !_SELF.isUndefined( poi.State ) ) ? poi.State + ', ' : '' ),
            ( ( poi.Postcode != '' && !_SELF.isUndefined( poi.Postcode ) ) ? poi.Postcode + ' ' : '' )
        ) );


  

        var listItem = $( '<li style="background: url(./images/markers/{0}.png) no-repeat 3px 3px;" />'.format( id ) ).html( '<h6>{0}</h6><p>{1}{2}{3}{4}</p>{5}{6}<p><b>Contact:</b> {7}</p><p><b>Ph:</b> {8}</p><p><b>Fax:</b> {9}</p>{10} {11}'.format( 
                poi.Name,
                ( ( poi.Address != '' && !_SELF.isUndefined( poi.Address ) ) ? poi.Address + ', ' : '' ), 
                ( ( poi.Suburb != '' && !_SELF.isUndefined( poi.Suburb ) ) ? poi.Suburb + ', ' : '' ),
                ( ( poi.Postcode != '' && !_SELF.isUndefined( poi.Postcode ) ) ? poi.Postcode + ', ' : '' ),
                poi.State,
                ( ( poi.Branch != '' && !_SELF.isUndefined( poi.Branch) ) ?'<p><b>Branch No: </b>' + poi.Branch + '</p>' : '' ),
                ( ( poi.Team != '' && !_SELF.isUndefined( poi.Team) ) ? '<p><b>Service Team No: </b>' +  poi.Team + '</p> ' : '' ),
                ( ( poi.Contact != '' && !_SELF.isUndefined( poi.Contact) ) ? poi.Contact + ' ' : '' ),
                ( ( poi.Phone != '' && !_SELF.isUndefined( poi.Phone) ) ? poi.Phone + ' ' : '' ),
                ( ( poi.Fax != '' && !_SELF.isUndefined( poi.Fax) ) ? poi.Fax + ' ' : '' ),
                ( ( poi.AreaCovered != '' && !_SELF.isUndefined( poi.AreaCovered) ) ? '<p><b>Area Covered (Installation, Maintenance, Repairs):</b> ' + poi.AreaCovered + '</p> ' : '' ),
                ( ( poi.Technicians != '' && !_SELF.isUndefined( poi.Technicians) ) ? '<p><b>No. of Technicians employed:</b>' +poi.Technicians + '</p> ' : '' )

            ) ).appendTo( this.getElementID( this.ElementIDs.POIResults ) );
           
        return mkr;
    },
    
    /// This method is called from within findNearest. The purpose of this function is to allow
    /// for control of how the searched location marker is created and how the extra information is displayed.
    processLocationPOI: function(poi) {
        var size = new MapDS.Size( 25, 35 );  
        var offset = new MapDS.Pixel( -12.5, -17.5 );
        var icn = new MapDS.Icon( './images/markers/tear-drop.png', size, offset );  
        var mkr = new MapDS.Marker( new MapDS.LatLng( poi.Position.Latitude, poi.Position.Longitude ), icn );
        mkr.setPopupContent( '<div class="popup-content"><h5>Your searched location</h5><br>{0}{1}{2}{3}</div>'.format( 
                ( poi.Street!='' ? poi.Street + ', ' : '' ), 
                ( poi.Suburb!='' ? poi.Suburb + ', ' : '' ),
                ( poi.Postcode!='' ? poi.Postcode + ', ' : '' ),
                poi.State 
            ) 
        );
        this.map.addMarker( mkr );
        
        // DO NOT REMOVE THE FOLLOWING LINE - It is needed for routing purposes.
        this.locationMarker = mkr;
    },
    
    /// This method is called from within createRouteCallback. It is the helper method for generating the caption that will appear on
    /// the driving directions listing for the point.
    buildRouteCaption: function(poi) {
        return poi.Description;
    },
    
    

/// Start of core Locator code

    map: null, // object that contains the QuickMap instance
    RouteMode: 1, // eventually this will be a setting that can change therefore it will move to the top of the script file.
    locationMarker: null,
    
    _isSidebarVisible: false, // private
    _scrollSidebar: false,
    _foundLocations: null, // private
    _lastSearchedLocation: null, // private
    _pois: Array(), // private
    _lastFieldFocussed: null, // private, the last search field to have focus
    
    /**
    * Returns true if position is a valid sidebar position
    */
    isValidSidebarPosition: function(position) { if ( position == this.SidebarPositions.Left || position == this.SidebarPositions.Bottom || position == this.SidebarPositions.Right || position == this.SidebarPositions.Top ) { return true; } return false; },
    
    /**
    * Returns true if object is undefined
    */
    isUndefined: function(object) { return typeof object === "undefined"; },
    
    /**
     * Private method
     * Returns true if object is an Image
     * IE doesn't support "instanceof Image" so instead we test that the object is in the DOM then test the tagName is 'IMG'
     */
    isImage: function(object) { return this.isHTMLElement( object, 'IMG' ); },

    /**
     * Private method
     * Returns true if object is an HTML Element
     * {type} is a string that represents the type of HTML element that the object should be.
     *      for example. if [type] is "div" then the element should be a div.
     */
    isHTMLElement: function(object, type) { if ( !type || !( typeof type === "string" ) ) { return false; } if ( object && object.nodeType ) { return (object.tagName===type.toUpperCase() ? true : false); } return false; },
    
    /**
     * Private method
     * Returns true if input is a number
     */
    isNumeric: function(input) { return (input - 0) == input && input.length > 0; },
    
    /**
     * Private method
     * Returns true if object is a function
     */
    isFunction: function(object) { return typeof object === "function"; },
    
    /**
    * Returns the id string asked for, will prepend # unless the second parameter is false.
    */
    getElementID: function( id, withHash ) {
        if( this.isUndefined( withHash ) ) {
            withHash = true;
        }
        return ( withHash ? "#" : "" ) + id;
    },

    /**
    * Will fire a call to load up the exception dialog, the HTML will be set using the message parameter.
    */
    displayException: function( errorMessage ) { $( this.getElementID( this.ElementIDs.ExceptionDialog ) ).html( errorMessage ).dialog( 'open' ); },


    displayResponse: function( msg ) { $(  this.getElementID( this.ElementIDs.ResponseDialog ) ).dialog( 'open' ); },

    /**
    * Will save a POI as the last location.
    */
    saveLocationPOI: function(poi) { this._lastSearchedLocation = poi; },
    
    /**
     * Private method
     * Assumption is made that at least the first parameter exists and that it is a number. 
     * The same number assumption is made for precision if it is given.
     * Default for precision is 1, therefore no change.
     */
    round: function(val, precision) { precision = (arguments.length>1) ? Math.pow(10, arguments[1]) : 1; return Math.round( val * precision ) / precision; },
    
    /**
    * Will return the full bearing from the shortened string.
    */
    expandBearing: function(bearing) { switch(bearing) { case 'N': return 'North'; break; case 'S': return 'South'; break; case 'E': return 'East'; break; case 'W': return 'West'; break; case 'NE': return 'North East'; break; case 'NW': return 'North West'; break; case 'SE': return 'South East'; break; case 'SW': return 'South West'; break; default: return ''; } },
    
    /**
     * Will convert seconds to a formatted time string
     */
    convertToTime: function(seconds) { var hrs = Math.floor(seconds/3600); var mins = Math.floor(seconds / 60) - (hrs * 60); var secs = seconds - ( (hrs * 3600) + (mins * 60) ); var msg = ''; if (hrs>0) { msg = hrs + 'hrs '; } if (mins>0) { msg += mins + 'mins '; } if(hrs==0 && mins==0) { msg = 'less than 1 minute'; } msg = msg + ' (approx driving time)'; return msg; },
    
    /**
    * Resets everything! 
    */
    reset: function() {
        this._foundLocations = null;
        this._lastSearchedLocation = null;
        this.locationMarker = null;
        this._pois = new Array();
        
        this.resetSearchFields();
        
        if ( $( this.getElementID( this.ElementIDs.AdvancedSearch ) ).is(":visible") ) {
            $( this.getElementID( this.ElementIDs.AdvancedSearch ) ).slideUp();
        }
        
        $( this.getElementID( this.ElementIDs.POIResults ) ).html('');
        $( this.getElementID( this.ElementIDs.DrivingDirections ) ).html('');
        
        if( this._isSidebarVisible ) {
            
            var newWidth = $( this.getElementID( this.ElementIDs.Map ) ).width() + ( $( this.getElementID( this.ElementIDs.Sidebar ) ).width() + 5 )
            
            $( this.getElementID( this.ElementIDs.Sidebar ) ).hide( "slide", { direction: "left" }, 1000 );

            var _SELF = this;
            setTimeout( 
                function() { 
                    $( _SELF.getElementID( _SELF.ElementIDs.Map ) ).animate( { width: newWidth }, { queue: true, duration: 1000 } ).width( newWidth ); 
                    // reset map window.
                    _SELF.map.clearMarkers();
                    _SELF.map.clearRoutes(false);
                    _SELF.map.setCentre( _SELF.InitialCentre, _SELF.InitialZoom );
                }, 1100 );
            
            this._isSidebarVisible = false;
        }
    },
    
    /**
    * Performs a call to geocode in order to geocode an address. 
    */
    findAddress: function() {
        
        var areSearchFieldsValid = this.validateSearchFields();
        if(!areSearchFieldsValid) {
            Locator.reset();
            return;
        }
        
        // lock down the find button
        $( this.getElementID( this.ElementIDs.SearchButton ) ).attr('disabled', 'true');
        
        var _SELF = this;
        this.geocode(this.buildAddressSearch(), function(address){_SELF.findNearest(address);}  );
        
    
    },
    
    /**
    * Calls findnearest.ashx passing X/Y and search filters. Results are displayed.
    */
    findNearest: function(address) {
     
        var _SELF = this;
        $.getJSON( "findnearest.ashx?x={0}&y={1}&pc={2}".format( address.Position.Longitude, address.Position.Latitude,address.Postcode), 
            this.getSearchFiltersJSON(),
            function(data) {
                
                // unlock down the find button
                $( _SELF.getElementID( _SELF.ElementIDs.SearchButton ) ).attr('disabled', '');
                
                if ( data.Code > 0 ) {
              
                    _SELF.displayException.apply( _SELF, [ data.Message ] );
                    Locator.reset();
                    return;
                } else {
                    
                    $( _SELF.getElementID( _SELF.ElementIDs.DrivingDirections ) ).hide( "slow" );
                    $( _SELF.getElementID( _SELF.ElementIDs.POIResults ) ).show( "slow" );
                    $( _SELF.getElementID( _SELF.ElementIDs.ItemsFound ) ).show( "slow" );
                    
                    $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).removeClass('scrollContent');
                    $( _SELF.getElementID( _SELF.ElementIDs.ItemsFound ) ).html( _SELF.Strings.Searching );
                    $( _SELF.getElementID( _SELF.ElementIDs.POIResults ) ).html("");
                    
                    var searchedAddress = new String( 
                        ( address.Suburb!='' ? address.Suburb.toProperCase() + ', ' : '' ) + 
                        ( address.State!='' ? address.State + ', ' : '' ) + 
                        ( address.Postcode!='' ? address.Postcode + ', ' : '' ) 
                    );
                    searchedAddress = searchedAddress.substr( 0, searchedAddress.lastIndexOf( ', ' ) );
                    $( _SELF.getElementID( _SELF.ElementIDs.ItemsFound ) ).html( 
                        _SELF.Strings.ItemsFound.format( data.items.length, ( data.items.length>1 ? 'es' : '' ) ) + ' for ' + searchedAddress
                    );
                    
                         _SELF.saveLocationPOI.apply( _SELF, [address] );
                          _SELF.processLocationPOI( address );
                    
                    _SELF._pois = new Array();
                    _SELF.map.clearMarkers();
                    _SELF.map.clearRoutes(false);
                    
                    $.each(data.items, function(i,poi){
            
                        var id = i+1;
            
                        poi.Marker = _SELF.processPOI( id, poi )
                        _SELF._pois.push( poi );
                        _SELF.map.addMarker( poi.Marker );
                      
                   
                               Locator.getDirections(i); 
                    });
                    
                    // build the animation method, need to hook this to allow modification of slide?
                    var animateSlide = function() { 
                        _SELF._scrollSidebar = false;
                        $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).show( "slide", { direction: "left" }, 1000, function() {
                            if ( $( _SELF.getElementID( _SELF.ElementIDs.POIResults ) ).attr('offsetHeight') + $( _SELF.getElementID( _SELF.ElementIDs.ItemsFound ) ).attr('offsetHeight') > 
                                    $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).attr('offsetHeight') ) {
                                // add scroll...
                                $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).addClass('scrollContent');
                                _SELF._scrollSidebar = true;
                            }
                        });
                    }
                    
                    if(!_SELF._isSidebarVisible) {   
                    
                        var newWidth = $( _SELF.getElementID( _SELF.ElementIDs.Map ) ).width() - 
                                ( $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).width() + 10 );
                        
                        $( _SELF.getElementID( _SELF.ElementIDs.Map ) ).animate( { width: newWidth }, { queue: true, duration: 1000 } ).width( newWidth );
                        
                        setTimeout( animateSlide, 1000 );
                        
                        _SELF._isSidebarVisible = true;
                    } else {
                        animateSlide();
                    }
                    
                    //_SELF.saveLocationPOI.apply( _SELF, [address] );
                  //  _SELF.processLocationPOI( address );
                    
                    _SELF.map.loadBestView( new MapDS.LatLng( address.Position.Latitude, address.Position.Longitude ) );
                    
                    
//                                 $.each(data.items, function(i,poi){
//                            Locator.getDirections(i); 
//                             });
//                    
                    
                }
            }
        );
    },
    
    /**
    * Performs a call to Geocode an address. If a single match is found then the method 
    * will automatically call findNearest. Multiple matches are displayed within a 
    * dialog for the user to select.
    */
    geocode: function(addressToGeocode, loadAddressCallBack) {
        
        // make jQuery Ajax call...
        var _SELF = this;
        $.getJSON("geocode.ashx",
            addressToGeocode,
            function(data){
                //Logger.log(data.addresses.length);
                if ( data.Code != '1' && data.Code != '100' ) {
                    // unlock down the find button
                    $( _SELF.getElementID( _SELF.ElementIDs.SearchButton ) ).attr('disabled', '');
                    
                    _SELF.displayException.apply( _SELF, [ data.Message ] );
                    Locator.reset();
                    return;
                } else {
                    
                    _SELF._foundLocations = data.Locations;
                    
                    // need to display selection if multiple
                    if(_SELF._foundLocations.length>1) {
                        // display modal window
                        
                        $( _SELF.getElementID( _SELF.ElementIDs.MultipleAddressDialog ) ).find( 
                                _SELF.getElementID( _SELF.ElementIDs.MultipleAddresses ) ).html('');
                        
                        $("<option/>").attr('value','0').html( _SELF.Strings.MultipleAddressPrompt ).appendTo(
                                $( _SELF.getElementID( _SELF.ElementIDs.MultipleAddressDialog ) ).find( 
                                _SELF.getElementID( _SELF.ElementIDs.MultipleAddresses ) ));
                        
                        $.each(_SELF._foundLocations, function(i,address){
                            $("<option/>").html( ( address.Street!='' ? address.Street + ', ' : '' ) + 
                                address.Suburb + ', ' + address.State + ', ' + address.Postcode ).appendTo(
                                $( _SELF.getElementID( _SELF.ElementIDs.MultipleAddressDialog ) ).find( 
                                _SELF.getElementID( _SELF.ElementIDs.MultipleAddresses ) ));
                        });
                        
                        // unlock down the find button
                        $( _SELF.getElementID( _SELF.ElementIDs.SearchButton ) ).attr('disabled', '');
                        
                        $( _SELF.getElementID( _SELF.ElementIDs.MultipleAddressDialog ) ).dialog('open');
                        
                    } else {
                        loadAddressCallBack(_SELF._foundLocations[0]);
                    }
                }
            }
        );
    },
    
    /**
    * Executes a request for driving directions for the given POI id.
    */
    getDirections: function(id) {

        var _SELF = this;
 
        if ( ! _SELF.isUndefined( _SELF._pois ) && _SELF._pois.length >= id ) {
            var cb = _SELF.createRouteCallback( _SELF._pois[id], undefined, id );
            if ( _SELF.isFunction( cb ) ) {
                cb();
            }
           
        }
    },
    
    /**
    * Creates a callback function that each marker will have in order to fire a route build request.
    */
    createRouteCallback: function(poi, startMarker, id) {
        var _SELF = this;
        if ( this.RouteMode == 1 ) {
            return ( function() { 
                        try {
                            var listCaption = new String( 
                                ( _SELF._lastSearchedLocation.Suburb!='' ? _SELF._lastSearchedLocation.Suburb.toProperCase() + ', ' : '' ) + 
                                ( _SELF._lastSearchedLocation.State!='' ? _SELF._lastSearchedLocation.State + ', ' : '' ) + 
                                ( _SELF._lastSearchedLocation.Postcode!='' ? _SELF._lastSearchedLocation.Postcode + ', ' : '' ) 
                            );
                            listCaption = listCaption.substr( 0, listCaption.lastIndexOf( ', ' ) );
                            
                            _SELF.buildRoute({
                                from: { caption: listCaption, position: _SELF._lastSearchedLocation.Position }, 
                                to: { caption: _SELF.buildRouteCaption(poi), position: poi.Position }
                            }, startMarker, poi.Marker, this , id); 
                        } catch(ex) {
                            Logger.debug(ex);
                        }
                        return false; 
                    });
        } else {
            // TODO: slot in code here for allowing custom starting address.
        }
        
    },
    
    /**
    * This method calls for a route to be built. The result is handled and a lot of processing occurs here in order to reformat the returned 
    * route information into a nice list.
    */
    buildRoute: function(routeRequest, startMarker, endMarker, srcImageLink, id) {
        var _SELF = this;
        
        if ( typeof routeRequest != "object" ) {
            throw "Invalid route request.";
        }
        if ( !( startMarker instanceof MapDS.Marker ) ) {
            if ( !( _SELF.locationMarker instanceof MapDS.Marker ) ) {
                throw "Invalid start marker.";
            }
            startMarker = _SELF.locationMarker;
        }
        if ( !( endMarker instanceof MapDS.Marker ) ) {
            throw "Invalid end marker.";
        }
        
        if( srcImageLink && _SELF.isImage( srcImageLink ) ) {
            // change icon image to loading image
            srcImageLink.src = _SELF.Images.Loading;
        }
 
        $.getJSON("routebuilder.ashx?s={0},{1}&e={2},{3}".format(
                routeRequest.from.position.Latitude, 
                routeRequest.from.position.Longitude, 
                routeRequest.to.position.Latitude, 
                routeRequest.to.position.Longitude
            ), 
            function(data) {
                //Logger.debug(o);
                $( _SELF.getElementID( _SELF.ElementIDs.DrivingDirections ) ).html('');
             
                
                var dd = $('<div></div>');
                
                $('<h5/>').html( "Start: <strong>{0}</strong>".format( routeRequest.from.caption ) ).appendTo(dd);
                
                $.each(data.Segments, function(i,segment){
                    var segmentTable = $('<table></table>');
                    $.each(segment.Directions, function(i){
                        // scope of this method is the direction itself.
                        //Logger.debug(this);
                        var instruction;
                        var distance = _SELF.round(this.Distance, 2) + 'km';
                        if(i==0) { // start
                            instruction = 'Head <strong>{0}</strong> on <strong>{1}</strong>'.format( 
                                (_SELF.expandBearing(this.Bearing)).toLowerCase(), 
                                this.Address.Street 
                            );
                        } else if (this.Instruction.toLowerCase() == 'continues as') {
                            instruction = 'Continue along <strong>{0}</strong>'.format( this.Address.Street );
                        } else if (this.Instruction.toLowerCase() == 'finish on') {
                            instruction = this.Instruction+' <strong>{0}</strong>'.format( this.Address.Street );
                            distance = '';
                        } else {
                            instruction = '{0} at <strong>{1}</strong>'.format(
                                this.Instruction.replace(/left/i,'<strong>left</strong>').replace(/right/i,'<strong>right</strong>'),
                                this.Address.Street
                            );
                        }
                        // NOTE: something here to be done for distances under 100 metres...
                        $( '<tr><td>{0}.</td><td>{1}</td><td>{2}</td></tr>'.format( (i+1), instruction, distance ) ).appendTo(segmentTable);
                    });
                    
                    $( '<tr class="segmentTotal"><td colspan="3">{0}km - {1}</td></tr>'.format(
                        _SELF.round(segment.Distance, 2),
                        _SELF.convertToTime.apply(_SELF,[segment.DrivingTime])
                    )).appendTo(segmentTable);
                    
                    segmentTable.appendTo(dd);
       
          
           
                $('<p><b>Distance to location:</b> {0} Kms <img src="./images/information.png" class="infoIcon" height="16px" width="16px" alt="Response Time Targets" title="Response Time Targets" /></p>'.format( _SELF.round(segment.Distance, 2))).click( function() { _SELF.displayResponse.apply( _SELF, [ 0 ] ); document.getElementById("responseTimeDialog").style.zIndex = "9999"; }).appendTo( $("#"+_SELF.ElementIDs.Poi +" li:eq("+id+")") );
                $('</p><p style="margin-top:-12px"><b>Travel time: </b>{1} </p>'.format( _SELF.round(segment.Distance, 2),   _SELF.convertToTime.apply(_SELF,[segment.DrivingTime]))).appendTo( $("#"+_SELF.ElementIDs.Poi +" li:eq("+id+")") );
              
              
                    
                });
                
               // $('<h5/>').html( "End: <strong>{0}</strong>".format( routeRequest.to.caption ) ).appendTo( this.getElementID( this.ElementIDs.POIResults ));
                

              
                // build the route object
                var myRoute = new MapDS.Route( null, data.MapFileName, new MapDS.Bounds( data.Bounds.Left, data.Bounds.Bottom, data.Bounds.Right, data.Bounds.Top ) );
                
                // build method for going back to the search results
                var backToSearch = function() {
                    _SELF.map.removeRoute(myRoute, false);
                    _SELF.map.clearMarkers();
                    $( _SELF.getElementID( _SELF.ElementIDs.POIResults ) ).html('');
                    $.each(_SELF._pois, function(i,poi){
                        var id = i+1;
                        _SELF.processPOI( id, poi );
                        _SELF.map.addMarker( poi.Marker );
                    });
                    
                    $( _SELF.getElementID( _SELF.ElementIDs.DrivingDirections ) ).hide( "slow" );
                    $( _SELF.getElementID( _SELF.ElementIDs.POIResults ) ).show( "slow" );
                    $( _SELF.getElementID( _SELF.ElementIDs.ItemsFound ) ).show( "slow" );
                    
                    if (! _SELF._scrollSidebar ) {
                        $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).removeClass('scrollContent');
                    }
                    
                    _SELF.processLocationPOI( _SELF._lastSearchedLocation );
                    _SELF.map.loadBestView(null,true);
                };
                
                   
                $( _SELF.getElementID( _SELF.ElementIDs.DrivingDirections ) ).show( "slow", function() {
                    if ( $( _SELF.getElementID( _SELF.ElementIDs.DrivingDirections ) ).attr('offsetHeight') > $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).attr('offsetHeight') ) {
                        // add scroll...
                        if ( ! $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).hasClass('scrollContent') ) {
                            $( _SELF.getElementID( _SELF.ElementIDs.Sidebar ) ).addClass('scrollContent');
                        }
                    }
                } );
                
                //_SELF.map.clearMarkers();
                _SELF.map.clearRoutes(false);
                
                // TODO: add back the route points...
                _SELF.map.addMarker(startMarker);
                _SELF.map.addMarker(endMarker);
                
                // add route to map
                _SELF.map.addRoute( myRoute );
                _SELF.map.loadBestView(null,true);
            }
        );
    },
    
    /**
    * This method is hooked into the window.onload automatically.
    */ 
    initMap: function( lat, lng, zoom ) {
        this.map = new MapDS.Map( this.getElementID( this.ElementIDs.Map, false ) );
        this.map.setCentre( this.InitialCentre, this.InitialZoom );
    },
    
    /**
    * The main initialisation method for the Locator object.
    */
    init: function(options) {
        var _SELF = this;
        $().ready(function() {
            
            // Grab all anchor elements on the page that have an href and target is external. Set their target to blank.
            $( 'a[rel="external"]' ).filter( '[href!=""]' ).each( function() { this.target = "_blank"; } );
            
            $( _SELF.getElementID( _SELF.ElementIDs.SearchButton ) ).click( function() { Locator.findAddress(); } );
            $( _SELF.getElementID( _SELF.ElementIDs.ResetButton ) ).click( function() { Locator.reset(); } );
            
            // hook up the HTML elements...
            $( _SELF.getElementID( _SELF.ElementIDs.AdvancedSearchTrigger ) ).click(
                function() {
                    if ( $( _SELF.getElementID( _SELF.ElementIDs.AdvancedSearch ) ).is(":visible") ) {
                        $( _SELF.getElementID( _SELF.ElementIDs.AdvancedSearch ) ).slideUp();
                    } else {
                        $( _SELF.getElementID( _SELF.ElementIDs.AdvancedSearch ) ).slideDown();
                    }
                }
            );
            
            // need to have the current text blanked on focus, reset on blur...
            $( _SELF.getElementID( _SELF.ElementIDs.Search ) + ' input[title!=""]').keypress(
                function(evt) {
                    _SELF._lastFieldFocussed = this;
                    if (evt.keyCode == 13 ) {
                        $( _SELF.getElementID( _SELF.ElementIDs.SearchButton ) ).click();
                    }
                }
            ).hint();
            
            // ensure that Enter presses on the form don't cause a submit
            $( "form" ).keypress( function(e) { if (e.keyCode == 13) { return false; } });
            
            // attach the exception dialog
            $( _SELF.getElementID( _SELF.ElementIDs.ExceptionDialog ) ).dialog({
                bgiframe: true,
                modal: true,
                draggable: false,
                resizable: false,
                autoOpen: false,
                close: function( evt, ui ) {
                        if( _SELF._lastFieldFocussed ) {
                            _SELF._lastFieldFocussed.focus();
                        }
                    },
                buttons: {
                    Ok: function() {
                        $(this).dialog('close');
                    }
                }
            });

            // attach the response dialog
            $( _SELF.getElementID( _SELF.ElementIDs.ResponseDialog ) ).dialog({
                bgiframe: true,
                zIndex: 9999,
                modal: true,
                draggable: false,
                resizable: false,
                autoOpen: false,
                close: function( evt, ui ) {
                        if( _SELF._lastFieldFocussed ) {
                            _SELF._lastFieldFocussed.focus();
                        }
                    },
                buttons: {
                  
                }
            });
     
            // attach the multiple results dialog.
		    $( _SELF.getElementID( _SELF.ElementIDs.MultipleAddressDialog ) ).dialog({
			    bgiframe: true,
			    height: 300,
			    width: 400,
			    modal: true,
                draggable: false,
                resizable: false,
                autoOpen: false,
                zIndex: 99999,
			    buttons: {
				    'Use Address': function() {
                        var idx = $(this).find( _SELF.getElementID( _SELF.ElementIDs.MultipleAddresses ) ).attr('selectedIndex');
                        if (idx!=0) {
                            // go back one to get correct array location.
                            idx--;
                            // Call FindNearest....
                            _SELF.findNearest(_SELF._foundLocations[idx]);
                        }
					    $(this).dialog('close');
				    },
				    Cancel: function() {
					    $(this).dialog('close');
				    }

			    }
		    });
		    
		    _SELF.reset();
		    
		    // load up options
		    if ( options ) {
		        
		    }
		    
            // need to wrap the call in a function so we can maintain scope
            window.onload = function() { _SELF.initMap.apply(_SELF); };
            
        });
    }
    
};

Locator.init(  );

