How do I create Geotriggers using polygon tables?

590
4
Jump to solution
06-29-2022 04:57 AM
MudassarJafer
New Contributor

I have a database with points data. Each has a group and points in a group represent a boundary (polygon) on the map. I am trying to use these boundaries with Geotriggers to alert the users when they move in and out of boundaries. I have used the location driven Geotriggers sample from git hub to get an idea on how to achieve this .

https://github.com/Esri/arcgis-runtime-samples-android/tree/main/kotlin/set-up-location-driven-geotr...

The only difference is that the sample uses portal items and the data I have is a set of points. So instead of using portal items I create a feature collection with polygon table with a single geometry in the shape of a square. On running the application I do not see alerts at all when the simulated location source moves in and out of the boundary. I have included the code below so it is easier to understand what I am doing.

Is it possible to implement this feature with polygon feature tables?

Are we able to use Geotriggers without showing a map to the user?

package uk.co.deloitte.foregroundservice;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;
import android.widget.Toast;

import com.esri.arcgisruntime.ArcGISRuntimeEnvironment;
import com.esri.arcgisruntime.arcade.ArcadeExpression;
import com.esri.arcgisruntime.data.Feature;
import com.esri.arcgisruntime.data.FeatureCollection;
import com.esri.arcgisruntime.data.FeatureCollectionTable;
import com.esri.arcgisruntime.data.Field;
import com.esri.arcgisruntime.geometry.GeometryType;
import com.esri.arcgisruntime.geometry.Point;
import com.esri.arcgisruntime.geometry.PolygonBuilder;
import com.esri.arcgisruntime.geometry.Polyline;
import com.esri.arcgisruntime.geometry.SpatialReferences;
import com.esri.arcgisruntime.geotriggers.FeatureFenceParameters;
import com.esri.arcgisruntime.geotriggers.FenceGeotrigger;
import com.esri.arcgisruntime.geotriggers.FenceGeotriggerNotificationInfo;
import com.esri.arcgisruntime.geotriggers.FenceNotificationType;
import com.esri.arcgisruntime.geotriggers.FenceRuleType;
import com.esri.arcgisruntime.geotriggers.GeotriggerMonitor;
import com.esri.arcgisruntime.geotriggers.GeotriggerMonitorNotificationEvent;
import com.esri.arcgisruntime.geotriggers.GeotriggerMonitorNotificationEventListener;
import com.esri.arcgisruntime.geotriggers.GeotriggerNotificationInfo;
import com.esri.arcgisruntime.geotriggers.LocationGeotriggerFeed;
import com.esri.arcgisruntime.layers.FeatureCollectionLayer;
import com.esri.arcgisruntime.location.SimulatedLocationDataSource;
import com.esri.arcgisruntime.location.SimulationParameters;
import com.esri.arcgisruntime.mapping.ArcGISMap;
import com.esri.arcgisruntime.mapping.BasemapStyle;
import com.esri.arcgisruntime.mapping.Viewpoint;
import com.esri.arcgisruntime.mapping.view.LocationDisplay;
import com.esri.arcgisruntime.mapping.view.MapView;
import com.esri.arcgisruntime.symbology.SimpleFillSymbol;
import com.esri.arcgisruntime.symbology.SimpleLineSymbol;
import com.esri.arcgisruntime.symbology.SimpleRenderer;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MainActivity extends AppCompatActivity {
    private Button btnStartService;
    private Button btnStopService;
    private Button btnDisplayLocation;

    private MapView mMapView;
    private LocationDisplay mLocationDisplay;
    private GeotriggerMonitor mRoadGeotriggerMonitor;

    private final int requestCode = 2;
    private final String[] reqPermissions = { Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission
            .ACCESS_COARSE_LOCATION };
    private static final String MAIN_ACTIVITY = "MainActivity";
    public static final String CHANNEL_ID = "MainActivityChannel";


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btnStartService = findViewById(R.id.buttonStartService);
        btnStopService = findViewById(R.id.buttonStopService);
        btnDisplayLocation = findViewById(R.id.buttonDisplayLocation);
        btnStartService.setOnClickListener(view -> startService());
        btnStopService.setOnClickListener(view -> stopService());
        btnDisplayLocation.setOnClickListener(view -> toggleLocationDisplay());
        ArcGISRuntimeEnvironment.setApiKey(BuildConfig.API_KEY);

        // inflate MapView from layout
        mMapView = findViewById(R.id.mapView);

        // initialize map with basemap
        ArcGISMap map = new ArcGISMap(BasemapStyle.ARCGIS_IMAGERY);

        // assign map to the map view
        mMapView.setMap(map);
        mMapView.setViewpoint(new Viewpoint( 54.518502, -6.0873785, 5000));

        // create feature collection and add to the map as a layer
        FeatureCollection featureCollection = new FeatureCollection();
        FeatureCollectionLayer featureCollectionLayer = new FeatureCollectionLayer(featureCollection);
        map.getOperationalLayers().add(featureCollectionLayer);

        // add point, line, and polygon geometry to feature collection
        createPolygonTables(featureCollection);

        // get the MapView's LocationDisplay
        mLocationDisplay = mMapView.getLocationDisplay();

        // Listen to changes in the status of the location data source.
        mLocationDisplay.addDataSourceStatusChangedListener(dataSourceStatusChangedEvent -> {

            // If LocationDisplay started OK, then continue.
            if (dataSourceStatusChangedEvent.isStarted())
                return;

            // No error is reported, then continue.
            if (dataSourceStatusChangedEvent.getError() == null)
                return;

            // If an error is found, handle the failure to start.
            // Check permissions to see if failure may be due to lack of permissions.
            boolean permissionCheck1 = ContextCompat.checkSelfPermission(this, reqPermissions[0]) ==
                    PackageManager.PERMISSION_GRANTED;
            boolean permissionCheck2 = ContextCompat.checkSelfPermission(this, reqPermissions[1]) ==
                    PackageManager.PERMISSION_GRANTED;

            if (!(permissionCheck1 && permissionCheck2)) {
                // If permissions are not already granted, request permission from the user.
                ActivityCompat.requestPermissions(this, reqPermissions, requestCode);
                Log.i(MAIN_ACTIVITY, "Location Permission granted");
            } else {
                // Report other unknown failure types to the user - for example, location services may not
                // be enabled on the device.
                String message = String.format("Error in DataSourceStatusChangedListener: %s", dataSourceStatusChangedEvent
                        .getSource().getLocationDataSource().getError().getMessage());
                Toast.makeText(this, message, Toast.LENGTH_LONG).show();
            }
        });

        LocationGeotriggerFeed locationGeotriggerFeed = initializeSimulatedLocationDisplay();
        // create feature collection and add to the map as a layer
        Log.d(MAIN_ACTIVITY, "Feature table size " + featureCollection.getTables().size());
        mRoadGeotriggerMonitor = createGeotriggerMonitor(featureCollection.getTables().get(0), 5.0, "BOUNDARY", locationGeotriggerFeed);
    }

    /**
     * Initialize a simulation using a simulated data source and then
     * feed it to the [LocationGeotriggerFeed]
     */
    private LocationGeotriggerFeed initializeSimulatedLocationDisplay() {
        SimulatedLocationDataSource simulatedLocationDataSource = new SimulatedLocationDataSource();

        // Create SimulationParameters starting at the current time, a velocity of 10 m/s, and a horizontal and vertical accuracy of 0.0
        SimulationParameters simulationParameters = new SimulationParameters(Calendar.getInstance(),
                3.0,
                0.0,
                0.0);

        // Use the polyline as defined above or from this ArcGIS Online GeoJSON to define the path.
        simulatedLocationDataSource.setLocations(
                (Polyline) Polyline.fromJson(getString(R.string.polyline_json)),
                simulationParameters);

        // Set map to simulate the location data source
        simulatedLocationDataSource.startAsync();

        // Set map to simulate the location data source
        mLocationDisplay.setLocationDataSource(simulatedLocationDataSource);
        mLocationDisplay.setAutoPanMode(LocationDisplay.AutoPanMode.RECENTER);
        mLocationDisplay.setInitialZoomScale(1000.0);
        mLocationDisplay.startAsync();

        // LocationGeotriggerFeed will be used in instantiating a FenceGeotrigger in createGeotriggerMonitor()
        return new LocationGeotriggerFeed(simulatedLocationDataSource);
    }

    private void toggleLocationDisplay() {
        if (!mLocationDisplay.isStarted()) {
            mLocationDisplay.startAsync();
            Log.i(MAIN_ACTIVITY, "Location display started.");
        } else {
            mLocationDisplay.stop();
            Log.i(MAIN_ACTIVITY, "Location display stopped.");
        }
    }

    public void startService() {
        Log.i("ForegroundService", "Staring Service!");
        Intent serviceIntent = new Intent(this, ForegroundService.class);
        serviceIntent.putExtra("inputExtra", "Foreground Service Example in Android");

        Context context = getApplicationContext();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(serviceIntent);
        } else {
            ContextCompat.startForegroundService(this, serviceIntent);
        }
    }
    public void stopService() {
        Log.i("ForegroundService", "Stopping Service!");
        Intent serviceIntent = new Intent(this, ForegroundService.class);
        stopService(serviceIntent);
    }

    /**
     * Creates a Polygon Feature Collection Table with one Polygon and adds it to the Feature collection that was passed.
     *
     * @param featureCollection that the polygon Feature Collection Table will be added to
     */
    private void createPolygonTables(FeatureCollection featureCollection) {

        // defines the schema for the geometry's attribute
        List<Field> polygonFields = new ArrayList<>();
        polygonFields.add(Field.createString("Area", "Area Name", 50));

        // a feature collection table that creates polygon geometry
        FeatureCollectionTable polygonTable = new FeatureCollectionTable(polygonFields, GeometryType.POLYGON, SpatialReferences.getWgs84());

        // set a default symbol for features in the collection table
        SimpleLineSymbol lineSymbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, 0xFF0000FF, 2);
        SimpleFillSymbol fillSymbol = new SimpleFillSymbol(SimpleFillSymbol.Style.BACKWARD_DIAGONAL, 0xFF00FFFF, lineSymbol);
        SimpleRenderer renderer = new SimpleRenderer(fillSymbol);
        polygonTable.setRenderer(renderer);

        // add feature collection table to feature collection
        featureCollection.getTables().add(polygonTable);

        // create feature using the collection table by passing an attribute and geometry
        Map<String, Object> attributes = new HashMap<>();
        attributes.put(polygonFields.get(0).getName(), "Restricted area");
        PolygonBuilder builder = new PolygonBuilder(SpatialReferences.getWgs84());
        builder.addPoint(new Point(-6.0875805, 54.518327));
        builder.addPoint(new Point(-6.0871674, 54.518326));
        builder.addPoint(new Point(-6.0871694, 54.518621));
        builder.addPoint(new Point(-6.0875711, 54.518622));
        Feature addedFeature = polygonTable.createFeature(attributes, builder.toGeometry());

        // add feature to collection table
        polygonTable.addFeatureAsync(addedFeature);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        // If request is cancelled, the result arrays are empty.
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // Location permission was granted. This would have been triggered in response to failing to start the
            // LocationDisplay, so try starting this again.
            mLocationDisplay.startAsync();
        } else {
            // If permission was denied, show toast to inform user what was chosen. If LocationDisplay is started again,
            // request permission UX will be shown again, option should be shown to allow never showing the UX again.
            // Alternative would be to disable functionality so request is not shown again.
            Toast.makeText(this, getString(R.string.location_permission_denied), Toast.LENGTH_SHORT).show();
        }
    }

    @Override
    protected void onPause() {
        super.onPause();
        mMapView.pause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mMapView.resume();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mMapView.dispose();
    }

    private GeotriggerMonitor createGeotriggerMonitor(FeatureCollectionTable featureTable,
                                                      Double bufferDistance,
                                                      String geotriggerName,
                                                      LocationGeotriggerFeed geotriggerFeed) {

        // Initialize FeatureFenceParameters with the polygon feature table and a buffer of 5 meters
        FeatureFenceParameters featureFenceParameters = new FeatureFenceParameters(featureTable, bufferDistance);
        FenceGeotrigger fenceGeotrigger = new FenceGeotrigger(geotriggerFeed,
                FenceRuleType.ENTER_OR_EXIT,
                featureFenceParameters,
                new ArcadeExpression("$fenceFeature.name"),
                geotriggerName);

        // Handles Geotrigger notification based on the FenceRuleType
        // Hence, triggers on fence enter/exit.
        GeotriggerMonitor geotriggerMonitor = new GeotriggerMonitor(fenceGeotrigger);
        geotriggerMonitor.addGeotriggerMonitorNotificationEventListener(new GeotriggerMonitorNotificationEventListener() {
            @Override
            public void onGeotriggerMonitorNotification(GeotriggerMonitorNotificationEvent geotriggerMonitorNotificationEvent) {
                Log.d(MAIN_ACTIVITY, "Received geo trigger notification");
                handleGeotriggerNotification(geotriggerMonitorNotificationEvent.getGeotriggerNotificationInfo());
            }
        });

        // Start must be explicitly called. It is called after the signal connection is defined to avoid a race condition.
        geotriggerMonitor.startAsync();

        return geotriggerMonitor;
    }

    /**
     * Handles the geotrigger notification based on [geotriggerNotificationInfo] depending
     * on the fenceNotificationType
     */
    private void handleGeotriggerNotification(GeotriggerNotificationInfo geotriggerNotificationInfo) {

        // FenceGeotriggerNotificationInfo provides access to the feature that triggered the notification
        FenceGeotriggerNotificationInfo fenceGeotriggerNotificationInfo =
                (FenceGeotriggerNotificationInfo) geotriggerNotificationInfo;

        // name of the fence feature
        String fenceFeatureName = fenceGeotriggerNotificationInfo.getMessage();

        if (fenceGeotriggerNotificationInfo.getFenceNotificationType() == FenceNotificationType.ENTERED) {
            // If the user enters a given geofence, add the feature's information to the UI and save the feature for querying
            Log.d(MAIN_ACTIVITY, "-----------------> Entered");
        } else if (fenceGeotriggerNotificationInfo.getFenceNotificationType() == FenceNotificationType.EXITED) {
            // If the user exits a given geofence, remove the feature's information from the UI
            Log.d(MAIN_ACTIVITY, "Exited ----------------->");
        }
        sendNotification(fenceGeotriggerNotificationInfo);
    }

    private void sendNotification(FenceGeotriggerNotificationInfo fenceGeotriggerNotificationInfo){
        Intent intent = new Intent(this, MainActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);

        FenceNotificationType notificationType = fenceGeotriggerNotificationInfo.getFenceNotificationType();

        NotificationCompat.Builder notificationBuilder = null;
        int notificationId = 0;
        if(notificationType == FenceNotificationType.ENTERED){
            notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
                    .setSmallIcon(R.drawable.ic_entering_geo_fence)
                    .setContentTitle("Entering " + fenceGeotriggerNotificationInfo.getFenceGeoElement().getAttributes().get("Area"))
                    .setContentText(fenceGeotriggerNotificationInfo.getMessage())
                    .setStyle(new NotificationCompat.BigTextStyle()
                            .bigText(fenceGeotriggerNotificationInfo.getMessage()))
                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(true);
            notificationId = 0;
        } else {
            notificationBuilder = new NotificationCompat.Builder(this, CHANNEL_ID)
                    .setSmallIcon(R.drawable.ic_leaving_geo_fence)
                    .setContentTitle("Leaving " + fenceGeotriggerNotificationInfo.getFenceGeoElement().getAttributes().get("Area"))
                    .setContentText(fenceGeotriggerNotificationInfo.getMessage())
                    .setStyle(new NotificationCompat.BigTextStyle()
                            .bigText(fenceGeotriggerNotificationInfo.getMessage()))
                    .setPriority(NotificationCompat.PRIORITY_DEFAULT)
                    .setContentIntent(pendingIntent)
                    .setAutoCancel(true);
            notificationId = 1;
        }
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);

        // notificationId is a unique int for each notification that you must define
        notificationManager.notify(notificationId, notificationBuilder.build());
    }


}

 

0 Kudos
1 Solution

Accepted Solutions
RamaChintapalli
Esri Contributor

Hi,
Your fenceGeotrigger seems to be using a feature attribute `$fenceFeature.name`  for arcade expression but your FeatureCollectionTable doesn't have that field. Can you try adding the following field to your table and try again.

 

polygonFields.add(Field.createString("name", "Attribute Name", 50));

 



For the other question,

Are we able to use Geotriggers without showing a map to the user?

Yes graphics or features can be used as fences without a map and for the feed side any derived LocationDataSource should work too.

Thanks
Rama

View solution in original post

4 Replies
RamaChintapalli
Esri Contributor

Hi,
Your fenceGeotrigger seems to be using a feature attribute `$fenceFeature.name`  for arcade expression but your FeatureCollectionTable doesn't have that field. Can you try adding the following field to your table and try again.

 

polygonFields.add(Field.createString("name", "Attribute Name", 50));

 



For the other question,

Are we able to use Geotriggers without showing a map to the user?

Yes graphics or features can be used as fences without a map and for the feed side any derived LocationDataSource should work too.

Thanks
Rama

MudassarJafer
New Contributor

Thank you for pointing that out to me that was a stupid mistake. After adding the parameter the application started working.

The error message that I was getting was not not very helpful as it said the "Arcade expression is invalid.". It would have been better if it gave the reason why it is invalid. I have never used Arcade expressions before.

0 Kudos
RamaChintapalli
Esri Contributor

I will take the feedback to the team on the error message to improve it.

0 Kudos
RamaChintapalli
Esri Contributor

Actually we can get the detailed error message from the additional message of the warning. It currently returns  - "Arcade message expression uses a field that is not found in the feature table (name). Geotrigger cannot be started with an invalid expression".

Ex:

 

 

geotriggerMonitor.startAsync().addDoneListener {
            when(geotriggerMonitor.status){
                GeotriggerMonitorStatus.FAILED_TO_START -> Log.d("GeotriggerMonitorStatus",geotriggerMonitor.warning.additionalMessage.toString())
            }
}

 

 

 

0 Kudos