Introduction
Welcome to the Proximi.io MapLibre iOS SDK reference, an easy helper to have fully featured map interactions and routing solution.
Version
Current stable version is: 5.15.0
with XCode
support 13.3
.
Latest changes: - improved offline algorithm
Installation
Installation in managed by Cocoapods, (if you don't already have it installed, see the "Getting Started" guide).
Add simply a few lines in your file like this:
- And then the library
pod 'ProximiioMapLibre'
And then proceed installing open your terminal to the directory where the Podfile
is stored and run this command:
pod repo update
pod install
Usage
In order to use the ProximiioMapLibre
plugin we need to have installed and working Proximiio, in particular add support for Synchronization and caching.
Here a simple overview on how use it:
import ProximiioMapLibre
/// setup map and proximiio integration
fileprivate func setupMap() {
/// setup map
let mapToken = Proximiio.sharedInstance()?.token() ?? ""
Proximiio.sharedInstance()?.delegate = self
let config = ProximiioMapLibreConfiguration(token: mapToken)
config.showRasterFloorplans = false
config.showGeoJSONFloorplans = true
/// prepare map
mapView = MGLMapView(frame: view.frame)
/// setup ProximiioMapLibre handler
if let mapView = mapView {
ProximiioMapLibre.shared.setup(mapView: mapView, configuration: config)
ProximiioMapLibre.shared.initialize { result in
if result == .success {
ProximiioMapLibre.shared.mapNavigation = self
ProximiioMapLibre.shared.mapInteraction = self
}
}
}
}
#import <ProximiioMapLibre/ProximiioMapLibre.h>
- (void) setupMap {
NSString *mapToken = [[Proximiio sharedInstance] token];
ProximiioMapLibreConfiguration *configuration = [[ProximiioMapLibreConfiguration alloc] initWithToken:mapToken];
self.mapView = [[MGLMapView alloc] initWithFrame:self.view.frame];
[self.view addSubview:self.mapView];
if (self.mapView != NULL) {
[[ProximiioMapLibre shared] setupWithMapView:self.mapView configuration:configuration apiVersion:@"v5"];
[[ProximiioMapLibre shared] initialize:^(enum ProximiioMapLibreAuthorizationResult result) {
if (result == ProximiioMapLibreAuthorizationResultSuccess) {
[ProximiioMapLibre shared].mapNavigation = self;
[ProximiioMapLibre shared].mapInteraction = self;
}
}];
}
}
Function & Helpers
ProximiioMapLibre provides you some functions and helpers to fast manage your implementation.
Here listed the most relevant ones.
Follow user position
Library has built-in automation to follow user position, you can enable/disable that with a simple snippet.
ProximiioMapLibre.shared.followingUser = true
ProximiioMapLibre.shared.followingUser = YES;
Floor
New map plugin provides easy handlers for map floor navigation:
/// force the map to change to upper floor
ProximiioMapLibre.shared.floorUp()
/// force the map to change to lower floor
ProximiioMapLibre.shared.floorDown()
/// force the map to change to user specified floor
ProximiioMapLibre.shared.floorAt(5)
/// get current floor of the map
let floor = ProximiioMapLibre.shared.mapFloor
/// force the map to change to upper floor
[ProximiioMapLibre.shared floorUp];
/// force the map to change to lower floor
[ProximiioMapLibre.shared floorDown];
/// force the map to change to user specified floor
[ProximiioMapLibre.shared floorAt:1];
/// get current floor of the map
NSUInteger floor = ProximiioMapLibre.shared.mapFloor;
Map position helper
You can easily manage to move the map with some quick helpers:
/// center the map to user position
ProximiioMapLibre.shared.centerAtUser(zoom: Double = 18, animated: Bool = true, completed: { success in
/// callback on completed
})
/// center the map to the specified feature
ProximiioMapLibre.shared.centerAtFeature(_ feature: ProximiioGeoJSON, zoom: Double = 18, animated: Bool = true, completed: { success in
/// callback on completed
})
/// center the map to user position
[ProximiioMapLibre.shared centerAtUserWithZoom:18 animated:YES
completed:^(BOOL) {
}];
/// center the map to the specified feature
[ProximiioMapLibre.shared centerAtFeature:feature zoom:18 animated:YES completed:^(ProximiioGeoJSON * _Nonnull) {
/// callback on completed
}];
Both the two functions expose a callback that can be optionally triggered when the animation ends.
Routing
We provide a built-in, offline, solution to calculate routes between user position and a target POI. Once a route has been calculated you can proceed starting/stopping it:
/// start route
ProximiioMapLibre.shared.routeStart()
/// stop route
ProximiioMapLibre.shared.routeCancel(silent: true)
/// start route
[ProximiioMapLibre.shared routeStart:nil];
/// stop route
[ProximiioMapLibre.shared routeCancelWithSilent:YES];
Routes can be calculate according some specific requirements, we add a PIORouteConfiguration
model that take care of this.
Also in order to provide async support, now the route is returned in a dedicate callback, with an optional PIORoute
object.
We provided also some quick helper for preview and auto start like:
ProximiioMapLibre.shared.routeFindAndStart(configuration: PIORouteConfiguration, callback: ((PIORoute?) -> Void))
ProximiioMapLibre.shared.routeFindAndPreview(configuration: PIORouteConfiguration, callback: ((PIORoute?) -> Void))
[ProximiioMapLibre.shared routeFindAndStartWithConfiguration: callaback:]
[ProximiioMapLibre.shared routeFindAndPreviewWithConfiguration: callaback:]
Or search only:
ProximiioMapLibre.shared.routeFind(configuration: PIORouteConfiguration, callback: ((PIORoute?) -> Void))
[ProximiioMapLibre.shared routeFindWithConfiguration: callaback:]
PIORouteConfiguration
This model provide information and limitation to be applied by the offline algorithm that generates the route to a POI.
The tweakable values are:
let options = PIORouteConfiguration(
start: ProximiioGeoJSON,
destination: ProximiioGeoJSON,
waypointList: [ProximiioWaypoint],
wayfindingOptions: PIOWayfindingOptions
)
Let's check one by one:
- start is an optional
ProximiioGeoJSON
, if nil current user position is used; - destination is
ProximiioGeoJSON
object; - waypointList is an array of object that adhere to protocol
ProximiioWaypoint
, those are intermediary steps between from and destination; - wayfindingOptions is an object of type
PIOWayfidingOptions
that provide information about the path (e.g. skip stairs, elevator);
PIOWaypoint
Waypoints are intermediary steps for a path, SDK actually supports two types: SimpleWaypoint
and VariableWaypoint
.
Simple support just a ProximiioGeoJSON
as feature, instead Variable support an array.
Basically the difference between the two is that, in Variable, you can provide a list of ProximiioGeoJSON (e.g. a list of parkings) and the wayfinding algorithm will take care of figure out the best that fit the route between your from and destination.
An example of usage is
let waypointSimple = SimpleWaypoint(feature: feature)
let waypointVariable = VariableWaypoint(features: [feature1, feature2, feature3])
PIOWayfidingOptions
PIOWayfidingOptions
is an evolution of previous model PIORouteOptions.
Here an example of implementation:
let options = PIOWayfidingOptions()
options.avoidBarriers = false
options.avoidElevators = false
options.avoidEscalators = false
options.avoidNarrowPaths = false
options.avoidRamps = false
options.avoidRevolvingDoors = false
options.avoidStairs = false
options.avoidTicketGates = false
options.pathFixDistance = 1.0
By default all the values are set to false
.
PIOAnnotation
PIOAnnotation
allows you to add custom annotation on the marker with extra level support.
Also it support onTap callback returning the object that triggered the event.
let annotation = PIOAnnotationWithData()
annotation.coordinate = CLLocationCoordinate2D(latitude: 60.1671950369849, longitude: 24.921695923476054)
annotation.image = UIImage.add
annotation.id = 0
annotation.data = 1
annotation.title = "A test annotation at level 0"
annotation.level = 0
annotation.onTap = { current in
print("TAPPED")
}
let annotation2 = PIOAnnotationWithData()
annotation2.coordinate = CLLocationCoordinate2D(latitude: 60.16746993048443, longitude: 24.922311676612107)
annotation2.image = UIImage.remove
annotation2.id = 1
annotation2.data = 2
annotation2.title = "A second annotation at level 0"
annotation2.level = 1
annotation2.onTap = { current in
guard let casted = current as? PIOAnnotationWithData else { return }
print("TAPPED \(casted.id)")
}
let annotation3 = PIOAnnotationWithData()
annotation3.image = UIImage.actions
annotation3.coordinate = CLLocationCoordinate2D(latitude: 60.16714117139544, longitude: 24.92290485238969)
annotation3.id = 2
annotation3.data = 3
annotation3.title = "A third annotation at level 0"
annotation3.level = 0
annotation3.onTap = { current in
guard let casted = current as? PIOAnnotationWithData else { return }
print("TAPPED \(casted.id) \(casted.data)")
}
In this example we are able to create a custom annotation, visible at a specific position and for a specific level. If the map changes the shown level, the annotation will be shown/hidden according.
Once we have an annotation in place we can render that using MapLibre
function:
mapView?.addAnnotations([annotation, annotation2, annotation3])
The solution allows you to set 1 to N annotations, is just enough to create all the single annotations and then use addAnnotations passing an array of annotation.
In order to support the onTap
callback, you will have to tweak the code you use to rendere the marker with something like:
public func mapView(_ mapView: MGLMapView, viewFor annotation: MGLAnnotation) -> MGLAnnotationView? {
if let current = annotation as? PIOAnnotation {
// weak identifier, if you have an ID prefer to use that
let identifier = "\(current.coordinate.longitude)\(current.level)"
var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)
if annotationView == nil {
annotationView = MGLAnnotationView(annotation: annotation, reuseIdentifier: identifier)
let imageView = UIImageView(image: current.image)
annotationView!.frame = CGRect(x: 0, y: 0, width: current.image.size.width, height: current.image.size.height)
annotationView?.addSubview(imageView)
annotationView?.centerOffset = CGVector(dx: 0, dy: -current.image.size.height / 2.0)
// introduces onTap callback
annotationView?.addTapGesture(handler: { gesture in
current.onTap?(current)
})
}
// handles hiding according the floor
annotationView?.isHidden = current.isHidden
return annotationView
}
return nil
}
Database
ProximiioMapLibre has an embedded local database to keep POI information.
You can easily access it using PIODatabase.sharedInstance()
shared object.
let db = PIODatabase.sharedInstance()
// amenities
let amenities = db.amenities
// features
let poi = db.features
// poi
let poi = db.pois
// poi and level changers
let poi = db.poisAndLevelChanger
ProximiioDatabase *db = PIODatabase.sharedInstance;
// amenities
let amenities = db.amenities
// features
let poi = db.features
// poi
let poi = db.pois
// poi and level changers
let poi = db.poisAndLevelChanger
Each is an array of elements of type ProximiioGeoJSON
that provide out of the box helpers for different information about the entity.
ProximiioGeoJSON
ProximiioGeoJSON model has automated localization handling for some fields like description and title.
In order to use feel free to use helpers like getTitle()
.
Delegates
ProximiioMapLibre exposes two delegates with different scopes:
ProximiioMapLibreInteraction
ProximiioMapLibreNavigation
Their implementation and naming can have some difference between swift and objective-c implementation, refer below.
ProximiioMapLibreInteraction
Takes care of providing delegate for interaction on the mapbox instance like: changing floor, tapping on map or request a re-route.
- (void)changeWithFloor:(NSInteger)floor;
- (void)onTapWithFeature:(ProximiioGeoJSON * _Nonnull)feature;
- (void)onRequestReRoute;
func change(floor: Int) {
// triggered when floor has changed
}
func onTap(feature: ProximiioGeoJSON) {
// triggered if user taps on a POI
}
func onRequestReRoute() {
// triggered if a re-route is manually requested
}
ProximiioMapLibreNavigation
Takes care of providing delegate for events connected to a navigation.
- (void)onRouteWithRoute:(PIORoute * _Nullable)route;
- (void)routeEventWithEventType:(enum PIORouteUpdateType)type text:(NSString * _Nonnull)text additionalText:(NSString * _Nullable)additionalText data:(PIORouteUpdateData * _Nullable)data;
- (void)onHazardEntered:(ProximiioGeoJSON * _Nonnull)hazard;
- (void)onSegmentEntered:(ProximiioGeoJSON * _Nonnull)segment;
- (void)onDecisionEntered:(ProximiioGeoJSON * _Nonnull)decision;
- (void)onLandmarkEntered:(NSArray<PIOLandmark *> * _Nonnull)landmark;
- (void)onHazardExit:(ProximiioGeoJSON * _Nonnull)hazard;
- (void)onSegmentExit:(ProximiioGeoJSON * _Nonnull)segment;
- (void)onDecisionExit:(ProximiioGeoJSON * _Nonnull)decision;
- (void)onLandmarkExit:(NSArray<ProximiioGeoJSON *> * _Nonnull)landmark;
func onRoute(route: PIORoute?) {
// triggered if a route has been found
}
func routeEvent(eventType type: PIORouteUpdateType, text: String, additionalText: String?, data: PIORouteUpdateData?) {
// triggered if a route event happen
}
func onHazardEntered(_ hazard: ProximiioGeoJSON) {
// triggered if user is close to hazard
}
func onSegmentEntered(_ segment: ProximiioGeoJSON) {
// triggered if user has entered a new segment
}
func onPositionUpdate(_ position: CLLocationCoordinate2D) {
// useful to have current user position
}
func onHeadingUpdate(_ heading: Double) {
// useful to have current user heading
}
func onDecisionEntered(_ decision: ProximiioGeoJSON){
// useful to have a decision poi
}
func onLandmarkEntered(_ landmark: [PIOLandmark]){
// useful to have landmarks information
}
func onHazardExit(_ hazard: ProximiioGeoJSON){
// hazard exit
}
func onSegmentExit(_ segment: ProximiioGeoJSON){
// segment exit
}
func onDecisionExit(_ decision: ProximiioGeoJSON){
// decision exit
}
func onLandmarkExit(_ landmarks: [ProximiioGeoJSON]){
// landmark exit
}
Models
The library makes use of several model and primitives. Here a list of the most relevant, their scope and values.
PIORouteUpdateType
This enum is used on routeEvent
response, it easily provide you information about how to handle the visual interfance for the navigation.
Supported values are the following:
@objc public enum PIORouteUpdateType: Int {
case calculating, recalculating // if route is being calculate or re-calculated
case routeNotfound // if route fails to be generated
case canceled // if the user dimiss a route
case finished // if the user has reached the end of a route
// priority of the update
case new, update, soon, immediate
}
PIORouteUpdateData
This model provides a reference to the current node of the route involved in an update. If exists it provides also a reference to the next node with a similar structure (but with optional values).
@objc public class PIORouteUpdateData: NSObject {
/// current index of the node is processed from the route nodeList
@objc public let nodeIndex: Int
/// bearing of current node
@objc public let stepBearing: Double
/// direction
@objc public let stepDirection: PIOGuidanceDirection
/// distance of the node
@objc public let stepDistance: Double
// optional reference to the next step
@objc public let nextStepBearing: Double?
@objc public let nextStepDistance: Double?
@objc public let nextStepDirection: PIOGuidanceDirection?
/// current position
@objc public let position: CLLocationCoordinate2D
/// remaining distance in meters
@objc public let pathLengthRemaining: Double
public var stepHeading: PIOHeading {
return calculateHeading(stepBearing.degreeNormalized)
}
}
PIOGuidanceDirection
This model provide an helper to the direction of the user.
@objc public enum PIOGuidanceDirection: Int, Equatable {
case start // beginning of the route
case turnAround // when user need to change direction
case finish // user reaches the end
case straight // continue straight
/// turn types
case leftSlight, leftNormal, leftHard
case rightSlight, rightNormal, rightHard
/// level change types
case upElevator, upEscalator, upStairs
case downElevator, downEscalator, downStairs
}
The type of turn is calculated agains the degree between user position and node according to this logic:
/// return current information about guidance direction
func degreesToStepDirection() -> PIOGuidanceDirection {
let test = self.degreeNormalized
if fabs(test) < 22.5 { return .straight }
if fabs(test) > 157.2 { return .turnAround}
if test < -112.5 { return .leftHard}
if test <= -67.5 { return .leftNormal}
if test <= -22.5 { return .leftSlight}
if test > 157.5 { return .rightHard}
if test >= 67.5 { return .rightNormal}
if test >= 22.5 { return .rightSlight}
return .straight
}
PIOHeading
This model provide an helper to the heading of the route.
@objc public class PIOHeading: NSObject {
@objc public var text: String
@objc public var rotation: Double
}
It exposes a degree value in rotation
and a text, localized, about heading direction.
PIOLandmark
It takes care of provide information about any landmark the routing system is able to discover close to current user location.
It provides two sub variables side
and feature
.
Side is an enum that give you information on the side the landmark is:
@objc public enum PIOLandmarkSide: Int {
case left, right
}
Feature is a normal ProxmiioGeoJSON
object.
Extra
Hide Amenities from map
A new feature allows you to hide amenities from map temporary. If you want for example to run a navigation and hide any other amenity to avoid any disturb to the user you can simply use this helper:
ProximiioMapLibre.shared.blacklistRenderAmenitiesIds = ["replace:identifier"]
The helper is able to accept as input an array of amenities that we want to remove. If we want to restore the initial contidition, we have just to pass an empty array.
Preload POI images
Map can make use of images for POI and Amenities, an interesting way to boost performance and lower time-await is to preload that before app starts.
In combination with Kingfisher (that you have to install as third party pod), you can easily create a function like the one presented below.
Looping the PIODatabase
will be possible to force pre-caching of the images needed.
Actually we suggest to run this func when both the syncAmenities
and syncFeatures
are finished.
import ProximiioMapLibre
private func preloadFeatureImages(callback: @escaping ((Bool) -> Void)) {
var completed = 0
PIODatabase.shared.features.forEach { feature in
feature.images.forEach {
if let imgUrl = URL(string: $0) {
completed += 1
ImageDownloader.default.downloadImage(with: imgUrl, retrieveImageTask: nil, options: [], progressBlock: nil) { (image, _, url, _) in
completed -= 1
if completed == 0 {
callback(true)
}
if let image = image, let url = url {
ImageCache.default.store(image, forKey: url.absoluteString)
}
}
}
}
}
}
Troubleshooting
If the library produce error on build, try add these few lines at the bottom of your Podfile
.
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '5.2'
config.build_settings['ENABLE_BITCODE'] = 'NO'
end
end
end
Instead if during submisison you receive a warning mail for Too many simbols
you can tweak your podfile like below:
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['DEBUG_INFORMATION_FORMAT'] = 'dwarf'
end
end
end
If needed you can even combine the two above in one solution.