This article continues the discussion of tracking moving objects in real-time. The previous article discussed using the Hangout operator  to discover points of interest (POI) based on where people or objects spent time. In this post, I’ll discuss scenarios where objects are near or around a known point of interest. You might want to know when people arrive at or leave a business or popular attraction to send them safety alerts or marketing promotions.

Table of Contents


Introduction

To detect when a moving object is near a known area of interest, the first step is to define a virtual perimeter around the area. Then, we can compare the location of each object to that of the perimeter. This perimeter is called a geofence.¬† The technique of using an object’s presence within a geofence to trigger actions is called unsurprisingly called geofencing.

In this article, I’ll cover how to use the geospatial toolkit to handle these common scenarios:

  • Simple geofencing: Detect when an object is within X m of a point of interest.
  • Polygon based geofencing:
    • Detecting when an object is within a predefined region, and
    • Detecting when an object enters or exits a known region.


Key terms and prerequisites

Prerequisites
Some familiarity with Streams, including concepts such as operators, toolkits, tuples. See the Quick Start Guide for an introduction to these topics.

If you do not have a Streams installation, download the Quick Start edition.

Key terms used in this tutorial:

Moving object: Any device which is reporting its location periodically to Streams: a bus, taxi, ship, rhinoceros or a cell phone with a person who is moving (walking, biking etc.).

Location data stream: This is the input to our application; a stream of data describing the current location of the objects being tracked.  At a minimum, each tuple in this stream must include the following attributes describing the current location of a moving object: latitude, longitude, timestamp and a unique string identifier.  All operators in the geospatial toolkit require this data as input.

Our location data stream in this article is NextBus data. The NextBus service receives data from public transit buses describing their current location, and makes it available for application developers.  The streamsx.transportation toolkit, which already has operators to connect to NextBus.


The problem

Imagine a municipality that wants to increase revenue and provide more value to riders.

We want to use the location data stream to accomplish the following:

  • Provide value: Whenever a bus is approaching an area with an incident, such as an emergency, an accident, pedestrian bridge closure, they would like to provide alerts that can be displayed within the bus for riders.
  • Increase revenue: Sell advertising to businesses on or near the route of buses in real time. Riders can receive alerts about ongoing, short term promotions as the bus travels by the business.

These two use cases are examples of a more generic problem: detecting when the moving objects being tracked are within a certain distance of a known point.

Using geofencing, we can send a message to any bus as it approaches a point of interest.


Simple geofencing

Along a bus’ route, we have known points of interest, e.g. a station where a road is closed, or a store with promotions, and we want to detect when the buses are within Xmeters of the point of interest.

In this case, our geofence, or virtual perimeter is a circle of radius X meters, with the point of interest P at the centre:

The solution is to calculate the distance between the point of interest and the location of each moving objects. If the calculated distance is less than the specified radius, the object must be within the geofence.

The distance function of the geospatial toolkit does exactly that: computes the distance between 2 points or geometries.

So, for each reported location in the location input stream, we’ll use the distance function in a Custom operator to compute the distance to P.

Points passed as parameters to the distance function must be specified as Well Known Text. ¬†WKT is a format for defining geospatial features like lines and polygons. Let’s look at an example.

Example

Our location data stream is data from buses moving in and around San Francisco. Let’s use AT&T Park as our first point of interest.  Whenever the Giants win a game, a local taxi company, GetThere, will offer discounted fares to anyone leaving the stadium if they present a game ticket.

To alert bus riders of this promotion, we will display an alert inside the bus when a bus is within 750 m away from the stadium.

Let’s start by defining our geofence. The centre of the park has latitude and longitude coordinates (37.7785951,-122.3914585). The well-known text representation of this point is POINT (-122.391458537 37.7785951).

Using a Beacon to send a tuple describing this point of interest to the Custom:

stream<POI_WKT_Type> POIStream as O = Beacon(){
     param
         iterations: 1u;
     output
       O : POI_ID ="AT & T Park",
           locationWKT¬† = ‚ÄúPOINT(-122.391458537 ¬†37.7785951)‚ÄĚ,
           radius =  750.0,
           message =¬†"If the Giants win, show your game ticket to get 15% off GetThere taxi!‚ÄĚ;
}

This data is sent to the Custom operator that will perform the geofencing:

stream <POIMatches>NearbyVehicles =  Custom(POIStream; BusLocationStream )
      {
      	logic 
      		state: {
      			mutable list POIList = [];
      		}
      		onTuple POIStream: {
      			appendM(POIList, POIStream); //add the POIs to a list.
...               }
      		}

The Custom operator is set up to receive information about points of interest on the first port. These POIs are added to a list.

When a tuple containing a bus’ location is received on the second input port,¬†BusLocations,¬† the distance between the bus and each POI in the list is calculated.

If the distance is within the desired radius, it will submit a new tuple that contains all the data on the location input stream, as well as the message that should be sent to the bus.


      		onTuple BusLocationStream: {  
      			//lookup the various POIs and check if the point is within any of them
                        rstring busWKT = point(BusStream.longitude,BusStream.latitude);
      			for (POI_WKT_Type poi in POIList){ //for each POI
                                //compute distance to POI
                                float64 distanceFromPOI = distance(busWKT, poi.locationWKT);

      				if (distanceFromPOI < poi.radius){ 
      					mutable POIMatches out = {};
      					
      					out.distance = distanceFromPOI;
      					out.fixedLocationId =  poi.POI_ID;
      					out.message = poi.message;
                                        assignFrom (out, BusStream);

      					submit(out, NearbyVehicles);
      				}
      			}
      		
      		}
}

Downstream,  a Customoperator prints the output:

Bus 28 is 337.331294301282 meters away from AT&T Park, message =If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus N is 261.595953232405 meters away from AT&T Park, message = If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus 28 is 251.621552608414 meters away from AT&T Park, message = If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus N is 187.984873993083 meters away from AT&T Park, message = If the Giants win, show your game ticket to get 15% off a GetThere taxi!

There are two problems with this very simple solution:

  • First, as you can see in the output, a bus that remained within the given radius would continually receive alerts. You could solve this by keeping state of which buses have been alerted.
  • Second, using a point and a radius around that point as our geofence means that we can only use circular geofences. Geofencing with polygons of arbitrary shapes can be done using the Geofence¬†operator.


Polygon based geofencing with the Geofence operator

Some use cases require a geofence of arbitrary shape. Consider a cell phone company wishing to bill customers for roaming when they leave the state. In that scenario, the geofence must match the state’s diameters exactly and cannot be a circle:

The diagram above shows how circles are sometimes insufficient for geofencing and a more complex shape is required.  Using a point in New York state as the central point of interest and adding a radius would create a geofence much larger than needed.

To solve this problem, use the Geofence operator.  It allows you to define  geofences as a region of any shape.  The geofences it uses are geometric shapes called polygons.   Instead of computing the distance from an object’s location to the point of interest, it determines if the interior of the polygon contains the point where the object is located.

You supply geofences to the Geofence operator, and it produces output indicating which geofences contain objects in the location data stream.


Defining a geofence for the Geofence operator

Each geofence used by the operator is defined by its id and the boundaries of the polygon covering the area.

A polygon must also be specified as  Well Known Text (WKT). WKT Polygons are defined as a list of line segments that make up the polygon’s boundaries. Each line segment is a made up of 2 points.
Polygons provided to the Geofence operator can overlap, or be contained within other polygons.

Drawing a polygon

There are tools to help you generate the WKT for a polygon simply by drawing it on a map.
These include Wicket and the OpenStreetMap WKT Playground.
Polygons used by the Geofence operator must be drawn using the left-handed rule, which just means you must draw the polygon in a counter clockwise fashion.

This animation shows how to use Wicket to generate a polygon as WKT:

If you are not drawing the polygon but are generating it as a set of line segments, the line segments must be arranged counter clockwise.

Once you have your fences defined as WKT, you are ready to use the Geofence operator.


Using the Geofence operator

The Geofence operator has 2 input ports, one for the location data stream, and the other to specify the geofences.
The operator’s output depends how the value of its outputMode parameter.  A value of membership will only produce the list of current fences, while events will also compute the list of fences entered and exited.

Let’s look at another example.


Reporting when an object is within the geofence

Our geofence is still AT&T Park, but this time we have defined a polygon around the area.  We’ll use a Beacon to send the id and WKT polygon for the park to the Geofence operator:


stream<FenceData> Geofences = Beacon(){

	param
              iterations: 1u;
	output
	      Geofences: 
                 fenceId= " AT&T Park", //name of the geofence 
		 fenceUpdateAction= 1,  //1 = add this geofence
		fencePolygon = "POLYGON((-122.3903103862495  37.77704967176213,-122.38741360051341 37.77835559206916,-122.38792858464427 37.78174748508426,-122.38870106084056 37.78218841974413,-122.39509544713206 37.77720231298866,-122.39316425664134 37.77562500511634,-122.38968811375804 37.77179183712787,-122.38913021428294 37.774539526387294,-122.3903103862495 37.77704967176213))";
			
}

The Geofence operator with outputMode set to membership will receive the geofence information on one port, and the Bus location data on another:


stream<LocationAndFenceData> BusLocationWithFences =Geofence(BusLocationStream ; Geofences){
	param
	     outputMode : membership ;
	output
	     BusLocationWithFences : 
                 currentFences = CurrentFences() ;
}

For each incoming tuple that reports an object’s location, the currentFences() function returns a list of all the geofences that contain that location.

Downstream, we again have a Custom that looks up the message for each fence, and prints the alert:


Bus 30 is within AT&T Park, message: If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus 30 is within AT&T Park, message: If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus 30 is in AT&T Park, message: If the Giants win, show your game ticket to get 15% off a GetThere taxi!

Let’s enhance this to only send the message when the bus enters and/or exits a geofence.


Determine when an object enters or exits the geofence

You could use this information to make sure that buses receive alerts only when they enter the geofence, and that the alert is removed when they exit the geofence. Or, this could be used to compute statistics such as total time spent within the geofence.
Change the Geofence operator to keep track of entry and exit events by changing the outputMode to events:


stream<LocationAndFenceEvents> BusLocationWithEvents =
			Geofence(BusLocationStream ; Geofences){
	param
	     outputMode : events ;
	output
	     BusLocationWithEvents : 
                  currentFences = CurrentFences(), 
                  fencesEntered = FencesEntered(), 
                  fencesExited = FencesExited() ;
}

Notice that there are 2 additional lists in the output: fencesEntered(), and fencesExited(). These functions report which geofences the object has just entered or has exited.

Printing the application’s output:
Bus 30 entered AT&T Park, entry message: If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus KT entered AT&T Park, entry message: If the Giants win, show your game ticket to get 15% off a GetThere taxi!
Bus 30 exited AT&T Park

Updating geofences at runtime

As mentioned before, use the second port of the Geofence operator to add geofences.  You can add or delete geofences at any time during the life of the application. The fences could come from an external data source such as Kafka, MQTT, or any supported database.

In addition to the geofence’s id and the WKT description of the polygon, use the fenceUpdateAction attribute to indicate whether the geofence is added or removed from the operator. (1 = add, 0 = remove).

In the case of a circular geofence using a Custom, the same principle applies. A second input port is used to receive geofences.

Best practices

It is a good idea to separate the geofencing code from the data sources that provides the geofences and the location input stream.

One way to do so is by using a microservice architecture.  This allows you to change a data source without having to recompile or resubmit the main geofencing application.

The sample applications for this article demonstrate this principle.   The geofences and bus data streams are exported using Publish, and the Geofence operator connects to both streams using Subscribe.


Running the samples

The samples are included in the streamsx.transportation toolkit.

Clone the repository and import the projects into Streams Studio.

Launch either of the 2 geofencing applications first.  The circular geofence application is called CircularGeofencing, and the polygon based geofencing using the Geofence operator is in GeofencingWithPolygons.

Then you can launch the data source applications:

  • PublishBusLocation: connects to NextBus and exports the stream of bus data.
  • GeofenceSource: exports a stream of geofences and the alerts

Check the console output of the AlertPrinter operator for output.

Summary

This article concludes a two-part series on the various ways the Geospatial toolkit can be used to track moving objects and detect patterns in their behaviour.

Demos featuring  the geofence operator:

Introducing real time geofencing (Youtube video)
Geofence: Smart Marketing

Useful links and reference information

Please leave a comment below if you have a question.

 

Join The Discussion