Skip to content

Set up location driven geotriggers

View on GitHubSample viewer app

Create a notification every time a given location data source has entered and/or exited a set of features or graphics.

Geotriggers

Use case

Geotriggers can be used to notify users when they have entered or exited a geofence by monitoring a given set of features or graphics. They could be used to display contextual information to museum visitors about nearby exhibits, notify hikers when they have wandered off their desired trail, notify dispatchers when service workers arrive at a scene, or more.

How to use the sample

Observe a virtual walking tour of the Santa Barbara Botanic Garden. Information about the user's current Garden Section, as well as information about nearby points of interest within 10 meters will display or be removed from the UI when the user enters or exits the buffer of each feature.

How it works

  1. Create a GeotriggerFeed with a LocationDataSource class (in this case, a SimulatedLocationDataSource).
  2. Create a FeatureFenceParameters class from a ServiceFeatureTable, a buffer distance at which to monitor each feature, an Arcade Expression, and a name for the specific geotrigger.
  3. Create a FenceGeotrigger with the geotrigger feed, a FenceRuleType, and the fence parameters.
  4. Create a GeotriggerMonitor with the fence geotrigger and call GeotriggerMonitor.start() to begin listening for events that meet the FenceRuleType.
  5. When GeotriggerMonitor.geotriggerNotification emits, capture the GeotriggerNotificationInfo.
  6. For more information about the feature that triggered the notification, cast the GeotriggerNotificationInfo to a FenceGeotriggerNotificationInfo and call FenceGeotriggerNotificationInfo.fenceGeoElement.
  7. Depending on the FenceGeotriggerNotificationInfo.fenceNotificationType display or hide information on the UI from the GeoElement's attributes.

Relevant API

  • ArcadeExpression
  • FeatureFenceParameters
  • FenceGeotrigger
  • FenceGeotriggerNotificationInfo
  • FenceRuleType
  • GeoElement
  • Geotrigger
  • GeotriggerFeed
  • GeotriggerMonitor
  • GeotriggerNotificationInfo
  • ServiceFeatureTable
  • SimulatedLocationDataSource

About the data

This sample uses the Santa Barbara Botanic Garden Geotriggers Sample ArcGIS Online Web Map which includes a georeferenced map of the garden as well as select polygon and point features to denote garden sections and points of interest. Description text and attachment images in the feature layers were provided by the Santa Barbara Botanic Garden and more information can be found on the Garden Sections & Displays portion of their website. All assets are used with permission from the Santa Barbara Botanic Garden. For more information, visit the Santa Barbara Botanic Garden website.

Tags

alert, arcade, fence, geofence, geotrigger, location, navigation, notification, notify, routing, trigger

Sample Code

MainActivity.ktMainActivity.ktFeatureViewFragment.ktFeatureListAdapter.kt
Use dark colors for code blocksCopy
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 /*  * Copyright 2023 Esri  *  * Licensed under the Apache License, Version 2.0 (the "License");  * you may not use this file except in compliance with the License.  * You may obtain a copy of the License at  *  * http://www.apache.org/licenses/LICENSE-2.0  *  * Unless required by applicable law or agreed to in writing, software  * distributed under the License is distributed on an "AS IS" BASIS,  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  * See the License for the specific language governing permissions and  * limitations under the License.  *  */  package com.esri.arcgismaps.sample.setuplocationdrivengeotriggers  import android.os.Bundle import android.util.Log import android.widget.Toast import com.esri.arcgismaps.sample.sampleslib.EdgeToEdgeCompatActivity import androidx.databinding.DataBindingUtil import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.arcgismaps.ApiKey import com.arcgismaps.ArcGISEnvironment import com.arcgismaps.arcade.ArcadeExpression import com.arcgismaps.data.ArcGISFeature import com.arcgismaps.data.ServiceFeatureTable import com.arcgismaps.geometry.Geometry import com.arcgismaps.geometry.Polyline import com.arcgismaps.geotriggers.FeatureFenceParameters import com.arcgismaps.geotriggers.FenceGeotrigger import com.arcgismaps.geotriggers.FenceGeotriggerNotificationInfo import com.arcgismaps.geotriggers.FenceNotificationType import com.arcgismaps.geotriggers.FenceRuleType import com.arcgismaps.geotriggers.GeotriggerMonitor import com.arcgismaps.geotriggers.GeotriggerNotificationInfo import com.arcgismaps.geotriggers.LocationGeotriggerFeed import com.arcgismaps.location.LocationDataSourceStatus import com.arcgismaps.location.LocationDisplayAutoPanMode import com.arcgismaps.location.SimulatedLocationDataSource import com.arcgismaps.location.SimulationParameters import com.arcgismaps.mapping.ArcGISMap import com.arcgismaps.mapping.PortalItem import com.arcgismaps.portal.Portal import com.esri.arcgismaps.sample.setuplocationdrivengeotriggers.databinding.SetUpLocationDrivenGeotriggersActivityMainBinding import com.google.android.material.button.MaterialButton import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.launch import java.time.Instant  class MainActivity : EdgeToEdgeCompatActivity() {   // set up data binding for the activity  private val activityMainBinding: SetUpLocationDrivenGeotriggersActivityMainBinding by lazy {  DataBindingUtil.setContentView(this, R.layout.set_up_location_driven_geotriggers_activity_main)  }   private val mapView by lazy {  activityMainBinding.mapView  }   private val playPauseFAB: FloatingActionButton by lazy {  activityMainBinding.playPauseFAB  }   private val sectionButton: MaterialButton by lazy {  activityMainBinding.sectionButton  }   // recycler list view to show the the points of interest  private val poiListView by lazy {  activityMainBinding.poiListView  }   // custom list adapter for the points of interest  private val poiListAdapter by lazy {  // create a new feature list adapter from the poiList  FeatureListAdapter(poiList) { feature ->  // set the item callback to show the feature view fragment  showFeatureViewFragment(feature)  }  }   private val simulatedLocationDataSource: SimulatedLocationDataSource by lazy {  // create SimulationParameters starting at the current time,  // a velocity of 10 m/s, and a horizontal and vertical accuracy of 0.0  val simulationParameters = SimulationParameters(  Instant.now(),  velocity = 3.0,  horizontalAccuracy = 0.0,  verticalAccuracy = 0.0  )  // create a SimulatedLocationDataSource using the polyline from  // ArcGIS Online GeoJSON to define the path. retrieved from  // https://arcgisruntime.maps.arcgis.com/home/item.html?id=2a346cf1668d4564b8413382ae98a956  SimulatedLocationDataSource(  Geometry.fromJsonOrNull(getString(R.string.polyline_json)) as Polyline,  simulationParameters  )  }   // feature list to store the points of interest of a geotrigger  private val poiList = mutableListOf<ArcGISFeature>()   // geotrigger names for the geotrigger monitors  private val sectionGeotrigger = "Section Geotrigger"  private val poiGeotrigger = "POI Geotrigger"   // make monitors properties to prevent garbage collection  private lateinit var sectionGeotriggerMonitor: GeotriggerMonitor  private lateinit var poiGeotriggerMonitor: GeotriggerMonitor   override fun onCreate(savedInstanceState: Bundle?) {  super.onCreate(savedInstanceState)   // authentication with an API key or named user is  // required to access basemaps and other location services  ArcGISEnvironment.apiKey = ApiKey.create(BuildConfig.ACCESS_TOKEN)  lifecycle.addObserver(mapView)   val portal = Portal("https://www.arcgis.com")  // this sample uses a web map with a predefined tile basemap, feature styles, and labels  val map = ArcGISMap(PortalItem(portal, "6ab0e91dc39e478cae4f408e1a36a308"))  // set the mapview's map  mapView.map = map  // set the map to display the location of the simulatedLocationDataSource  mapView.locationDisplay.apply {  dataSource = simulatedLocationDataSource  setAutoPanMode(LocationDisplayAutoPanMode.Recenter)  initialZoomScale = 1000.0  }   // instantiate the service feature tables to later create GeotriggerMonitors for  val gardenSections =  ServiceFeatureTable(PortalItem(portal, "1ba816341ea04243832136379b8951d9"), 0)  val gardenPOIs =  ServiceFeatureTable(PortalItem(portal, "7c6280c290c34ae8aeb6b5c4ec841167"), 0)  // create geotriggers for each of the service feature tables  sectionGeotriggerMonitor =  createGeotriggerMonitor(gardenSections, 0.0, sectionGeotrigger)  poiGeotriggerMonitor =  createGeotriggerMonitor(gardenPOIs, 10.0, poiGeotrigger)   // play or pause the simulation data source when the FAB is clicked  playPauseFAB.setOnClickListener {  when (simulatedLocationDataSource.status.value) {  LocationDataSourceStatus.Started -> {  stopSimulatedDataSource(true)  }  LocationDataSourceStatus.Stopped -> {  startSimulatedDataSource(true)  }  else -> {  // show an error if the status is anything else  showError(  "Error modifying location data source state: " +  "${simulatedLocationDataSource.status.value}"  )  }  }  }   // set the recycler view layout to a vertical linear layout  poiListView.layoutManager = LinearLayoutManager(this)  // assign its adapter  poiListView.adapter = poiListAdapter   lifecycleScope.launch {  // wait for the map load  map.load().onFailure {  // if the map load fails, show the error and return  showError("Error loading map: ${it.message}")  return@launch  }  // start the section geotrigger monitor  sectionGeotriggerMonitor.start().onFailure {  // if the monitor start fails, show the error and return  showError("Section Geotrigger Monitor failed to start: ${it.message}")  return@launch  }  // start the points of interest geotrigger monitor  poiGeotriggerMonitor.start().onFailure {  // if the monitor start fails, show the error and return  showError("POI Geotrigger Monitor failed to start: ${it.message}")  return@launch  }  // finally, start the simulated location data source  simulatedLocationDataSource.start().onFailure {  // if it fails, show the error and return  showError("Simulated Location DataSource failed to start: ${it.message}")  }  }  }   /**  * Creates and returns a geotrigger monitor with the [geotriggerName] name,  * using the [serviceFeatureTable] and [bufferSize] to initialize  * FeatureFenceParameters for the geotrigger  */  private fun createGeotriggerMonitor(  serviceFeatureTable: ServiceFeatureTable,  bufferSize: Double,  geotriggerName: String  ): GeotriggerMonitor {  // create a LocationGeotriggerFeed that uses the SimulatedLocationDataSource  val geotriggerFeed = LocationGeotriggerFeed(simulatedLocationDataSource)  // initialize FeatureFenceParameters to display the section the user has entered  val featureFenceParameters = FeatureFenceParameters(serviceFeatureTable, bufferSize)  // create a fence geotrigger  val fenceGeotrigger = FenceGeotrigger(  geotriggerFeed,  // triggers on enter/exit  FenceRuleType.EnterOrExit,  featureFenceParameters,  // arcade expression to get the feature name  ArcadeExpression("\$fenceFeature.name"),  geotriggerName  )   // initialize a geotrigger monitor with the fence geotrigger  val geotriggerMonitor = GeotriggerMonitor(fenceGeotrigger)  lifecycleScope.launch {  // capture and handle geotrigger notification based on the FenceRuleType  // hence, triggers on fence enter/exit.  geotriggerMonitor.notifications.collect { geotriggerNotificationInfo ->  handleGeotriggerNotification(geotriggerNotificationInfo)  }  }  return geotriggerMonitor  }   /**  * Handles the [geotriggerNotificationInfo] based on its geotrigger type  * and FenceNotificationType  */  private fun handleGeotriggerNotification(geotriggerNotificationInfo: GeotriggerNotificationInfo) {  // cast it to FenceGeotriggerNotificationInfo which provides  // access to the feature that triggered the notification  val fenceGeotriggerNotificationInfo =  geotriggerNotificationInfo as FenceGeotriggerNotificationInfo  // name of the fence feature, returned from the set arcade expression  val fenceFeatureName = fenceGeotriggerNotificationInfo.message  // get the specific geotrigger name we set during initialization  val geotriggerType = fenceGeotriggerNotificationInfo.geotriggerMonitor.geotrigger.name  // check for the type of notification  when (fenceGeotriggerNotificationInfo.fenceNotificationType) {  FenceNotificationType.Entered -> {  // if the user location entered the geofence, add the feature information to the UI  addFeatureInformation(  fenceFeatureName,  geotriggerType,  fenceGeotriggerNotificationInfo.fenceGeoElement as ArcGISFeature  )  }  FenceNotificationType.Exited -> {  // if the user exits a given geofence, remove the feature's information from the UI  removeFeatureInformation(fenceFeatureName, geotriggerType)  }  }  }   /**  * Adds the [fenceFeature] ArcGISFeature with the [fenceFeatureName] and [geotriggerType] to the current UI state  * and refreshes the UI  */  private fun addFeatureInformation(  fenceFeatureName: String,  geotriggerType: String,  fenceFeature: ArcGISFeature  ) {  // recenter the mapview  mapView.locationDisplay.setAutoPanMode(LocationDisplayAutoPanMode.Recenter)   when (geotriggerType) {  // if it's a section geo trigger type  sectionGeotrigger -> {  // update the section button's onClickListener  // to show a new FeatureViewFragment  sectionButton.setOnClickListener { showFeatureViewFragment(fenceFeature) }  // update the section button text to the feature name  sectionButton.text = fenceFeatureName  // enable the button  sectionButton.isEnabled = true  }  // or a point of interest geo trigger  poiGeotrigger -> {  // add it to the stored list  poiList.add(fenceFeature)  // notify the list adapter to refresh its recycler views  poiListAdapter.notifyItemInserted(poiList.lastIndex)  }  }  }   /**  * Removes the ArcGISFeature with the given [fenceFeatureName] and corresponding  * [geotriggerType] from the current UI state and refreshes the UI.  */  private fun removeFeatureInformation(fenceFeatureName: String, geotriggerType: String) {  // check the type of geotrigger  when (geotriggerType) {  sectionGeotrigger -> {  // if it's a section geo trigger,  // remove the section information and disable the button  sectionButton.text = "N/A"  sectionButton.isEnabled = false  }  poiGeotrigger -> {  // if it's a point of interest geotrigger  // find its index from the stored list  val index = poiList.indexOfFirst { feature ->  feature.attributes["name"] == fenceFeatureName  }  if (index >= 0) {  // if the feature exists remove it  poiList.removeAt(index)  // notify the list adapter to refresh its recycler views  poiListAdapter.notifyItemRemoved(index)  }  }  }  }   /**  * Creates and shows a new FeatureViewFragment using the given [feature]  */  private fun showFeatureViewFragment(feature: ArcGISFeature) {  // stop the simulated data source  stopSimulatedDataSource(false)  // create a new FeatureViewFragment  val featureViewFragment = FeatureViewFragment(feature) {  // set its onDismissedListener to  // resume the simulated data source  startSimulatedDataSource(false)  }  // show the fragment  featureViewFragment.show(supportFragmentManager, "FeatureViewFragment")  }   /**  * Starts the simulated data source and shows a status toast if [showAlert] is true.  * The data source is resumed from its previous location if stopped before.  */  private fun startSimulatedDataSource(showAlert: Boolean) = lifecycleScope.launch {  // start the simulated location data source  simulatedLocationDataSource.start()  // recenter the map view  mapView.locationDisplay.setAutoPanMode(LocationDisplayAutoPanMode.Recenter)  // show a toast if true  if (showAlert) {  Toast.makeText(this@MainActivity, "Resumed Simulation", Toast.LENGTH_SHORT)  .show()  }  // update the action button's drawable to a pause icon  playPauseFAB.setImageResource(R.drawable.ic_baseline_pause_24)  }   /**  * Stops the simulated data source and shows a status toast if [showAlert] is true.  */  private fun stopSimulatedDataSource(showAlert: Boolean) = lifecycleScope.launch {  // stop the simulated location data source  simulatedLocationDataSource.stop()  // show a toast if true  if (showAlert) {  Toast.makeText(this@MainActivity, "Stopped Simulation", Toast.LENGTH_SHORT)  .show()  }  // update the action button's drawable to a play icon  playPauseFAB.setImageResource(R.drawable.ic_baseline_play_arrow_24)  }   private fun showError(message: String) {  Log.e(localClassName, message)  Snackbar.make(mapView, message, Snackbar.LENGTH_SHORT).show()  } }

Your browser is no longer supported. Please upgrade your browser for the best experience. See our browser deprecation post for more details.