SDK for Indoor Navigation and Beyond
infsoft’s technology is also available as plugins for integration into third party apps. This means that existing applications can be upgraded with infsoft indoor positioning and indoor navigation. The SDK (Software Development Kit) is currently available for the Android and iOS mobile operating systems and as a HTML5 plugin. In addition to a native implementation, the use of frameworks such as PhoneGap or Xamarin is also possible.
Here you can find more information on the different class libraries and their implementation:
iOS
infsoft enables you to build iOS map apps using Mapbox.
This means:
- Mapbox-style-object generated server side, containing our maps, based on your data.
- 2D and 3D mode
- infsoft Locator library to display the user’s position
- All Mapbox functionality
GETTING STARTED
Resources
The following examples show how the infsoft services can be integrated into the Mapbox SDK.
Documentation for the Mapbox Maps SDK for iOS comes in the form of:
Installation
Install the Mapbox Maps SDK as described in the instructions of the official homepage.
The current supported SDK Version is 5.6 There is no guarantee that the integration will work with other versions.
The step about placing the access token can be skipped.
Official Mapbox iOS documentation
Configuration
No access token has to be set, because the infsoft style is used for the map.
To give your users a brief explanation of how the app will use their location data if you choose to access their location, add NSLocationWhenInUseUsageDescription and NSLocationAlwaysUsageDescription key to the Info.plist file with a description.
Remove attribution
Because no data is hosted by Mapbox itself it is not necessary to include an attribution.
Attribution and telemetry can be disabled in the following way.
Add a MGLMapboxMetricsEnabledSettingShownInApp key to the Info.plist file with the value YES
// let mapView = ...
// Disable attribution and hide buttons
UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
mapView.attributionButton.isHidden = true
mapView.logoView.isHidden = trueDISPLAY MAP
ViewController
import UIKit
import Mapbox
class ViewController: UIViewController {
    let styleHost = "tilesservices.webservices.infsoft.com"
    let stylePath = "/api/mapstyle/style/"
    let apiKey = "8c97d7c6-0c3a-41de-b67a-fb7628efba79"
  
    override func viewDidLoad() {
        super.viewDidLoad()
        var components = URLComponents()
        components.scheme = "https"
        components.host = styleHost
        components.path = stylePath + apiKey
      
        let mapView = MGLMapView(frame: view.bounds, styleURL: components.url)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.setCenter(CLLocationCoordinate2D(latitude: 49.867630660511715,
                                                 longitude: 10.89075028896332),
                          zoomLevel: 16,
                          animated: false)
        view.addSubview(mapView)
        // Disable telemetry and hide button
        UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
        mapView.attributionButton.isHidden = true
        mapView.logoView.isHidden = true
    }
}DISPLAY MAP 3D
ViewController
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
    let styleHost = "tilesservices.webservices.infsoft.com"
    let stylePath = "/api/mapstyle/style/"
    let apiKey = "8c97d7c6-0c3a-41de-b67a-fb7628efba79"
    let initial3D = "false"
    override func viewDidLoad() {
        super.viewDidLoad()
      
        var components = URLComponents()
        components.scheme = "https"
        components.host = styleHost
        components.path = stylePath + apiKey
        let queryItemConfig = URLQueryItem(name: "config", 
                                           value: "3d:\(initial3D)")
        components.queryItems = [queryItemConfig]      
      
        let mapView = MGLMapView(frame: view.bounds, styleURL: components.url)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self
        mapView.setCenter(CLLocationCoordinate2D(latitude: 49.867630660511715,
                                                 longitude: 10.89075028896332),
                          zoomLevel: 16,
                          animated: false)
        view.addSubview(mapView)
        // Disable telemetry and hide button
        UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
        mapView.attributionButton.isHidden = true
        mapView.logoView.isHidden = true
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
    /// Toggle 2D/3D map
    func setDimension(in mapView: MGLMapView, enabled3D: Bool) {
        guard let style = mapView.style else {
            return
        }
        if enabled3D {
            for layer in style.layers {
                if layer.identifier.contains("-loc3d-") {
                    layer.isVisible = true
                } else if layer.identifier.contains("-loc2d-") {
                    layer.isVisible = false
                }
            }
        } else {
            for layer in style.layers {
                if layer.identifier.contains("-loc3d-") {
                    layer.isVisible = false
                } else if layer.identifier.contains("-loc2d-") {
                    layer.isVisible = true
                }
            }
        }
    }
    // MARK: - Mapbox mapView delegate
    func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
        setDimension(in: mapView, enabled3D: true)
    }
}SWITCH LEVELS
ViewController
import UIKit
import Mapbox
class ViewController: UIViewController, MGLMapViewDelegate {
    let styleHost = "tilesservices.webservices.infsoft.com"
    let stylePath = "/api/mapstyle/style/"
    let apiKey = "8c97d7c6-0c3a-41de-b67a-fb7628efba79"
    let initial3D = "false"
    var currentLevel = 0
    var numberOfLevels = 4
    weak var mapView: MGLMapView?
    override func viewDidLoad() {
        super.viewDidLoad()
        var components = URLComponents()
        components.scheme = "https"
        components.host = styleHost
        components.path = stylePath + apiKey
        let queryItemConfig = URLQueryItem(name: "config", 
                                           value: "3d:\(initial3D)")
        components.queryItems = [queryItemConfig]
      
        let mapView = MGLMapView(frame: view.bounds, styleURL: components.url)
        self.mapView = mapView
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self
        mapView.setCenter(CLLocationCoordinate2D(latitude: 49.867630660511715,
                                                 longitude: 10.89075028896332),
                          zoomLevel: 16,
                          animated: false)
        view.insertSubview(mapView, at: 0)
        // Disable telemetry and hide button
        UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
        mapView.attributionButton.isHidden = true
        mapView.logoView.isHidden = true
    }
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
    }
    // MARK: - Actions
    @IBAction private func switchLevel(_ sender: Any) {
        currentLevel = (currentLevel + 1) % numberOfLevels
        guard let mapView = mapView else {
          return
      }
        setLevel(in: mapView, level: currentLevel)   
    }
    // MARK: - Map dimension handling
    /// Toggle 2D/3D map
    func setDimension(in mapView: MGLMapView, enabled3D: Bool) {
        guard let style = mapView.style else {
            return
        }
        if enabled3D {
            for layer in style.layers {
                if layer.identifier.contains("-loc3d-") {
                    layer.isVisible = true
                } else if layer.identifier.contains("-loc2d-") {
                    layer.isVisible = false
                }
            }
        } else {
            for layer in style.layers {
                if layer.identifier.contains("-loc3d-") {
                    layer.isVisible = false
                } else if layer.identifier.contains("-loc2d-") {
                    layer.isVisible = true
                }
            }
        }
    }
    // MARK: - Map level handling
    private func setLevel(in mapView: MGLMapView, level: Int) {
        guard let style = mapView.style else {
            return
        }
        let filterLayers: [MGLVectorStyleLayer] = style.layers.compactMap {
            $0 as? MGLVectorStyleLayer
        }.filter {
            $0.identifier.contains("locls")
        }
        for layer in filterLayers {
            if let predicate = layer.predicate {
                guard let rawData = predicate.mgl_jsonExpressionObject as? [Any] else {
                    return
                }
                let newPredicate = NSPredicate(mglJSONObject: setFilter(filter: rawData, level: level))
                layer.predicate = newPredicate
            }
        }
    }
    func setFilter(filter: Any, level: Int) -> Any {
        guard var predicates = filter as? [Any] else {
            return filter
        }
        for (index, element) in predicates.enumerated() {
            if element as? [Any] != nil {
                predicates[index] = self.setFilter(filter: element, level: level)
                continue
            }
            if index == 0 {
                continue
            }
            guard let prev = predicates[index - 1] as? String else {
                continue
            }
            if predicates[index] as? Int == nil {
                continue
            }
            if prev != "level" {
                continue
            }
            predicates[index] = level
        }
        return predicates
    }
    // MARK: - Mapbox mapView delegate
    func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
        setDimension(in: mapView, enabled3D: true)
    }
}DISPLAY ROUTE
RouteViewController
import UIKit
import Mapbox
class RouteViewController: UIViewController, MGLMapViewDelegate {
    let styleHost = "tilesservices.webservices.infsoft.com"
    let stylePath = "/api/mapstyle/style/"
    let apiKey = "8c97d7c6-0c3a-41de-b67a-fb7628efba79"
    let initial3D = "false"
    override func viewDidLoad() {
        super.viewDidLoad()
        var components = URLComponents()
        components.scheme = "https"
        components.host = styleHost
        components.path = stylePath + apiKey
        let queryItemConfig = URLQueryItem(name: "config", 
                                           value: "3d:\(initial3D)")
        components.queryItems = [queryItemConfig]
        let mapView = MGLMapView(frame: view.bounds, styleURL: components.url)
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.delegate = self
        mapView.setCenter(CLLocationCoordinate2D(latitude: 49.867630660511715,
                                                 longitude: 10.89075028896332),
                          zoomLevel: 18,
                          animated: false)
        view.insertSubview(mapView, at: 0)
        // Disable telemetry and hide button
        UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
        mapView.attributionButton.isHidden = true
        mapView.logoView.isHidden = true
    }
    /// This creates a valid URL for requesting route information
    /// - Parameters:
    ///   - startLat: Latitude of starting point
    ///   - startLon: Longitude of starting point
    ///   - startLevel: Level ID of starting point
    ///   - endLat: Latitude of endpoint
    ///   - endLon: Longitude of endpoint
    ///   - endLevel: Level ID of endpoint
    ///   - apiKey: To be used apiKey
    ///   - localization: Localication identifier, eg. "DE", "EN"
    ///   - wayContext: Waycontext for route calculation. Empty string is the default value
    /// - Returns: A route service URL for the given parameters
    func createRouteURL(startLat: Double, startLon: Double, startLevel: Int, endLat: Double, endLon: Double, 
                        endLevel: Int, apiKey: String, localization: String = "EN", wayContext: String = "") -> URL {
        var compoments = URLComponents()
        compoments.scheme = "https"
        compoments.host = "routes.webservices.infsoft.com"
        compoments.path = "/API/Calc"
        let queryItemAPIKey = URLQueryItem(name: "apikey", value: apiKey)
        let queryItemStartLat = URLQueryItem(name: "startlat", value: "\(startLat)")
        let queryItemStartLon = URLQueryItem(name: "startlon", value: "\(startLon)")
        let queryItemStartLevel = URLQueryItem(name: "startlevel", value: "\(startLevel)")
        let queryItemEndLat = URLQueryItem(name: "endlat", value: "\(endLat)")
        let queryItemEndLon = URLQueryItem(name: "endlon", value: "\(endLon)")
        let queryItemEndLevel = URLQueryItem(name: "endlevel", value: "\(endLevel)")
        let queryItemLocalization = URLQueryItem(name: "lcid", value: "\(localization)")
        let queryItemWayContext = URLQueryItem(name: "context", value: "\(wayContext)")
        compoments.queryItems = [queryItemAPIKey, queryItemStartLat, queryItemStartLon,
                                 queryItemStartLevel, queryItemEndLat, queryItemEndLon, 
                                 queryItemEndLevel,
                                 queryItemLocalization, queryItemWayContext]
        return compoments.url!
    }
    func renderGeoJSON(from route: Route, in mapView: MGLMapView) {
        guard let routeElement = route.first?.geoJSON, let geoJSONData = try? JSONEncoder().encode(routeElement) else {
            return
        }
        guard let shapeFromGeoJSON = try? MGLShape(data: geoJSONData, encoding: String.Encoding.utf8.rawValue) else {
            return
        }
        guard let style = mapView.style, let source = style.source(withIdentifier: "loc-routes") as? MGLShapeSource else {
            return
        }
        source.shape = shapeFromGeoJSON
    }
    // MARK: - Mapbox mapView delegate
    func mapView(_ mapView: MGLMapView, didFinishLoading style: MGLStyle) {
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
        let routeServiceURL = createRouteURL(startLat: 49.86739, startLon: 10.89190, startLevel: 0,
                                             endLat: 49.86701, endLon: 10.89054, endLevel: 0, apiKey: apiKey)
        let task = session.dataTask(with: routeServiceURL, completionHandler: { (data: Data?, _: URLResponse?,
            error: Error?) -> Void in
            guard error == nil, let routeData = data else {
                // Handle error case
                return
            }
            if let routeModel = try? JSONDecoder().decode(Route.self, from: routeData) {
                self.renderGeoJSON(from: routeModel, in: mapView)
            }
        })
        task.resume()
    }
}Route JSON model
public typealias Route = [RouteElement]
// MARK: - RouteElement
public struct RouteElement: Codable {
    enum CodingKeys: String, CodingKey {
        case copyrights = "Copyrights"
        case distance = "Distance"
        case duration = "Duration"
        case endTS = "EndTS"
        case startTS = "StartTS"
        case endAddress = "EndAddress"
        case endLocation = "EndLocation"
        case startAddress = "StartAddress"
        case startLocation = "StartLocation"
        case steps = "Steps"
        case context = "Context"
        case valid = "Valid"
        case revision = "Revision"
        case geoJSON = "GeoJson"
    }
    let copyrights: String
    let distance: Distance
    let duration: Duration
    let endTS, startTS: String
    let endAddress: String
    let endLocation: PolyPoint
    let startAddress: String
    let startLocation: PolyPoint
    let steps: [Step]
    let context: String
    let valid: Bool
    let revision: Int
    let geoJSON: GeoJSON
}
// MARK: - Distance
struct Distance: Codable {
    enum CodingKeys: String, CodingKey {
        case value = "Value"
        case text = "Text"
    }
    let value: Int
    let text: String
}
// MARK: - Duration
struct Duration: Codable {
    enum CodingKeys: String, CodingKey {
        case value = "Value"
        case text = "Text"
    }
    let value: Int
    let text: String
}
// MARK: - EndLocation
struct PolyPoint: Codable {
    enum CodingKeys: String, CodingKey {
        case latitude = "Latitude"
        case longitude = "Longitude"
        case level = "Level"
    }
    let latitude, longitude: Double
    let level: Int
}
// MARK: - GeoJSON
struct GeoJSON: Codable {
    let type: String
    let features: [Feature]
}
// MARK: - Feature
struct Feature: Codable {
    let properties: Properties
    let type: FeatureType
    let geometry: Geometry
}
// MARK: - Geometry
struct Geometry: Codable {
    let type: GeometryType
    let coordinates: [GeometryCoordinate]
}
enum GeometryCoordinate: Codable {
    case double(Double)
    case unionArray([RouteCoordinate])
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode([RouteCoordinate].self) {
            self = .unionArray(value)
            return
        }
        if let value = try? container.decode(Double.self) {
            self = .double(value)
            return
        }
        throw DecodingError.typeMismatch(GeometryCoordinate.self,
                                         DecodingError.Context(codingPath: decoder.codingPath,
                                                               debugDescription: "Wrong type for GeometryCoordinate"))
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .double(let value):
            try container.encode(value)
        case .unionArray(let value):
            try container.encode(value)
        }
    }
}
enum RouteCoordinate: Codable {
    case double(Double)
    case doubleArray([Double])
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let value = try? container.decode([Double].self) {
            self = .doubleArray(value)
            return
        }
        if let value = try? container.decode(Double.self) {
            self = .double(value)
            return
        }
        throw DecodingError.typeMismatch(RouteCoordinate.self,
                                         DecodingError.Context(codingPath: decoder.codingPath,
                                                               debugDescription: "Wrong type for RouteCoordinate"))
    }
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .double(let value):
            try container.encode(value)
        case .doubleArray(let value):
            try container.encode(value)
        }
    }
}
enum GeometryType: String, Codable {
    case lineString = "LineString"
    case point = "Point"
    case polygon = "Polygon"
}
// MARK: - Properties
struct Properties: Codable {
    let level: Int
    let style: Style?
    let width: Int?
    let color: String?
    let icon, title: String?
    let step: Int?
}
enum Style: String, Codable {
    case route
    case routedirection
}
enum FeatureType: String, Codable {
    case feature = "Feature"
}
// MARK: - Calc
struct Calc: Codable {
    enum CodingKeys: String, CodingKey {
        case unit = "Unit"
        case startVal = "StartVal"
        case endVal = "EndVal"
    }
    let unit: String
    let startVal, endVal: Int
}
struct Step: Codable {
    enum CodingKeys: String, CodingKey {
        case direction = "Direction"
        case endLocation = "EndLocation"
        case startLocation = "StartLocation"
        case endTS = "EndTS"
        case startTS = "StartTS"
        case distance = "Distance"
        case distanceCalc = "DistanceCalc"
        case duration = "Duration"
        case durationCalc = "DurationCalc"
        case instructions = "Instructions"
        case instructionsTemplate = "InstructionsTemplate"
        case polyline = "Polyline"
        case travelMode = "TravelMode"
        case travelObjectName = "TravelObjectName"
        case travelObjectID = "TravelObjectID"
    }
    let direction: Int
    let endLocation, startLocation: PolyPoint
    let endTS, startTS: String
    let distance: Distance
    let distanceCalc: Calc
    let duration: Duration
    let durationCalc: Calc
    let instructions, instructionsTemplate: String
    let polyline: [PolyPoint]
    let travelMode: Int
    let travelObjectName, travelObjectID: String?
}USER LOCALIZATION
Add the Locator library
Add the locator library as embedded content to your project. It should look like the following image.
For swift projects you have to import the Locator header in your bridging header file as follows.
//
//  Use this file to import your target's public headers that you would like to expose to Swift.
//
#import <InfsoftLocatorLib/InfsoftLocator.h>Info.plist##
Since the position of the user is determined via Bluetooth, the NSBluetoothAlwaysUsageDescription and NSBluetoothPeripheralUsageDescription key must be additionally added to your Info.plist.
The Locator library also requires NSMotionUsageDescription as a key in your Info.plist file. The library pauses the location updates if no device motion is detected. In this way the battery consumption is reduced.
UserLocalizationViewController
To display the user’s position via BLE, the map’s default location manager must be overwritten.
import UIKit
import Mapbox
class UserLocalizationViewController: UIViewController {
    // Sample UI controls. This can be extended as desired
    @IBOutlet private weak var userTrackingModeButton: UIButton!
    @IBOutlet private weak var mapLevelButton: UIButton!
    let styleHost = "tilesservices.webservices.infsoft.com"
    let stylePath = "/api/mapstyle/style/"
    let apiKey = "316cf74a-290b-4c4f-91c0-43989cdb15d3"
    let initial3D = "false"
    
    let numberOfMapLevels = 4
    var mapLevel = 0
    weak var mapView: MGLMapView?
    override func viewDidLoad() {
        super.viewDidLoad()
        var components = URLComponents()
        components.scheme = "https"
        components.host = styleHost
        components.path = stylePath + apiKey
        let queryItemConfig = URLQueryItem(name: "config", value: "3d:\(initial3D)")
        components.queryItems = [queryItemConfig]
        let mapView = MGLMapView(frame: view.bounds, styleURL: components.url)
        self.mapView = mapView
        mapView.delegate = self
        mapView.userTrackingMode = .follow
        mapView.locationManager = CustomLocationManager()
        mapView.showsUserLocation = true
        mapView.showsUserHeadingIndicator = true
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.setCenter(CLLocationCoordinate2D(latitude: 50.99319673751623,
                                                 longitude: 11.012463569641115),
                          zoomLevel: 16,
                          animated: false)
        view.insertSubview(mapView, at: 0)
        
        // Disable telemetry and hide button
        UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
        mapView.attributionButton.isHidden = true
        mapView.logoView.isHidden = true
    }
    // MARK: - Example action outlets
  
    /**
     Example action outlet for switching the user tracking mode.
     If you want to use this feature just connect an UIButton to this outlet
     */
    @IBAction private func switchUserTrackingMode(_ sender: Any) {
        guard let mapView = mapView else {
            return
        }
        switch mapView.userTrackingMode {
        case .follow:
            mapView.userTrackingMode = .none
        default:
            mapView.userTrackingMode = .follow
        }
    }
    /**
     Example action outlet for switching the map level.
     If you want to use this feature just connect an UIButton to this outlet
     */
    @IBAction private func switchLevel(_ sender: Any) {
        let newLevel = (mapLevel + 1) % numberOfMapLevels
        guard let mapView = mapView else {
            return
        }
        mapView.userTrackingMode = .none
        setMapLevel(in: mapView, level: newLevel)
        if let userLocation = mapView.userLocation {
            updateUserAnnotation(userLocation, in: mapView)
        }
    }
    /**
     Optional method for updating the UI in case the tracking mode
     changes
     */
    func updateUserTrackingControl(for mode: MGLUserTrackingMode) {
        userTrackingModeButton.isSelected = mode == .follow
    }
    /**
      Optional method for updating the UI in case the map level
      changes
     */
    func updateMapLevelControl(for level: Int) {
        mapLevelButton.setTitle("\(level)", for: .normal)
    }
    // MARK: - Map level handling
    /**
     This method syncs the map level to the user level if the corresponding mode is selected
     */
    func syncMapLevel() {
        guard let mapView = mapView else {
            return
        }
        switch mapView.userTrackingMode {
        case .follow, .followWithHeading:
            guard let userLevel = (mapView.userLocation?.location as? CLLocation3D)?.level else {
                break
            }
            self.setMapLevel(in: mapView, level: userLevel)
        default:
            break
        }
    }
    func setMapLevel(in mapView: MGLMapView, level: Int) {
        guard let style = mapView.style, level != mapLevel else {
            return
        }
        let filterLayers: [MGLVectorStyleLayer] = style.layers.compactMap {
            $0 as? MGLVectorStyleLayer
        }.filter {
            $0.identifier.contains("locls")
        }
        for layer in filterLayers {
            if let predicate = layer.predicate {
                guard let rawData = predicate.mgl_jsonExpressionObject as? [Any] else {
                    return
                }
                let newPredicate = NSPredicate(mglJSONObject: setFilter(filter: rawData, level: level))
                layer.predicate = newPredicate
            }
        }
        mapLevel = level
        updateMapLevelControl(for: level)
    }
    func setFilter(filter: Any, level: Int) -> Any {
        guard var predicates = filter as? [Any] else {
            return filter
        }
        for (index, element) in predicates.enumerated() {
            if element as? [Any] != nil {
                predicates[index] = self.setFilter(filter: element, level: level)
                continue
            }
            if index == 0 {
                continue
            }
            guard let prev = predicates[index - 1] as? String else {
                continue
            }
            if predicates[index] as? Int == nil {
                continue
            }
            if prev != "level" {
                continue
            }
            predicates[index] = level
        }
        return predicates
    }
}
// MARK: - Mapbox mapView delegate
extension UserLocalizationViewController: MGLMapViewDelegate {
    func mapView(_ mapView: MGLMapView, didUpdate userLocation: MGLUserLocation?) {
        guard let userLocation = mapView.userLocation else {
            return
        }
        syncMapLevel()
        updateUserAnnotation(userLocation, in: mapView)
    }
    func mapView(_ mapView: MGLMapView, didChange mode: MGLUserTrackingMode, animated: Bool) {
        syncMapLevel()
        updateUserTrackingControl(for: mode)
    }
    /**
     Hide the annotation view if the wrong level is selected. If the default view is used,
     this is achieved by setting a large offset. If you implement the annotation view by yourself,
     you can do this explicitly using the update() method
     */
    func updateUserAnnotation(_ userLocation: MGLUserLocation, in mapView: MGLMapView) {
        guard let location3DCoordinate = userLocation.location as? CLLocation3D else {
            return
        }
        let view = mapView.view(for: userLocation)
        if location3DCoordinate.level != mapLevel {
             view?.centerOffset = CGVector(dx: 0, dy: Int.max)
         } else {
             view?.centerOffset = CGVector(dx: 0, dy: 0)
         }
         mapView.updateUserLocationAnnotationView()
    }
}CustomLocationManager
import UIKit
import Mapbox
class CustomLocationManager: NSObject, MGLLocationManager {
    weak var delegate: MGLLocationManagerDelegate?
    let apiKey = "8c97d7c6-0c3a-41de-b67a-fb7628efba79"
    /// CLLocationManager is used for requesting the user heading
    lazy var clLocationManger: CLLocationManager = {
        let locationManager = CLLocationManager()
        locationManager.delegate = self
        return locationManager
    }()
    /// ILLocationManager is used for requesting the user position
    lazy var ilLocationManger: ILLocationManager = {
        let iLLocationServiceManager = ILLocationServiceManager.getInstance(ILLocationServiceManager_UseAll)
        iLLocationServiceManager?.apiKey = apiKey
        return ILLocationManager()
    }()
    var authorizationStatus: CLAuthorizationStatus {
        CLLocationManager.authorizationStatus()
    }
    var headingOrientation: CLDeviceOrientation {
        get {
            clLocationManger.headingOrientation
        }
        set {
            clLocationManger.headingOrientation = newValue
        }
    }
    func requestAlwaysAuthorization() {
        clLocationManger.requestAlwaysAuthorization()
    }
    func requestWhenInUseAuthorization() {
        clLocationManger.requestWhenInUseAuthorization()
    }
    func startUpdatingLocation() {
        ilLocationManger.requestLocationUpdates(self, minTime: 0, minDistance: 0)
    }
    func stopUpdatingLocation() {
        clLocationManger.stopUpdatingLocation()
    }
    func startUpdatingHeading() {
        clLocationManger.startUpdatingHeading()
    }
    func stopUpdatingHeading() {
        clLocationManger.stopUpdatingHeading()
    }
    func dismissHeadingCalibrationDisplay() {
        clLocationManger.dismissHeadingCalibrationDisplay()
    }
}
// MARK: - CLLocationManagerDelegate
extension CustomLocationManager: CLLocationManagerDelegate {
    /// This method should do nothing. Use ILLocationManager instead
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    }
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        delegate?.locationManager(self, didUpdate: newHeading)
    }
    func locationManagerShouldDisplayHeadingCalibration(_ manager: CLLocationManager) -> Bool {
        delegate?.locationManagerShouldDisplayHeadingCalibration(self) ?? false
    }
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        delegate?.locationManager(self, didFailWithError: error)
    }
}
// MARK: - ILLocation
extension CustomLocationManager: ILLocationListener {
    func onLocationChanged(_ location: ILLocation!) {
        let clLocation3D = CLLocation3D(latitude: location.latitude, longitude: location.longitude)
        clLocation3D.level = Int(location.level)
        delegate?.locationManager(self, didUpdate: [clLocation3D])
    }
}
/// Extension of the class so that the user level can also be specified
class CLLocation3D: CLLocation {
    var level: Int?
}POI SELECTION
POISelectionViewController
import UIKit
import Mapbox
class POISelectionViewController: UIViewController {
    // Map style
    let styleHost = "tilesservices.webservices.infsoft.com"
    let stylePath = "/api/mapstyle/style/"
    let apiKey = "8c97d7c6-0c3a-41de-b67a-fb7628efba79"
    let initial3D = "false"
    // GeoObjects
    let geoObjectsHost = "tiles.infsoft.com"
    let geoObjectsPath = "/api/geoobj/json/"
    // Revision
    let revisionHost = "tilesservices.webservices.infsoft.com"
    let revisionPath = "/api/geojson/revision"
    let numberOfMapLevels = 4
    var mapLevel = 0
    var geoObjects: GeoObjects?
    weak var mapView: MGLMapView?
    var revisionNumber: String? {
        didSet {
            loadGeoObjects()
        }
    }
    override func viewDidLoad() {
        super.viewDidLoad()
        var components = URLComponents()
        components.scheme = "https"
        components.host = styleHost
        components.path = stylePath + apiKey
        let queryItemConfig = URLQueryItem(name: "config", value: "3d:\(initial3D)")
        components.queryItems = [queryItemConfig]
        let mapView = MGLMapView(frame: view.bounds, styleURL: components.url)
        self.mapView = mapView
        mapView.delegate = self
        mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        mapView.setCenter(CLLocationCoordinate2D(latitude: 49.867630660511715,
                                                 longitude: 10.89075028896332),
                          zoomLevel: 16,
                          animated: false)
        view.insertSubview(mapView, at: 0)
        // Disable telemetry and hide button
        UserDefaults.standard.set(false, forKey: "MGLMapboxMetricsEnabled")
        mapView.attributionButton.isHidden = true
        mapView.logoView.isHidden = true
        addGestureRecognizer()
        loadRevisionNumber()
    }
    // MARK: - Handle annotation
    func addGestureRecognizer() {
        guard let mapView = mapView else {
            return
        }
        let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(sender:)))
        for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
            guard let recognizer = recognizer as? UITapGestureRecognizer else {
                continue
            }
            if recognizer.numberOfTapsRequired == 1 {
                continue
            }
            singleTap.require(toFail: recognizer)
        }
        mapView.addGestureRecognizer(singleTap)
    }
    @objc func handleMapTap(sender: UITapGestureRecognizer) {
        guard let mapView = mapView else {
            return
        }
        if sender.state == .ended {
            let point = sender.location(in: sender.view!)
            let layerIdentifiers = Set<String>(arrayLiteral: "locls-pois")
            for feature in mapView.visibleFeatures(at: point, styleLayerIdentifiers: layerIdentifiers)
                where feature is MGLPointFeature {
                    guard let selectedFeature = feature as? MGLPointFeature else {
                        fatalError("Failed to cast selected feature as MGLPointFeature")
                    }
                    showAnnotation(feature: selectedFeature)
                    return
            }
            let touchCoordinate = mapView.convert(point, toCoordinateFrom: nil)
            let touchLocation = CLLocation(latitude: touchCoordinate.latitude, longitude: touchCoordinate.longitude)
            let touchRect = CGRect(origin: point, size: .zero).insetBy(dx: -22.0, dy: -22.0)
            let possibleFeatures = mapView.visibleFeatures(in: touchRect, styleLayerIdentifiers: Set(layerIdentifiers)).filter { $0 is MGLPointFeature }
            // Select the closest feature to the touch center.
            let closestFeatures = possibleFeatures.sorted(by: {
                return CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude).distance(from: touchLocation) < CLLocation(latitude: $1.coordinate.latitude, longitude: $1.coordinate.longitude).distance(from: touchLocation)
            })
            if let feature = closestFeatures.first {
                guard let closestFeature = feature as? MGLPointFeature else {
                    fatalError("Failed to cast selected feature as MGLPointFeature")
                }
                showAnnotation(feature: closestFeature)
                return
            }
            // If no features were found, deselect the selected annotation, if any.
            mapView.deselectAnnotation(mapView.selectedAnnotations.first, animated: true)
        }
    }
    @objc func showAnnotation(feature: MGLPointFeature) {
        guard let geoObjects = self.geoObjects, let featureUID = feature.attribute(forKey: "uid") as? String else {
            return
        }
        // Search for the matching GeoObject in the loaded data
        let matchingGeoObjects = geoObjects.filter { (geoObject) -> Bool in
            geoObject.uid == featureUID
        }
        guard let firstMatchingGeoObject = matchingGeoObjects.first else {
            return
        }
        let annotation = MGLPointAnnotation()
        // Set the custom title for the annotation.
        annotation.title = firstMatchingGeoObject.type
        annotation.coordinate = feature.coordinate
        mapView?.selectAnnotation(annotation, animated: true, completionHandler: nil)
    }
    // MARK: - GeoObjects
    func loadRevisionNumber() {
        var components = URLComponents()
        components.scheme = "https"
        components.host = revisionHost
        components.path = revisionPath + "/" + apiKey
        guard let url = components.url else {
            return
        }
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
            let task = session.dataTask(with: url, completionHandler: { (data: Data?, _: URLResponse?, error: Error?) -> Void in
                guard error == nil, let revisionData = data else {
                    // Handle error case
                    return
                }
                self.revisionNumber = String(data: revisionData, encoding: .utf8)
            })
            task.resume()
    }
    func loadGeoObjects() {
        guard let revisionNumber = revisionNumber else {
            return
        }
        var components = URLComponents()
        components.scheme = "https"
        let localization = "en"
        components.host = geoObjectsHost
        components.path = geoObjectsPath + apiKey + "/" + localization + "/" + revisionNumber
        guard let url = components.url else {
            return
        }
        let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
        let task = session.dataTask(with: url, completionHandler: { (data: Data?, _: URLResponse?, error: Error?) -> Void in
            guard error == nil, let objectsData = data else {
                // Handle error case
                return
            }
            self.geoObjects = try? JSONDecoder().decode(GeoObjects.self, from: objectsData)
        })
        task.resume()
    }
}
extension POISelectionViewController: MGLMapViewDelegate {
    func mapView(_ mapView: MGLMapView, didDeselect annotation: MGLAnnotation) {
        mapView.removeAnnotation(annotation)
    }
    func mapView(_ mapView: MGLMapView, annotationCanShowCallout annotation: MGLAnnotation) -> Bool {
        return geoObjects != nil
    }
    func mapView(_ mapView: MGLMapView, calloutViewFor annotation: MGLAnnotation) -> MGLCalloutView? {
        return nil
    }
}GeoObject
public typealias GeoObjects = [GeoObject]
// MARK: - GeoObject
@objc public class GeoObject: NSObject, Codable {
    public let uid: String
    public let type: String
    public let props: [String: String]
    public func propertyFor(key: String) -> String? {
       return props[key]
    }
}Android
infsoft enables you to build Android map apps using Mapbox. This means:
- Mapbox-style-object generated server side, containing our maps, based on your data.
- 2D and 3D mode
- infsoft Locator library to display the user’s position
- All Mapbox functionality
GETTING STARTED
The samples are written in Kotlin.
Dependencies:
Add MapBox to your build.gradle.kts dependencies. MapBox 11.7.0 is supported.
...
dependencies {
    implementation(libs.mapbox)
    implementation(libs.mapbox.annotations)
}Versions are defined in the file gradle/libs.versions.toml
[versions]
mapbox = "11.7.0"
mapboxBase = "0.11.0"
[libraries]
mapbox = { module = "com.mapbox.maps:android", name = "mapbox", version.ref = "mapbox" }
mapbox-annotations = { module = "com.mapbox.base:annotations", name = "mapbox.annotations", version.ref = "mapboxBase" }Mapbox requires a token being set in the gradle.properties to allow downloads of the SDK.
...
MAPBOX_DOWNLOADS_TOKEN=<yourToken>Additionally an AccessToken is required to access mapBox data. Create the following file res/values/mapbox_access_token-xml.
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools">
    <string name="mapbox_access_token" translatable="false" tools:ignore="UnusedResources">/*Your token*/</string>
</resources>COMMON CODE
Here you can find code that is used in multiple demo apps.
Switching levels
The following code is used in every example application, to change the selected level.
import com.mapbox.maps.Style
import com.mapbox.maps.extension.style.expressions.generated.Expression
import com.mapbox.maps.extension.style.layers.generated.FillExtrusionLayer
import com.mapbox.maps.extension.style.layers.generated.FillLayer
import com.mapbox.maps.extension.style.layers.generated.LineLayer
import com.mapbox.maps.extension.style.layers.generated.SymbolLayer
import com.mapbox.maps.extension.style.layers.getLayer
/**
* Helper class to change the selected level.
*/
object LevelSwitch {
    /**
     * Changes the MapBox filters to display only data for the given level.
     *
     * @param style MapBox style object on which the filters will be changed.
     * @param level The level that should be displayed.
     */
    fun updateLevel(style: Style, level: Int) {
        val layers = style.styleLayers.map { it.id }
        for (layerId in layers) {
            if (layerId.contains("locls")) {
                val layer = style.getLayer(layerId)
                if (layer is SymbolLayer) {
                    val filter = calculateFilter(layer.filter, level)
                    if (filter != null)
                        layer.filter(filter)
                }
                else if (layer is FillExtrusionLayer) {
                    val filter = calculateFilter(layer.filter, level)
                    if (filter != null)
                        layer.filter(filter)
                }
                else if (layer is FillLayer) {
                    val filter = calculateFilter(layer.filter, level)
                    if (filter != null)
                        layer.filter(filter)
                }
                else if (layer is LineLayer) {
                    val filter = calculateFilter(layer.filter, level)
                    if (filter != null)
                        layer.filter(filter)
                }
            }
        }
    }
    private fun calculateFilter(filter: Expression?, level: Int) : Expression? {
        if (filter == null)
            return null;
        var json = filter.toJson()
        json = json.replace(Regex("\"level\",.+?]"), "\"level\",$level]")
        return Expression.fromRaw(json)
    }
}DISPLAY MAP
DisplayMapActivity
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.geojson.Point;
import com.mapbox.maps.CameraOptions;
import com.mapbox.maps.MapView;
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style;
class DisplayMapActivity : AppCompatActivity(), Style.OnStyleLoaded {
    private val styleUrl = "https://tilesservices.webservices.infsoft.com/api/mapstyle/style/"
    private val apiKey = "<your api key here>"
    private val initial3d = "FALSE"
    private lateinit var mapboxMap: MapboxMap
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mapView = MapView(this)
        mapboxMap = mapView.mapboxMap
        setContentView(mapView)
        val styleUrl = "$styleUrl$apiKey?config=3d:$initial3d"
        mapboxMap.loadStyle(styleUrl, this)
    }
    override fun onStyleLoaded(style: Style) {
        LevelSwitch.updateLevel(style, 0)
        setInitialCamera()
    }
    private fun setInitialCamera() {
        val cameraPosition = CameraOptions.Builder()
                .center(Point.fromLngLat(10.89075028896332,49.867630660511715)) // Sets the new camera position
                .zoom(18.0) // Sets the zoom
                .bearing(0.0) // Rotate the camera
                .pitch(0.0) // Set the camera tilt
                .build();
        mapboxMap.setCamera(cameraPosition)
    }
}
 LevelSwitch
DISPLAY MAP 3D
DisplayMap3DActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.mapbox.maps.extension.style.layers.getLayer
import com.mapbox.maps.extension.style.layers.properties.generated.Visibility
import java.util.Locale
class DisplayMap3DActivity : AppCompatActivity(), Style.OnStyleLoaded {
    private val styleUrl = "https://tilesservices.webservices.infsoft.com/api/mapstyle/style/"
    private val apiKey = "<your api key here>"
    private val initial3d = "FALSE"
    private lateinit var mapboxMap: MapboxMap
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val mapView = MapView(this)
        mapboxMap = mapView.mapboxMap
        setContentView(mapView)
        val styleUrl = "$styleUrl$apiKey?config=3d:$initial3d"
        mapboxMap.loadStyle(styleUrl, this)
    }
    override fun onStyleLoaded(style: Style) {
        LevelSwitch.updateLevel(style, 0)
        setInitialCamera()
        activate3D()
    }
    private fun activate3D() {
        val style = mapboxMap.style ?: return
        val layers = style.styleLayers.map { it.id }
        for (layerId in layers) {
            if (layerId.lowercase(Locale.getDefault()).contains("-loc3d-")) {
                style.getLayer(layerId)?.visibility(Visibility.VISIBLE)
            }
            if (layerId.lowercase(Locale.getDefault()).contains("-loc2d-")) {
                style.getLayer(layerId)?.visibility(Visibility.NONE)
            }
        }
    }
    private fun setInitialCamera() {
        val cameraPosition = CameraOptions.Builder()
            .center(Point.fromLngLat(10.89075028896332,49.867630660511715)) // Sets the new camera position
            .zoom(18.0) // Sets the zoom
            .bearing(0.0) // Rotate the camera
            .pitch(0.0) // Set the camera tilt
            .build()
        mapboxMap.setCamera(cameraPosition)
    }
}LevelSwitch
SWITCH LEVELS
activity_switch_levels
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SwitchLevelsActivity">
    <com.mapbox.maps.MapView
        android:id="@+id/mapView"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </com.mapbox.maps.MapView>
    <Button
        android:id="@+id/switchLevelButton"
        android:layout_marginTop="15dp"
        android:layout_marginStart="15dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="SWITCH LEVEL"
        android:layout_gravity="left|top"
        android:orientation="vertical"
        android:textColor="#444444"
        />
</RelativeLayout>SwitchLevelsActivity
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
class SwitchLevelsActivity : AppCompatActivity(), Style.OnStyleLoaded {
    private val styleUrl = "https://tilesservices.webservices.infsoft.com/api/mapstyle/style/"
    private val apiKey = "<your api key here>"
    private val initial3d = "FALSE"
    private lateinit var mapboxMap: MapboxMap
    private var currentLevel = 0
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_switch_levels)
        val mapView = findViewById<MapView>(R.id.mapView)
        mapboxMap = mapView.mapboxMap
        val styleUrl = "$styleUrl$apiKey?config=3d:$initial3d"
        mapboxMap.loadStyle(styleUrl, this)
    }
    override fun onStyleLoaded(style: Style) {
        LevelSwitch.updateLevel(style, 0)
        setInitialCamera()
        val levelSwitch: Button = findViewById(R.id.switchLevelButton)
        levelSwitch.setOnClickListener {
            currentLevel = (currentLevel + 1) % 4
            LevelSwitch.updateLevel(style, currentLevel)
        }
    }
    private fun setInitialCamera() {
        val cameraPosition = CameraOptions.Builder()
            .center(Point.fromLngLat(10.89075028896332,49.867630660511715)) // Sets the new camera position
            .zoom(18.0) // Sets the zoom
            .bearing(0.0) // Rotate the camera
            .pitch(0.0) // Set the camera tilt
            .build();
        mapboxMap.setCamera(cameraPosition)
    }
}
 LevelSwitch
DISPLAY ROUTE
Setup
This code uses the OkHttp library to perform REST calls.
Add the following to your build.gradle.kts
dependencies {
    implementation(libs.okhttp)
}And define the corresponding version in gradle/libs.versions.toml.
[versions]
okhttp = "4.10.0"
[libraries]
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }DisplayRouteActivity
import android.os.Bundle
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.mapbox.maps.extension.style.sources.generated.GeoJsonSource
import com.mapbox.maps.extension.style.sources.getSourceAs
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.json.JSONArray
import org.json.JSONException
import java.io.IOException
class DisplayRouteActivity : AppCompatActivity(), Style.OnStyleLoaded {
    private val styleUrl = "https://tilesservices.webservices.infsoft.com/api/mapstyle/style/"
    private val apiKey = "<your api key here>"
    private val initial3d = "TRUE"
    private lateinit var mapboxMap: MapboxMap
    private val client = OkHttpClient()
    private lateinit var style: Style
    private var routeSource: GeoJsonSource? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_display_route)
        val mapView = findViewById<MapView>(R.id.mapView)
        mapboxMap = mapView.mapboxMap
        val styleUrl = "$styleUrl$apiKey?config=3d:$initial3d"
        mapboxMap.loadStyle(styleUrl, this)
    }
    override fun onStyleLoaded(style: Style) {
        LevelSwitch.updateLevel(style, 0)
        initSource(style)
        setInitialCamera()
        this.style = style
        val routeSwitch: Button = findViewById(R.id.calcRouteButton)
        routeSwitch.setOnClickListener { onCalcRouteClicked() }
    }
    private fun setInitialCamera() {
        val cameraPosition = CameraOptions.Builder()
            .center(Point.fromLngLat(10.89075028896332,49.867630660511715)) // Sets the new camera position
            .zoom(18.0) // Sets the zoom
            .bearing(0.0) // Rotate the camera
            .pitch(0.0) // Set the camera tilt
            .build();
        mapboxMap.setCamera(cameraPosition)
    }
    private fun initSource(style: Style) {
        val sourceIds = style.styleSources.map { it.id }
        for (sourceId in sourceIds) {
            if (sourceId.contains("route")) {
                val source = style.getSourceAs<GeoJsonSource>(sourceId)
                if (source != null)
                    routeSource = source
            }
        }
    }
    private fun onCalcRouteClicked() {
        val url = ("https://routes.webservices.infsoft.com/API/Calc?apikey=$apiKey&startlat=49.86739&startlon=10.89190&startlevel=0&endlat=49.86701&endlon=10.89054&endlevel=0")
        val request = Request.Builder()
            .url(url)
            .build()
        client.newCall(request).enqueue(object: Callback {
            override fun onFailure(call: Call, e: IOException) {
                e.printStackTrace()
            }
            override fun onResponse(call: Call, response: Response) {
                setGeoJson(response.body?.string())
            }
        })
    }
    private fun setGeoJson(rawRouteJson: String?) {
        var json = rawRouteJson
        val array: JSONArray
        try {
            array = JSONArray(rawRouteJson)
            for (i in 0 until array.length()) {
                val jsonObj = array.getJSONObject(i)
                json = jsonObj.getString("geoJson")
            }
        } catch (e: JSONException) {
            e.printStackTrace()
        }
        if (json == null) return
        routeSource?.data(json, "routeId")
    }
}LevelSwitch
USER LOCALIZATION
Locator library
Add the following to your build.gradle.kts to include the locator sdk.
dependencies {
    implementation(files("relativePathToLibrary/com.infsoft.android.locator.aar"))
}Add the necessary permissions and the locator service to your AndroidManifest.xml:
<manifest ...>
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    ...
    <service
        android:name="com.infsoft.android.locator.LocatorService"
        android:enabled="true"
        android:exported="true">
        <intent-filter>
            <action android:name="android.infsoft.com.library.locator.LocatorService.SERVICE"/>
            <category android:name="android.intent.category.LAUNCHER"/>
        </intent-filter>
        <meta-data
            android:name="useBLE"
            android:value="true"/>
        <meta-data
            android:name="ignoreWIFI"
            android:value="true" />
    </service>
</manifest>Custom Location Provider
import com.mapbox.geojson.Point
import com.mapbox.maps.plugin.locationcomponent.LocationConsumer
import com.mapbox.maps.plugin.locationcomponent.LocationProvider
class CustomLocationProvider : LocationProvider {
    private var consumers: MutableList<LocationConsumer>  = mutableListOf()
    override fun registerLocationConsumer(locationConsumer: LocationConsumer) {
        consumers.add(0, locationConsumer)
    }
    override fun unRegisterLocationConsumer(locationConsumer: LocationConsumer) {
        consumers.remove(locationConsumer)
    }
    fun updateLocation(point: Point) {
        for (consumer in consumers) {
            consumer.onLocationUpdated(point)
        }
    }
}UserLocalizationActivity
import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.infsoft.android.locator.Location
import com.infsoft.android.locator.LocationListener
import com.infsoft.android.locator.LocationManager
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.Style
import com.mapbox.maps.plugin.PuckBearing
import com.mapbox.maps.plugin.locationcomponent.createDefault2DPuck
import com.mapbox.maps.plugin.locationcomponent.location
import com.mapbox.maps.plugin.viewport.viewport
class UserLocalizationActivity : AppCompatActivity(), Style.OnStyleLoaded, LocationListener {
    private val styleUrl = "https://tilesservices.webservices.infsoft.com/api/mapstyle/style/"
    private val apiKey = "<your api key here>"
    private val subscriptionId = 0 // your subscription id here
    private val initial3d = "FALSE"
    private val initialLevel = 0
    private lateinit var mapboxMap: MapboxMap
    private lateinit var mapView: MapView
    private lateinit var locationProvider: CustomLocationProvider
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.BLUETOOTH_SCAN
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf<String>(Manifest.permission.BLUETOOTH_SCAN),
                200
            )
        }
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.BLUETOOTH_ADMIN
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf<String>(Manifest.permission.BLUETOOTH_ADMIN),
                200
            )
        }
        if (ContextCompat.checkSelfPermission(
                this,
                Manifest.permission.ACCESS_FINE_LOCATION
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            ActivityCompat.requestPermissions(
                this,
                arrayOf<String>(Manifest.permission.ACCESS_FINE_LOCATION),
                200
            )
        }
        mapView = MapView(this)
        mapboxMap = mapView.mapboxMap
        setContentView(mapView)
        locationProvider = CustomLocationProvider()
        val styleUrl = "$styleUrl$apiKey?config=level:$initialLevel|3d:$initial3d"
        mapboxMap.loadStyle(styleUrl, this)
    }
    private fun setInitialCamera() {
        val cameraPosition = CameraOptions.Builder()
            .center(Point.fromLngLat(10.89075028896332,49.867630660511715)) // Sets the new camera position
            .zoom(18.0) // Sets the zoom
            .bearing(0.0) // Rotate the camera
            .pitch(0.0) // Set the camera tilt
            .build();
        mapboxMap.setCamera(cameraPosition)
    }
    override fun onStyleLoaded(style: Style) {
        LevelSwitch.updateLevel(style, 0)
        setInitialCamera()
        initMapBoxLocationComponent()
        initLocator(this)
    }
    // Make MapBox show the user's position
    private fun initMapBoxLocationComponent() {
        mapView.location.locationPuck = createDefault2DPuck(withBearing = true)
        mapView.location.enabled = true
        mapView.location.puckBearing = PuckBearing.COURSE
        mapView.location.puckBearingEnabled = true
        mapView.viewport.transitionTo(mapView.viewport.makeFollowPuckViewportState(), mapView.viewport.makeImmediateViewportTransition())
        mapView.location.setLocationProvider(locationProvider)
    }
    // Listen to updates from the infsoft Locator library
    private fun initLocator(activity: Activity) {
        if (locationManager == null)
            locationManager = LocationManager.getService(activity, apiKey, subscriptionId)
        locationManager?.requestLocationUpdates(activity, 2000, 1000F, this)
    }
    override fun onLocationChanged(location: Location) {
        locationProvider.updateLocation(Point.fromLngLat(location.longitude, location.latitude))
        mapView.location.enabled = location.level == initialLevel
    }
    companion object {
        private var locationManager: LocationManager? = null
    }
}
 LevelSwitch
POI SELECTION
This code uses the OkHttp libary to perform REST calls and the Jackson libary to parse json.
Add the following to your build.gradle.kts
dependencies {
    implementation(libs.okhttp)
    implementation(libs.jackson)
    implementation(libs.jackson.module.kotlin)
}And define the corresponding version in gradle/libs.versions.toml.
[versions]
okhttp = "4.10.0"
jackson = "2.17.2"
jacksonModuleKotlin = "2.17.2"
[libraries]
jackson = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" }
jackson-module-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jacksonModuleKotlin" }
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }SelectPOIActivity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.fasterxml.jackson.module.kotlin.jsonMapper
import com.fasterxml.jackson.module.kotlin.kotlinModule
import com.fasterxml.jackson.databind.MapperFeature
import com.fasterxml.jackson.module.kotlin.readValue
import com.mapbox.bindgen.Expected
import com.mapbox.geojson.Feature
import com.mapbox.geojson.Geometry
import com.mapbox.geojson.Point
import com.mapbox.maps.CameraOptions
import com.mapbox.maps.MapView
import com.mapbox.maps.MapboxMap
import com.mapbox.maps.QueriedRenderedFeature
import com.mapbox.maps.RenderedQueryGeometry
import com.mapbox.maps.RenderedQueryOptions
import com.mapbox.maps.Style
import com.mapbox.maps.plugin.annotation.annotations
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationManager
import com.mapbox.maps.plugin.annotation.generated.PointAnnotationOptions
import com.mapbox.maps.plugin.annotation.generated.createPointAnnotationManager
import com.mapbox.maps.plugin.gestures.addOnMapClickListener
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import java.io.IOException
class SelectPOIActivity : AppCompatActivity(), Style.OnStyleLoaded {
    private val styleUrl = "https://tilesservices.webservices.infsoft.com/api/mapstyle/style/"
    private val apiKey = "<your api key here>"
    private val initial3d = "FALSE"
    private lateinit var mapboxMap: MapboxMap
    private lateinit var mapView: MapView
    private lateinit var pointAnnotationManager: PointAnnotationManager
    private var pois: List<PoiGeoJsonObject>? = null
    private val client = OkHttpClient()
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mapView = MapView(this)
        mapboxMap = mapView.mapboxMap
        setContentView(mapView)
        pointAnnotationManager = mapView.annotations.createPointAnnotationManager()
        val styleUrl = "$styleUrl$apiKey?config=3d:$initial3d"
        mapboxMap.loadStyle(styleUrl, this)
        loadPOIs()
    }
    override fun onStyleLoaded(style: Style) {
        LevelSwitch.updateLevel(style, 0)
        setInitialCamera()
        mapboxMap.addOnMapClickListener { point ->
            removeMarkers()
            mapboxMap.queryRenderedFeatures(
                RenderedQueryGeometry(mapboxMap.pixelForCoordinate(point)),
                RenderedQueryOptions(null, null)
            ) { expected: Expected<String, List<QueriedRenderedFeature>> ->
                run {
                    val feature = expected.value?.firstOrNull()?.queriedFeature?.feature
                    val selectedPoi = findClickedPoi(feature)
                    createMarker(selectedPoi, feature)
                }
            }
            true
        }
    }
    private fun findClickedPoi(selectedFeature: Feature?): PoiGeoJsonObject? {
        if (selectedFeature == null) return null
        val id = selectedFeature.getStringProperty("uid")
        for (poi in pois!!) {
            if (poi.uid != null && poi.uid?.startsWith(id) == true) {
                return poi
            }
        }
        return null
    }
    private fun createMarker(selectedPoi: PoiGeoJsonObject?, selectedFeature: Feature?) {
        if (selectedPoi == null || selectedFeature == null) return
        val typeField = selectedPoi.type
        val selectedPOI = featureToAnnotationPoint(selectedFeature)
        val lat = selectedPOI!!.coordinates!![1]
        val lon = selectedPOI.coordinates!![0]
        pointAnnotationManager.create(
            PointAnnotationOptions().withPoint(Point.fromLngLat(lon, lat)).withTextField(
                typeField.toString()
            )
        )
    }
    private fun removeMarkers() {
        pointAnnotationManager.deleteAll()
    }
    private fun setInitialCamera() {
        val cameraPosition = CameraOptions.Builder()
            .center(
                Point.fromLngLat(
                    10.89075028896332,
                    49.867630660511715
                )
            ) // Sets the new camera position
            .zoom(18.0) // Sets the zoom
            .bearing(0.0) // Rotate the camera
            .pitch(0.0) // Set the camera tilt
            .build();
        mapboxMap.setCamera(cameraPosition)
    }
    private fun loadPOIs() {
        val geojsonbaseURL = "https://tiles.infsoft.com/api/geoobj/json/"
        val lcid = "/en/"
        val revision = "0"
        val url = geojsonbaseURL + apiKey + lcid + revision
        val request = Request.Builder()
            .url(url)
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                e.printStackTrace()
            }
            override fun onResponse(call: Call, response: Response) {
                try {
                    val objectMapper = jsonMapper {
                        addModule(kotlinModule()).configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
                    }
                    val json = response.body?.string() ?: return
                    pois = objectMapper.readValue<List<PoiGeoJsonObject>>(json)
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
        })
    }
    private fun featureToAnnotationPoint(feature: Feature): AnnotationPoint? {
        val geometry: Geometry = feature.geometry() ?: return null
        val annotationPoint: AnnotationPoint?
        val objectMapper = jsonMapper {
            addModule(kotlinModule()).configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true)
        }
        try {
            annotationPoint = objectMapper.readValue(
                geometry.toJson(),
                AnnotationPoint::class.java
            )
        } catch (e: IOException) {
            e.printStackTrace()
            return null
        }
        return annotationPoint
    }
}AnnotationPoint
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
@JsonIgnoreProperties(ignoreUnknown = true)
internal class AnnotationPoint {
    // longitude/latitude, order is reversed!
    @JvmField
    @JsonProperty("coordinates")
    var coordinates: DoubleArray? = null
}PoiGeoJsonObject
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import java.io.Serializable
@JsonIgnoreProperties(ignoreUnknown = true)
class PoiGeoJsonObject : Serializable {
    @JvmField
    @JsonProperty("uid")
    var uid: String? = null
    @JvmField
    @JsonProperty("type")
    var type: String? = null
    @JsonProperty("props")
    var props: HashMap<String, String>? = null
}LevelSwitch
HTML
DISPLAY MAP
Initialize a map in an HTML element with Mapbox GL JS.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Display a map</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <script src="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css" rel="stylesheet" />
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <script>
        // infsoft api key, please replace this with our own api key to display your maps
        var apiKey = '8c97d7c6-0c3a-41de-b67a-fb7628efba79';
        // generating the style url for map box
        var styleURL = 'https://tilesservices.webservices.infsoft.com/api/mapstyle/style/{apiKey}?config=level:{level}|3d:{3d}';
        styleURL = styleURL.replace('{apiKey}', apiKey);
        styleURL = styleURL.replace('{level}', '0');
        styleURL = styleURL.replace('{3d}', 'false');
        var map = new mapboxgl.Map({
            container: 'map', // container id
            style: styleURL, // stylesheet location including infsoft api key
            center: [10.89075028896332, 49.867630660511715], // starting position [lng, lat]
            zoom: 18 // starting zoom
        });
    </script>
</body>
</html>APIKey
DISPLAY MAP 3D
Initialize a 3D map in an HTML element with Mapbox GL JS.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Display a map</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <script src="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css" rel="stylesheet" />
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <script>
        // infsoft api key, please replace this with our own api key to display your maps
        var apiKey = '8c97d7c6-0c3a-41de-b67a-fb7628efba79';
        // generating the style url for map box
        var styleURL = 'https://tilesservices.webservices.infsoft.com/api/mapstyle/style/{apiKey}?config=level:{level}|3d:{3d}';
        styleURL = styleURL.replace('{apiKey}', apiKey);
        styleURL = styleURL.replace('{level}', '0');
        styleURL = styleURL.replace('{3d}', 'true');
        var map = new mapboxgl.Map({
            container: 'map', // container id
            style: styleURL, // stylesheet location including infsoft api key
            center: [10.89075028896332, 49.867630660511715], // starting position [lng, lat]
            zoom: 18, // starting zoom
            pitch: 60 // starting pitch
        });
    </script>
</body>
</html>APIKey
SWITCH LEVELS
Switch level of a map in an HTML element with Mapbox GL JS.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Display a map</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <script src="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css" rel="stylesheet" />
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
        button {
            position: absolute;
            margin: 20px;
        }
    </style>
</head>
<body>
    <script>
        // extension for LocAware SDK
        function LocAwareExt(map) {
            this.map = map;
            // internal helper
            this.setLevelPropertyOfFilter = function (filter, level) {
                if (!Array.isArray(filter))
                    return filter;
                for (var i = 0; i < filter.length; i++) {
                    var item = filter[i];
                    if (Array.isArray(item)) {
                        filter[i] = this.setLevelPropertyOfFilter(item, level);
                        continue;
                    }
                    if (i === 0)
                        continue;
                    var prev = filter[i - 1];
                    var current = filter[i];
                    if (typeof prev !== 'string')
                        continue;
                    if (prev !== 'level')
                        continue;
                    if (typeof current !== 'number')
                        continue;
                    filter[i] = level;
                }
                return filter;
            };
            // set the floor level of the map
            this.setLevel = function (level) {
                // get all layers from map box gl
                var layers = this.map.getStyle().layers;
                // iterate all layers
                for (var i = 0; i < layers.length; i++) {
                    var layer = layers[i];
                    // layer is not a specific level layer
                    if (!layer.id.includes('locls'))
                        continue;
                    // get the filter of the layer from map box gl
                    var filter = this.map.getFilter(layer.id);
                    filter = this.setLevelPropertyOfFilter(filter, level);
                    this.map.setFilter(layer.id, filter);
                }
            };
        }
    </script>
    <div id="map"></div>
    <button id="switchlevel">SWITCH LEVEL</button>
    <script>
        // infsoft api key, please replace this with our own api key to display your maps
        var apiKey = '8c97d7c6-0c3a-41de-b67a-fb7628efba79';
        // generating the style url for map box
        var styleURL = 'https://tilesservices.webservices.infsoft.com/api/mapstyle/style/{apiKey}?config=level:{level}|3d:{3d}';
        styleURL = styleURL.replace('{apiKey}', apiKey);
        styleURL = styleURL.replace('{level}', '0');
        styleURL = styleURL.replace('{3d}', 'false');
        var map = new mapboxgl.Map({
            container: 'map', // container id
            style: styleURL, // stylesheet location including infsoft api key
            center: [10.89075028896332, 49.867630660511715], // starting position [lng, lat]
            zoom: 18 // starting zoom
        });
        // add click event for button 'SWITCH LEVEL'
        var currentLevel = 0;
        var butSwitchLevel = document.getElementById('switchlevel');
        butSwitchLevel.addEventListener('click', function () {
            // apply new level
            currentLevel = (currentLevel + 1) % 4;
            // apply it to map via helper class
            var helper = new LocAwareExt(map);
            helper.setLevel(currentLevel);
        });
    </script>
</body>
</html>APIKey
DISPLAY ROUTE
Display route in an HTML element with Mapbox GL JS.
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Display a map</title>
    <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no" />
    <script src="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.6.1/mapbox-gl.css" rel="stylesheet" />
    <style>
        body {
            margin: 0;
            padding: 0;
        }
        #map {
            position: absolute;
            top: 0;
            bottom: 0;
            width: 100%;
        }
        button {
            position: absolute;
            margin: 20px;
        }
    </style>
</head>
<body>
    <div id="map"></div>
    <button id="but">CALC ROUTE</button>
    <script>
        // methode to demo calc route and display route in map
        function calcRoute(map) {
            // calc route | url
            var calcRouteURL = 'https://routes.webservices.infsoft.com/API/Calc?apikey={apiKey}&startlat={startlat}&startlon={startlon}&startlevel={startlevel}&endlat={endlat}&endlon={endlon}&endlevel={endlevel}&lcid=EN&context=';
            // set correct api key
            calcRouteURL = calcRouteURL.replace('{apiKey}', apiKey);
            // set start position
            calcRouteURL = calcRouteURL.replace('{startlat}', 49.86739);
            calcRouteURL = calcRouteURL.replace('{startlon}', 10.89190);
            calcRouteURL = calcRouteURL.replace('{startlevel}', 0);
            // set destination position
            calcRouteURL = calcRouteURL.replace('{endlat}', 49.86701);
            calcRouteURL = calcRouteURL.replace('{endlon}', 10.89054);
            calcRouteURL = calcRouteURL.replace('{endlevel}', 0);
            // cal rest api to get route
            fetch(calcRouteURL).then(res => res.json()).then((out) => {
                // our calced route
                var calcedRoute = out[0];
                // we could not calc a route
                if (!calcedRoute.Valid) {
                    console.log('Calc route failed!');
                    return;
                }
                // get the geo json for the map
                var geoJsonForMap = calcedRoute.GeoJson;
                // set the layers data
                var source = map.getSource('loc-routes').setData(geoJsonForMap);
            }).catch(err => { throw err });
        }
        // infsoft api key, please replace this with our own api key to display your maps
        var apiKey = '8c97d7c6-0c3a-41de-b67a-fb7628efba79';
        // generating the style url for map box
        var styleURL = 'https://tilesservices.webservices.infsoft.com/api/mapstyle/style/{apiKey}?config=level:{level}|3d:{3d}';
        styleURL = styleURL.replace('{apiKey}', apiKey);
        styleURL = styleURL.replace('{level}', '0');
        styleURL = styleURL.replace('{3d}', 'false');
        var map = new mapboxgl.Map({
            container: 'map', // container id
            style: styleURL, // stylesheet location including infsoft api key
            center: [10.89075028896332, 49.867630660511715], // starting position [lng, lat]
            zoom: 18 // starting zoom
        });
        // add click event for button
        var but = document.getElementById('but');
        but.addEventListener('click', function () {
            // calc a route
            calcRoute(map);
        });
    </script>
</body>
</html> 
 