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-enabled 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.

IPS-enabled maps

To use ArcGIS IPS in your app, you must use an IPS-enabled 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-enabled, the map must contain an IPS positioning feature table named "IPS_Positioning". For detailed instructions about how to create an IPS-enabled map, see Get started with ArcGIS IPS.

You should also consider the following:

  • Wi-Fi IPS is not supported on iOS devices because Apple blocks 3rd-party software from using Wi-Fi for positioning.

  • If your facility has a mix of beacon-based IPS and Apple indoors positioning, the IndoorsLocationDataSource will utilize the beacon-based IPS, in preference.

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

To show the device location on an indoor map in your app, you have two options:

  1. You can use an IPS-enabled map that was created and shared from ArcGIS Pro. This is the recommended approach as the IndoorsLocationDataSource related data is stored with the map.
  2. You can manually use an indoor location data source from individual IPS positioning, pathways, and levels tables.

If you want to handle location change events to respond to floor changes or read other metadata for locations, see Handle location changes. If your app is using Bluetooth on the device to scan for beacon signals or GPS, make sure to add the appropriate permissions, see App permissions.

Use an IPS-enabled map

An IPS-enabled map contains layers to visualize the indoor space and stand-alone tables to determine a device's location and navigate within that space. For more information, see Prepare a map for use in ArcGIS IPS. To use this API in your app, obtain the indoor location data source from an IPS-enabled map as described in these steps:

  1. Load the IPS-enabled 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 Map.indoorPositioningDefinition. If this value is null, you cannot create a IndoorsLocationDataSource using this approach. Instead, manually use 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.

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
var body: some View {
    MapView(map: map)

        .locationDisplay(locationDisplay)

        .task {
             do {
                // Load the IPS-enabled map
                try await map.load()

                // Obtain the indoor positioning definition from the map
                guard let indoorPositioningDefinition = map.indoorPositioningDefinition else { return }

                // Load the indoor positioning definition and its related data
                try await indoorPositioningDefinition.load()

                // Create the indoors location data source using the indoor positioning definition
                let indoorLocationDataSource = IndoorsLocationDataSource(definition: indoorPositioningDefinition)

                // Create the map view's location display using the indoors location data source.
                locationDisplay = LocationDisplay(dataSource: indoorLocationDataSource)
                try await locationDisplay.dataSource.start()
                locationDisplay.autoPanMode = .recenter
            } catch {
                 print("Error starting location display.")
            }
        }
}

Use an indoor location data source

If your IPS-enabled 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-enabled 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. Assign the IndoorsLocationDataSource to the map view's LocationDisplay.

  7. Enable the map view's location display. 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.

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
    let pathwaysTableName = "ips_positioning"
    let positioningTable = ServiceFeatureTable(url: URL(string: "Your-feature-table")!)

    // Queries the ID that identifies a row in the positioning table. IndoorsLocationDataSource can be initialized without a row ID.
    func queryGlobalID(positioningTable: ServiceFeatureTable) async throws -> UUID? {
        let queryParameters = QueryParameters()
        let result = try await positioningTable.queryFeatures(using: queryParameters)
        let feature = result.features().makeIterator().next()
        return feature?.attributes[positioningTable.globalIDField] as? UUID
    }

    func fetchPathwayTable(map: Map) -> ArcGISFeatureTable? {
        let featureLayers = map.operationalLayers
        let pathwayLayer = featureLayers.first { $0.name == pathwaysTableName } as? FeatureLayer
        return pathwayLayer?.featureTable as? ArcGISFeatureTable
    }

    // Makes an indoor location display.
    func makeLocationDisplay(positioningTable: FeatureTable, pathwaysTable: ArcGISFeatureTable, globalID: UUID) -> LocationDisplay {
        // Setting up IndoorsLocationDataSource with positioning, pathways tables and positioning ID.
        // positioningTable - the "ips_positioning" feature table from IPS-enabled 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.
        // positioningID - an ID which identifies a specific row in the positioningTable that should be used for setting up IPS.
        let indoorLocationDataSource = IndoorsLocationDataSource(positioningTable: positioningTable, pathwaysTable: pathwaysTable, positioningID: globalID)
        let locationDisplay = LocationDisplay(dataSource: indoorLocationDataSource)
        locationDisplay.autoPanMode = .compassNavigation
        return locationDisplay
    }

Assign the IndoorsLocationDataSource as the geoview's location display data source. Enable the location display to start receiving locations.

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
    // A web map of the Esri campus with indoor positioning data.
    @State private var map: Map = {
        let item = PortalItem(
            portal: .arcGISOnline(connection: .anonymous),
            id: Item.ID("5e32d088d01c4fbcb359fef5504da0ec")!
        )
        let map = Map(item: item)
        return map
    }()

    /// A location display using the system location data source.
    @State private var indoorLocationDisplay = LocationDisplay(dataSource: SystemLocationDataSource())
    var body: some View {
        MapView(map: map)
            .locationDisplay(indoorLocationDisplay)
            .task {
                do {
                    if let pathwaysTable = fetchPathwayTable(map: map),
                       let globalID = try await queryGlobalID(positioningTable: positioningTable) {
                        indoorLocationDisplay = makeLocationDisplay(positioningTable: positioningTable, pathwaysTable: pathwaysTable, globalID: globalID)
                        // Start the map view's location display.
                        try await indoorLocationDisplay.dataSource.start()
                    }
                } catch {
                    print("Error starting location display.")
                }
            }
    }

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
        let additionalSourceProperties = location.additionalSourceProperties
        let floor = additionalSourceProperties[.floor] as? Int
        let positionSource = additionalSourceProperties[.positionSource]
        let satelliteCount = additionalSourceProperties[.satelliteCount]

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
        guard let featureLayers = map.operationalLayers.filter({ $0 is FeatureLayer }) as? [FeatureLayer] else { return }
        featureLayers.forEach { featureLayer in
            featureLayer.definitionExpression = "VERTICAL_ORDER = \(currentFloor)"
        }

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 this key to your app's Info.plist file.

Use dark colors for code blocksCopy
1
2
3
4
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth access is required for indoor positioning</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Location services are required for GNSS positioning</string>

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