ViSit Anywhere Development

Development news and release notes

Implementing Sample Location Actions

This article is a continuation of the discussion on customized sample location actions.  That article described how we configure the basic ViSit Anywhere objects.  In this article I want to talk about how we can implement the required naming and action functions.

The Naming Function

The first function is rather simple.  It receives a .NET DataTable as an argument, and expects that a string is returned (which give the user interface text for the action).  Here is the implementation of the function for my reverse geocoding function.

dt => {
	var result = "Reverse Geocoding (Base Nationale d'Adresse)";
	var lang = System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
	if (string.Equals ("fr", lang, StringComparison.InvariantCultureIgnoreCase)) {
		result = "Géocodage inverse (Base Nationale d'Adresse)";
	}
	return result;
}

First note that this function (defined using the lambda function syntax for C#) receives a DataTable (dt) as an argument.  This DataTable will contain a single row with a single column containing the user data that was defined in our sample location action object.  I am not using that value here, but it might be useful for creating re-usable or parameterized functions.  For example, if you wanted to create a function that could provide a google map street view result or a bing map street side result it might be possible to create a single function and send the map provider as the user data argument.

In this example, I am just returning the text that I want to see in the configuration dialog.  To make things interesting, I show how we could return the text either in French or in English, depending on the current culture of the application.  That is, I get the language code from the current culture, then use that to determine which text to use.


The Action Function 

The action function is a little bit more complicated than the naming function.  We wish to perform the following steps in this function.

  1. First we want to get the location from the incoming data.
  2. Next we create a web client and call the address web service.
  3. The web service will return the result as a GeoJSON text string that we must parse.
  4. After creating a GeoJSON object we want to extract the best address using the provided ranking.
  5. Finally, we will send a message to the log, and copy the address to the system clipboard as a string.

To understand the web service we are using, we just have to read the API page of the French national address database web site (shown in this link).  We can see on this page, the format of the URL we want to execute is like this:

http://api-adresse.data.gouv.fr/reverse/?lon=2.37&lat=48.357

Notice that we simply have to send the longitude and latitude to the reverse endpoint. The values must be in decimal degrees, but otherwise the request is very simple.
Executing the sample query gives the following JSON result.

{
	"limit": 1, 
	"attribution": "BAN", 
	"version": "draft", 
	"licence": "ODbL 1.0", 
	"type": "FeatureCollection", 
	"features": [
		{"geometry": {
			"type": "Point", "coordinates": [2.372289, 48.357303]}, 
		"properties": {
			"street": "Rue de l'\u00c9glise", 
			"label": "6 Rue de l'\u00c9glise 91720 Prunay-sur-Essonne", 
			"distance": 172, 
			"context": "91, Essonne, \u00cele-de-France", 
			"id": "91507_0070_243144", 
			"citycode": "91507", 
			"name": "6 Rue de l'\u00c9glise", 
			"score": 0.9999881191971288, 
			"postcode": "91720", 
			"housenumber": "6", 
			"city": "Prunay-sur-Essonne", 
			"type": "housenumber"
		}, 
		"type": "Feature"
		}
	]
}

What we wish to do, is to find the result with the highest score field and extract the formatted address as the label field.

Before we start, we have to add some references and some namespaces for our C# functions.  First, the incoming locations are represented as Vector3D, so we will include Sharp3D.Math and the appropriate namespace.  The clipboard object is in the System.Windows.Forms assembly so we will add a reference to that.  In addition, we need to parse the JSON and create a GeoJSON Feature (and FeatureCollection) object.  For this, we will reference the Newtonsoft.Json and GeoJSON.Net assembly and add the appropriate namespaces.  Note, all required assemblies are available with the standard ViSit Anywhere installation, so we just have to add the references.  The screenshots below show the required references and namespaces respectively.

 

Now we are ready to define our function. 

dt => {
	
	// I will return the address label
	string result = string.Empty;
	
	// Get my language (for messages)
	var lang = System.Globalization.CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;

	if (dt.Rows.Count > 0) {
		
		// Here is all the data that is being pass to me
		Vector3D inUor = (Vector3D) dt.Rows[0]["InUor"];
		Vector3D inMeters = (Vector3D) dt.Rows[0]["InMeters"];
		Vector3D inRadians = (Vector3D) dt.Rows[0]["InRadians"];
		Vector3D inDegrees = (Vector3D) dt.Rows[0]["InDecimalDegrees"];
		bool showInBrowser = (bool) dt.Rows[0]["ShowInBrowser"];
		string userData = dt.Rows[0]["UserData"] as string;
		
		// create the web client and execute the request
		var client = new WebClient();
		var url = string.Format ("http://api-adresse.data.gouv.fr/reverse/?lon={0:f5}&lat={1:f5}", inDegrees.X, inDegrees.Y);
		string json = client.DownloadString (url);
		
		// convert the result to a GeoJson feature collection
		var fc = JsonConvert.DeserializeObject<FeatureCollection> (json);
		if (fc != null) {
			
			// get the feature with the maximum score
			Feature f = fc.Features.Aggregate((curMax, x) => (curMax == null || (double)(x.Properties ["score"]) > (double)(curMax.Properties ["score"])) ? x : curMax);
			
			// create a message with the addrees (label) and the score.
			var msg = string.Empty;
			if (string.Equals ("fr", lang, StringComparison.InvariantCultureIgnoreCase)) {			
				msg = string.Format ("trouvé l'adresse '{0}' avec un score de {1:p2}", f.Properties ["label"].ToString(), (double) f.Properties ["score"]);
			} else {
				msg = string.Format ("Found address '{0}' with a score of {1:p2}", f.Properties ["label"].ToString(), (double) f.Properties ["score"]);
			}
			Logger.Log (typeof (SampleLocationAction), MsgLevel.Info, msg);
			
			// copy the address (label) to the clipboard
			Clipboard.SetText (f.Properties ["label"].ToString(), TextDataFormat.Text);
			
			// return the label as result (not used)
			result = f.Properties ["label"].ToString();
		} else {
			string msg = string.Empty;
			if (string.Equals ("fr", lang, StringComparison.InvariantCultureIgnoreCase)) {
				msg = string.Format ("Impossible de trouver l'adresse à ({0:f3}, {1:f3})", inDegrees.X, inDegrees.Y);
			} else {
				msg = string.Format ("Could not find address at ({0:f3}, {1:f3})", inDegrees.X, inDegrees.Y);
			}
			Logger.Log (typeof (SampleLocationAction), MsgLevel.Info, msg);
		}
		
	}
	return result;
}

First, we see that I am extracting six items from the incoming DataTable.  These items include four different representations of the sampled location - in UOR and meters for the projected coordinate system, and in decimal degrees and radians for the geographic coordinate system.  I am also getting the flag indicating whether or not to display the result in the browser (in this case I ignore this value) and the user data string (again I ignore this value).  I have placed these 6 lines here, so that they might be easily copied and pasted into a function to ensure that the data can be correctly extracted.

Next, I create a .NET WebClient, format my URL using the location in decimal degrees, and synchronously download the JSON string.  Once this is done I only have to create my GeoJson FeatureCollection object by deserializing the JSON text.  Note, that the request returns a FeatureCollection, but the service automatically adds a limit of 1 to the collection.  In my code, I process as if there might be many results.  The Aggregate linq function is used to find the returned Feature with the best score property.  Once I have this value, I simply need to extract the address as the label property and copy it to the clipboard.

Note again that I am building either English or French text strings and sending the text to the ViSit Anywhere log to allow the user to trace the samples taken.

Summary

This article described how we might implement a custom sample location handler.  The provided naming function can serve as a template for most naming functions for this type of custom action.  The goal is to return a user interface text for the action.  A user data value can be extracted from DataTable argument.

The action function is more complex, but as there are a lot of tools available in C# and ViSit Anywhere, we can perform complex tasks with relatively few lines of code.  In this case, we first have to extract the information in the incoming DataTable.  The six lines here can be re-used for your own action functions.  After that, we used standard C# code and some of the assemblies delivered in the ViSit Anywhere installation to go out and get an address from a location.

This simple example shows how ViSit Anywhere can be easily adapted to provide complex operations on spatial data.

Comments are closed