top

Siebel Open UI and Google Maps integration – Part 2

 
Posted: Thursday, February 12th, 2015

Last time we’ve seen how to transfer address information from Siebel to Google Maps and display a location on the map, i.e. implemented a unidirectional integration. Now the time has come to make the address data travel the other way around: from Google Maps picker to Siebel, so let us see what needs to be done to make it happen!

The bidirectional integration

What we wanted to accomplish with this type of integration was to pick a location on Google Maps control and save the corresponding address to Siebel.  We have decided to use a third-party Location Picker JQuery plugin, because of the functionality of location marker that it provides, and a structure for returning location data that suited our purposes well. In your particular circumstances you might want to also check out other plugins, or, perhaps, even write your own using Google Maps API – but for this series we’ll settle with jQuery Location Picker.

The solution described below implements the following key behaviors:

  • Displays Google Maps control with a location marker set to the address stored in the selected record. If the record in question is empty, the current browser location is used.
  • Allows picking another location using the location marker.
  • Saves the address information to Siebel
  • Displays it on the map later on, using the previously described unidirectional integration approach (see Part 1 of the series).

Siebel configuration

We’ve decided to avoid popups altogether, since the solution was originally intended for Task UI. Therefore the Google Maps control has been implemented as an iFrame on a Form Applet. So what we need on the Siebel side for this solution is a new View with two applets, based on the business component, for which an address is to be picked. The upper applet in the view is a regular detail applet for the entity, and the lower applet will contain a number of new fields to hold an address. We cannot use the regular address fields for this, since there are picklists in them, joined fields, certain format of the data, etc.

This means that we need to create some new fields, at least for Street Address, City, State or Province, Country, Postal Code, Latitude, and Longitude. We will also need to create a small separate field – a placeholder, where we will later put an iFrame with the map. The placeholder should be visible on the applet; otherwise it will not work as intended:

001

Now, it only makes sense to perform a write to Business Component, when the user has finally picked the right location, and confirmed that by pressing the Pick Address button. So we’ll have to add this button, and create a PickAddress method for it.

Finally, we will also place the following script in the PreInvoke event of the Business Component:

function BusComp_PreInvokeMethod (MethodName)
{
      if (MethodName == "PickAddress")
      {
            this.WriteRecord();
            return (CancelOperation);
      }
      return (ContinueOperation);
}

 

Now, after we add the new view to the Screen and compile everything into an SRF, our Siebel configuration is over!

Installing the plug-in

Download jQuery Location Picker plugin, unzip the archive, and put locationpicker.jquery.js in the following folder on your web server, local client, or maybe both (create the folders if you need to):

PUBLIC\enu\[build number]\scripts\siebel\custom\locationpicker

Preparing Presentation Model and Presentation Renderer templates

Template samples for PM and PR can easily be found online (e.g., here); rename the templates to GoogleMapBoxPM.js and GoogleMapBoxPR.js, respectively, and then change all entries of <PM Template Name>  to GoogleMapBoxPM and all entries of  <PR Template Name>  to GoogleMapBoxPR. Of course, you can rename the templates to anything else, but in this article we will stick to these file names.

Both template files should be stored in the following folder (on both the web server and the local client):

PUBLIC\enu\[build number]\scripts\siebel\custom

Now we need to register our new applet to work with the new PM and PR, and here’s how:

  1. Go to Administration – Application screen.
  2. Go to Manifest Files view.
  3. In the Files list, create a new record and enter (or, better, copy & paste) the relative path to the new file, for example: siebel/custom/GoogleMapBoxPM.js.
  4. Navigate to the Manifest Administration view.
  5. In the UI Objects list, create a new record with the following values:
    • Type = Applet
    • Usage Type = Presentation Model
    • Name = the name of your new applet
  6. In the Object Expression list, create a new record with the following values :
    • Expression = Desktop (use the picklist)
    • Level = 1
  7. In the Files list, click the Add button and associate the file you’ve registered on step 3.
  8. Go through the same steps 1 through 7 for js as well.

At this point we will need to clear browser temporary files (and, by the way, restrict it from caching files!), then launch the local client and navigate to our new view. Having done that, open the JS console and check that the newly created PM and PR files are actually loaded.

Presentation Model modifications

In the PM, what we need to accomplish is this:

  1. Prepare HTML code to insert iFrame
  2. Prepare controls data to use in the PR
  3. Implement event handling for button clicks

As the first step to achieve this, add CreateMapHTML(),PostShowSelection()  and  Applet_PreInvokeMethod() functions to the PM:

function CreateMapHTML () {
try {
          var control, o;
          var sFullAddress = "";
          var sMapHTML = "";
          var controls = this.Get("GetControls"); // get controls array
          var addressControls = {street : "cross", city : "municip", postal : "postal", state : "state", country : "country", 
                                 latitude : "latitude", longitude : "longitude", mapframe : "mapifr"};
          var sTemp = "";
          for (control in controls){
                   sTemp = control;
                   for (o in addressControls){
                             sTemp = sTemp.toLowerCase();
                             if (sTemp.indexOf(addressControls[o]) != -1) {
                                addressControls[o] = controls[control]; //replace the 'search string' with the control object
                             }
                   }
          }
          this.SetProperty("addressControls", addressControls);
          if (typeof(addressControls["street"]) !== "string") {
                   for (o in addressControls){
                             if (typeof(addressControls[o]) !== "string") {
                                      if (o.toString() != "mapframe")
                                      {
                                                sFullAddress += this.ExecuteMethod("GetFieldValue", addressControls[o]) + ",";
                                      }
                             }
                   }
          }
                   sMapHTML += '&lt;div id="c_map_anchor" style="width: 500px; height: 350px;"&gt;';
                   sMapHTML += '&lt;/div&gt;';
}
catch(e){
          alert("PM: Error in custom method 'GoogleMapBoxPM.CreateMapHTML'\nError Message: " + e.toString());
}
return sMapHTML;
}

This CreateMapHTML() function prepares an array of controls on an addressControls applet that will be passed to the PR and used therein. It also creates a piece of HTML code –sMapHTML – that will be inserted on the applet. Pay attention to the UI element called c_map_anchor here: we’ll use it in the PR as a control for the map. It also receives an address from controls into the sFullAddress variable. It can later be used for forming an HTTP query to the Google Maps engine, but for now this piece of code can be skipped.

function PostShowSelection(){
	var sMapHTML = this.ExecuteMethod("CreateMapHTML");
	this.SetProperty("sMapHTML", sMapHTML);
}

The PostShowSelection() function calls CreateMapHTML functon we’ve added before, right after an applet is loaded.

function Applet_PreInvokeMethod(methodName, psInputArgs, lp, returnStructure)
{
	try {
		var applet = this;
		if (methodName == "PickAddress")
		{
			var controls = this.Get("GetControls");
						
			applet.ExecuteMethod("SetFormattedValue", applet.ExecuteMethod("GetControl", "Cross Street"), this.Get("GM Street"));
			applet.ExecuteMethod("SetFormattedValue", applet.ExecuteMethod("GetControl", "Municipality"), this.Get("GM City"));						
			applet.ExecuteMethod("SetFormattedValue", applet.ExecuteMethod("GetControl", "Postal Code"), this.Get("GM Postal"));
			applet.ExecuteMethod("SetFormattedValue", applet.ExecuteMethod("GetControl", "Country"), this.Get("GM Country"));
			applet.ExecuteMethod("SetFormattedValue", applet.ExecuteMethod("GetControl", "Longitude"), this.Get("Longitude"));
			applet.ExecuteMethod("SetFormattedValue", applet.ExecuteMethod("GetControl", "Latitude"), this.Get("Latitude"));
						
			//To remove the wait icon
			SiebelApp.S_App.uiStatus.Free(); 
			//Return cancel operation so nothing on Server side is invoked
			returnStructure ["CancelOperation"] = false;						
		}
	}
	catch(e){
		SiebelJS.Log("PM: Error in custom method 'GoogleMapBoxPM.Applet_PreInvokeMethod'\nError Message: " + e.toString());
	}
}

The PostShowSelection() function intercepts the PreInvoke event of an applet, loads properties set in the PR, and then writes those into the corresponding fields on an applet. This code will be run after the Pick Address button is pressed on the Applet. Towards the end of the function code, we set CancelOperation to false, which means that a call of PickAddress method will be passed further to the Business Component. Remember the PreInvoke  code that we wrote earlier on the BC? It will store the record after the values are set.

Having added the three functions to PM, let’s make some changes in the Init method – to register all our functions and properties:

GoogleMapBoxPM.prototype.Init = function(){
        SiebelAppFacade.GoogleMapBoxPM.superclass.Init.apply(this,arguments);
        //implement Init method here
	this.AddProperty("sMapHTML",""); // HTML to be 'injected' in PR
	this.AddProperty("GM Street","");
	this.AddProperty("GM City","");
	this.AddProperty("GM State","");
	this.AddProperty("GM Postal","");
	this.AddProperty("GM Country","");
	this.AddProperty("Longitude","");
	this.AddProperty("Latitude","");
	this.AddProperty("NoRefresh","N");
        this.AddMethod("FieldChange", PostFieldChange, {sequence:false, scope:this});
	this.AddMethod("ShowSelection", PostShowSelection, {sequence:false, scope:this});
	this.AddMethod("CreateMapHTML", CreateMapHTML, {sequence:false, scope:this});
	this.AddMethod("InvokeMethod", Applet_PreInvokeMethod, {sequence: true, scope: this});				
};

Finally, the Presentation Model is ready, and we can move over to Presentation Renderer modifications.

Presentation Renderer modifications

First of all, we need to modify the define clause, so that it looks like this:

define("siebel/custom/GoogleMapBoxPR", "https://maps.googleapis.com/maps/api/js?v=3.exp&amp;sensor=false&amp;libraries=places&amp;callback=redrawMap", "siebel/custom/locationpicker/locationpicker.jquery"], function(){

Here we have added a path to external libraries we use(the location picker and Google Maps API in our case). Note the redrawMap() call after Google Maps API is loaded: it is needed to avoid an error after first execution.

Then, we’ll make bindings in Init, like this:

GoogleMapBoxPR.prototype.Init = function() {
	SiebelAppFacade.GoogleMapBoxPR.superclass.Init.apply(this, arguments);
	PM = this.GetPM();
	PRName = PM.Get("GetName") + "_GoogleMapBoxPR";
	PM.AttachPMBinding("sMapHTML",ShowMapIcon);
	PM.AttachPMBinding("InvokeMethod",ShowMapIcon);
};

And define simple a redrawMap() function, which  will receive the callback:

window.redrawMap = function() {				 
	 var tempMap = currentPM.Get("sMapHTML"); 
	 currentPM.SetProperty("sMapHTML", "");
	 currentPM.SetProperty("sMapHTML", tempMap);				 
	}

As you can see from the code above, we are changing the value of sMapHTML property here. This causes the control to redraw, showing the map iFrame after the Google Maps API has been loaded.

Now let’s insert the map control itself:

function ShowMapIcon(){                
	var sMapHTML = PM.Get("sMapHTML"); //get the map icon HTML				                		
				
	if (sMapHTML != ""){ //if HTML string is not empty	
		var controls = PM.Get("addressControls");  //get the address control object collection		
                var ctrl = controls["mapframe"]; //obtain the control which will show the icon			
                var ctrlID = ctrl.GetInputName(); //get the name of the DOM element					
                $('[name="' + ctrlID + '"]').after(sMapHTML);  //insert the icon HTML after the control
		$('[name="' + ctrlID + '"]').remove();  // remove placeholder control

		// get separate controls. We will set values to them later
		var cStreet = controls["street"];
		var cCity = controls["city"];
		var cPostal = controls["postal"];					
		var cCountry = controls["country"];
                var cLat = controls["latitude"];
		var cLong = controls["longitude"];

		// get Id of each control to global variable
		cStreetId = cStreet.GetInputName();
		cCityId = cCity.GetInputName();
		cPostalId = cPostal.GetInputName();					
		cCountryId = cCountry.GetInputName();								
                cLatId = cLat.GetInputName();
		cLongId = cLong.GetInputName();
		var sLat = PM.Get("Latitude");
		var sLong = PM.Get("Longitude");
		if (sLat == "" || sLong == "")
		{
			var options = {
			  enableHighAccuracy: true,					  
			  timeout: 10000,
			  maximumAge: 0
			};
			navigator.geolocation.getCurrentPosition(successGeoloc, errorGeoloc, options);
		}
		else
		{
			var ps = SiebelApp.S_App.NewPropertySet();
			$("#c_map_anchor").locationpicker(
			{
				location: {latitude: sLat, longitude: sLong},
				radius: 10,
				onchanged: function (currentLocation, radius, isMarkerDropped) {
				  var addressComponents = $(this).locationpicker('map').location.addressComponents;
				  $('[name="' + cStreetId + '"]').val(addressComponents.addressLine1);				  
                                  $('[name="' + cPostalId + '"]').val(addressComponents.postalCode);
				  $('[name="' + cCountryId + '"]').val(addressComponents.country);
				  ps.SetProperty ("GM Street", addressComponents.addressLine1);
				  if (addressComponents.city) {
					ps.SetProperty ("GM City", addressComponents.city);		
				  }
				  else{
					ps.SetProperty ("GM City", addressComponents.political);					  
                                  }
				  $('[name="' + cCityId + '"]').val(ps.GetProperty("GM City"));
				  $('[name="' + cLatId + '"]').val(currentLocation.latitude.toFixed(6));
				  $('[name="' + cLongId + '"]').val(currentLocation.longitude.toFixed(6));
				  ps.SetProperty ("GM State", addressComponents.stateOrProvince);
				  ps.SetProperty ("GM Postal", addressComponents.postalCode);	
				  ps.SetProperty ("GM Country", addressComponents.country);
				  ps.SetProperty ("formatted_address", addressComponents.formatted_address);
				  ps.SetProperty ("addressLine2", addressComponents.addressLine2);
				  ps.SetProperty ("streetName", addressComponents.streetName);
				  ps.SetProperty ("streetNumber", addressComponents.streetNumber);
				  ps.SetProperty ("state", addressComponents.state);
				  PM.SetProperty("GM Street", ps.GetProperty("GM Street"));
				  PM.SetProperty("GM City", ps.GetProperty("GM City"));
				  PM.SetProperty("GM State", ps.GetProperty("GM State"));
				  PM.SetProperty("GM Postal", ps.GetProperty("GM Postal"));
				  PM.SetProperty("GM Country", ps.GetProperty("GM Country"));
				  PM.SetProperty("Longitude", currentLocation.longitude.toFixed(6));
				  PM.SetProperty("Latitude", currentLocation.latitude.toFixed(6));
				}
			});
		}					
	}
}

Here the condition if (sLat == “” || sLong == “”) checks whether Google address already exists on the component. If it does exist, we call locationpicker constructor, passing the values of longitude and latitude that we have as parameters. Otherwise we try to get the current location from the browser, like this:

navigator.geolocation.getCurrentPosition(successGeoloc, errorGeoloc, options);

This browser method call attempts to get the location data: for the first time the user will be asked by the browser if using the current location is allowed. If the user allows this operation, successGeoloc() function would be called after location is determined; otherwise errorGeoloc() method would be called – and that’s how one can determine, which choice did the user make.

The property set options here defines how accurate the location definition should be, how long a time it may take, and whether the caching is allowed.

Let’s have closer look at functions that are invoked after the location has been determined, successGeoloc() and errorGeoloc(). The former is called upon a successful operation:

function successGeoloc(pos) {
   var crd = pos.coords;
   var ps = SiebelApp.S_App.NewPropertySet();
   $("#c_map_anchor").locationpicker(
   {
      location: {latitude: crd.latitude, longitude: crd.longitude},
      radius: 10,
      onchanged: function (currentLocation, radius, isMarkerDropped) {
         var addressComponents = $(this).locationpicker('map').location.addressComponents;
         $('[name="' + cStreetId + '"]').val(addressComponents.addressLine1);
         $('[name="' + cPostalId + '"]').val(addressComponents.postalCode);
         $('[name="' + cCountryId + '"]').val(addressComponents.country);
         ps.SetProperty ("GM Street", addressComponents.addressLine1);
         if (addressComponents.city) {
            ps.SetProperty ("GM City", addressComponents.city);
         }
         else{
            ps.SetProperty ("GM City", addressComponents.political);
         }
         $('[name="' + cCityId + '"]').val(ps.GetProperty("GM City"));
         ps.SetProperty ("GM State", addressComponents.stateOrProvince);
         ps.SetProperty ("GM Postal", addressComponents.postalCode);
         ps.SetProperty ("GM Country", addressComponents.country);
          
         PM.SetProperty("GM Street", ps.GetProperty("GM Street"));
         PM.SetProperty("GM City", ps.GetProperty("GM City"));
         PM.SetProperty("GM State", ps.GetProperty("GM State"));
         PM.SetProperty("GM Postal", ps.GetProperty("GM Postal"));
         PM.SetProperty("GM Country", ps.GetProperty("GM Country"));
         PM.SetProperty("Longitude", currentLocation.longitude.toFixed(6));
         PM.SetProperty("Latitude", currentLocation.latitude.toFixed(6));
      }
   })
}

Here a c_map_anchor control defined in the PM and inserted by ShowMapIcon() function  instead of our placeholder is used to call locationpicker() function of the JQuery plugin. In this call we use the coordinates returned by browser in pos parameter, passed to successGeoloc(), that sets initial position of the location marker to our defined location. When the marker position changes, the event is processed by function defined inline in the  onchanged() event. This function retrieves the addressComponents address structure, and stores its properties in the corresponding properties of the Presentation Model defined before.

Further on, we step on an unstable ground of the data returned by Google. The thing is that Google does not guarantee that the address format is the same for all the countries in the world; even more, you can count on it differing. For us this means that we cannot simply map the properties of addressComponents to PM properties, because for some of the countries out there one of the properties could be empty, while another is filled – and vice versa. For that purpose, an intermediate property set is used and some extra logic is to be applied here, defined more or less empirically and on a case-by-case basis.

If, however, the current location cannot be received from the browser, we end up in the following function:

function errorGeoloc(err) {
   SiebelJS.Log("NOT Found location!");
   SiebelJS.Log("ERROR(" +  + err.code + "): " + err.message);
   $("#c_map_anchor").locationpicker();
};

Here we invoike locationpicker constructor as well, but this time – without any parameters, as you can see; this way the map control can still be visible, but a user won’t be able to save an address. Alternatively, we could have chosen to implement the same functionality as in successGeoloc() – but without the current location; however, for the sake of this case study let us stick with a relatively simpler approach.

This concludes our description of the modifications needed for the presentation model and presentation renderer; we are, however, not done with the whole bidirectional Siebel OpenUI  – Google Maps integration theme yet; next time, in the final article of this series, we’ll round up the description of this approach, so stay tuned!


IljaDubovskis About the author: Ilja Dubovskis has deep technical expertise in Siebel CRM. For 9 years he has been working for clients in Telecommunications, Finance, Insurance, Industry and Public Sector areas. In addition to working for clients, he leads Siebel Core Developer School at IPR, as well as acts as a coach for newcomers. He has been with Idea Port Riga since September 2009.