Displaying Captured Geolocation on a Google Map

Hello, and welcome to Another Salesforce Blog! Here I will be posting solutions to problems that I couldn’t find an answer to in hopes of helping those who find themselves stuck when using the Salesforce platform.

Introduction

Today I’ll be expanding on my previous post, Capturing Geolocation Using Lightning Components. If you don’t need to capture your device’s geolocation for a lead, that’s okay! This tutorial can be used on any object that has a geolocation field.

User Story

We are selling cookies door to door, and we want to keep track of the houses we’ve hit! We already captured the geolocation of each house on a lead object using our phones, now we want to be able to see where the lead is on a map.

Background

I found a nifty tutorial by Dreamforce speaker Luciano Straga that serves as a jumping off point for this follow up. The base code for this project can be found in a GitHub repo here. I’ll highlight the code that I’ve borrowed below, and break everything down line by line. The component file and controller file are mostly borrowed, but I have made some important updates to the helper code for our purposes.

Solution

Step one – Write the Lightning Component.

Here’s what our code will look like:

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes,force:hasRecordId,force:hasSObjectName">
    <!-- attributes -->
    
    <aura:attribute name="recordId" type="String" />
    
    <aura:attribute name="latitudeField" type="String" />
    <aura:attribute name="longitudeField" type="String" />
    
    <aura:attribute name="mapMarkers" type="Object" />
    <aura:attribute name="zoomLevel" type="Integer" />
    
    <aura:attribute name="simpleRecord" type="Object" />
    <aura:attribute name="recordError" type="String" />
    
    <aura:attribute name="mapReady" type="Boolean" default="false"/>
    
    <force:recordData aura:id="record"
                      layoutType="FULL"
                      mode="VIEW"
                      recordId="{!v.recordId}"
                      fields="Name, House_Location__Latitude__s, House_Location__Longitude__s"
                      targetFields="{!v.simpleRecord}"
                      recordUpdated="{!c.handleRecordUpdated}"
    />
    
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
    
    <!-- the map component -->
     <lightning:card iconName="action:map" title="House Map">
        <p class="slds-p-horizontal_small">
            <aura:if isTrue="{!and(!empty(v.latitudeField),!empty(v.longitudeField))}">
                <aura:if isTrue="{!v.mapReady}">
                    <lightning:map mapMarkers="{!v.mapMarkers}" zoomLevel="{!v.zoomLevel}" />
                <aura:set attribute="else">
                    <ui:message title="Error" severity="error" closable="false">
                        Coordinates are not defined for this {!v.sObjectName}. <br/>
                        Complete <b>{!v.latitudeField}</b> and <b>{!v.longitudeField}</b> fields for displaying the map.
                    </ui:message>
                </aura:set>
                </aura:if>
            <aura:set attribute="else">
                <ui:message title="Error" severity="error" closable="false">
                 	Latitude and Longitude API Field Names are already set.
                </ui:message>
            </aura:set>
            </aura:if> 
        </p>
    </lightning:card>
</aura:component>

Let’s break it down line by line.

We want this component to be able to be used in Lightning Tabs, so we implement force:appHostable, we want to be able to use it on Record Pages and other page types, so we implement flexipage:availableForAllPageTypes, and we want to pull the ID from the Lead record that we are looking at, so we implement force:hasRecordId. We also want to eventually be able to use this for multiple objects, so let’s pull in force:hasSObjectName as well.

<aura:component implements="force:appHostable,flexipage:availableForAllPageTypes,force:hasRecordId,force:hasSObjectName">
...

For our attributes, we need:

  • recordId so we know which record we’re looking at
  • latitudeField and a longitudeField to keep track of where we’re putting our map marker
  • mapMarkers and zoomLevel attributes to display the content of our map markers and to specify how zoomed in on the map we’ll be
  • simpleRecord to keep track of our record details
  • recordError to keep track of any errors we may encounter loading the record data
    and
  • mapReady to keep track of whether or not our data is loaded yet – we’ll get to why in a second!
... 
    <!-- attributes -->
    
    <aura:attribute name="recordId" type="String" />
    
    <aura:attribute name="latitudeField" type="String" />
    <aura:attribute name="longitudeField" type="String" />
    
    <aura:attribute name="mapMarkers" type="Object" />
    <aura:attribute name="zoomLevel" type="Integer" />
    
    <aura:attribute name="simpleRecord" type="Object" />
    <aura:attribute name="recordError" type="String" />
    
    <aura:attribute name="mapReady" type="Boolean" default="false"/>
...

Next, we need a force:recordData component. This is what we’ll use to pull information from the record we’re looking at. Details on this component can be found in this Trailhead. This part is a little bit tricky, because force:recordData runs asynchronously. Basically, we can’t access the information on our record the second the page loads (ie, in our initialization controller), we have to wait a minute until our simplified record updates. For this reason, we specify that when recordUpdated, we call !c.handleRecordUpdated. This is also why we have a boolean to verify if our map is ready to load or not, which we’ll check in our next step!

...
    <force:recordData aura:id="record"
                      layoutType="FULL"
                      mode="VIEW"
                      recordId="{!v.recordId}"
                      fields="Name, House_Location__Latitude__s, House_Location__Longitude__s"
                      targetFields="{!v.simpleRecord}"
                      recordUpdated="{!c.handleRecordUpdated}"
    />
...

Lastly, we need our map component! We’ll start by creating a lightning:card component with a title of “House Map.” We’ll check to make sure that our latitudeField and longitudeField values are not empty using an aura:if component, and then we’ll check and make sure that our map is ready to load. If we pass those tests, we’ll load our lightning:map component, if not, we’ll display an error.

...
    <!-- the map component -->
     <lightning:card iconName="action:map" title="House Map">
        <p class="slds-p-horizontal_small">
            <aura:if isTrue="{!and(!empty(v.latitudeField),!empty(v.longitudeField))}">
                <aura:if isTrue="{!v.mapReady}">
                    <lightning:map mapMarkers="{!v.mapMarkers}" zoomLevel="{!v.zoomLevel}" />
                <aura:set attribute="else">
                    <ui:message title="Error" severity="error" closable="false">
                        Coordinates are not defined for this {!v.sObjectName}. <br/>
                        Complete <b>{!v.latitudeField}</b> and <b>{!v.longitudeField}</b> fields for displaying the map.
                    </ui:message>
                </aura:set>
                </aura:if>
            <aura:set attribute="else">
                <ui:message title="Error" severity="error" closable="false">
                 	Latitude and Longitude API Field Names are already set.
                </ui:message>
            </aura:set>
            </aura:if> 
        </p>
    </lightning:card>
</aura:component>

Step two – Write the Controller.

After our lightning component is complete, we’ll write our controller.

({
	doInit : function (component, event, helper) {
        component.set('v.zoomLevel', 12);
    },
    
    handleRecordUpdated : function(component, event, helper) {
        var eventParams = event.getParams();
        if(eventParams.changeType === "LOADED") {
            
            try{
                helper.setMapCoordinates(component);
            }
            
            catch(err) {
                console.log("ERROR")
            }
            
        } else if(eventParams.changeType === "ERROR") {
            console.log("ERROR")
            // there’s an error while loading, saving, or deleting the record
        }
    }
})

All we want to do in our initialization function is set the zoomLevel attribute, because, again, force:recordData is called asynchronously.

({
	doInit : function (component, event, helper) {
        component.set('v.zoomLevel', 12);
    },
...

Once our asynchronous call is made and our simpleObject updates, we’ll call the handleRecordUpdated function. We’ll use the JavaScript event.getParams() and .changeType to determine whether or not the record was loaded successfully, and throw an error if not. If the record was loaded successfully, we call our helper function, helper.setMapCoordinates(component).

    handleRecordUpdated : function(component, event, helper) {
        var eventParams = event.getParams();
        if(eventParams.changeType === "LOADED") {
            
            try{
                helper.setMapCoordinates(component);
            }
            
            catch(err) {
                console.log("ERROR")
            }
            
        } else if(eventParams.changeType === "ERROR") {
            console.log("ERROR")
            // there’s an error while loading, saving, or deleting the record
        }
    }
})

Step three – Write the Helper.

Lastly, we have to write the helper. This is where there’s some slight tweaks to the code in Luciano’s tutorial, although these changes are crucial to the function of the code.

({
	setMapCoordinates : function(component, event) {
        var latitude = parseFloat(component.get('v.simpleRecord').House_Location__Latitude__s);
        var longitude = parseFloat(component.get('v.simpleRecord').House_Location__Longitude__s);
        component.set('v.latitudeField',latitude);
        component.set('v.longitudeField',longitude);
        var name = component.get('v.simpleRecord').Name;
        if(latitude == null || longitude == null){
            component.set('v.mapReady', false);
        }else{
            component.set('v.mapMarkers', [
                {
                    location: {
                        Latitude : latitude,
                        Longitude : longitude
                    },
                    
                    title: name
                }
            ]);
            
            component.set('v.mapReady', true);
        }
	},    
})

First, we set variables latitude and longitude to be the values of House_Location__Latitude__s and Longitude__s, respectively, using component.get('v.simpleRecord').House_Location__Lat/Longitude__s. We must parse this variable as a Float because this number must be passed into the Google Maps API as a decimal.

({
	setMapCoordinates : function(component, event) {
        var latitude = parseFloat(component.get('v.simpleRecord').House_Location__Latitude__s);
        var longitude = parseFloat(component.get('v.simpleRecord').House_Location__Longitude__s);
...

Next, we set the values of latitudeField and longitudeField in our Lightning Component to the values of the variables latitude and longitude. We also want to get the name of our record, so we’ll assign the value to a variable name using component.get('v.simpleRecord').Name.

...
        component.set('v.latitudeField',latitude);
        component.set('v.longitudeField',longitude);
        var name = component.get('v.simpleRecord').Name;
...

Lastly, we want to check and make sure our map is ready to load by making sure our latitude and longitude values aren’t empty. If they are, we set the value of mapReady to false, and if not, we set the values for our mapMarkers by passing the information pulled from the record page we are on and set mapReady to true.

...
        if(latitude == null || longitude == null){
            component.set('v.mapReady', false);
        }else{
            component.set('v.mapMarkers', [
                {
                    location: {
                        Latitude : latitude,
                        Longitude : longitude
                    },
                    
                    title: name
                }
            ]);
            
            component.set('v.mapReady', true);
        }
	},    
})

It’s a little change, but it makes all the difference in the world.

Step four – Add Lightning Component to Lightning Record Page.

Once everything is saved, all we have to do is add it to our record page.

Open up a lead and select “Edit Page” under the Setup gear.

Edit Page

Once on the Lightning App Builder page, on the left hand side, search for our custom Lightning Component.

Search for GeolocationMap

Drag this component anywhere on the page that you would like to place it.

Once the component is placed, on the right hand side, there will be an option to add a filter for our component’s visibility. We don’t want to waste resources by showing a map if we don’t have coordinates for a lead, so let’s hide the map if our House_Location__c field is empty.

Set Component Visibility

We’ll add a filter to hide the map if House_Location__Latitude__s is equal to zero…

Latitude Filter

…and another one to filter for if our House_Location__Longitude__s is equal to zero.

Longitude Filter

We’ll set the component to show when both filters are true.

Show Component When

Once we save our Lightning Record Page, we’ll be able to see the map once the Geolocation is populated on our record!

Lead with no geolocation

Once populated, either manually or via our Capture House Location button from our previous tutorial, a map will display.

Lead with geolocation

Thanks for reading, let me know if you have any comments or questions!

-Evelyn, Another Salesforce Blog

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: