Skip to Content
Technical Articles

How to use Geolocation Services to check violation of geofences

SAP Leonardo IoT is currently exposing several functionalities and modules. One of them is the Geolocation Services.

The Geolocation Service is a new functionality that permits to enrich SAP Leonardo IoT with the capability to locate a specific object or device based on its geographical coordinates.

With the Geolocation Service, it’s required to take confidence with some concepts and the related terminology.

 

Concepts

 

The first concept is the Geometry type. The geometry is the type of physical entity we are considering for the geolocation.

There are currently supported two type of geometries:

  • Point Of Interest, in short POI, is a single point categorized troughs its Longitude and Latitude
  • Area Of Interest, also called AOI, that is an array of more than two coordinates. Basically, it is the physical representation of a polygon and the array of coordinates is a closed array (the first coordinate of the list is exactly equal to the last).

 

Another important concept the Geolocation Space. It represents the concept of the current scope.  It’s possible to specify inside each space a set of geometries, geolocations, and hierarchies of geolocations.

The last but not the least for importance is the concept of Geofence.

The geofence is a Geolocation (a point or an area of interest) that need to be monitored by the business logic in order to identify if another POI is inside or outside of it.

In addition, it permits to easily identify what geofence has been violated, which means that a device or an object is entering or exiting the geofence.

SAP Leonardo IoT Geolocation Services offers a set of APIs to define the object discussed above and to test or compute the proximity of the geofences.

 

All the APIs use the GeoJSON format, that uses [Longitude, Latitude] to identify a coordinate like expressed in https://tools.ietf.org/html/rfc7946#section-3

 

The story

ACME is a company and its business is the deliveries.

They are going to monitor the trucks in order to keep tracks of the trucks that are loading at the storage, and the trucks that are next to arrive at it.

The goal is to reduce the downtime during the loading phase, optimize the loading process, the storage of the packages that will be prepared in time accordingly with the planned arrival time of the truck.

 

The company has decided to use SAP Leonardo IoT Geolocation Services to make some optimizations and to implement a monitoring system, by creating some geofences and an application to monitor them.

 

The physical device of the story is a truck; its digital twin representation is a Thing with the name “ACMEtruck1” that implements the Thing Type “truck”.

This device is currently sending as measurement the position of the truck (longitude and latitude) and status of the operations.

This status permits, in particular, to evaluate the delivery time, the time passed by the truck into the storage, including the time lost (waiting for the next operation) and the loading time.

 

Implementation

 

The implementation consists of a UI that permits to see in real-time the position of the truck into a map (where there are also plotted the geofences), and a list of geofences events that permit to anticipate the preliminary operations required for the loading and unloading of the truck.

The UI is consuming data and services provided by a service that is running on Cloud Foundry, which implements the required logic for this use case.

 

The other services involved in this example are: SAP Cloud Platform Internet of Things for the part of the data ingestion and device onboarding; SAP Leonardo IoT for the Geolocation Services, the digital twin definition, and the creation of custom events; SAP Cloud Foundry Application Runtime required to deploy the custom service to Cloud Foundry.

This is the schema of the implementation:

In particular, in this blog post, the focus will be on the implementation of the services.

 

Create the datastructure

 

It’s a good practice the definition of a new geolocation space accordingly with the geolocation APIs structure.

We have used Postman to create it, but you can use your favorite HTTP client (such as curl).

To proceed with its creation, you need to discover what are your endpoints (both for the events and geolocations) and your oAuth2 authentication details.

To discover them go to SAP Cloud Platform Cockpit and open the Cloud Foundry subaccount, then to Spaces and navigate the space where the instance of SAP Leonardo IoT have been created during the onboarding phase.

Click Service Instances and open the instance with Service name “iotae” and then Service Keys.

Note it down from the json of your keys the addresses of:

 

Now copy the oAuth2 details; in the uua part copy the following fields:

 

The creation of the space is a POST method

In the header list add a new header with:

  • Key: “Content-Type”
  • Value: “application/json”

 

In the authorization tab select oAuth2 compile the form to Get New Access Token.

The access token url is the field url you have copied before from the Service Keys that are stored in your Cloud Platform cockpit. at the end of url add the string: /oauth/token (e.g: https://iotpmdemosenv.authentication.eu10.hana.ondemand.com/oauth/token)

 

In the header list add a new header with:

  • Key: “Content-Type”
  • Value: “application/json”

 

The format of the body is the following:

 

[{
	"SpaceName": "Storage",
	"Geometry": {
		"Type": "Polygon"
	},
	"Descriptions": [{
		"Locale": "en",
		"Label": "Storage Area"
	}, {
		"Locale": "it",
		"Label": "Area Magazzino"
	}],
	"ParentSpaceId": null
}]

 

It’s possible to define for the field Descriptions all the desired localized descriptions.

Once it’s invoked a 201 message identifies that the operation has been completed successfully.

 

Event – Create property set

 

The creation of a custom event is necessary to have the geofence violation events semantically isolated from the standard SAP events.

For the Authentication and headers use the same details used for the creation of the geolocation space.

Now let’s compile the body of the POST and the address:

  • Address: <“event-sap” endpoint>/ES/v1/EventPropertySetTypes

(e.g. https://events-sap.cfapps.eu10.hana.ondemand.com/ES/v1/EventPropertySetTypes )

  • Body:
{
	"Name": "iot.iotpmdemosenv.truck:geofenceViolationSet",
	"PackageName": "iot.iotpmdemosenv.truck",
	"Descriptions": [{
		"LanguageCode": "en",
		"Description": "geofenceViolationSet"
	}, {
		"LanguageCode": "it",
		"Description": "geofenceViolationSet"
	}],
	"DataCategory": "EventData",
	"Properties": [{
		"Name": "geofenceViolation",
		"Type": "String",
		"PropertyLength": "127",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "geofence violation"
		}, {
			"LanguageCode": "it",
			"Description": "Violazione di geofence"
		}]
	}]
}

 

Press Send to create it. An HTTP status 201 for the response identifies a success.

The events name are always with the following format <org>.<tenantname>.<package> à iot.iotpmdemosenv.truck

 

Event – Create EventType

 

Use the same oAuth token and headers of the step before, and compile the POST method with the following details:

  • Address: <“event-sap” endpoint>/ES/v1/EventTypes

(e.g. https://events-sap.cfapps.eu10.hana.ondemand.com/ES/v1/EventTypes )

  • Body:
{
	"Name": "iot.iotpmdemosenv.truck:geofenceViolation",
	"EventTypeState": "Mutable",
	"Descriptions": [{
		"LanguageCode": "en",
		"Description": "Event type for geofence violation"
	}, {
		"LanguageCode": "it",
		"Description": "Tipo evento per violazione di geofence"
	}],
	"PackageName": "iot.iotpmdemosenv.truck",
	"PropertySetId": "geofenceViolationSet",
	"PropertySetType": "iot.iotpmdemosenv.truck:geofenceViolationSet",
	"PropertySetDescriptions": [{
		"LanguageCode": "en",
		"Description": "geofence violation property set"
	}],
	"Statuses": [{
		"EventStatus": "Ingress",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Entering the geofence"
		}]
	}, {
		"EventStatus": "Outgress",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Exiting the geofence"
		}]
	}, {
		"EventStatus": "In",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Inside the geofence"
		}]
	}, {
		"EventStatus": "Out",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Outside the geofence"
		}]
	}, {
		"EventStatus": "Undefined",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Illegal State"
		}]
	}],
	"Severities": [{
		"EventSeverity": 1,
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "High"
		}]
	}, {
		"EventSeverity": 2,
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Normal"
		}]
	}, {
		"EventSeverity": 3,
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Low"
		}]
	}],
	"Codes": [{
		"EventCode": "EQ12",
		"Descriptions": [{
			"LanguageCode": "en",
			"Description": "Event Code 12"
		}]
	}]
}

 

The EventTypeState is set to Mutable. This means that you will be able to modify or remove the events.

If this field is not specified in the json or specified as Immutable there is no way to modify or remove the created events.

Press Send to create it. An HTTP status 201 of the response identify a success.

 

Creation of a backend process to monitor the geofence

 

This is the core of the logic. This process is currently implemented within a Maven Java project and it’s responsible to get the current position of the device and to monitor the geofence.

In case of violation, a new event is also generated.

The core of this service is basically made with a scheduled executor pool that is responsible to get the measurement of the geolocation and invoke the geolocation APIs to identify the geofence status.

in the following piece of code, there is the class that implements the geofence checks; it uses open-sources Scribejava and Underscore. In the example, we have called the class GeofenceEvaluation:

package com.sap.iot;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.oauth.OAuth20Service;
import com.github.underscore.lodash.U;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.ExecutionException;

public class GeofenceEvaluation implements Runnable {

    private ObjectMapper mapper = new ObjectMapper();
    private Oauth2 oauth2;
    private OAuth20Service serviceToken;
    private Map<String, String> lastStatus_thingid_fenceid = new HashMap<>();
    private String space;
    private String clientid;
    private String clientsecret;
    private String eventsEndPoint;
    private String thingsEndPoint;
    private String thingtype;
    private String packagename;
    private String eventname;
    private String geolocationEndPoint;
    private boolean firstInit = false;

    GeofenceEvaluation(Map<String, String> configurationSet) {
        space = configurationSet.get("spacename");
        clientid = configurationSet.get("clientid");
        clientsecret = configurationSet.get("clientsecret");
        eventsEndPoint = configurationSet.get("events-sap");
        geolocationEndPoint = configurationSet.get("geolocation");
        thingsEndPoint = configurationSet.get("details-thing-sap");
        thingtype = configurationSet.get("thingtype");
        packagename = configurationSet.get("packagename");
        eventname = configurationSet.get("eventname");

        if (StringUtils.isEmpty(clientid)){
            throw new IllegalStateException("clientid parameter not defined");
        }
        if (StringUtils.isEmpty(clientsecret)){
            throw new IllegalStateException("clientsecret parameter not defined");
        }
        if (StringUtils.isEmpty(eventsEndPoint)){
            throw new IllegalStateException("events-sap parameter not defined");
        }
        if (StringUtils.isEmpty(geolocationEndPoint)){
            throw new IllegalStateException("geolocation parameter not defined");
        }
        if (StringUtils.isEmpty(thingtype)){
            throw new IllegalStateException("thingtype parameter not defined");
        }
    }

    private Map<String, ThingDetails> getThingsDetails(List<String> ids){

        Map<String, ThingDetails> map = new HashMap<>();
        try {
            final OAuth2AccessToken accessToken = serviceToken.getAccessTokenClientCredentialsGrant();
            for (String id : ids) {
                String query = thingsEndPoint + "/CompositeThings/ThingType/v1/" + packagename + ":" + thingtype
                        + "/Things('" + id + "')?$expand=DYN_ENT_" + packagename.replaceAll("[.]","_") + "__Tracking";
                OAuthRequest request = new OAuthRequest(Verb.GET, query);
                request.addHeader("Accept", "*");
                serviceToken.signRequest(accessToken, request);
                final Response response = serviceToken.execute(request);
                //The response body is xml. Use your preferred method to navigate into it.
                //Navigate the xml response content-->m:properties-->d:ThingName-->link-->m:inline-->entry-->content-->m:properties-->d:Tracking.Longitude
                //Navigate the xml response content-->m:properties-->d:ThingName-->link-->m:inline-->entry-->content-->m:properties-->d:Tracking.Latitude
                Map<String, Object> tmpmap = (Map<String, Object>) U.fromXml(response.getBody());
                Object l1Map = tmpmap.get("entry");
                if (l1Map != null){
                    Map<String, Object> things = (Map<String, Object>)l1Map;
                    String thingname = (String)((Map<String, Object>)((Map<String, Object>)things.get("content")).get("m:properties")).get("d:ThingName");
                    List<Object> links = (List<Object>)((Map<String, Object>)l1Map).get("link");

                    ThingDetails pos = new ThingDetails();
                    for (Object link : links) {
                        Object inline = ((Map<String, Object>)link).get("m:inline");
                        if (inline != null){
                            Map<String, Object> content = (Map<String, Object>)((Map<String, Object>)((Map<String, Object>)inline).get("entry")).get("content");
                            try {
                                String lng = (String) ((Map<String, Object>) content.get("m:properties")).get("d:Tracking.Longitude");
                                String lat = (String) ((Map<String, Object>) content.get("m:properties")).get("d:Tracking.Latitude");
                                pos.setLat(lat);
                                pos.setLng(lng);
                                pos.setThingName(thingname);
                                map.put(id, pos);
                            }
                            catch(Exception e){
                                //no valid measurements
                            }
                            break;
                        }
                    }

                }

            }

        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        return map;
    }

    private Map<String,String> getGeofences(){
        Map<String,String> map = new HashMap<>();
        try {
            final OAuth2AccessToken accessToken = serviceToken.getAccessTokenClientCredentialsGrant();
            String query = geolocationEndPoint + "/geolocation/v1/GeoLocations";
            if (!StringUtils.isEmpty(space)){
                query += "?SpaceName=" + space.replaceAll(" ", "%20");
            }
            OAuthRequest request = new OAuthRequest(Verb.GET, query);
            serviceToken.signRequest(accessToken, request);
            final Response response = serviceToken.execute(request);

            JsonNode tmpmap = mapper.readValue(response.getBody(), JsonNode.class);
            //get from the response value-->Properties-->GeoLocationId and value-->Properties-->GeoLocationName
            JsonNode node = tmpmap.get("value");
            for (int i = 0 ; i < node.size(); i++) {
                String id = tmpmap.get("value").get(0).get("Properties").get("GeoLocationId").asText();
                String name = tmpmap.get("value").get(0).get("Properties").get("GeoLocationName").asText();
                map.put(id, name);
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
        return map;
    }

    private boolean geotest(String id, ThingDetails details){
        JsonNode map = null;
        try {
            final OAuth2AccessToken accessToken = serviceToken.getAccessTokenClientCredentialsGrant();
            String query = geolocationEndPoint + "/geolocation/v1/GeoTest/Check/Within/GeoPosition?Latitude=" +
                     details.getLat() + "&Longitude=" + details.getLng() + "&GeoLocationId=" + id;
            OAuthRequest request = new OAuthRequest(Verb.GET, query);
            serviceToken.signRequest(accessToken, request);
            final Response response = serviceToken.execute(request);

            map = mapper.readValue(response.getBody(), JsonNode.class);
            //has a property named Within
            return map.get("Within").asBoolean();
        } catch (IOException | ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    private void postGeofenceViolation(String id, String geofenceName, String thing, String thingName, boolean within){
        try {
            final OAuth2AccessToken accessToken = serviceToken.getAccessTokenClientCredentialsGrant();
            String query = eventsEndPoint + "/ES/EventType/" + packagename + ":" + eventname + "/v1/Events";
            OAuthRequest request = new OAuthRequest(Verb.POST, query);
            SimpleDateFormat fmtTimestamp = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
            Date d = new Date();
            //the payload accordingly with the APIs of Leonardo IoT
            String payload ="{\"BusinessTimestamp\": \"" + fmtTimestamp.format(d) + "\",\"Type\": \"Alert\",\"EventInfo\": \"Alert on geofence " +
                    geofenceName + "\",\"EventStatus\": \"" + (within ? "In" : "Out") + "\",\"EventSeverity\": 1,\"EventCode\": null,\"EventSource\": null,\"ThingId\": \"" +
                    thing + "\",\"ThingProperty\": \"Geofence Violation for Thing: " + thingName + "\",\"ExternalId\": \"" + id + "\"}";
            request.setPayload(payload);
            request.addHeader("Content-Type", "application/json");
            request.addHeader("Accept", "*");
            serviceToken.signRequest(accessToken, request);
            final Response response = serviceToken.execute(request);
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    public void run() {
        if(oauth2 == null){
            oauth2 = new Oauth2(clientid, clientsecret);
        }

        if(serviceToken == null){
            serviceToken = oauth2.getNewService();
        }

        List<String> things = DiscoveryThings.getThingIds();
        List<String> ids;
        synchronized (things) {
            ids = new ArrayList<>(things);
        }
        //init status map
        initFenceStatus(ids);

        Map<String, ThingDetails> thingsDetails = getThingsDetails(ids);
        //getGeofences
        Map<String,String> fenceIds = getGeofences();
        for (String id : fenceIds.keySet()) {
            for (String thing : thingsDetails.keySet()){
                boolean within = geotest(id, thingsDetails.get(thing));
                boolean differentStatus = checkLastStatus(thing, id, within);
                if (differentStatus){
                    //create Event
                    postGeofenceViolation(id, fenceIds.get(id), thing, thingsDetails.get(thing).getThingName(), within);
                }
            }
        }
    }

    private void initFenceStatus(List<String> ids) {
        if (firstInit) {
            return;
        }
        try {
            final OAuth2AccessToken accessToken = serviceToken.getAccessTokenClientCredentialsGrant();
            for (String id : ids) {
                String query = eventsEndPoint + "/ES/EventType/" + packagename + ":" + eventname + "/v1/Events?$filter=substringof(%27" +
                        id + "%27,ThingId)%20and%20substringof(%27In%27,EventStatus)&$orderby=BusinessTimestamp%20desc";
                OAuthRequest request = new OAuthRequest(Verb.GET, query);
                request.addHeader("Accept", "*");
                serviceToken.signRequest(accessToken, request);
                final Response response = serviceToken.execute(request);
                //The response body is xml. Use your preferred method to navigate into it.
                //Navigate the xml response feed-->entry-->content-->m:properties-->d:ExternalId
                Map<String, Object> map = (Map<String, Object>) U.fromXml(response.getBody());
                Object l1Map = map.get("feed");
                Object l2Map = null;
                if (l1Map != null){
                    l2Map = ((Map<String, Object>)l1Map).get("entry");
                }
                if (l2Map != null){
                    Map<String, Object> events = (Map<String, Object>)l2Map;
                        String fenceid = (String)((Map<String, Object>)((Map<String, Object>)events.get("content")).get("m:properties")).get("d:ExternalId");
                        lastStatus_thingid_fenceid.put(id, fenceid);
                }
            }


            firstInit = true;
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }

    private boolean checkLastStatus(String thing, String id, boolean within) {
        String lastfence = lastStatus_thingid_fenceid.get(thing);
        boolean differentStatus = false;
        //egress
        if (lastfence != null && id.contentEquals(lastfence) && !within){
            lastStatus_thingid_fenceid.remove(thing);
            differentStatus = true;
        }
        //fence changed
        else if(lastfence != null && !id.contentEquals(lastfence) && within){
            lastStatus_thingid_fenceid.put(thing, id);
            differentStatus = true;
        }
        //ingress
        else if(lastfence == null && within){
            lastStatus_thingid_fenceid.put(thing, id);
            differentStatus = true;
        }
        return differentStatus;
    }
}

We also have defined a class to discover if new things have been onboarded and need to be analyzed from the main services to identify any geofence violation, the class name is DiscoveryThings:

package com.sap.iot;

import com.github.scribejava.core.model.OAuth2AccessToken;
import com.github.scribejava.core.model.OAuthRequest;
import com.github.scribejava.core.model.Response;
import com.github.scribejava.core.model.Verb;
import com.github.scribejava.core.oauth.OAuth20Service;
import com.github.underscore.lodash.U;
import org.apache.commons.lang3.StringUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

public class DiscoveryThings implements Runnable {
    private Oauth2 oauth2;
    private OAuth20Service serviceToken;
    private String clientid;
    private String clientsecret;
    private String thingsEndPoint;
    private String thingtype;
    private static List<String> thingIds = Collections.synchronizedList(new ArrayList<>());

    DiscoveryThings(Map<String, String> configurationSet) {
        clientid = configurationSet.get("clientid");
        clientsecret = configurationSet.get("clientsecret");
        thingsEndPoint = configurationSet.get("advancedlist-thing-sap");
        thingtype = configurationSet.get("thingtype");

        if (StringUtils.isEmpty(clientid)){
            throw new IllegalStateException("clientid parameter not defined");
        }
        if (StringUtils.isEmpty(clientsecret)){
            throw new IllegalStateException("clientsecret parameter not defined");
        }
        if (StringUtils.isEmpty(thingsEndPoint)){
            throw new IllegalStateException("advancedlist-thing-sap parameter not defined");
        }

    }


    static List<String> getThingIds() {
        return thingIds;
    }

    public static void setThingIds(List<String> thingIds) {
        DiscoveryThings.thingIds = thingIds;
    }

    @Override
    public void run() {
        if(oauth2 == null){
            oauth2 = new Oauth2(clientid, clientsecret);
        }

        if(serviceToken == null){
            serviceToken = oauth2.getNewService();
        }
        try {
            final OAuth2AccessToken accessToken = serviceToken.getAccessTokenClientCredentialsGrant();
            String query = thingsEndPoint + "/CompositeThings/v1/Things?$filter=substringof(%27" + thingtype + "%27,ThingType)";
            OAuthRequest request = new OAuthRequest(Verb.GET, query);
            request.addHeader("Accept", "*");
            serviceToken.signRequest(accessToken, request);
            final Response response = serviceToken.execute(request);
            //The response body is xml. Use your preferred method to navigate into it.
            //Navigate the xml response feed-->entry-->content-->m:properties-->d:ThingId
            Map<String, Object> map = (Map<String, Object>) U.fromXml(response.getBody());
            Object l1Map = map.get("feed");
            Object l2Map = null;
            if (l1Map != null){
                l2Map = ((Map<String, Object>)l1Map).get("entry");
            }
            if (l2Map != null){
                List<Map<String, Object>> things = (List<Map<String, Object>>)l2Map;
                synchronized (thingIds) {
                    thingIds.clear();
                    for(Map<String, Object> thing : things){
                        String thingId = (String)((Map<String, Object>)((Map<String, Object>)thing.get("content")).get("m:properties")).get("d:ThingId");
                        thingIds.add(thingId);
                    }
                }
            }
        } catch (IOException | InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}

Into the class LeonardoIoTAPIAuth is implemented the Scribejava implementation to get the oAuth token for SAP Leonardo Iot:

package com.sap.iot;

import com.github.scribejava.core.builder.api.DefaultApi20;

import java.util.Map;

public class LeonardoIoTAPIAuth extends DefaultApi20 {
    private static String accessToken;
    private static String url;

    public static void setAccessToken(String accessToken) {
        LeonardoIoTAPIAuth.accessToken = accessToken + "/oauth/token";
    }

    public static void setUrl(String url) {
        LeonardoIoTAPIAuth.url = url;
    }

    protected LeonardoIoTAPIAuth() {
    }

    private static class InstanceHolder {
        private static final LeonardoIoTAPIAuth INSTANCE = new LeonardoIoTAPIAuth();
    }

    public static LeonardoIoTAPIAuth instance() {
        return InstanceHolder.INSTANCE;
    }

    public String getAccessTokenEndpoint() {
        return accessToken;
    }

    protected String getAuthorizationBaseUrl() {
        return url;
    }

    @Override
    public String getAuthorizationUrl(String responseType, String apiKey, String callback, String scope, String state, Map<String, String> additionalParams) {
        return super.getAuthorizationUrl(responseType, apiKey, callback, scope, state, additionalParams);
    }
}

The class OAuth2 is also part of the Scribejava implementation:

package com.sap.iot;

import com.github.scribejava.core.builder.api.DefaultApi20;

import java.util.Map;

public class LeonardoIoTAPIAuth extends DefaultApi20 {
    private static String accessToken;
    private static String url;

    public static void setAccessToken(String accessToken) {
        LeonardoIoTAPIAuth.accessToken = accessToken + "/oauth/token";
    }

    public static void setUrl(String url) {
        LeonardoIoTAPIAuth.url = url;
    }

    protected LeonardoIoTAPIAuth() {
    }

    private static class InstanceHolder {
        private static final LeonardoIoTAPIAuth INSTANCE = new LeonardoIoTAPIAuth();
    }

    public static LeonardoIoTAPIAuth instance() {
        return InstanceHolder.INSTANCE;
    }

    public String getAccessTokenEndpoint() {
        return accessToken;
    }

    protected String getAuthorizationBaseUrl() {
        return url;
    }

    @Override
    public String getAuthorizationUrl(String responseType, String apiKey, String callback, String scope, String state, Map<String, String> additionalParams) {
        return super.getAuthorizationUrl(responseType, apiKey, callback, scope, state, additionalParams);
    }
}

These classes implement Runnable and are launched as a service at a fixed rate, class TruckOrganizer:

public static void main (String[] args){
        //Settings is a class not provided in the example that just import your configuration from a json file
        Settings set = new Settings();
        try {
            set.importConfiguration();
        } catch (URISyntaxException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        //The services
        Runnable evaluateGeofence = new GeofenceEvaluation(set.getConfigurationSet());
        Runnable thingDiscovery = new DiscoveryThings(set.getConfigurationSet());

        //thing discovery
        scheduledExecutorService.scheduleAtFixedRate(thingDiscovery, 0l, 5l, TimeUnit.HOURS);
        //geofence evaluation
        scheduledExecutorService.scheduleAtFixedRate(evaluateGeofence, 5l, 5l, TimeUnit.SECONDS);

    }

In the code we are also using some details that came from a configuration file (imported as Map); the file is the following, file settings.json:

{
  "details-thing-sap": "https://details-thing-sap.cfapps.eu10.hana.ondemand.com",
  "advancedlist-thing-sap": "https://advancedlist-thing-sap.cfapps.eu10.hana.ondemand.com",
  "geolocation": "https://sap-iot-noah-live-geolocation-runtime.cfapps.eu10.hana.ondemand.com",
  "events-sap": "https://events-sap.cfapps.eu10.hana.ondemand.com",
  "clientid": "sb-dba2b987-9dc2-4e92-983a-06c7bccd5fa1!b11300|iotae_service!b5",
  "spacename": "Storage",
  "clientsecret": "<here my secret>",
  "url": "https://iotpmdemosenv.authentication.eu10.hana.ondemand.com",
  "thingtype": "PalletType",
  "eventname": "geofenceViolation",
  "packagename": "iot.iotpmdemosenv.truck"
}

 

Now we can run the service in the background. We have decided to run it as a standalone service on Cloud Foundry:

 

cf push truckTrack -u process -p target/truckTrack-jar-with-dependencies.jar

 

Conclusion

As a final operation, Acme has created a simple UI5 application to collect all the events of geofence violation and verify in real-time where the trucks are.

This application has:

  • A map where all the geofences are charted
  • A thing list to permits to real-time track the position of the trucks
  • A list to collect all the events of geofence violation

The frontend of the UI is also responsible to commit changes and create (graphically) the geofence.

You can also commit it directly within Postman by invoking the following API:

 

In this blog post we have explained how to use SAP Leonardo IoT Geolocation Services and how to implement a custom service to compute the geofence violation with its capabilities.

3 Comments
You must be Logged on to comment or reply to a post.
  • Thanks a lot for this nice write up. Have always enjoyed following your blog. In your coding the get the GeoFences, does the call to GeoLocationService always return all the GeoFences ? If so, I suspect if it would be scalable when the customer can define 1000’s of Geofences ? Can we not fetch only the subset of Geofences based on the current location of the truck ? If such a functionality does not exist on the Geolocation service, can this not be a relevant feature request ?

    • Hi Yashas,

      Currently, there are few options to organize the geofence.

      The solution offered out of the box with the geolocation services is to use the hierarchy.

      With this solution, you will be able to identify a subset of geofences that could be invoked for the current asset, instead of invoking the geofence for every asset.

      Another solution is to reverse the process. it means to search for geolocations in close vicinity to another geolocation by using the API

      /geolocation/v1/GeoTest/Search/Within/GeoLocation

      it should solve every scalability problem

      Best Regards

      Marco