Localization
Localization can be initiated on Buildings and Regions by calling the startLocalization method on an instance of these classes and providing a LocationListener, which listens for location events.
This method returns an instance of the LocationSession class, which allows interaction with the ongoing localization process. For example, it can be used to retrieve the device's current Attitude, enabling the visualization of a directional cone indicating where the user is pointing.
It is not possible to start two localization sessions simultaneously on different instances, as this will result in a MdkException.Busy exception.
Events
Location events are notified through listeners. The methods of the LocationListener class are invoked from a CoroutineScope returned by GiPStech.defaultScope. If desired, a different scope can be provided when calling startLocalization, which accepts this optional parameter.
Calibration request
If sensor calibration is required, localization is halted, the LocationListener.onCalibrationRequest method is triggered, and the calibration must be performed using the CalibrationManager class. For more details, please refer to the calibration page.
Exceptions
During localization, various errors may occur. Since these errors happen asynchronously (i.e. without calling a specific function), the MDK cannot throw an exception from a specific function. Instead, the LocationListener.onException event is triggered, providing an instance of any subclass of the GiPStechException class.
When an exception occurs, localization is stopped to allow it to be restarted within the LocationListener.onException function after the error has been handled.
If an exception is thrown during the startLocalization call, localization is not started.
Indoor localization
In the simplest scenario, the user may want to use the GiPStech MDK within a specific building. If the building is known, it can be retrieved using the SpatialManager.requestBuilding method, followed by a call to Building.startLocalization on the retrieved instance.
In this case, the localization system will attempt to determine the device's position exclusively inside the building. However, if the user is nearby but outside, the system may incorrectly place them inside, as it prioritizes localization within the selected building.
Example
The following example shows hot to start localizion inside a building and log the positions.
object MyLocationListener : LocationListener {
override suspend fun onLevelChanged(level: Int) {
building?.let {
println("Floor: ${it.floors[level].name}")
} ?: println("Level: $level")
}
override suspend fun onLocationUpdated(location: Location) {
println("You are at ${location.longitude} ${location.latitude}")
}
override suspend fun onCalibrationRequest(type: CalibrationType) {
...
}
override suspend fun onException(exception: GiPStechException) {
...
}
}
...
val building = SpatialManager.requestBuilding(buildingInfo.id)
building.startLocalization(MyLocationListener)
To halt localization, simply invoke the Building.stopLocalization method on the same instance.
building.stopLocalization()
Universal localization
The MDK is also capable of seamlessly localizing both indoors and outdoors, as well as determining which building the device is in.
However, this process is more complex as it may require continuously downloading map data during localization. To provide developers with maximum flexibility, the MDK does not automatically download map data. Instead, it is up to the developer to decide on the best strategy for managing data retrieval.
Below are three possible approaches:
- Localization within a predefined area
If the app is intended to function within a specific area, such as a university campus or an exhibition center, which includes multiple buildings and outdoor spaces, the developer can request a region that encompasses all relevant areas by providing a coordinate pair. Localization can then be initiated within this region. - Offline map data preloading
If the app allows users to explicitly download data for offline use, the developer can provide a dedicated page for managing map downloads. Before starting localization, the function SpatialManager.loadRegion can be used to load the specified area. - Seamless localization
For apps requiring continuous and universal localization, the developer can obtain an initial Region from the SpatialManager. As the user moves and approaches the region's boundary, additional map data can be loaded dynamically by calling the Region.requestAdditionalArea function.
Additional considerations for seamless localization
In case of seamless localization, the app must avoid calling Region.requestAdditionalArea more frequently than necessary, in order to prevent:
- Overloading the GiPStech Server, which may result in the server temporarily or permanently blocking requests from the device.
- Excessive consumption of the user's data bandwidth.
- Unnecessary performance degradation that could slow down the application.
Note: invoking requestAdditionalArea will have no effect if the specified area is already included in the currently loaded region.
For this reason, the Region.isNearOrOutsideBorder method can be used to determine when new data should be fetched from the server. This method, however, requires a detailed explanation, as its behavior may differ from what one might intuitively expect.
When a user requests a region by specifying a pair of coordinates, what is actually retrieved is a set of tiles that include those coordinates. For example, as shown in the image, requesting the red dotted area might result in the download of tiles 5, 6, 9 and 10.

Therefore, the actual area loaded for this region is the one highlighted in red - not just the exact area specified in the request.
Now, when calling Region.isNearOrOutsideBorder with a threshold value - for instance, 100 meters - the method does not return true when the user is near the originally requested coordinates. Instead, it triggers when the user gets close to the edge of the downloaded tiles (in red).
When Region.isNearOrOutsideBorder returns true and a new area is requested around the current location, the region is expanded to include all tiles that intersect the new coordinates.
This enlarged region (which includes tiles 2, 3 and 7) is illustrated in blue in the following image.

How to create a Region
A Region can be initialized in two ways:
1. Using the current location
The first method involves retrieving the last known location from the operating system using a convenient function such as SpatialManager.getLocationFromOS. You can then create a bounding box around this point with Coord2D.getBoundingBox, and finally load the region using either SpatialManager.requestRegion or SpatialManager.loadRegion.
2. Creating an empty region
Alternatively, you can create an empty region with SpatialManager.createEmptyRegion, and then incrementally request additional areas as needed, using the same methods described above.
Key Difference
The main distinction between the two approaches lies in the availability of localization data: with the first method, you ensure that all necessary data is already available at startup, allowing for immediate and accurate localization.
In contrast, the second method may lead to incomplete localization until the necessary data is fully downloaded.
Nonetheless, it has the advantage of not relying on SpatialManager.getLocationFromOS, which could return null or an imprecise position.
This issue also applies during localization. However, if the user moves sufficiently far — beyond a threshold distance defined by Region.isNearOrOutsideBorder — the necessary data is likely to be downloaded in time, before reaching the region’s outer boundary.
Example
This example demonstrates how to seamlessly utilize universal localization by continuously downloading data as the user moves.
object MyLocationListener : LocationListener {
private var currentBuilding: Building? = null
override suspend fun onLevelChanged(level: Int) {
currentBuilding?.let {
println("Going to ${it.floors[level].name}")
} ?: println("Level $level")
}
override suspend fun onBuildingChanged(building: Building?) {
currentBuilding = building
if (building != null) {
println("Entering ${building.name}")
} else {
println("Going outside")
}
}
override suspend fun onLocationUpdated(location: Location) {
println("You are at ${location.longitude} ${location.latitude}")
if (region.isNearOrOutsideBorder(location, 100.0)) {
val (sw, ne) = location.getBoundingBox(1000.0, 1000.0)
region.requestAdditionalArea(sw, ne, true)
}
}
override suspend fun onCalibrationRequest(type: CalibrationType) {
...
}
override suspend fun onException(exception: GiPStechException) {
...
}
}
...
val location = SpatialManager.getLocationFromOS()
if (location != null) {
val (sw, ne) = location.getBoundingBox(1000.0)
var region = SpatialManager.requestRegion(
southWest = sw,
northEast = ne,
fallback = true,
)
region.startLocalization(MyLocationListener)
}
Geofencing
If geofences have been defined in the GiPStech Map Manager, the GiPStech MDK can detect when the user enters or exits a specific area in both indoor and universal modes.
In this case, the LocationListener.onGeofenceEnter and LocationListener.onGeofenceExit events are triggered accordingly.