Axeda: All about Geofences

    Requirements: 6.1.2+

    Geofences are geometric shapes drawn virtually on a geographical area that represents a fence that can be crossed by a device.  The Axeda Platform has built-in support for mobile locations and geofences, which can be linked to the rules engine to enable notifications based on geofence crossing.

    What this tutorial covers

    This tutorial demonstrates the workflow of creating a geofence through to creating the expression rules with notifications, then how the mobile location can trigger the rules.

    1) Creating the Geofence
    2) Creating the Expression Rule

    There is currently no user interface built into the Axeda Applications Console which interacts with geofences.  For a sample application with a geofence user interface, see Sample Project: Traxeda (TODO).  For a single Custom Object that includes all of the functionality described below, see the end of  this document.

    The properties of a geofence are a name, a description, and a series of coordinates based on Well-Known Text (WKT) syntax (see the OpenGIS Simple Features Specification).

    def addGeofence(CONTEXT, map){
        Geofence myGeofence = new Geofence(CONTEXT)
           myGeofence.name = map.name
        if(map.type != "polygon" && map.type != "circle")
        {
            throw new Exception("Invalid type: need 'polygon' or 'circle', not '$map.type'")
        }
        else if(map.type == "polygon")
        {
            def geo = map.locs.loc.inject( "POLYGON (("){ str, item ->
                def lng = item.lng
                def lat = item.lat
                str += "$lng $lat,"
            str
            }
            //the first location also has to be the last location
            myGeofence.geometry = geo + map.locs.loc[0].lng + " " + map.locs.loc[0].lat + "))"
            //Something like this is built:
            //POLYGON ((-71.082118 42.383892,-70.867198 42.540923,-71.203654 42.495374,-71.284678 42.349394,-71.163829 42.221382,-71.003154 42.266114,-71.082118 42.383892))
        }
        else if(map.type == "circle")
        {
            def lng = map.locs.loc[0].lng
            def lat = map.locs.loc[0].lat
            myGeofence.geometry = "POINT ($lng $lat)"
            //POINT (-71.082118 42.383892)
            myGeofence.buffer = map.radius.toDouble()
        }
        myGeofence.description = "ALERT:::$map.alertType:::$map.alert"
        try {
             myGeofence.store()
        }
        catch (e){
            logger.info e.localizedMessage 
                return null
        }
        myGeofence
    }
    

    The geofence itself does not interact with devices in any way.  Rather it is the Expression Rule that is applied to models and devices and that invokes the geofence when a mobile location is passed in.

    Creating the Expression Rule

    The Expression Rule for the Geofence is built as follows:

    TYPE: MobileLocation
    IF:  Expression set to "InNamedGeofence" for entering and "!InNamedGeofence" for exiting.

    The following function creates this expression rule:

    /*
    Sample call
    
    createGeofenceExpressionRule(CONTEXT, "My Geofence", "rule_MyGeofence", "in", "You entered the geofence!", "SDK Generated Geofence Rule", 100)
    */
    
    def createGeofenceExpressionRule(com.axeda.drm.sdk.Context CONTEXT, String geofencename, String rulename, String alertType, String alertMessage, String ruledescription, int severity){
        ExpressionRuleFinder erf = new ExpressionRuleFinder(CONTEXT)
        erf.setName(rulename)
        ExpressionRule expressionRule1 = erf.findOne()
        expressionRule1?.delete()
        
        def expressionRule = new ExpressionRule(CONTEXT)
        expressionRule.setName(rulename) 
        expressionRule.setDescription(ruledescription)
        expressionRule.setTriggerName("MobileLocation")
    
        def ifExpStr = "InNamedGeofence(\"$geofencename\", Location.location)"
        if(alertType == "out"){
            ifExpStr = "!" + ifExpStr
        }
        expressionRule.setIfExpression(new Expression(ifExpStr))
    
        expressionRule.setThenExpression(new Expression("CreateAlarm(\"$alertMessage\", severity)"))
        expressionRule.setEnabled(true)
        expressionRule.setConsecutive(false)
        expressionRule.store()
        expressionRule
    }
    

    Then the rule associations must be created to apply the rule to a model or device.

    /*
    Sample call
    
    findOrCreateRuleAssociations(CONTEXT, myModel, expressionRule, "EXPRESSION_RULE", "MODEL")
    Where expressionRule is the rule created in the above example
    
    */
    def findOrCreateRuleAssociations(Context CONTEXT, Object entity, Object rule, String ruleType, String entityType){
        // rule type is whether this is an expression rule
        ruleType = ruleType ?: "EXPRESSION_RULE"
        entityType = entityType ?: "DEVICE_INCLUDE"
        RuleAssociationFinder ruleAssociationFinder = new RuleAssociationFinder(CONTEXT)
        ruleAssociationFinder.setRuleId(rule.id.value)
        ruleAssociationFinder.setRuleType(RuleType.valueOf(ruleType))
        ruleAssociationFinder.setEntityId(entity.id.value)
        ruleAssociationFinder.setEntityType(EntityType.valueOf(entityType))
        def ruleAssociations = ruleAssociationFinder.findAll()
        if (!ruleAssociations || ruleAssociations?.size() == 0){
            def ruleAssociation = new RuleAssociation(CONTEXT)
            ruleAssociation.entityId = entity.id.value
            ruleAssociation.entityType = EntityType.valueOf(entityType)
            ruleAssociation.ruleType = RuleType.valueOf(ruleType)
            ruleAssociation.setRuleId(rule.id.value)
            ruleAssociation.store()
            ruleAssociations = [ruleAssociation]
        }
        return ruleAssociations
    }
    

    The rule will now be triggered when any device of the applied model sends a mobile location within the geofence, which in turn will create an alarm.

    Here is a custom object with the complete geofence functionality:

    import com.axeda.drm.sdk.Context
    import com.axeda.drm.sdk.geofence.Geofence
    import com.axeda.drm.sdk.geofence.GeofenceFinder
    import com.axeda.drm.sdk.rules.engine.Expression
    import com.axeda.drm.sdk.rules.engine.ExpressionRule
    import com.axeda.drm.sdk.rules.engine.ExpressionRuleFinder
    import com.axeda.drm.sdk.rules.engine.RuleAssociation
    import com.axeda.drm.sdk.rules.engine.RuleAssociationFinder
    import com.axeda.drm.sdk.rules.engine.RuleType
    import com.axeda.drm.sdk.common.EntityType
    import com.axeda.drm.sdk.device.Model
    import com.axeda.drm.sdk.device.ModelFinder
    
    try {
        def Context CONTEXT = Context.getSDKContext()
    
        def model = findOrCreateModel(CONTEXT, "FooModel")
    
        def sampleCircle = [
            "name": "My Circle",
            "alert": "My Geofence Alert Text",
            "type": "circle",
            "alertType": "in",
            "radius": "65.76",
            "locs": [
                [
                    "loc": [   "lat": "42.60970621339408",   "lng": "-73.201904296875"   ]
                ]
            ]
        ]
    
        def samplePolygon = [
            "name": "My Polygon",
            "alert": "My Geofence Alert Text",
            "type": "polygon",
            "alertType": "out",
            "locs": [
                ["loc": [  "lng": -71.2604999542236,  "lat": 42.3384903145478  ]],
                ["loc": [  "lng": -71.4218616485596,  "lat": 42.3242772020001  ]],
                ["loc": [  "lng": -71.5585041046143,  "lat": 42.2653600946699  ]],
                ["loc": [  "lng": -71.5413379669189,  "lat": 42.1885837119108  ]],
                ["loc": [  "lng": -71.4719867706299,  "lat": 42.1137514551207  ]],
                ["loc": [  "lng": -71.3737964630127,  "lat": 42.0398506628541  ]],
                ["loc": [  "lng": -71.2508869171143,  "lat": 42.0311807962068  ]],
                ["loc": [  "lng": -71.1355304718018,  "lat": 42.2084223174036  ]],
                ["loc": [  "lng": -71.2604999542236,  "lat": 42.3384903145478  ]]
            ]
        ]
    
    
        // find geofence if it exists
        def circle = findGeofenceByName(CONTEXT, sampleCircle.name)
    
        // create circular geofence
        if (!circle){
            circle = addGeofence(CONTEXT, sampleCircle)
        }
    
        // create rule for circular geofence
        def circleRule = createGeofenceExpressionRule(CONTEXT, circle.name, "${circle.name}__Rule",
                                                                               sampleCircle.alertType, sampleCircle.alert, "SDK Generated Geofence Rule", 100)
    
        // apply rule to new Model
        findOrCreateRuleAssociations(CONTEXT, model, circleRule, "EXPRESSION_RULE", "MODEL")
    
        def polygon = findGeofenceByName(CONTEXT, samplePolygon.name)
    
        if (!polygon){
            polygon = addGeofence(CONTEXT, samplePolygon)
        }
    
        def polygonRule = createGeofenceExpressionRule(CONTEXT, polygon.name, "${polygon.name}__Rule",
                                                                                  samplePolygon.alertType, samplePolygon.alert, "SDK Generated Geofence Rule", 100)
    
        // apply rule to new Model
        findOrCreateRuleAssociations(CONTEXT, model, polygonRule, "EXPRESSION_RULE", "MODEL")
    
    } catch (Exception e) {
        logger.info(e.localizedMessage)
    }
    return true
    
    def findGeofenceByName(CONTEXT, name){
        GeofenceFinder geofenceFinder = new GeofenceFinder(CONTEXT)
        geofenceFinder.setName(name)
        def geofence = geofenceFinder.find()
        geofence
    }
    
    def addGeofence(CONTEXT, map){
        Geofence myGeofence = new Geofence(CONTEXT)
        myGeofence.name = map.name
        if(map.type != "polygon" && map.type != "circle") {
            throw new Exception("Invalid type: need 'polygon' or 'circle', not '$map.type'")
        } else if(map.type == "polygon") {
            def geo = map.locs.loc.inject( "POLYGON (("){ str, item ->
                def lng = item.lng
                def lat = item.lat
                str += "$lng $lat,"
                str
            }
            //the first location also has to be the last location
            myGeofence.geometry = geo + map.locs.loc[0].lng + " " + map.locs.loc[0].lat + "))"
            //Something like this is built:
            //POLYGON ((-71.082118 42.383892,-70.867198 42.540923,-71.203654 42.495374,-71.284678 42.349394,-71.163829 42.221382,-71.003154  42.266114,-71.082118 42.383892))
        } else if(map.type == "circle") {
            def lng = map.locs.loc[0].lng
            def lat = map.locs.loc[0].lat
            myGeofence.geometry = "POINT ($lng $lat)"
            //POINT (-71.082118 42.383892)
            myGeofence.buffer = map.radius.toDouble()
        }
        myGeofence.description = "ALERT:::$map.alertType:::$map.alert"
        try {
            myGeofence.store()
        }  catch (e) {
            logger.info e.localizedMessage
            return null
        }
        myGeofence
    }
    
    def createGeofenceExpressionRule(com.axeda.drm.sdk.Context CONTEXT, String geofencename, String rulename,
                                                         String alertType, String alertMessage, String ruledescription, int severity)
    {
        ExpressionRuleFinder erf = new ExpressionRuleFinder(CONTEXT)
        erf.setName(rulename)
        ExpressionRule expressionRule1 = erf.findOne()
        expressionRule1?.delete()
    
        def expressionRule = new ExpressionRule(CONTEXT)
        expressionRule.setName(rulename)
        expressionRule.setDescription(ruledescription)
        expressionRule.setTriggerName("MobileLocation")
    
        def ifExpStr = "InNamedGeofence(\"$geofencename\", Location.location)"
        if(alertType == "out"){
            ifExpStr = "!" + ifExpStr
        }
        expressionRule.setIfExpression(new Expression(ifExpStr))
    
        expressionRule.setThenExpression(new Expression("CreateAlarm(\"$alertMessage\", severity)"))
        expressionRule.setEnabled(true)
        expressionRule.setConsecutive(false)
        expressionRule.store()
        expressionRule
    }
    
    def findOrCreateRuleAssociations(Context CONTEXT, Object entity, Object rule, String ruleType, String entityType) {
        // rule type is whether this is an expression rule
        ruleType = ruleType ?: "EXPRESSION_RULE"
        entityType = entityType ?: "DEVICE_INCLUDE"
        RuleAssociationFinder ruleAssociationFinder = new RuleAssociationFinder(CONTEXT)
        ruleAssociationFinder.setRuleId(rule.id.value)
        ruleAssociationFinder.setRuleType(RuleType.valueOf(ruleType))
        ruleAssociationFinder.setEntityId(entity.id.value)
        ruleAssociationFinder.setEntityType(EntityType.valueOf(entityType))
        def ruleAssociations = ruleAssociationFinder.findAll()
        if (!ruleAssociations || ruleAssociations?.size() == 0){
            def ruleAssociation = new RuleAssociation(CONTEXT)
            ruleAssociation.entityId = entity.id.value
            ruleAssociation.entityType = EntityType.valueOf(entityType)
            ruleAssociation.ruleType = RuleType.valueOf(ruleType)
            ruleAssociation.setRuleId(rule.id.value)
            ruleAssociation.store()
            ruleAssociations = [ruleAssociation]
        }
        return ruleAssociations
    }
    
    def findOrCreateModel(Context CONTEXT, String modelName) {
        ModelFinder modelFinder = new ModelFinder(CONTEXT)
        modelFinder.setName(modelName)
        def model = modelFinder.find()
        if (!model){
            model = new Model(CONTEXT, modelName);
            model.store();
        }
        return model
    }
    
    

    https://gist.github.com/axeda/6529288/raw/5ffca58c3c48256b81287d6a6f2d2db63cd5cd2b/AddGeofence.groovy