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