0

Geocoding in Sharepoint Lists

Today I was thinking it would be nice to do some Geocoding in Sharepoint. Specifically, I wanted to make it so list items could have longitude and latitude fields that could be populated with a button click from the EditItem page. Geocoding is pretty easy to do with both Google and Yahoo. For my situation, Yahoo seemed more appropriate.

My first intuition was that this should be easy to do with Javascript. Just call the Yahoo Maps API with the right data and parse the response. Except.. this usage violates the same site origin policy for Javascript. Drat. Well, there are several things that can be done. If you’re handy with C# and M$ technologies, you can just create a proxy on the same server. Unfortunately, I don’t know the first thing about the Microsoft stack and I’m too lazy to learn. As an alternative, I opted to create a PHP proxy on another server and force it to return JSON, which skirts around the same site origin problem.

First, the code for the PHP proxy which lives somewhere besides the Sharepoint server. This proxy forwards requests to Yahoo, parses the response, and emits JSON back to the caller.

<?php
//get params from request
$appid = 'YOUR_YAHOO_APPID';
$street = $_GET['street'];
$city = $_GET['city'];
$state = $_GET['state'];
 
//build new request
$req = 'http://local.yahooapis.com/MapsService/V1/geocode?';
$req .= 'appid=' . $appid;
$req .= '&amp;street=' . urlencode($street);
$req .= '&amp;city=' . urlencode($city);
$req .= '&amp;state=' . urlencode($state);
 
//fetch XML using cURL
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $req);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$result = trim(curl_exec($ch));
curl_close($ch);
 
//parse XML
$xml = simplexml_load_string($result);
$lat = $xml-&gt;Result[0]-&gt;Latitude;
$lng = $xml-&gt;Result[0]-&gt;Longitude;
 
//return JSON
echo $_GET['jsoncallback'] . "({lat: \"$lat\", lng: \"$lng\"})";
?>

Now, the Javascript part of this uses jQuery to make life much much easier. It basically injects a new Button onto the page. When clicked, it builds a request from the location data on the page, sends the request to the proxy, parses the response, and puts the latitude and longitude into the form fields. Then the user can click ‘Save’ as usual. This code needs to be added to the EditItem.aspx page for that particular list. You also need to have jQuery on the page. In my case, I just included them both as external scripts to keep my changes to .aspx pages to a minimum.

$(document).ready(function() {
	//create a new Button, match Sharepoint styles
	var geoButton = $('&lt;input type="button"&gt;').attr({'class':'ms-ButtonHeightWidth', 'value':'GeoCode'});
 
	//add our Button after the default 'Cancel' Button
	$('.ms-formtoolbar .ms-toolbar:last').after($('&lt;td&gt;&amp;nbsp;&lt;/td&gt;').attr({'class':'ms-separator'}));
	$('.ms-formtoolbar .ms-separator:last').after(geoButton);
 
	//wrap Button in a Table to match Sharepoint's style
	geoButton.wrap($('&lt;td&gt;&lt;/td&gt;').attr({'class': 'ms-toolbar', 'no-wrap':'true'}))
		 .wrap($('&lt;table&gt;&lt;/table&gt;').attr({'cellspacing':'0','cellpadding':'0','width':'100%'}))
		 .wrap($('&lt;tbody&gt;&lt;/tbody&gt;'))
	         .wrap($('&lt;tr&gt;&lt;/tr&gt;'))
                 .wrap($('&lt;td&gt;&lt;/td&gt;').attr({'nowrap':'','align':'right','width':'100%'}));
 
	//onClick, perform geoCode and put Long/Lat into form fields
	geoButton.click(function() {
 
	//get data from form fields
	var street = $('input[title=Street]').val();
	var city =  $('input[title=City]').val();
	var state =  $('input[title=State]').val();
 
	//fail early if some data is absent, since we wouldn't get a good geocode
	if(street == '' || city == '' || state == '')
  	    return;
 
	//wrap data into a URL so we can do an HTTP GET
	var address = '&amp;street='+street+'&amp;city='+city+'&amp;state='+state;
 
	//use jQuery.getJSON to avoid that pesky cross domain security restriction
	//?jsoncallback=? is a peculiarity required by jQuery, the server must echo this back
	$.getJSON("http://path_to/yahoo-geocoder.php?jsoncallback=?"+address,
		function(json) {
    		    //async callback, unpack the data
 		    var lat = json.lat;
		    var lng = json.lng;
       		    //simple error detection
		    if(lat == '' || lng == '') {
			//show '!!!' after form fields to indicate that geocoding failed
			$('input[title=Latitude]')
			.after($('&lt;span&gt;!!!&lt;/span&gt;').attr({'id':'error-lat'}).css('color','red'));
			$('input[title=Longitude]')
			.after($('&lt;span&gt;!!!&lt;/span&gt;').attr({'id':'error-lng'}).css('color','red'));
		    } else {
			//success, place results into form fields
			$('input[title=Latitude]').val(lat);
			$('input[title=Longitude]').val(lng);
			//remove any previous error indicators
			$('#error-lat').remove();
			$('#error-lng').remove();
		    }
		});
	});
});
2

Hello Sharepoint, Meet PHP

I am not a huge fan of Sharepoint, but we are starting to use it at work and it does do certain things well.

Trying to make Sharepoint do something non-obvious can be a lot of fun, especially on a slow day.

So I was trying to create a simple feedback form that would be stored by Sharepoint as a List. While this is well within Sharepoint’s capabilities, it ends up being kind of a pain. You end up either creating a custom WebPart or designing something in InfoPath or writing ASP code. None of these options really appeals to me, mostly because I don’t know much about Sharepoint and I don’t have priviledges to deploy code on the server. Luckily most of the Sharepoint API is exposed through Web Services. Just make a PHP-backed webpage, produce the XML and fire it off to Sharepoint – no problem, right?

Unfortunately, this is easier said than done. SOAP is typically an all-or-nothing proposition. You don’t “kind of” get a response, you either get success or an opaque error message.

I spent the better part of a morning piecing this together from a zillion blog posts. There are two big tricks – getting the XML right and using NTLM authentication.

This code sample requires the NuSOAP library for PHP – you could also do it with PHP’s native SOAP I suppose. You will need to have a fairly recent version of CURL to get the NTLM authentication.

<?php
/* Requires the NuSOAP library */
require_once('lib/nusoap.php');
 
/*  Your username and password, separated by a colon
    Domain may be optional, depending on your setup */
$auth = "domainusername:password";
 
/*  Location of the Lists.asmx file
    If the list is in a subsite, the subsite must be in the path */
$wsdl = "http://domain.com/some-site/some-subsite/_vti_bin/Lists.asmx?WSDL";
 
/*  GUID of the list */
$guid = "{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}";
 
/* Setup NuSOAP
   Sharepoint requires NTLM Authorization
   You need a fairly recent version of CURL installed for this	*/
$client = new soapclient($wsdl, true);
$client->setCredentials("","","ntlm");
$client->setCurlOption(CURLOPT_USERPWD, $auth);
 
/*  XML for the request, add extra Fields as necessary */
$xml =
'
<UpdateListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">
<listName>'.$guid.'</listName>
<updates>
<Batch>
<Method ID="1" Cmd="New">
<Field Name="Title">My Title</Field>
<Field Name="ABC">My Value</Field>
</Method>
</Batch>
</updates>
</UpdateListItems>
';
 
/* Invoke the Web Service */
$result = $client->call('UpdateListItems', $xml);
 
/* Check for Errors */
if(isset($fault)) {
  echo("<h2>Error</h2>". $fault);
}
 
/* Debugging Info */
echo("<h2>Request</h2>" . htmlspecialchars($client->request, ENT_QUOTES));
echo("<h2>Response</h2>" . htmlspecialchars($client->response, ENT_QUOTES));
echo("<h2>Debug</h2>" . htmlspecialchars($client->debug_str, ENT_QUOTES));
 
unset($client);
?>

That’s pushing data into Sharepoint. You can also pull data using the GetListItems Web Service. The code is basically the same with different XML.