infsoft SDK für Indoor Navigation und mehr

Die Technologie von infsoft gibt es auch als Plugin zur Integration in Apps von Dritten. Das SDK (Software Development Kit) steht derzeit für die mobilen Betriebssysteme Android und iOS sowie als HTML5 Plugin zur Verfügung. Neben einer nativen Implementierung ist auch die Nutzung von Frameworks wie PhoneGap oder Xamarin möglich. Bereits bestehende Applikationen können mit der Indoor Positionsbestimmung und Indoor Navigation von infsoft erweitert werden.

Hier finden Sie weitere Informationen zu den verschiedenen Klassenbibliotheken und deren Implementierung (Englisch):

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.

Mapbox 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 = true

DISPLAY MAP

ViewController

  • Replace the apiKey variable with your api key to display your own maps
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

  • Replace the apiKey variable with your api key to display your own maps
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

  • Replace the apiKey variable with your api key to display your own maps
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

  • Replace the apiKey variable with your api key to display your own maps
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

  • Replace the apiKey variable with your api key to display your own maps
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

Official Mapbox website

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>

Official Mapbox Android documentation

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

  • Please replace the apiKey variable with your own ApiKey to display your own maps.
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

See common code

DISPLAY MAP 3D

DisplayMap3DActivity

  • Please replace the apiKey variable with your own ApiKey to display your own maps.
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

See common code

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

  • Please replace the apiKey variable with your own ApiKey to display your own maps.
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

See common code

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

  • Please replace the apiKey variable with your own ApiKey to display your own maps.
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

See common code

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

  • Please replace the apiKey and subscriptionId variables with your own ApiKey to display your own maps.
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

See common code

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

  • Please replace the apiKey variable with your own ApiKey to display your own maps.
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

See common code


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

  • Please replace the apiKey variable with your own APIKey to display your own maps

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

  • Please replace the apiKey variable with your own APIKey to display your own maps

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

  • Please replace the apiKey variable with your own APIKey to display your own maps

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>

APIKey

  • Please replace the apiKey variable with your own APIKey to display your own maps

Sie haben Fragen oder möchten Ihr Projekt mit uns besprechen? Kontaktieren Sie uns.

Keyboard Typing
DatenschutzerklärungDie gesendeten Daten werden nur zur Bearbeitung Ihres Anliegens verarbeitet.