Spatial support in Web API and ODATA

3 ביוני 2014

no comments

The spatial support in ASP.NET WEB API is currently very limited. When creating a WEB API project a reference to System.Spatial is created automatically. When creating an OData 4.0 (WEB API 2.2) a reference to Microsoft.Spatial is created. Both System.Spatial and Microsoft.Spatial are actually the same. It is the spatial library that was developed for WCF Data Services and can be installed by importing the NuGet package Microsoft.Spatial. Microsoft.Spatial contains wide collection of spatial types and formatters for GeoJson and GML. Unfortunately this is where the WEB API and ODATA support for spatial functionality ends.
The OData standard defines how spatial types and queries should be supported. Details can be found here.

For example the following query should be supported.
http://localhost:7817/odata/Products()?$filter=geo.distance(Location,geometry'Point(-122.03547668457+47.6316604614258)')+lt+900

In this query we can see definition examples of both spatial operator (geo.distance) and spatial type representation (geometry'Point(-122.03547668457+47.6316604614258)')

Unfortunately none of this is implemented in the current stack of ASP.NET WEB API.

So what can be done?

We can use the Microsoft.Spatial library to represent various spatial types in GML or GeoJson and create ODATA function \ WEB API actions for implementing geographic calculations and queries.

This is NOT the ideal solution but integrating the OData standard into the ASP.NET stack involves a serious and expensive development effort.
Defining a number of spatial functions and providing an implementation using the current OData or WEB API stack is simple but it breaks the restfulness of the service. Such group of functions is will form a RPC contract similar to WCF and ASMX web services.

Implementation details:

Microsoft.Spatial supports two main type of classes:

  • Geographic classes that define location with longitude and altitudes.
  • Geometric classes that define location with X,Y,Z,W coordinates.

Both classes support 4 dimensions and configurable coordinate system.

Remark: The OData specification as well as MongoDB indexing engine currently support two dimensions only.
From the Odata Blog:
"The OGC type system is defined for only 2-dimensional geospatial data. We extend the definition of a position to be able to handle a larger number of dimensions. In particular, we handle 2d, 3dz, 3dm, and 4d geospatial data. Because OGC only standardized 2d, different implementations differ on how they extended to support 3dz, 3dm, and 4d. We may add support for higher dimensions when they stabilize."

Geography Classes:

clip_image002

Geometric Classes:

clip_image004

Formatters:

clip_image006

ISpatial classes are uses for representing spatial information as well as execute simple operations on it.
Microsoft.Spatial provides built-in implementation for the operation "Distance". Other operations can be implemented by extending the package.

The formatters are used for serializing ISpatial classes into GeoJson or GML (as demonstrated next)

It is important that to use GML or GeoJson representation on the wire. For that we should build a JSON Converter and plug it into the Web API serialization stack by decorating each spatial property of the model's entities:

Code Snippet
  1.  
  2. public class Product
  3. {
  4.     public string Id { get; set; }
  5.     public string Name { get; set; }
  6.  
  7.    [JsonConverter(typeof(GeoJsonConverter<Geography>))]
  8.    public GeographyPoint[] Locations { get; set; }
  9.  
  10.    [JsonConverter(typeof(GeoJsonConverter<Geometry>))]
  11.    public GeometryPoint Location { get; set; }
  12.  
  13.    public int Price { get; set; }
  14.  
  15. }

Here is the implementation of the JSON Converter for GeoJson I wrote:

Code Snippet
  1.  
  2.  public class GeoJsonConverter<T> : JsonConverter where T: class , ISpatial
  3.  {
  4.      public override bool CanConvert(Type objectType)
  5.      {
  6.          return typeof(T).IsAssignableFrom(objectType);
  7.      }
  8.  
  9.      public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
  10.      {
  11.          var formatter = SpatialImplementation.CurrentImplementation.CreateGeoJsonObjectFormatter();
  12.          var data = serializer.Deserialize(reader);
  13.          if (data is JArray)
  14.          {
  15.              var jArray = data as JArray;
  16.              List<Object> res = new List<object>();
  17.              
  18.              foreach (var token in jArray.Children())
  19.              {
  20.                  var jObject = token as JObject;
  21.                  var dic = CreateGeoJsonDictionary(jObject);
  22.                  res.Add(formatter.Read<T>(dic));
  23.              }
  24.              
  25.              var elementType = objectType.GetElementType();
  26.              
  27.              // call res.Cast<elementType>().ToArray<elementType>() using reflection
  28.              var cast_mi = GenericMethodOf<IEnumerable<int>>((_) => ((IEnumerable)null).Cast<int>());
  29.              var toArray_mi = GenericMethodOf<IEnumerable<int>>((_) => ((IEnumerable<int>)null).ToArray());
  30.              var cast_mi_of_t = cast_mi.MakeGenericMethod(elementType);
  31.              var toArray_mi_of_t = toArray_mi.MakeGenericMethod(elementType);
  32.              var tmp_result = cast_mi_of_t.Invoke(null, new object[]{res});
  33.              var result = toArray_mi_of_t.Invoke(null, new object[]{tmp_result});
  34.              return result;
  35.              
  36.          }
  37.          if (data is JObject)
  38.          {
  39.              var jObject = data as JObject;
  40.              var dic = CreateGeoJsonDictionary(jObject);
  41.              return formatter.Read<T>(dic);;
  42.          }
  43.          return null;
  44.      }
  45.  
  46.      private static MethodInfo GenericMethodOf<TReturn>(Expression<Func<object, TReturn>> expression)
  47.      {
  48.          return ((expression).Body as MethodCallExpression).Method.GetGenericMethodDefinition();
  49.      }
  50.  
  51.      private static Dictionary<string, object> CreateGeoJsonDictionary(JObject jObject)
  52.      {
  53.          var dic = new Dictionary<string, object>();
  54.          foreach (var token in jObject.Properties())
  55.          {
  56.              if (token.Value.Type == JTokenType.String)
  57.                  dic.Add(token.Name, token.Value.ToObject(typeof(string)));
  58.  
  59.              if (token.Value.Type == JTokenType.Integer)
  60.                  dic.Add(token.Name, token.Value.ToObject(typeof(int)));
  61.  
  62.              if (token.Value.Type == JTokenType.Date)
  63.                  dic.Add(token.Name, token.Value.ToObject(typeof(DateTime)));
  64.  
  65.              if (token.Value.Type == JTokenType.Float)
  66.                  dic.Add(token.Name, token.Value.ToObject(typeof(float)));
  67.  
  68.              if (token.Value.Type == JTokenType.Guid)
  69.                  dic.Add(token.Name, token.Value.ToObject(typeof(Guid)));
  70.  
  71.              else if (token.Value.Type == JTokenType.Array)
  72.                  dic.Add(token.Name, token.Value.ToObject(typeof(List<object>)));
  73.  
  74.              else if (token.Value.Type == JTokenType.Object)
  75.                  dic.Add(token.Name, CreateGeoJsonDictionary(token.Value as JObject));
  76.          }
  77.          
  78.          return dic;
  79.      }
  80.  
  81.      public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  82.      {
  83.          var formatter = SpatialImplementation.CurrentImplementation.CreateGeoJsonObjectFormatter();
  84.          var points = value as ICollection<T>;
  85.          object collection;
  86.          if (points != null)
  87.          {
  88.              collection = points.Select(formatter.Write);
  89.          }
  90.          else
  91.          {
  92.              var geo = value as T;
  93.              collection = formatter.Write(geo);
  94.  
  95.          }
  96.          string json = JsonConvert.SerializeObject(collection, Formatting.Indented, new JsonSerializerSettings
  97.          {
  98.              TypeNameHandling = TypeNameHandling.None
  99.          });
  100.        
  101.          // Serialize the object data as GeoJson
  102.          serializer.Serialize(writer, collection);
  103.         
  104.      }
  105.  }

It is also possible to attach the JSON Converter imperatively using the following code (webApiConfig.cs)

Code Snippet
  1.  
  2. foreach (var formatter in GlobalConfiguration.Configuration.Formatters)
  3. {
  4.     var jsonFormatter = formatter as JsonMediaTypeFormatter;
  5.     if (jsonFormatter == null)
  6.         continue;
  7.  
  8.     jsonFormatter.SerializerSettings.Converters.Add(new GeoJsonConverter<System.Spatial.Geometry>());
  9.     jsonFormatter.SerializerSettings.Converters.Add(new GeoJsonConverter<System.Spatial.Geography>());
  10. }

Now we can build WEB API actions or OData functions that will receive entities with GeoJSON spatial attributes.
For example:

POST http://localhost:7817/odata/Products HTTP/1.1

Content-Type: application/json; charset=utf-8
Accept: application/json
Host: localhost:7817
Content-Length: 534

{
"Id":"1",
"Name":"product1",
"Location": {"type":"Point","coordinates":[90.0,10.0],"crs":{"type":"name","properties":{"name":"EPSG:4326"}}},
"Price":100
}

GET http://localhost:7817/odata/Products HTTP/1.1
Content-Type: application/json; charset=utf-8
Accept: application/json
Host: localhost:7817

Response:

{
"Id":"1",
"Name":"product1",
"Location": {"type":"Point","coordinates":[90.0,10.0],"crs":{"type":"name","properties":{"name":"EPSG:4326"}}},
"Price":100
}

Hope this helps

Manu

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*