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 .
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());
}
}
Solved! Go to Solution.
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
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
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.
I will take the feedback to the team on the error message to improve it.
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())
}
}