Creating a Geolocation Trigger in Salesforce Winter ’13

NOTE: Due to a change in the Google Maps API licensing, we recommend you have a look at Geopointe on the AppExchange instead of following this blog post.

In Winter ’13, we will see a brand new field type for Geolocation that will store latitude and longitude coordinates. This can open up a whole slew of new possibilites for the platform, such as calculating distances between two objects in Salesforce or searching for Accounts / Contacts within a certain radius.

But when you finally have the option to use Geolocation in your org this October, you might be at a loss for finding useful functionality with it. Just for starters, let’s add a custom location field for Accounts and add it to the Account page layout.

Great… so now what?

As we can see, there is nothing in the field. If we go to the edit page, we have the option of manually entering the latitude and longitude coordinates, but I am sure that typing in all those numbers would drive anyone crazy.

We need to get some useful data into the system without having to manually type it in. If we had a spreadsheet of coordinates, we could import them into the system via Data Loader. But I think that we can use the platform itself with a little help from the Google Maps API.

First, make sure that your Location__c field is set to decimals to the 7th digit. The coordinates sent from Google will be in this format. Next, go to Setup -> Administration Setup -> Security Controls -> Remote Site Settings and then add the Google Maps API domain http://maps.googleapis.com to your org.

Now we’re going to create a static remote function to callout to the Google Maps API with an Account billing address. If we get back coordinates from Google, we write them to Salesforce. Note that if we name our Geolocation field Location__c, the coordinates will be stored in the Location__Latitude__s and Location__Longitude__s fields.

public class LocationCallouts {

     @future (callout=true)  // future method needed to run callouts from Triggers
      static public void getLocation(id accountId){
        // gather account info
        Account a = [SELECT BillingCity,BillingCountry,BillingPostalCode,BillingState,BillingStreet FROM Account WHERE id =: accountId];

        // create an address string
        String address = '';
        if (a.BillingStreet != null)
            address += a.BillingStreet +', ';
        if (a.BillingCity != null)
            address += a.BillingCity +', ';
        if (a.BillingState != null)
            address += a.BillingState +' ';
        if (a.BillingPostalCode != null)
            address += a.BillingPostalCode +', ';
        if (a.BillingCountry != null)
            address += a.BillingCountry;

        address = EncodingUtil.urlEncode(address, 'UTF-8');

        // build callout
        Http h = new Http();
        HttpRequest req = new HttpRequest();
        req.setEndpoint('http://maps.googleapis.com/maps/api/geocode/json?address='+address+'&sensor=false');
        req.setMethod('GET');
        req.setTimeout(60000);

        try{
            // callout
            HttpResponse res = h.send(req);

            // parse coordinates from response
            JSONParser parser = JSON.createParser(res.getBody());
            double lat = null;
            double lon = null;
            while (parser.nextToken() != null) {
                if ((parser.getCurrentToken() == JSONToken.FIELD_NAME) &&
                    (parser.getText() == 'location')){
                       parser.nextToken(); // object start
                       while (parser.nextToken() != JSONToken.END_OBJECT){
                           String txt = parser.getText();
                           parser.nextToken();
                           if (txt == 'lat')
                               lat = parser.getDoubleValue();
                           else if (txt == 'lng')
                               lon = parser.getDoubleValue();
                       }

                }
            }

            // update coordinates if we get back
            if (lat != null){
                a.Location__Latitude__s = lat;
                a.Location__Longitude__s = lon;
                update a;
            }

        } catch (Exception e) {
        }
    }
}

We can call this function anywhere in our Salesforce org, but I think it would be most appropriate in a trigger. So let’s add a trigger.

// Trigger runs getLocation() on Accounts with no Geolocation
trigger SetGeolocation on Account (after insert, after update) {
    for (Account a : trigger.new)
        if (a.Location__Latitude__s == null)
            LocationCallouts.getLocation(a.id);
}

Now when we insert or update an account with a billing address, we will get the coordinates populated into the field.

This is just the start of what we can do with Geolocation in Salesforce, but we will save distance functions for future blog posts. You can download the source code used in this post on the Internet Creations GitHub repository. If you have any questions about our geolocation code, contact us by adding a comment below, or @ reply us on Twitter. We’re on Facebook too!

Tags: , , , , , , , , , , , ,

  • First of all, thank you for sharing your work with the community – it is appreciated.

    I wanted to comment about some things people might run into with this method.

    The first is that in larger orgs you might run into the limits on the geocoding service. Google’s service has a limit of 2500 requests per day (I think by IP address) and has a limit on number of requests per second (I don’t know the exact limit ATM).

    For that and other reasons (including geo-coding existing data w/o having to make the trigger fire), I made a scheduleable class a while back and it can be found here: http://www.forcedisturbances.com/2012/03/caching-data-from-googles-geocoding.html

    Also, I noticed that your trigger will only populate the geocoding once upon an update (i.e. when Location__Latitude__s == null) and doesn’t re-code / pickup address changes. In my write up, I add a last geo coded date/time field so the scheduled class can re-encode any updated records (based on record created date – not optimal but it works).

    I think an ideal solution would be a modified version of your trigger (used to geo-code new and/or updated records) and my scheduled class (used to encode existing records not being edited).

    Thanks again for sharing!

    Tony

    • Imma

      hello scott
      hope you are doing fine
      I have written your code ( the same code ) but I get this error :
      Error: Erreur de compilation : Invalid field Location__Latitude__s for SObject Account à la ligne 4 colonne 15
      Can you help me please

      • benortiz21

        Hi Imma, Please see the note at the top of this post: “Due to a change in the Google Maps API licensing, we recommend you have a look at Geopointe on the AppExchange instead of following this blog post.”

        • Imma

          ok I see now, I thought that my error doesn’t have any relation with the note above…thank you for your replay

  • Kal

    How would you write the test code for the callout class?

  • Vimal

    this is work…..fine…
    but what happen when anyone insert invalid address information…???

  • Eric

    This blog is very helpful, but for some odd reason I created the same class and trigger and not getting an update.

    Do you think I can get your feedback on my code?

  • Anil

    Hi,

    Can anyone give test class for the above class.

    Thanks,
    Anil

    • i need same test class if u got please post it asap

  • Hi,

    I want test class for the above apex class please send me asap

    • Alex Kozlov

      Hi, I was able to test it using this resource almost exactly as it is written here: http://blogs.developerforce.com/developer-relations/2013/03/testing-apex-callouts-using-httpcalloutmock.html

      I changed a couple of things in the test class so mine looks like this:

      @isTest
      public class CalloutLocationTest{
      public static testmethod void testLocationCallout() {
      TM_Office__c tmo = new TM_Office__c();
      tmo.Name = ‘test name’;
      tmo.Street__c = ‘1600 Amphitheatre Parkway’;
      tmo.City__c = ‘Mountain View’;
      tmo.State__c = ‘CA’;
      insert tmo;
      Test.startTest();
      SingleRequestMock fakeResponse = new SingleRequestMock(200,’Complete’,'[{“location” : {“lat” : 37.42291810, “lng” : -122.08542120}}]’,null);
      Test.setMock(HttpCalloutMock.class, fakeResponse);
      LocationCallouts.getLocation(tmo.id);
      //System.assertEquals(/*check for expected results here…*/);
      }
      }

      I got the sample json response from google here (if you want to test anything other than just coordinates): https://developers.google.com/maps/documentation/geocoding/#StatusCodes

      Hope this helps 🙂

  • Mike

    I just deployed with the workbench as instructed, but I don’t seem to be getting any data upon updates or inserts of new accounts. Am I missing something?

    • Hi Mike,
      It looks like the limit of 2,500 requests per 24 hour period that Google enforces for the free version of the Maps API is being shared across Salesforce as it is determined by the IP of the request which does not happen locally.
      Unfortunately, the only option to use this reliably is to get a Google Maps API license which will provide you with a client ID that ensures only requests that you make count toward your total and will also up your limits to 100,000 requests per 24 hour period.

  • Alex Kozlov

    Hi Thank you for the post.. I have it exactly like here, and it even worked once. Now I can’t get it to work again. My log shows that my httpResponse is “OK” but when it gets into JSONparser.nextToken() it keeps running producing an enormous amount of SYSTEM_METHOD_ENTRY and EXIT events until it just ends without setting lag lon or updating anything.. it actually ends with system.JSONParser.nextToken().. I spent countless hours trying to figure it out.. any help would be greatly appreciated.

    • Steve Babula

      Hi Alex,
      Have you tried printing the body of the response to the debug log to confirm that you are getting a proper response? You may be running into the issue detailed below where the daily request limit has been reached. When that happens you will get a valid http response but it won’t actually include any of the geolocation data.
      Paste the following into line 33 of the LocationCallouts class and check the debug log:
      System.debug(res.getBody());

      If that is indeed the problem, you will see the following in the debug log:
      {
      “error_message” : “You have exceeded your daily request quota for this API.”,
      “results” : [],
      “status” : “OVER_QUERY_LIMIT”
      }

      • Alex Kozlov

        Thank you very much Steve! This is exactly what happens.. The weirdest part is that I have just tried to update a single record today (haven’t touched it for a few days) and it gave me this response 🙁

        • I imagine Google caps requests by IP address. Since the code is invoked by Apex, the http request originates from a Salesforce server which hosts multiple customers. In that case, it’s likely other customers have made invocations to Google’s map API. If the code were written in javascript in a visualforce page then the request would come from the end-user, which is far less likely to hit the query limit.

  • Ranjeet Singh

    Hi Scott,
    is this code work for when we upload the record through data_loder? or this code only written for record update when create from UI.

    • saikrishna

      This code work for data_loder also.

  • Lori

    I searched for solutions for 8 hours straight. There were many, but with your simple slick solution, I was able to populate the geo location and then calculate the distance from my business to the contact. Thank you!

  • Jesse

    2 things. 1 can you provide the test class and 2 can you help me set this up for the contacts object instead of accounts?

  • SaiKrishnaTavva

    By using this code we can update 50 records at a time. Can you provide another code which should update more than 50records. & test class for it

  • Travis

    Hi! Thanks for this example, it works great!

    How would I make it so the trigger clears the location custom field before it runs?

    What I’m running into is once an address is entered and saved, if you go back and edit the address it won’t overwrite the geocode it put in the location field.

    How can I do this? I’ve tried acc.setFieldsToNull(new String[]{“Location__c”}); but can’t seem to get it to work, please help!

  • Srikanth

    Hi,

    For me it worked only for one day.

  • AntiRightWing

    This is brilliant…thanks!

  • Navee

    it is not updating latitude and longitude fields Can any one help please

    • benortiz21

      Hi Navee, Please see the note at the top of this post: “Due to a change in the
      Google Maps API licensing, we recommend you have a look at Geopointe on
      the AppExchange instead of following this blog post.”

      • Navee

        Thank you benortiz21
        that means I have to use API key or what? please suggest em the best way to achieve the above same fuctionality