How do I integrate Salesforce with Google Maps?

EDIT:

Thanks to tggagne‘s comment I’ve realized that people still see this answer. The code that was here is over 2.5 years old. If you want to see it – check the history of edits.

A lot has changed in the meantime, more mashup examples were created. Not the least of them being “SF Bus Radar” (github, youtube) app by Cory Cowgill (created on Dreamforce’11 I think).

Nonetheless – here’s my updated example with server-side geocoding, new field of type Geolocation and usage of JSON parsers.

It tries to cache the geocoding results in the contact records. Bear in mind it might not be ‘production-ready’ (no Google Business API key = as all our requests come out from same pool of Salesforce IP servers there might be error messages). That’s why I’ve left the client-side geocoding too.



You’ll need to make 2 changes in your environment before checking it out:

  1. Add “Remote Site Setting” that points to https://maps.googleapis.com to enable callouts from Apex
  2. Add field “Location” in Setup -> Customize -> Contacts -> fields. Type should be “Geolocation”. I’ve selected display as decimals and precision of 6 decimal places.

    public with sharing class mapController {
    public String searchText {get;set;}
    public List<Contact> contacts{get; private set;}
    
    public static final String GEOCODING_URI_BASE = 'https://maps.googleapis.com/maps/api/geocode/json?sensor=false&address=";
    
    // For purposes of this demo I"ll geocode only couple of addresses server-side. Real code can use the commented out value.
    public static final Integer MAX_CALLOUTS_FROM_APEX = 3; // Limits.getLimitCallouts()
    
    public mapController(){
        searchText = ApexPages.currentPage().getParameters().get('q');
    }
    
    public void find() {
        if(searchText != null && searchText.length() > 1){
            List<List<SObject>> results = [FIND :('*' + searchText + '*') IN ALL FIELDS RETURNING 
                Contact (Id, Name, Email, Account.Name,
                    MailingStreet, MailingCity, MailingPostalCode, MailingState, MailingCountry, 
                    Location__Latitude__s, Location__Longitude__s)
                ];
            contacts = (List<Contact>)results[0];
            if(contacts.isEmpty()){
                ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'No matches for "' + searchText + '"'));
            } else {
                serverSideGeocode();
            }
        } else {
            if(contacts != null) {
                contacts.clear();
            }
            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.INFO, 'Please provide at least 2 characters for the search.'));
        }
    }
    
    public void clearGeocodedData(){
        for(Contact c : contacts){
            c.Location__Latitude__s = c.Location__Longitude__s = null;
        }
        Database.update(contacts, false);
        contacts.clear();
    }
    
    public String getContactsJson(){
        return JSON.serialize(contacts);
    }
    public String getDebugContactsJson(){
        return JSON.serializePretty(contacts);
    }
    
    private void serverSideGeocode(){
        List<Contact> contactsToUpdate = new List<Contact>();
        Http h = new Http();  
        HttpRequest req = new HttpRequest();
        req.setMethod('GET'); 
        req.setTimeout(10000);
    
        for(Contact c : contacts){
            if((c.Location__Latitude__s == null || c.Location__Longitude__s == null)){
                String address = c.MailingStreet != null ? c.MailingStreet + ' ' : '' +
                    c.MailingCity != null ? c.MailingCity + ' ' : '' +
                    c.MailingState != null ? c.MailingState + ' ' : '' +
                    c.MailingPostalCode != null ? c.MailingPostalCode + ' ' : '' +
                    c.MailingCountry != null ? c.MailingCountry : '';
                if(address != ''){
                    req.setEndpoint(GEOCODING_URI_BASE + EncodingUtil.urlEncode(address, 'UTF-8'));
                    try{
                        HttpResponse res = h.send(req);
                        GResponse gr = (GResponse) JSON.deserialize(res.getBody(), mapController.GResponse.class);
                        if(gr.status == 'OK'){
                            LatLng ll = gr.results[0].geometry.location;
                            c.Location__Latitude__s = ll.lat;
                            c.Location__Longitude__s = ll.lng;
                            contactsToUpdate.add(c);
                        } else {
                            ApexPages.addMessage(new ApexPages.Message(ApexPages.Severity.ERROR, 'Geocoding of "' + address + '" failed:' + gr.status));
                        }
                    }catch(Exception e){
                        ApexPages.addMessages(e);
                    }
                }
                // Bail out if we've reached limit of callouts (not all contacts might have been processed).
                if(Limits.getCallouts() == MAX_CALLOUTS_FROM_APEX) {
                    break;
                }
            }
        }
        if(!contactsToUpdate.isEmpty()) {
            Database.update(contactsToUpdate, false); // some data in Developer editions is invalid (on purpose I think).
            // If update fails because "j.davis@expressl&amp;t.net" is not a valid Email, I want the rest to succeed
        }
    }
    
    // Helper class - template into which results of lookup will be parsed. Some fields are skipped!
    // Visit https://developers.google.com/maps/documentation/geocoding/#Results if you need to create full mapping.
    public class GResponse{
        public String status;
        public GComponents[] results;
    }
    public class GComponents{
       public String formatted_address;
       public GGeometry geometry;
    }
    public class GGeometry {
        public LatLng location;
    }
    public class LatLng{
        public Double lat, lng;
    }
    }
    

<apex:page controller="mapController" tabStyle="Contact" action="{!find}" id="page">
    <head>
        <style>
            div #map_canvas { height: 400px; }
        </style>
        <script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?sensor=false"></script>
    </head>
    <apex:sectionHeader title="Hello StackOverflow!" subtitle="Contact full text search + Google Maps integration" />
    <apex:pageMessages />
    <apex:form id="form">
        <apex:pageBlock id="searchBlock">
            <apex:inputText value="{!searchText}" />
            <apex:commandButton value="Search" action="{!find}"/>
            <p>Examples: <a href="http://stackoverflow.com/apex/{!$CurrentPage.Name}?q=USA">"USA"</a>, "Singapore", "Uni", "(336) 222-7000". If it works in the global search box, it will work here.</p>
        </apex:pageBlock>
        <apex:pageBlock title="Found {!contacts.size} Contact(s)..." rendered="{!NOT(ISNULL(contacts)) && contacts.size > 0}" id="resultsBlock">
            <apex:pageBlockButtons location="top">
                <apex:commandButton value="Clear cached locations" title="Click if you want to set 'null' as geolocation info for all these contacts" action="{!clearGeocodedData}" />
            </apex:pageBlockButtons>
            <apex:pageBlockTable value="{!contacts}" var="c" id="contacts">
                <apex:column headerValue="{!$ObjectType.Contact.fields.Name.label}">
                    <apex:outputLink value="../{!c.Id}">{!c.Name}</apex:outputLink>
                </apex:column>
                <apex:column headerValue="Address">
                    {!c.MailingStreet} {!c.MailingCity} {!c.MailingCountry}
                </apex:column>
                <apex:column value="{!c.Account.Name}"/>
                <apex:column headerValue="Location (retrieved from DB or geocoded server-side)">
                    {!c.Location__Latitude__s}, {!c.Location__Longitude__s}
                </apex:column>
            </apex:pageBlockTable>
            <apex:pageBlockSection columns="1" id="mapSection">
                <div id="map_canvas" />
            </apex:pageBlockSection>
            <apex:pageBlockSection title="Click to show/hide what was geocoded server-side and passed to JS for further manipulation" columns="1" id="debugSection">
                <pre>{!debugContactsJson}</pre>
            </apex:pageBlockSection>
            <pre id="log"></pre>
        </apex:pageBlock>
    </apex:form>
    <script type="text/javascript">
    twistSection(document.getElementById('page:form:resultsBlock:debugSection').childNodes[0].childNodes[0]); // initially hide the debug section

    var contacts = {!contactsJson};    // Array of contact data, some of them might have lat/long info, some we'll have to geocode client side
    var coords = [];                   // Just the latitude/longitude for each contact
    var requestCounter = 0;

    var markers = [];                  // Red things we pin to the map.
    var balloon = new google.maps.InfoWindow(); // Comic-like baloon that floats over markers.

    function geocodeClientSide() {
        for(var i = 0; i < contacts.length; i++) {
            if(contacts[i].Location__Latitude__s != null && contacts[i].Location__Longitude__s != null) {
                coords.push(new google.maps.LatLng(contacts[i].Location__Latitude__s, contacts[i].Location__Longitude__s));
            } else {
                ++requestCounter;
                var address = contacts[i].MailingStreet + ' ' + contacts[i].MailingCity + ' ' + contacts[i].MailingCountry;
                var geocoder = new google.maps.Geocoder();
                if (geocoder) {
                    geocoder.geocode({'address':address}, function (results, status) {
                        if (status == google.maps.GeocoderStatus.OK) {
                            coords.push(results[0].geometry.location);
                        } else {
                            var pTag = document.createElement("p");
                            pTag.innerHTML = status;
                            document.getElementById('log').appendChild(pTag);
                        }
                        if(--requestCounter == 0) {
                            drawMap();
                        }
                    });
                }
            }
        }
        // It could be the case that all was geocoded on server side (or simply retrieved from database).
        // So if we're lucky - just proceed to drawing the map.
        if(requestCounter == 0) {
            drawMap();
        }
    }

    function drawMap(){
        var mapOptions = {
            center: coords[0],
            zoom: 3,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        };
        var map = new google.maps.Map(document.getElementById("map_canvas"),  mapOptions);

        for(var i = 0; i < coords.length; ++i){
            var marker = new google.maps.Marker({map: map, position: coords[i], title:contacts[i].Name, zIndex:i});

            google.maps.event.addListener(marker, 'click', function() {
                var index = this.zIndex;
                balloon.content="<b>"+contacts[index].Name + '</b><br/>' + contacts[index].Account.Name + '<br/>' + contacts[index].Email;
                balloon.open(map,this);
            });
            markers.push(marker);
        }
    }

    geocodeClientSide();
    </script>
</apex:page>

Leave a Comment