Monday, January 21, 2013

Integration of Salesforce.com and Google Maps JavaScript API V3

Google maps JavaScript API can be used to create visual force pages with map to enhance the business experience. We can display partners, contacts and leads on map to identify their locations, find the routes and display the contacts within the specific range.

JavaScript API V3 is the latest version and all maps API applications should load the map API using an API key which can be used to monitor the API usages. We can purchase additional quota if application maps API exceeds the usage limits.

JavaScript API V3 limits: 25000 maps loads per day, a map load is counted when a map is initialized on web page. User interaction with a map after it has loaded does not have any impact on usages limits. 

Let’s start with an example to search Accounts by Name or Postal code and display all results on Google Maps. 

First Step: To get Google Map API key

1)  Go to APIs console https://code.google.com/apis/console and log in with your Google account.

2)  Click on create project button

3)  Left hand side, click on Services

4)  Select the Google Maps API v3 and Activate it by clicking on “Off” button it becomes green “On”.

5)  Left hand side, Click on API Access link, In Simple API Access section you will find the API key.

     6) By default, a key can be used on any site. You can restrict the use of key to domains to         prevent use on unauthorized sites. You can specify which domains are allowed to use your API key by clicking the Edit allowed referrers... 

Create a controller to search Accounts:

public class AccountSearchController {
    public String selectedAccName {set;get;}
    public String selectedZip {set;get;}
    public String qry {set;get;}
    public List<innerCls> lstAcc {set;get;}
    static Integer globalCounter;
   
    public PFinderController(){
        lstAcc = new List<innerCls>();
        globalCounter = 0;
    }
    public void searchAcc(){
        globalCounter = 1;
        lstAcc = new List<innerCls>();
        String qry = ' SELECT Id,Name,BillingStreet, BillingState,Longitude__c,Latitude__c, BillingPostalCode, ' ;
                  qry += ' BillingCountry, BillingCity From Account WHERE Longitude__c!=NULL AND Latitude__c != NULL ';
        if(selectedAccName != NULL && selectedAccName != ''){
                 qry += ' AND Name like \''+ selectedAccName + '%\'';
          }
          if(selectedZip != NULL && selectedZip != ''){
                qry += ' AND BillingPostalCode like \''+ selectedZip + '%\'';
          }
          qry += ' LIMIT 1000 ';
          for(Account acc:Database.query(qry))
                lstAcc.add(new innerCls(acc));
    }
    public class innerCls{
        public Integer count{set;get;}
        public Account acc {set;get;}
        public innerCls(Account acc){
                this.count = globalCounter++;
                this.acc = acc;
        }
    }
}

Visual force page to display search result on Google Map

<apex:page showHeader="true" sidebar="false" controller=" AccountSearchController">
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=AIzaSyDjIY5HKM1dAlao6yUU8EzwPzdUh2ZNj0g&sensor=false"> </script>
<script>
var gAccount = [];
var map = null;
var counter = 0;
var delay = 500;

function load() {
    map = null;
    mapDiv = document.getElementById("map");
    var mapOptions = {
          center: new google.maps.LatLng(-34.397, 150.644),
          zoom: 4,
          mapTypeId: google.maps.MapTypeId.ROADMAP,
          mapMaker: true
    };
    map = new google.maps.Map(mapDiv,mapOptions);
    finishSearch();
}

function finishSearch() {
   if(counter < gAccount.length){
      showAddress(gAccount[counter++],finishSearch);
  }
}

function showAddress(record,finishSearch) {
var html='';
try{
 if(record.Longs != '' && record.Lats != ''){
        var points = new google.maps.Point(record.Lats,record.Longs);
        var latLongs = new google.maps.LatLng(record.Lats,record.Longs);
       
        html ='<hr>' + counter +'.&nbsp;'+ record.Name + '<br/>';
        html += record.City +'&nbsp;'+ record.State + '&nbsp;'+ record.Country + ',&nbsp;' + record.Zip + '<br/>';
        html +="<hr><b>URL:<b> <a href=https://ap1.salesforce.com/" + record.Id + " target=_blank >account details</a>";
           
        var infowindow = new google.maps.InfoWindow({
            content: html
        });
        var marker = new google.maps.Marker({
            position: latLongs,
            map: map,
            title: record.Name
        });
       
        google.maps.event.addListener(marker, 'click', function() {
          infowindow.open(map,marker);
        });
       
        //set the center for last flag
        if((gAccount.length - 1) == counter)
            map.setCenter(latLongs, 3);
       
  }
 }catch(error){}   
    setTimeout(finishSearch,delay);
}

function Group(id,name,lats,longs,displayNumber,city,state,country,zip) {
    this.Id = id;
    this.Name = name;
    this.Lats = lats;
    this.Longs = longs;
    this.DisplayNumber = displayNumber;
    this.City = city;
    this.State = state;
    this.Country = country;
    this.Zip = zip;
}

function setFocusOnMap(Lats,Longs){
    map.setCenter(new google.maps.LatLng(Lats,Longs), 3);
}
</script>
<apex:form >
<apex:actionStatus id="status" startText='Loading....' />
<apex:outputPanel id="outMap">
     <script type="text/javascript">
      function populateMap(){
         gAccount=[];
        <apex:repeat value="{!lstAcc}" var="act" id="TmpLoop" >
           gAccount.push(new Group("{!act.acc.id}","{!act.acc.Name}","{!act.acc.Latitude__c}","{!act.acc.Longitude__c}",true,'{!act.acc.BillingCity}','{!act.acc.BillingState}','{!act.acc.BillingCountry}','{!act.acc.BillingPostalCode}'));
        </apex:repeat>
         load();
       }
      </script>
  </apex:outputPanel>
<apex:pageBlock title="Partner Finder">
<apex:pageBlockSection columns="1">
<apex:pageBlockSectionItem >
 <apex:outputLabel value="Name:"></apex:outputLabel>
 <apex:outputPanel >
    <apex:inputText value="{!selectedAccName}"/>
 </apex:outputPanel>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
 <apex:outputLabel value="Postal Code:"></apex:outputLabel>
 <apex:outputPanel >
<apex:inputText value="{!selectedZip}"/>
<apex:commandButton value="Search" action="{!searchAcc}" status="status" reRender="secMap,outMap" oncomplete="populateMap();" />
 </apex:outputPanel>
</apex:pageBlockSectionItem>
</apex:pageBlockSection>
<apex:pageBlockSection id="secMap" title="Search result" columns="1">
    <apex:outputPanel >
      <table width="100%">
        <tr>
            <td width="100%">
               <div id="map" style="height: 600px;"></div>
           </td>
        </tr>
      </table>
   </apex:outputPanel>
</apex:pageBlockSection>
</apex:pageBlock>
</apex:form>
</apex:page>


 How the given code will work? Lets go one by one..

Script to load the Google Map API:

<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&sensor=false"> </script>


The SRC contained in the script tag is the location of a JavaScript file that loads all of the symbols and definitions you need for using the Google Maps API. This script tag is required.
The key parameter contains your application's API key. The sensor parameter of the URL must be included and indicates whether this application uses a sensor (such as a GPS locator) to determine the user's location. You must set this value to either true or false explicitly.

<apex:pageBlockSection columns="1">
<apex:pageBlockSectionItem >
 <apex:outputLabel value="Name:"></apex:outputLabel>
 <apex:outputPanel >
    <apex:inputText value="{!selectedAccName}"/>
 </apex:outputPanel>
</apex:pageBlockSectionItem>
<apex:pageBlockSectionItem >
 <apex:outputLabel value="Postal Code:"></apex:outputLabel>
 <apex:outputPanel >
<apex:inputText value="{!selectedZip}"/>
<apex:commandButton value="Search" action="{!searchAcc}" status="status" reRender="secMap,outMap" oncomplete="populateMap();" />
 </apex:outputPanel>
</apex:pageBlockSectionItem>
</apex:pageBlockSection>

This section will be used to display input fields for name and postal code which are used to filter accounts, commandButton  action {!searchAcc}  is used to call searchAcc method of controller which returns a list of accounts.

There is a JS method call on commendButton’s “oncomplete” event which will create an Array of account records in JavaScript. <apex:repeat> can be used to push records into Group Object, you can put any name instead of “Group”.

At the end of <apex:repeat>, we can call the JS method load();

<apex:outputPanel id="outMap">
     <script type="text/javascript">
      function populateMap(){
         gAccount=[];
        <apex:repeat value="{!lstAcc}" var="act" id="TmpLoop" >
           gAccount.push(new Group("{!act.acc.id}","{!act.acc.Name}","{!act.acc.Latitude__c}","{!act.acc.Longitude__c}",true,'{!act.acc.BillingCity}','{!act.acc.BillingState}','{!act.acc.BillingCountry}','{!act.acc.BillingPostalCode}'));
        </apex:repeat>
         load();
       }
      </script>
  </apex:outputPanel>

Note: Please make sure that data does not contain the single quote. We can use the JSENCODE().

Group object define in the script section, properties should be same and must have some value if you want to display on info window of markers.

We can define the a Object in JavaScript, for ex: here is the code for Group Object.

function Group(id,name,lats,longs,displayNumber,city,state,country,zip) {
    this.Id = id;
    this.Name = name;
    this.Lats = lats;
    this.Longs = longs;
    this.DisplayNumber = displayNumber;
    this.City = city;
    this.State = state;
    this.Country = country;
    this.Zip = zip;
}

Latitudes and Longitudes values must have some values to display marker on Google Map, We can store these values on Account object using two custom fields. Salesforce.com is planning to put these two fields as a standard field for account, contacts, opportunity and lead. We have another option to create markers using address but Lat and Long is better to use.

Load Google Map on Visual force:

var gAccount = [];
var map = null;
var counter = 0;
var delay = 500;

At the top of the page we have define few Global variables, gAccount = [] will be used to create a JavaScript array for markers, which contains the information to point the markers on maps and display additional info on click of markers, delay will be used to put some gap between two markers requests.

function load() {
    map = null;
    mapDiv = document.getElementById("map");
    var mapOptions = {
          center: new google.maps.LatLng(-34.397, 150.644),
          zoom: 4,
          mapTypeId: google.maps.MapTypeId.ROADMAP,
          mapMaker: true
    };
    map = new google.maps.Map(mapDiv,mapOptions);
    finishSearch();
}

function finishSearch() {
   if(counter < gAccount.length){
      showAddress(gAccount[counter++],finishSearch);
  }
}

Once Google Map API script tag has been added at top of the page, we can start JS code to load map on Visual force page with Map (mapotions: mapotions) method. Create a method load() in VF page using this code.  
Map Options:
Center: display the default center location,
zoom: map zoom level,
mapTypeId: define the type of map, it can be roadmap, satellite etc.  
finishSearch() will be used to call method showAddress(Account object,finishSearch); it is a recursive call to display markers on map for all record of search result.
A <div> should be used to display map in Visual Force pages as a container, we need to define a <div> section before loading map through JavaScript.

<apex:pageBlockSection id="secMap" title="Search result" columns="1">
    <apex:outputPanel >
      <table width="100%">
        <tr>
            <td width="100%">
               <div id="map" style="height: 600px;"></div>
           </td>
        </tr>
      </table>
   </apex:outputPanel>
</apex:pageBlockSection>

We can set the height and width as per our display area on visual force page.

To display marker with info window we can use the code below:

function showAddress(record,finishSearch) {
var html='';
 if(record.Longs != '' && record.Lats != ''){
        var points = new google.maps.Point(record.Lats,record.Longs);
        var latLongs = new google.maps.LatLng(record.Lats,record.Longs);
       
        html ='<hr>' + counter +'.&nbsp;'+ record.Name + '<br/>';
        html += record.City +'&nbsp;'+ record.State + '&nbsp;'+ record.Country + ',&nbsp;' + record.Zip + '<br/>';
        html +="<hr><b>URL:<b> <a href=https://ap1.salesforce.com/" + record.Id + " target=_blank >account details</a>";
           
        var infowindow = new google.maps.InfoWindow({
            content: html
        });
        var marker = new google.maps.Marker({
            position: latLongs,
            map: map,
            title: record.Name
        });
       google.maps.event.addListener(marker, 'click', function() {
          infowindow.open(map,marker);
        });
        //set the center for last flag
        if((gAccount.length - 1) == counter)
            map.setCenter(latLongs, 3);
  }
  setTimeout(finishSearch,delay);
}

google.maps.infowindow will display information onclick on markers. Content can be formatted using HTML tags in string binding with Group Object properties.

Add content to info window:

html ='<hr>' + counter +'.&nbsp;'+ record.Name + '<br/>';
        html += record.City +'&nbsp;'+ record.State + '&nbsp;'+ record.Country + ',&nbsp;' + record.Zip + '<br/>';
        html +="<hr><b>URL:<b> <a href=https://ap1.salesforce.com/" + record.Id + " target=_blank >account details</a>";
           
        var infowindow = new google.maps.InfoWindow({
            content: html
        });
Create a marker with position and assign it to map:

var marker = new google.maps.Marker({
            position: latLongs,
            map: map,
            title: record.Name
  });

Add “click” event to marker to display info window onclick:

       google.maps.event.addListener(marker, 'click', function() {
          infowindow.open(map,marker);
        });


For more information: https://developers.google.com/maps/documentation/javascript/reference#event


You can copy and paste the controller class and visual force page code if you want to use it but remember to use API KEY in script tag.

6 comments:

  1. I copied the same code which you have posted here. I created two text fields for latitude and longitude and you i changed the constructorname from PF controller to Accountsearchcontroller . I got the map inside the VF page but the thing is the button action method isnt working. Can you tell me what went wrong please?

    ReplyDelete
    Replies
    1. Attn: Re: Google ZipCode Map API 2014

      Please indicate your fixed fee & time required to install a stand alone
      Google Store Locator Map on my website that is similar to this site,
      for reference only. Google Zipcode Map v3 See code.
      http://www.curves.com/locations Enter Zipcode 19802 See results.
      Must install on this website blank page, with an Excel CSV file that
      we can manually add new store locations as we get them.
      https://sites.google.com/a/mediablitzers.net/ovenus1
      Click on Find Us tab - must remove this broken map. Install new map.
      Create Map on local server to deterine if it can function flawlessly ?
      Please advise us of your possible interest or tell us, who is capable.
      Include the URLs for the sites which you have installed this Google
      Store Locator Zipcode v3 Map.

      Sincerely, JamesBarbone@verizon.net + Thanks

      Sample:
      http://www.curves.com/find_a_club/locationsresults?lat=39.75715637207&lng=-75.527565002441&code=19802
      http://www.lowes.com/LowesStoreSearchCmd Enter Zipcode 19802 or State - Delaware To see the Map Store Locations

      Delete
    2. Like Nirmal, I changed the constructor name.
      I also added an initialiser for the counter inside populateMap
      function populateMap(){
      gAccount=[];
      counter = 0;

      gAccount.push(new Group("{!act.acc.id}","{!act.acc.Name}","{!act.acc.Location__Latitude__S}","{!act.acc.Location__Longitude__s}",true,'{!act.acc.BillingCity}','{!act.acc.BillingState}','{!act.acc.BillingCountry}','{!act.acc.BillingPostalCode}'));

      load();
      }

      I also had to change latitude__c to location__latitude__s (ditto longitude).

      For additional debugging aid I added a list Blocksection:














      and added its Id to the reRender command
      reRender="secMap,outMap,listAccs".

      Many thanks for this blog, Pandey.
      Dave Roberts.

      Delete
    3. This comment has been removed by the author.

      Delete
  2. The missing section...it doesn't like <> so stripped every line. Don't forget to add them back.

    apex:pageBlockSection id="listAccs" title="List of names" columns="1"

    apex:repeat value="{!lstAcc}" var="c"
    apex:outputText value="Account {0}:{1} has location lat={2}, long={3}...{4}"
    apex:param value="{!c.acc.Id}"/
    apex:param value="{!c.acc.Name}"/
    apex:param value="{!c.acc.location__latitude__s}"/
    apex:param value="{!c.acc.location__longitude__s}"/
    <pex:param value="{!c.acc.BillingStreet}"/

    /apex:outputText
    /apex:repeat
    apex:pageBlockSection

    ReplyDelete
  3. Found all the information need here. Thanks for sharing the amazing information.
    Salesforce Integration Services

    ReplyDelete