Indoor positioning

ArcGIS IPS is an indoor positioning system that allows you to locate yourself and others inside a building in real time. Similar to GPS, it puts a blue dot on indoor maps and uses location services to help you navigate to any point of interest or destination.

ArcGIS IPS supports:

  • Wayfinding
  • Location tracking and sharing
  • Location data collection
  • Analytics

Your app can work with ArcGIS IPS to show the device location by using an IPS-aware map or by manually creating an indoor location data source.

Indoor positioning in a mobile app

ArcGIS IPS provides geoprocessing tools for setting up and authoring your IPS environment in ArcGIS Pro. It also includes ArcGIS IPS Setup, a mobile app to collect radio signals from Bluetooth Low Energy (BLE) beacons inside your building(s). The mobile app can make use of an existing or new beacon infrastructure and is beacon vendor agnostic.

For more information about IPS-aware maps, see ArcGIS Pro's help section on IPS-aware maps.

IPS-aware maps

To use ArcGIS IPS in your app, you must use an IPS-aware map that conforms to the ArcGIS IPS Information Model. This model specifies components such as the following:

  • Floor plan: sites, facilities, levels, and other details.
  • Transitions: exits and entrances.
  • Pathways: line features that the device location can snap to.
  • Beacons: Bluetooth Low Energy (BLE) beacons.

For a map to be IPS-aware, the map must have one of the following:

  • A connection to an Indoor Positioning Data Service set in the map properties. For information on how to create an Indoor Positioning Dataset (hosted as an Indoor Positioning Data Service), see the ArcGIS Pro help topic Indoor Positioning Datset. Also see Pro's instructions on how to create the service.
  • An IPS positioning feature table named "IPS_Positioning". For detailed instructions about how to create an IPS-aware map, see Get started with ArcGIS IPS.

You should also consider the following:

  • This API supports both Wi-Fi and BLE for indoor positioning on Android devices. When using ArcGIS IPS, we recommend that you only employ one of these systems in a facility.

  • Android 9 introduced Wi-Fi scan throttling to limit the number of Wi-Fi scans. To use Wi-Fi IPS, Android devices (9 or higher) must enable developer mode and disable Wi-Fi scan throttling (Developer Options > Networking > Wi-Fi scan throttling).

After you have loaded your indoor map into your app's map view, you can use the following classes to display the device's location on the map:

Add indoor positioning to your app

An IPS-aware map contains layers to visualize indoor space and has access to indoor positioning data to determine a device's location and navigate within that space. The indoor positioning data can be stored in an Indoor Positioning Data Service or an Indoor Positioning File. To show the device location on an indoor map, you have two options:

Use the indoor positioning definition of the map

If your IPS-aware map contains a IndoorPositioningDefinition, you can display the device location in a map by obtaining the indoor location data source from the map's indoor positioning definition. Follow these steps:

  1. Load the IPS-aware map in your app. This is a web map created with ArcGIS Pro that is hosted as a portal item in ArcGIS Online or in ArcGIS Enterprise.

  2. Obtain an indoor positioning definition from the loaded map's ArcGISMap.indoorPositioningDefinition. If this value is null, you cannot create a IndoorsLocationDataSource using this approach. Instead, manually create the indoor location data source.

  3. Load the indoor positioning definition to avoid any delays when the IndoorsLocationDataSource is started.

  4. Create an IndoorsLocationDataSource using the IndoorPositioningDefinition.

  5. Assign the IndoorsLocationDataSource to the map view's LocationDisplay.

  6. Start the map view's location display. The device location appears on the display as a blue dot and updates as the user moves throughout the indoor space.

Manually create an indoor location data source

If your IPS-aware map does not contain a IndoorPositioningDefinition, you can manually create an indoors location data source by using the IPS_Positioning feature table that is stored within the map.

  • IPS_Positioning feature table: Each row in the table contains an indoor positioning file that was created when the IPS environment was set up using the Generate indoor positioning file geoprocessing tool. When working with an indoor location source in your app, the most recent positioning file is used unless you specify a different one using the Row ID.

Optionally, you can also provide the following.

  • Row ID: A globally unique ID that identifies a row in the IPS_Positioning feature table. The positioning file associated with this row will be used to find indoor locations. If not specified in the constructor, the positioning file from the most recent survey is used.

  • Levels table: An ArcGISFeatureTable with information about the levels, or floors, of a building. If the table is available, the IndoorsLocationDataSource will add the LEVEL_ID to the Location.additionalSourceProperties key-value pairs. You can obtain the floor number by using the floor key. The ground floor has a value of 0 and floors below ground have negative values.

  • Pathways table: An ArcGISFeatureTable with line features that represent paths through the indoor space. Locations provided by the IndoorsLocationDataSource are snapped to the lines in this feature class. For example, in the image below, the red + represents raw locations determined by the IPS. These locations are snapped to the nearest line feature in the pathways feature table before being displayed. This provides a more consistent display of the blue dot as it moves across the map.

Locations are snapped to line features in the pathways feature table

To create an indoors location data source by using an IPS_Positioning feature table, follow these steps:

  1. Load the IPS-aware map. This can be a web map hosted as a portal item in ArcGIS Online or in ArcGIS Enterprise or as a map in a mobile map package (.mmpk) created with ArcGIS Pro.

  2. Obtain the feature table, named "IPS_Positioning", from the map.

  3. Find the pathways layer, named "Pathways", in the map's operational layers. Obtain the pathways feature table from the pathways layer.

  4. Find the feature table, named "Levels", in the map's tables. This table is optional.

  5. Create an IndoorsLocationDataSource using the IPS_Positioning and Pathways feature tables. Provide the Levels feature tables, if it is present.

  6. Pass the IndoorsLocationDataSource to the MapView composable.
  7. Set the LocationDisplayAutoPanMode and start the data source. Device location will appear on the display as a blue dot and update as the user moves throughout the space.

You can read the positioning table and pathways dataset from the map and use them to create the IndoorsLocationDataSource.

Create IndoorLocationDataSource
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
        // get the positioning table from the map
        val positioningTable = map.tables.firstOrNull { ft: FeatureTable -> ft.tableName == "ips_positioning" }
            ?: return showError("Positioning table not found")

        // query positioning table to get the positioning ID
        val queryParameters = QueryParameters().apply {
            maxFeatures = 1
            whereClause = "1 = 1"
            orderByFields.add(OrderBy("DateCreated", SortOrder.Ascending))
        }
        val serviceFeatureTable = positioningTable as ServiceFeatureTable
        val queryResult = serviceFeatureTable.queryFeatures(queryParameters).getOrNull()
        val positioningId = if (queryResult == null) {
            null
        } else {
            val feature = queryResult.first() as ArcGISFeature
            feature.attributes[serviceFeatureTable.globalIdField] as Guid
        }

        // get the pathways layer (optional for creating the IndoorsLocationDataSource)
        val pathwaysLayer : FeatureLayer = map.operationalLayers.firstOrNull { layer: Layer -> layer.name == "Pathways" } as FeatureLayer
        // get the levels layer (optional for creating the IndoorsLocationDataSource)
        val levelsLayer:  FeatureLayer = map.operationalLayers.firstOrNull { layer: Layer -> layer.name == "Levels" } as FeatureLayer

        // Setting up IndoorsLocationDataSource with positioning, pathways tables and positioning ID.
        // positioningTable - the "ips_positioning" feature table from an IPS-aware map.
        // pathwaysTable - An ArcGISFeatureTable that contains pathways as per the ArcGIS Indoors Information Model.
        // Setting this property enables path snapping of locations provided by the IndoorsLocationDataSource.
        // levelsTable - An ArcGISFeatureTable that contains floor levels in accordance with the ArcGIS Indoors Information Model.
        // Providing this table enables the retrieval of a location's floor level ID.
        // positioningID - an ID which identifies a specific row in the positioningTable that should be used for setting up IPS.
        indoorsLocationDataSource = IndoorsLocationDataSource(
            positioningTable = positioningTable,
            pathwaysTable = (pathwaysLayer.featureTable as ArcGISFeatureTable),
            levelsTable = (levelsLayer.featureTable as ArcGISFeatureTable),
            positioningId = positioningId
        )

Create a LocationDisplay instance using rememberLocationDisplay() and assign IndoorsLocationDataSource as its data source. Also set the showLocation property to true if you want location symbols displayed.

Then pass the instance as the locationDisplay parameter to the MapView composable.

Start LocationDisplay
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
        val locationDisplay = rememberLocationDisplay().apply {
            // Assign the IndoorLocationDataSource to the dataSource property of the of the LocationDisplay.
            dataSource = indoorsLocationDataSource
            showLocation = true
        }

        // Pass this location display to the Composable MapView.
        MapView(
            arcGISMap = map,
            locationDisplay = locationDisplay
        )

        locationDisplay.setAutoPanMode(LocationDisplayAutoPanMode.CompassNavigation)

        LaunchedEffect(Unit) {
            // Start the location display, which will trigger IndoorsLocationDataSource to start receiving IPS updates.
            locationDisplay.dataSource.start().onFailure {
                    showError("Error when starting location display")
            }

            // Handle location changes emitted by the IndoorsLocationDataSource...

        }

Handle location changes

You can handle a status changed event so that the IndoorsLocationDataSource is notified when the location data source starts, stops, or fails to start.

An IPS location populates additional properties with the current floor and the transmitter (beacon) count. When the floor changes, you can update the map to filter the display of features for the current floor. The floor value returned with the location is an integer that represents the vertical offset, where 0 is the ground floor. This value increases by one for each floor above the ground floor and decreases by one for each floor below.

Use code like the following to read additional properties for the location when it changes.

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
            // Handle location changes emitted by the IndoorsLocationDataSource...

            locationDisplay.dataSource.locationChanged.first().let { location ->
                if (location.additionalSourceProperties.isEmpty()) {
                    return@LaunchedEffect showError("Indoor positioning data source does not have any property fields")
                }
                // filter layers for the current floor (level)
                val floor = (location.additionalSourceProperties["floor"] ?: "").toString()
                setupLayers(floor.toInt())
            }

In addition to getting the floor, you can get the position source, which will be BLE (Bluetooth Low Energy) when using IPS and GNSS (Global Navigation Satellite Systems) when using GPS. You can also get the count of transmitters (beacons) or satellites used to determine the location.

You can use a definition expression to filter layers in the map to only show features for the current floor. For efficiency, you should only filter features when the floor changes rather than with each location update. Depending on the schema for your floor-aware data, you may need to map the vertical offset value to a level ID in order to filter features by floor (level).

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
    private fun setupLayers(floor: Int) {
        map.operationalLayers.forEach { layer ->
            val name = layer.name
            // filter applicable layers to show features for the current floor (level)
            if (layer is FeatureLayer && (name == "Details" || name == "Units" || name == "Levels")) {
                layer.definitionExpression = "VERTICAL_ORDER = $floor"
            }
        }
    }

App permissions

If your app is using Bluetooth on the device to scan for beacon signals or GPS, make sure to add the appropriate permissions.

Add these permissions to your Androidmanifest.xml file.

Use dark colors for code blocksCopy
1
2
3
4
5
6
7
android.permission.BLUETOOTH_ADMIN
android.permission.BLUETOOTH

<!-- Needed if your app targets Android 12 or higher -->
android.permission.BLUETOOTH_SCAN

android.permission.ACCESS_FINE_LOCATION

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