A geofence is, according to proximi.io, “… a virtual fence or a perimeter around a physical location.”. Through it, we can monitor an area and, for example, get alarms for unauthorized individuals who enter it. Tile38 provides us with a geospatial database, along with geofencing services, so let’s see how we can use it in .NET.
Considerations
Before we start, I’d like to make it clear that I’m no Tile38 or Redis expert, so this article will not cover best practices. It might even be violating a couple of those, to be honest. So, treat the code in this article as a starting point for your implementation, not as production ready.
Also, reading it will be easier if you are familiar with a couple of concepts, such as PubSub. And although not mandatory, understanding how Redis works might also help you, since it is used by Tile38.
The entire source code is available in this GitHub repository. So if you would like to skip directly to the code, go ahead.
Scenario
We have gotten a request from a client, who says that he runs some sort of security business. We were asked to provide an application where he could monitor the establishments under his protection. The software must raise notifications whenever a blacklisted person enters the establishment perimeter. So this is how our brand new blockbuster app, My Lite Mafia, begins.
Starting a Tile38 server
There are several ways to install Tile38 and get it running, and all of them are fairly easy to accomplish. In this tutorial, we will be using the Docker approach, as it is the simplest one in my opinion. In order to run it, simply execute the following command and you should get a Tile38 container going.
docker run -p 9851:9851 tile38/tile38
Connecting to the Tile38 server
In order to connect to our Tile38 server, we will be using StackExchange.Redis, a high-performance general-purpose Redis client for .NET languages. In our solution, we will leave the project “MyLiteMafia.Tile38Facade” responsible for all communications with it. The StackExchange.Redis client can be installed through the following command:
Install-Package StackExchange.Redis -Version 2.6.111
And then, let’s take a look at the connection code:
public static void RegisterDependencies(IServiceCollection services)
{
services.AddSingleton<IConnectionMultiplexer>(CreateConnectionMultiplexer());
}
private static IConnectionMultiplexer CreateConnectionMultiplexer()
{
var connection = ConnectionMultiplexer.Connect("localhost:9851");
return connection;
}
All we need to connect to our Tile38 instance is to call “ConnectionMultiplexer.Connect(“localhost:9851”);“. Note that we register it as a singleton, as recommended by the StackExchange.Redis documentation. Remember when I told you that we wouldn’t get into best practices? Don’t worry, this will be the only exception, especially since I don’t know any other best practices regarding this library.
Adding items to Tile38 collections
Now that we have a connecting up and running, it’s time to make use of it. We need to store the “blacklisted person” that we’ve mentioned in our scenario, so let’s call this entity “Rival”. There are several supported object types in Tile38, such as GeoJSON, which is the one we will use in this solution. The GeoJSON.Net library provides converters for serialization and deserialization, but be careful to use Newtonsoft.Json when serializing and deserializing, as other tools are not supported and will cause weird errors later on. It can be installed through the following command:
Install-Package GeoJSON.Net
Then let’s see how we can use GeoJSON.Net to represent coordinates for our Rival class:
public class Rival
{
public int RedisId { get; set; }
public Point Coordinates { get; set; }
public Rival(int id, double latitude, double longitude) :
this(latitude, longitude)
{
RedisId = id;
}
public Rival(double latitude, double longitude)
{
Coordinates = new Point(new Position(latitude, longitude));
}
}
Easy right? We just have to provide the position to it while creating it and we’re good to go. Let’s proceed and check how we can store the coordinates information in Tile38.
private int pkCounter = 1;
private IDatabase _database => _redis.GetDatabase();
public async Task StoreEntityCoordinatesAsync(Rival entity)
{
var json = JsonConvert.SerializeObject(entity.Coordinates);
if (entity.RedisId == 0)
entity.RedisId = pkCounter++;
await _database.ExecuteAsync("SET", _collection, entity.RedisId.ToString(), "OBJECT", json);
We first need to serialize the Point instance, which is named Coordinates. We don’t serialize the whole entity, since Tile38 is only interested in spatial data. Then we have a very simple way of generating unique IDs (UIDs), and finally, we execute the command in Tile38. In order to understand it better, let’s first get an example from Tile38’s official documentation regarding the SET command.
SET cities tempe OBJECT {"type":"Polygon","coordinates":[[[-111.9787,33.4411],[-111.8902,33.4377],[-111.8950,33.2892],[-111.9739,33.2932],[-111.9787,33.4411]]]}
Of course, this command can have many variations, so we’ll be focusing on our case. Whenever calling the SET command, we need the following arguments:
- cities – The collection
- tempe – The id
- OBJECT – The object identifier
- The GeoJSON
The ExecuteAsync method takes 2 parameters: the command and the array of arguments. So that means that we need to specify each of the arguments separately. For example, to add the example’s GeoJSON, we would need to call the method in the following way:
_database.ExecuteAsync("SET", "citites", "tempe", "OBJECT", @"{""type"":""Polygon"",""coordinates"":[[[-111.9787,33.4411],[-111.8902,33.4377],[-111.8950,33.2892],[-111.9739,33.2932],[-111.9787,33.4411]]]}");
If we were to try to concatenate all the arguments in a single string, like this:
_database.ExecuteAsync("SET", @"citites tempe OBJECT {""type"":""Polygon"",""coordinates"":[[[-111.9787,33.4411],[-111.8902,33.4377],[-111.8950,33.2892],[-111.9739,33.2932],[-111.9787,33.4411]]]}");
… it would not work. Trust me, I accidentally tried this on my first attempt and it took me a long while to figure it out.
Creating geofences in Tile38
Now that we know how to insert objects in Tile38, we can create geofences that will react to those insertions. A geofence notification can be raised for different reasons, such as by an object entering the area, exiting it, and other ones. We can also specify which collection we will be monitoring, which in this case will be the “rivals” collection. And, obviously, we must specify the location of the geofence. Let’s analyze the following code to understand how we can accomplish that.
public async Task<string> CreateNewGeofenceAsync(int establishmentId, IPosition southwesternPoint, IPosition northeasternPoint)
{
var name = $"eg{establishmentId}";
await _database.ExecuteAsync("SETCHAN",
name,
"WITHIN",
"rivals",
"FENCE",
"DETECT",
"enter,exit",
"OBJECT",
JsonConvert.SerializeObject(establishment.Polygon));
return name;
}
As we’ve seen before, the ExecuteAsync call takes the command, in this case, SETCHAN, and a list of arguments as parameters. Let’s check them:
- A unique name for each geofence. It needs to be unique otherwise it will be overwritten.
- “WITHIN” – The search type. We chose “WITHIN“, which will only search elements that are fully contained inside the geofence. It could also be “INTERSECTS” or “NEARBY“.
- “rivals” – the collection that shall be monitored. Only updates in this collection will trigger geofence violations.
- “FENCE” – a required keyword
- “DETECT enter,exit” – an optional argument that specifies what type of detections the channel shall raise. Here’s a list of all possible detections, extracted from the official documentation:
- inside is when an object is inside the specified area.
- outside is when an object is outside the specified area.
- enter is when an object that was not previously in the fence has entered the area.
- exit is when an object that was previously in the fence has exited the area.
- cross is when an object that was not previously in the fence has entered and exited the area.
- “OBJECT geoJSON” – the geofence’s area. We can use any of the object types supported by Tile38, such as POINT and BOUNDS.
Subscribing to Tile38 geofence notifications
Now that we know how to create geofences, it’s time we act on their notifications. Don’t worry, it’s much more simple than creating them.
public event EventHandler<GeofenceNotificationEventArgs> GeofenceNotificationReceived;
public async Task SubscribeToGeofenceAsync(string channelName)
{
var channel = await _redis.GetSubscriber().SubscribeAsync(channelName);
channel.OnMessage(x => GeofenceNotificationReceived?.Invoke(this, new GeofenceNotificationEventArgs(x.Message)));
}
We begin by subscribing to the geofence, by calling SubscribeAsync(channelName), where the channel name is the unique name we created earlier when executing the SETCHAN command. After that, we just have to decide what do we want to do with the notifications. In this example, we are raising the GeofenceNotificationReceived event. Here is a sample notification, copied from the official documentation:
{
"command": "set",
"group": "5c5203ccf5ec4e4f349fd038",
"detect": "inside",
"hook": "warehouse",
"key": "fleet",
"time": "2019-01-30T13:06:36.769273-07:00",
"id": "bus",
"object": { "type": "Point", "coordinates": [-112.26, 33.46] }
}
Conclusion
Setting up a Tile38 can be very easy, and once you know how it works, so is adding data to it. Through the various object types supported by Tile38, it can be pretty easy to set up new geofences, and also powerful if we need polygon-shaped areas. Of course, in this article, we didn’t cover how to get spatial data, which can be a huge challenge, so creating a full-fledged software can be really complex.
If you would like to fiddle with geofences, the source code for this software can be found here. It uses a canvas to simulate a map, so you can play around adding establishments and moving rivals to raise geofence notifications.