Skip to content
Developers Docs

Mapsted Maps - Mobile Docs

Overview

Introduction

What is the Mapsted Maps Mobile Framework?

The Mapsted Maps mobile framework is a cross-platform development toolkit which allows you to quickly integrate Mapsted's core location technology into your own applications. The framework is organized into several modules for easy integration.

Core Module: This module contains Mapsted's core location positioning technology, analytics data collection, and various data retrieval and wayfinding logic functionality.

Map Module: Use the Map Module to integrate our interactive map and corresponding customizable map UI/UX into your application. The interface includes views for the map, wayfinding, itinerary management, and more.

Prebuilt UI/UX Components: A collection of beautiful, modern prebuilt UI/UX components, which are optimized for, and include, built-in integration with the Mapsted Maps mobile framework. You can import these components directly for use in your own application.

App Template: The App Template is a fully functional application right out-of-the-box, which includes some basic customization.

Mapsted Key Terms

This section will go over some key terminology that is unique to our products, to make it easier for you to get started with the Mapsted Maps Mobile Framework.

Properties, Buildings, and Floors

Each venue is referred to as a Property, which consists of one or more Building structures. Each of these Building structures contains one or more Floor structures. The Property establishes a relationship between related buildings. For reference, a visualization of the Property, Building, and Floor structures can be seen in the Figure below.

Each Property is identified by a PropertyId, each Building is identified by a BuildingId, and each Floor is identified by a FloorId. The PropertyId, BuildingId, and FloorId are unique across the Mapsted system.

Entities

An Entity is a fundamental element within a Property or Building. An Entity consists of a geometry type (i.e., MapPolygon, MapPolyline, MapPoint), a physical interpretation (e.g., Structure-Building, Structure-Room, or Obstacle-Wall), and at least one Category (e.g., Restaurant). One Entity can be associated with multiple categories, but each Entity will have one main Category.

Please see the figure below for a visualization of the Entity structure. Note that an Entity which is on the property-level (e.g., a building, parking garage, park, pond) is found in the PropertyEntities, and an Entity which is on the building-level (e.g., a room, elevator, wall) is found in the BuildingEntities.

Each Entity is identified by an EntityId, and depending on the geometry type, will have an associated PolygonId, PolylineLineId, or PointId. Note that EntityId and corresponding geometry type id are only unique within their scope. This means that for PropertyEntities, the EntityId will be unique to that particular Property. In the case of BuildingEntities, the EntityId will be unique to that Building.


Interactive Map

Map interactions are a fundamental component of Mapsted's mobile map SDK. This section will demonstrate some sample usages.

Map Interactions

The map view supports two types of map interactions, map gestures and map clicks. Map gestures allow for pan, zoom, tilt, and rotations, while map clicks allow for customizable actions (e.g., selection of entities).

Map Gestures

All common map gestures are supported by default. Learn more about the available map gestures and their effects below.

A Map Event listener can be registered to be notified when the map state changes. There are three types of map event notifications available.

  • Idle Map: A user is notified when the map view is finished rendering
  • Stable Map: A notification is sent out when all map animations have finished, and the user has lifted their fingers off the screen
  • Map Moved: This type of notification occurs when a map gesture is in progress, such as when a map is being panned, rotated, tilted, or zoomed

Below is some sample code to register a listener to be notified of map state changes.

// Handle Map's event types
mapApi.getMapEvents().observe(this, mapEvent -> {
    switch (mapEvent.getEventType()) {
        case EventType.MAP_IDLE:
            // handle idle event
            break;
        case EventType.MAP_STABLE:
            // handle stable event
            break;
        case EventType.MAP_MOVED:
            // handle map moved event
            break;
    }
});
extension YourViewController: MNMapListenerDelegate {
    func onMapMoved() {
    }

    func onMapStable() {
    }

    func onMapIdle(){
    }
}

Map Clicks

Map click events indicate when the user has clicked on the map. A map click event consists of a click type, a click location, and a click entity. See below for definitions of the various types of clicks, including Single Tap, Double Tap, Long Press, and Dual Tap.

// Register map click listener
mapApi.getMapClickEvents().observe(this, mapClickEvent -> {
    ClickType clickType = mapClickEvent.getClickType();
    IMercatorZone clickLocation = mapClickEvent.getClickLocation();
    Entity clickEntity = mapClickEvent.getClickEntity();

    // Handle event
});
/*Your view controller hosting the Mapsted map viewcontroller will 
automatically receive map event notifications if it conforms to the 
MNMapListenerDelegate delegate. When user taps on the map outside any 
vector elements, outsideBuildingTapped gets called with the tap position 
and the tap type.*/

extension YourViewController: MNMapListenerDelegate {
    func outsideBuildingTapped(tapPos: MNMercator, tapType: MapstedMapApi.TapType) {
        DispatchQueue.main.async {
            if tapType == .eSingle {
                //handle single tap
            }
            else if tapType == .eLong {
            //handle long tap
            }
            else if tapType == .eDouble {
                //handle double tap
            }
        }
    }
}

/*To receive map vector element notifications, your viewcontroller 
needs to conform to MNMapVectorElementListenerDelegate. The 
onPolygonTapped gets called when user taps on a vector element 
on map. The onBalloonClicked gets called when user taps a pop up.*/

extension YourViewController: MNMapVectorElementListenerDelegate {
    func onPolygonTapped(polygon: MNMapPolygon, tapType: MapstedMapApi.TapType, tapPos: MNMercator) {
        //From polygon clicked, get info such as name, centroid etc.

        let centroid = polygon.centroid()
        let name = polygon.name()
    }

    func onBalloonClicked(searchEntity: MNSearchEntity) {
    }
}

Automatic Map Actions

The below code sample demonstrates how to enable or disable automatic pan, zoom, tilt, and rotation functionalities. These actions are controlled automatically in Mapsted’s UI SDK.

// Disable auto-follow
mapApi.setCameraFollowUser(false);

// Enable auto-follow
mapApi.setCameraFollowUser(true);

// Check auto-follow Status
boolean isCameraFollowUser = mapApi.isCameraFollowUser();
// Currently, automatically handled in iOS SDK
// Programmatic control will be implemented in an upcoming release

Programmatic Map Actions

You can also programmatically adjust the map to control zoom, tilt, and rotation actions.

/*
Add custom parameters for map view events like zoom, tilt and rotate. 
Create a list of UpdateMapEvent. 
Add individual map view events with the necessary values. 
Set the map view events on UpdateMapEvent object.
Call onUpdateMapEvent passing updateMapEvent object
*/

ArrayList<UpdateMapViewEvent> updateMapViewEvents = new ArrayList<>();
// Add the required map view events
// Specify the type of event, value for that event and the speed at which you want the event to occur
updateMapViewEvents.add(new UpdateMapViewEvent(UpdateMapViewEvent.TILT, 45.0f, 1.0f));
updateMapViewEvents.add(new UpdateMapViewEvent(UpdateMapViewEvent.ZOOM, 20.0f, 0.5f));
updateMapViewEvents.add(new UpdateMapViewEvent(UpdateMapViewEvent.RECENTER, 0f, 0.5f));
updateMapViewEvents.add(new UpdateMapViewEvent(UpdateMapViewEvent.ROTATE, 0f, 0.5f));

UpdateMapEvent updateMapEvent = new UpdateMapEvent();
updateMapEvent.setUpdateMapViewEvents(updateMapViewEvents);
mapApi.onUpdateMapEvent(updateMapEvent);
//To simply center the map at a specific location at existing zoom, use:
MapstedMapApi.shared.mapView()?.moveToLocation(mercator: MNMercator)

//To center and update zoom use:
MapstedMapApi.shared.mapView()?.moveToLocation(mercator: MNMercator, zoom: Float, duration: Float)

//To rotate map, use:
MapstedMapApi.shared.mapView()?.setRotation(angle: Float, durationSeconds: Float)

//To tilt map, use:
MapstedMapApi.shared.mapView()?.tiltMap(angle: Float, duration: Float)

Offline Maps

All downloaded map data is automatically stored in a local database and available for use offline. The SDK will automatically download map data, as needed, based on what is nearby. Map data downloads can also be managed programmatically, triggered by UI interactions. See below for sample code.

// start downloading map data for a given propertyId
coreApi.propertyManager().startPropertyDownload(propertyId, new PropertyDownloadManager.Listener() {
    @Override
    public void onProgress(int propertyId, int current, int total) {
        //onprogress
    }

    @Override
    public void onComplete(int propertyId) {
        //oncomplete
    }

    @Override
    void onFail(int propertyId, Exception e){
        //onfail
    }
});

// When using the Map SDK, you can select and draw the property on the map
// Note that this will internally trigger a map download if it has not previously been downloaded 
mapApi.selectPropertyAndDrawIfNeeded(propertyId, new SelectPropertyListener() {
        @Override
        public void onCached(boolean isSuccess, int propertyId){
            // If successful, property data has been loaded into memory and property is selected
        }

        @Override
        public void onPlotted(boolean isSuccess, int propertyId){
            // If successful, property has completed being drawn on the map
        }
    });

// Properties can also be undrawn from the map as follows:
mapApi.removePropertyFromMap(propertyId);
let myPropertyId = 504

//Get the property manager from CoreApi
let propertyManager = CoreApi.Propertymanager

// To download property data, provide a delegate to 
propertyManager.startDownload(propertyId: myPropertyId, propertyDownloadListener: self)

extension myViewController; PropertyDownloadListener {
    public func onSuccess(propertyId: Int) {

    }
    public func onFailureWithProperty(propertyId: Int) {

    }

    public func onProgress(propertyId: Int, percentage: Float) {

    }
}

// To check the download status of a property 
let downloadStatus = propertyManager.getDownloadStatus(propertyId: myPropertyId)


// You can retrieve property data using the property identifier
let propertyData = propertyManager.getCached(propertyId: myPropertyId)

// When using the Map SDK, you can select and draw the property on the map
MapstedMapApi.shared.drawProperty(isSelected, propertyData)

// To delete downloaded property data
propertyManager.delete(propertyId: myPropertyId, listener: self)

// To undraw a property from the map using MapSDK
MapstedMapApi.shared.removeProperty(propertyId)

Prebuilt Map View

You have the option to customize the Prebuilt Map View in a variety of different ways. A few examples are detailed below.

// Set custom params prior to initializing the Mapsted SDK
CustomParams.newBuilder()
                .setBaseMapStyle(BaseMapStyle.DARK) // Set theme to dark
                .setMapPanType(MapPanType.RESTRICT_TO_SELECTED_PROPERTY) // restrict map panning to property
                .setMapZoomRange(new MapstedMapRange(6.0f, 24.0f)) // restrict map zoom range
                .build();

// ...

// Note: For runtime map parameter adjustments, use the following api
mapApi.setCameraFollowUser(false);
//Set the map theme to dark
MapstedMapApi.shared.setBaseMapStyle(style: .DARK)

//Set map panning to restricted which limits panning to a region around the property
MapstedMapApi.shared.setMapPanType(type: MapstedMapMeta.MapPanType.eRestrictToProperty)

Custom Map View

You can power your application using our position location technology, while using your own MapView. The Location Positioning Technology guide shows you how to access user location information. Mapsted uses Open Street Maps as the georefence for user locations, buildings, and other map elements. If your system uses a basemap such as Google maps or Apple maps, please contact us, so we can make sure that our user locations, buildings, and other map elements are appropriately georeferenced for your basemap.


Mapsted's prebuilt UI/UX includes intelligent search. This type of search finds matches based on names, and keywords or categories, and offers suggestions which can handle various typos or spelling errors.

Prebuilt UI/UX

Prebuilt UI/UX automatically uses the intelligent search functionality.

Search via Names/Auto Suggestions

As the user types, auto suggestions are also provided, as shown below.

Search via Categories/Keywords

Search is supported by category and keyword.

Search via Reverse Geocoding (Coming Soon)

Reverse geocoding is supported within properties and buildings. Using reverse geocoding, a MercatorZone object, like a ClickLocation from a ClickEvent, can be passed in and an Entity object will be returned. If the specified MercatorZone corresponds to a specific Entity, that entity will be returned, otherwise it will return null.

Customizable UI/UX

If Mapsted's prebuilt UI/UX is not used, the intelligent search functionality can also be programmatically accessed, as shown in the sample code in the section named "Points of Interest".


Points of Interest

Mapsted's SDK provides several ways to searching for, selecting and navigating to points of interest

Find and Sort Searchables

If Mapsted's prebuilt UI/UX is not used, the intelligent search functionality can also be programmatically accessed, as shown in the sample code below.

 int propertyId = 504
// Get a list of searchables for the property
coreApi.propertyManager().getSearchEntityListByPropertyId(propertyId, searchEntities -> {
    // Apply filter and sort based on input string query (e.g., query = "ga" would provide a good match for "Gap")
    // filterAndSortSearchables method will filter and re-order searchEntities
    List<ISearchable> filteredResult = coreApi.utilities().filterAndSortSearchables("ga", searchEntities);
    // When complete, UI can now be updated
    showInUi(filteredResult);

    void showInUi(List<ISearchable> searchEntities) {
        ...
    }
});
let myPropertyId = 512 
// Get a list of searchables for the property
let searchables = CoreApi.PropertyManager.getSearchEntities(propertyId: myPropertyId)
if searchables.isEmpty {
    //No matches found.
    return; 
}

//Sort list of searchables based on intelligent search
let entities = CoreApi.UtilsManager.filterAndSortSearchables(input: "shoe", listOfSearchables: searchables)

for entity in entities {
    //Print display name
    print(entity.displayName)

    //Find matching word and category
    let matchWord = entity.matchWord
}

Filters for Points of Interest

To narrow down points of interest by floor, building, category, etc, you can make use of PoiFilters. A PoiFilter is composed of any number of PoiIntersectionFilter constructs.

Each PoiIntersectionFilter will look for items that match a combination of floors, buildings, and categories.

PoiFilter will then combine these to filter items that match ANY of the PoiIntersection filters provided to return the final result.

PoiFilter poiFilter = new PoiFilter.Builder()
    .addFilter(new PoiFilter.PoiIntersectionFilter.Builder()
        .addFloor(941)
        .addCategory("5ff5e93aa950eb2320f177ac") //Fast Food
        .build()
    ).addFilter(new PoiFilter.PoiIntersectionFilter.Builder()
        .addBuilding(504)
        .addCategory("5ff5e937a950eb2320f1778d") //Department Stores
        .build()
    ).build();

coreApi.propertyManager()
    .searchPoi(selectedPropertyId, poiFilter, iSearchables -> {
        Logger.d("onViewCreated: iSearchables.size = %s, iSearchables = %s", iSearchables.size(), iSearchables);
    });
let poiFilter = PoiFilter.Builder()
        .addFilter( //Filter #1. Floor 123 and CategoryId "abc1123""
            PoiIntersectionFilter
            .Builder()
            .addFloor(id: 123)
            .addCategory(id: "abc1123")
            .build()
        )
        .addFilter( //Filter #2. Floors 456 or 789, AND CategoryId “def923”
            PoiIntersectionFilter
            .Builder()
            .addFloor(id: 456)
            .addFloor(id: 789)
            .addCategory(id:  def923)
            .build()
        )
        /*
        .addFilter( ... ) //You can add more filters
        */
        .build()

let myPropertyId = 512 
CoreApi.PropertyManager.searchPOIs(filter: poiFilter, propertyId: myPropertyId, completion: { (searchables: [ISearchable] ) in
        for searchable in searchables {
            print("#SearchPOI: Found \(searchable.entityId) = \(searchable.displayName) - Floor: \(searchable.floorId)")
        }
})

Get nearby entities

When the user is at the property venue and his position is initialized by the sdk, you can get a list of near by entities using following api.

//once the sdk is initialized and user is at a location, you can use `getNearByEntities` to get nearby entities.
//you can use `coreApi.locationManager().getLastKnownPosition()` to be sure user position is resolved before calling this method.
if(coreApi.locationManager().getLastKnownPosition() != null) {
    coreApi.locationManager().getNearByEntities(nearbyEntities -> {
        //nearbyEntities is a list of `EntityZoneDistance` which contain entity zone and its distance from the user. This list is already sorted with nearest first, farthest last.
        Log.d(TAG, "nearByEntities " + nearByEntities.size())
    });
}
    CoreApi.LocationManager.getNearbyEntities { listOfEntities in
        for entity in listOfEntities {
            //do something with each
            print("Found \(entity.entityId) - \(entity.displayName)")
        }
    }

Search Entity

In many venues, there are repeated points of interest (e.g., Washroom entities). As such, Entity objects with the same name are grouped into SearchEntity objects. A SearchEntity is an ISearchable, so it can be used for finding POIs or for wayfinding. In general, a SearchEntity describes the scenario where navigating to any of a list of Entity objects would be equivalent.

In some cases, a user may want to select a specific Entity from a SearchEntity object (e.g., for selecting on the map or for navigating to). If you are using the prebuilt UI/UX, for such scenarios, a dialog UI view is generated which allows the user to select a specific Entity, or alternatively, they can request the closest Entity. Please find the example below.

If you are not using the prebuild UI/UX, you can create your own UI and access the data as follows:

int propertyId = 504;

coreApi.propertyManager().getSearchEntityListByPropertyId(propertyId, searchEntities -> {
    for (SearchEntity se : searchEntities){
        EntityZoneVector entityZones = se.getEntityZones();
        // A searchEntity may have multiple entityZones. To retrieve entities use propertyManager().
        for (EntityZone ez: entityZones) {
            Entity entity  = coreApi.propertyManager().getCachedEntity(ez);
            Log.d(TAG, "entity name=" + entity.getName());
        }
    }
});
//Get grouped search entities
let myPropertyId = 123

//Variable to collect groups of search entities having common name
var result: [CMSSearchEntityGroup] = []

//Make sure property data exists
guard let propertyData = CoreApi.PropertyManager.getCached(propertyId: propertyId) else { return }

// Add property's entities
if let entities = propertyData.searchEntities()?.searchEntitiesGrouped() {
    result = result + entities
}

for building in property.getBuildingInfos() {
    let buildingId = building.getBuildingId()
    if let buildingData = CoreApi.BuildingManager.getCached(buildingId: buildingId) {
        // Add building's entities
        if let entities = buildingData.searchEntities()?.searchEntitiesGrouped() {
            result = result + entities
        }
    }
}

//Iterate through each group
for group in result {
    print(group.groupName)

    //Find entities in the group that share the same name
    for entity in group.entities {
        //Use propertyId, buildingId, etc
    }
}

Find Entities by Name

If Mapsted's prebuilt UI/UX is not used, the intelligent search functionality can also be programmatically accessed, as shown in the sample code below.

coreApi.propertyManager().findEntityByName(name, propertyId, searchEntities -> {
    //use searchEntities (empty if no matches found)
});
let myPropertyId = 512 
let nameOfEntity = "Shoe Vendor"
// Get a matching list by name of searchables for the property (empty if no matches found)
let searchables = CoreApi.PropertyManager.findEntityByName(name: nameOfEntity, propertyId: myPropertyId)

Select and Deselect Entity

To select or deselect entities on map ...

Entity entity = ... //use coreApi.propertyManager() to get Entity
mapApi.selectEntity(entity);

//To clear selected Entity
mapApi.deselectEntity();
let myEntity = ... //(Any of those entities you obtained by search)
//Select entity on map
MapstedMapApi.shared.selectEntity(entity: myEntity)

//Deselect map
MapstedMapApi.shared.deselectEntity()

Distance or Time estimate to an entity

You can get distance between entities or user & entity or between two mercator zones using the following api methods. Check the callback for the result DistanceTime object. DistanceTime contains the distance in meters and time to reach the destination in minutes.

//distance time between user and searchEntity
SeachEntity searchEntity = ...;
RouteOptions routeOptions = null; //uses default

coreApi.routingManager().getDistanceTimeEstimate(searchEntity, routeOptions, distanceTime -> {
    Log.d(TAG, "distanceTime = " + distanceTime);
    if (distanceTime != null) {
        Log.d(TAG, "distance " + distanceTime.getDistanceMeters() + " meters");
        Log.d(TAG, "time  " + distanceTime.getTimeMinutes() + " minutes");
    }
});

//distance time between user and a mercator zone
IMercatorZone mercatorZone = ...;
RouteOptions routeOptions = null; //uses default

coreApi.routingManager().getDistanceTimeEstimate(mercatorZone, routeOptions, distanceTime -> {
    Log.d(TAG, "distanceTime = " + distanceTime);
});

//distance time between two mercator zones
IMercatorZone startMercatorZone = ...;
IMercatorZone toMercatorZone = ...;
RouteOptions routeOptions = null; //uses default

coreApi.routingManager().getDistanceTimeEstimate(startMercatorZone, toMercatorZone, routeOptions, distanceTime -> {
    Log.d(TAG, "distanceTime = " + distanceTime);
});
//distance from current user to some destination entity
let destination: MNSearchEntity = ...
let routeOptions: MNRouteOptions = ....
    CoreApi.RoutingManager.requestEstimateFromCurrentLocation(
                                               destination: destination,
                                               routeOptions: routeOptions,
                                               completion: { distTime in

            if let distanceTime = distTime {
                print("Estimated distance is \(distanceTime.distanceInMeters)")
                print("Estimated time is \(distanceTime.timeInMinutes)")
            }

        })



//distance from one entity to another entity
let from: MNSearchEntity = ... 
let to: MNSearchEntity = ...
let routeOptions: MNRouteOptions = ....

CoreApi.RoutingManager.requestEstimate(start: from,
                                               destination: to,
                                               routeOptions: routeOptions,
                                               completion: { distTime in

            if let distanceTime = distTime {
                print("Estimated distance is \(distanceTime.distanceInMeters)")
                print("Estimated time is \(distanceTime.timeInMinutes)")
            }

        })

Categories

Property-level Entities or those inside a Building can have a category associated with them. A category has a unique identifier (UID), name, and associated icon. A category can also have child categories such that the category becomes their parent. Currently there are upto three levels of hierarchies in the system. Root Categories are at the top level, Each root category can have any number of child categories which make the second level. And each of those categories can have any number of child categories which comprise the third level.

You can get all categories associated with a property using CoreApi.PropertyManager. The CategoriesResult object has multiple methods to provide the categories as hierarchical or as flattened list.

int propertyId = 504;
coreApi.propertyManager().getCategories(propertyId, categoriesResult -> {
    // get all categories in flat list
    List<Category> allCategories = categoriesResult.getAllCategories();

    // get tree-based root categories
    List<Category> rootCategories = categoriesResult.getRootCategories();
});
let myPropertyId =  123

CoreApi.PropertyManager.getCategories(propertyId: myPropertyId, callback: { result in
    guard let result = result else {
        return
    }
    //All available categories
    let allCategories = result.getAllCategories()
    self.doSomethingWithCategories(categories: allCategories)

    //Find categories by id
    let wantedCategoryId = "abc123"
    if let category = result.findById(uuid: wantedCategoryId) {
        //Found category by its id
    }

    //Try to find by name
    let name = "room"
    let categoriesWithName = result.findByName(name: name)
    for match in categoriesWithName {
        print("#Category.findByName(\"\(name)\") matched by \(match.id) = \(match.name)")
    }
}

func doSomethingWithCategories(categories: [iCategory]) {

    //Get information about the category
    for category in categories {
        //Get name and id
        print("Category \(category.id) = \(category.name)")

        //Check if it's a root category
        let isRoot = (category.type == .Root)

        //Get child categories, if any
        for child in category.getChildCategories() { //Level 2 categories
            //Get name
            print("Category \(child.name)")

            //Check if it's a root category. Always false except for the first level
            let isRoot = (category.type == .Root)

            for grandChild in child.getChildCategories() {
                //Print information
                print("Category Level 3: \(grandChild.name)")
            }
        }
    }
}

Location Positioning Technology

Our Location positioning technology allows for user position updates in location-enabled indoor venues and outdoor spaces. Mapsted’s technology relies on innovative, adaptive, data-fusion, and self-learning algorithms to deliver accurate, scalable indoor and outdoor positioning technology using any off-the-shelf smart-phone. The technology is forward-thinking in nature. It uses disturbances (e.g., magnetic, wireless) in the environment to learn and determine location. Basically, it approaches the problem by converting the interference, or noise, in the environment into useful information. The Figure below will give you a high-level overview of how this location positioning technology works.

User Positioning

A user's position is defined by a Position object. The Position object includes the user's IZone, which includes a propertyId, buildingId, and floorId, representing where the user is located. If any of these Ids show a value of -1, then it means they are not available. Some examples are detailed below.

Outdoor Positioning

The location positioning technology engine is seamless. It uses the same mechanism to return positions whether the user is indoors or outdoors. Based on the returned Position object, you can determine whether a user is outdoors, for example.

  • propertyId = -1 means the user is not on any premises,
  • propertyId > 0 and buildingId = -1 means the user is on the premises of the specified propertyId, but not inside any of its buildings.

Indoor Positioning

Similarly to outdoor positioning, when the user is indoors, this can be identified based on the returned Position object, as follows.

  • propertyId > 0, buildingId > 0, and floorId > 0 means that the user is on the propertyId within the buildingId, and on the floorId.

Programmatically Handling User Positions

There are two types of Position events that you can register as a listener for. The first type is a standard Position event. Position events are continuously updated by the positioning SDK, depending on the user's current activity and the type of data sources available. The second event type is PositionAnimation, which occurs when the user is in motion. PositionAnimation events dynamically update the user position at 30 frames per second, creating a smooth UI/UX experience when the user marker is in motion.

The Position event listener should be used for business logic processing based on a position update, and the PositionAnimation event listener should be used for displaying the user marker on a map UI to ensure the best user experience. The Position event listener is typically updated roughly every second, when the user is in motion. The PositionAnimation event listener is updated approximately 30 times per second when the user is in motion, to ensure smooth animation for enhanced UI/UX.

// Observe Position change events
// Updated roughly every 1 second
PositionChangeListener positionChangeListener = position -> {
    int propertyId = position.getPropertyId();
    int buildingId = position.getBuildingId();
    int floorId = position.getFloorId();
    double x = position.getX();
    double y = position.getY();
};
coreApi.locationManager().addPositionChangeListener(positionChangeListener);
//...
//when no longer needed, remove the listener
coreApi.locationManager().removePositionChangeListener(positionChangeListener);

// Observe Position Animation change events
// Updated roughly 30 fps when user is in motion
PositionAnimationListener positionAnimationListener = position -> {
    int propertyId = position.getPropertyId();
    int buildingId = position.getBuildingId();
    int floorId = position.getFloorId();
    double x = position.getX();
    double y = position.getY();
};
coreApi.locationManager().addPositionAnimationListener(positionAnimationListener);
//...
//when no longer needed, remove the listener
coreApi.locationManager().removePositionAnimationListener(positionAnimationListener);
// Observe Position change events
// Updated roughly every 1 second
//Your viewcontroller needs to register for position updates as follows:
CoreApi.LocationManager.addPositionChangeListener(listener: self)

//You can satisfy the protocol requirements by implementing the required method 
extension YourViewController: PositionChangeListener {

    // Updated roughly every 1 second
    public func onPositionChange(position: MNPosition) {
        let propertyId = position.zone().propertyId()
        let buildingId = encrypted.zone().buildingId()
        let floorId = encrypted.zone().floorId()
        let x = position.loc.x
        let y = position.loc.y
    }

}

/ Observe Position Animation change events
// Updated roughly 30 fps when user is in motion
//Your viewcontroller needs to register for position animation updates as follows:
CoreApi.LocationManager.addPositionAnimationListener(listener: self)

//You can satisfy the protocol requirements by implementing the required method 
extension YourViewController: PositionAnimationListener {
    func onPositionAnimation(position: MNPosition, animationBegins: Bool) {
        //process new position
    }
}

User Bearing

The Bearing is a float which represents the direction that the user is currently facing. Bearing is specified in degrees clockwise from North. This listener is only called when the user is on the premises of a property.

BearingChangeListener bearingChangeListener = bearing -> {
    // bearing is specified in degrees clockwise from North
};
coreApi.locationManager().addBearingChangeListener(bearingChangeListener);
//...
//when no longer needed, remove listener
coreApi.locationManager().removeBearingChangeListener(bearingChangeListener);
//Your viewcontroller needs to register for bearing updates as follows:
CoreApi.LocationManager.addBearingChangeListener(listener: self)

//You can satisfy the protocol requirements by implementing the required method 
extension YourViewController : BearingChangeListener {
    func onBearingChanged(degrees: CGFloat) {
        // bearing is specified in degrees clockwise from North
    }
}

Coordinate Systems

Mapsted uses the Universal Transverse Mercator (UTM, EPSG:3857) coordinate system to repesent the global (x,y) position in meters. Support is also provided for latitude and longitude (WGS84) in degrees, as shown below.

LatLng latLng_1 = new LatLng(someLatitude, someLongitude);
Mercator mercator_1 = MapCalc.toMercator(latLng); // From WGS84 to EPSG:3857

Mercator mercator_2 = new Mercator(someX, someY);
LatLng latLng_2 = MapCalc.toLatLng(mercator.getX(), mercator.getY());  // From EPSG:3857 to Lat, Lng
let pos1 = MNMercator(lat: someLatitude, lng: someLongitude)
print("X: \(pos1.x) Y: \(pos1.y)") // From WGS84 to EPSG:3857

let pos2 = MNMercator(x: someX, y: someY, z: someZ)
let latLng:CLLocationCoordinate2D = MNMercator.latLong(from: pos2)  // From EPSG:3857 to Lat, Lng
print("Lat: \(latLng.latitude) Long: \(latLng.longitude)") 

Wayfinding

Wayfinding uses Mapsted's advanced routing technology to generate optimal routes to help users get around in selected properties and buildings. The wayfinding engine can handle property-wide multi-destination routing. Each RouteRequest consists of a start location, such as the user's current location or a point of interest, and a list of destination locations or points of interest. The RouteRequest also consists of several RouteOptions that determine which accessibility measures are necessary, the order the user would like to navigate to the destinations in, and allow for the use of optimized routes.

The RouteResponse consists of a list of Route objects, where each Route consists of a list of RouteSegment objects. Each RouteSegment corresponds to a portion of the route on a specific floor of a building, or property level, for example outdoors, and is linked via a TransitionType enum. A figure showing an illustrative example of a RouteResponse is shown below. In the illustrative example above, the user is navigating from a location on Level 2 of Building A to a location on Level 1 of Building B. As shown, the route from point A to point B involves four RouteSegments and includes three transitions - between floors, to outdoors, and to indoors.

Prebuilt UI/UX

When using the Prebuilt UI/UX, the wayfinding functionality is automatically in use.

Customizable UI/UX

There are a number of customizable routing parameters available, which can be set programmatically, as outlined below.

Programmatic Wayfinding

// Setup desired routing options
// OptimizedRoute will re-order destinations optimally, if set to false will navigate in order
// If Accessibility is true, it may override the behaviour of Stairs/Elevator/Escalator
RouteOptions routingOptions = new RouteOptions();
routingOptions.setOptimizeRoute(true);
routingOptions.setFromCurrentLocation(false); // true if routing from user's position
routingOptions.setAccessibility(false);
routingOptions.setIncludeElevators(true);
routingOptions.setIncludeEscalators(true);
routingOptions.setIncludeStairs(true);
//enable use of emergency exits 
routingOptions.setEmergency(false);

//create waypoints using WaypointHelper or Waypoint.Builder
Waypoint waypoint1 = WaypointHelper.from(iSearchable1);  
Waypoint waypoint2 = new Waypoint.Builder().addLocation(mercatorZone2).build();
Waypoint waypoint3 = WaypointHelper.from(searchEntity3);

RouteRequest routeRequest = new RouteRequest.Builder()
                    .setRouteOptions(routingOptions)
                    .setStartWaypoint(waypoint1) // only if not from 'MyLocation'
                    .addDestinationWaypoint(waypoint2) // add possibly multiple destinations
                    .addDestinationWaypoint(waypoint3)
                    .build();

// Start Async route request
// Response will be passed to the RoutingRequestCallback
// UI should be updated accordingly
coreApi.routingManager().requestRouting(routingRequest, new RoutingRequestCallback() {
    @Override
    public void onSuccess(RoutingResponse routingResponse) {
        //use routingResponse
        // If user desires to begin navigation, can be started by using:            
        Route route = routingResponse.getRoutes().get(0);
        RoutingStatusCallback routingStatusCallback = new RoutingStatusCallback() {
            @Override
            public void onRoutingStatus(boolean isRoutingModeOn, RoutingResponse latestRouteResponse) {
                // Indicates a status change for the latest route response
                // For example, if isRoutingModeOn is false, UI can adjust accordingly
            }

            @Override
            public void onRouteSegmentReached(RouteSegment currentRouteSegment, List<RouteSegment> visitedRouteSegments, List<RouteSegment> upcomingRouteSegments) {
                //Called when a route segment has been reached. 
            }

            @Override
            public void onRouteInstruction(RouteNode routeNode) {
                // Called when a new instruction is available for the real-time navigating user
            }

            @Override
            public void onUserProgressAlongRoute(RouteUserProgress routeUserProgress) {
                //This function will be called as user continues progress along route
            }

            @Override
            public void onRouteRecalculation(RoutingResponse routingResponse) {
                // Called when the user deviates from the route provided
                // and a new route is automatically recalculated
            }

            @Override
            public void onDestinationReached(int destinationEntityId) {
                //reached destination of this route
            }
        }
        coreApi.routingManager().startNavigation(route, routingStatusCallback);

        // Navigation can be cancelled midway through (e.g., user UI interactions) by calling:
        coreApi.routingManager().stopNavigation();
    }

    @Override
    public void onError(CppRouteResponse.ErrorType errorType, String error, List<String> alertIds) {
        //failed
    }
});
// To make a route request, you should first setup your desired routing options

/*
    - optimizedRoute = false will navigate in order
    - optimizedRoute = true will re-order destinations optimally 
*/
let optimizedRoute = true 

//Preferred transition options
let useStairs = false
let useEscalators = true
let useElevators = true

// There is also an accessibility option provided - it may override the behaviour of Stairs/Elevator/Escalator
routeOptions.setAccesibility(true)

let calculateFromCurrentLocation = true

let routeOptions = MNRouteOptions.init(useStairs, escalators: useEscalators, elevators: useElevators, current: calculateFromCurrentLocation, optimized: optimizedRoute)

//Build a route request
var routeRequest: MNRouteRequest?

if calculateFromCurrentLocation { //startEntity is passed as nil
    routeRequest = MNRouteRequest.init(routeOptions: routeOptions, destinations: destinations, startEntity: nil)
}
else { //destinationsMinusStart is an array of destinations excluding the start 
    routeRequest = MNRouteRequest.init(routeOptions: routeOptions, destinations: destinationsMinusStart, startEntity: startEntity)
}

if let routeRequest = routeRequest {
    CoreApi.RoutingManager.requestRoute(request: routeRequest, routingRequestCallback: self)
}

var routes : [MNRoute] = []

// To receive the callback after you make the route request, you will need to have implemented the RoutingRequestCallback protocol 
extension YourViewController: RoutingRequestCallback {
    func onSuccess(routeResponse: MNRouteResponse)
        //You can proceed to request using one of the routes returned with routeResponse
        //Check for error
        if routeResponse.errorType == .noError {
            routes = routeResponse.routes
        }
    }

    func onError(errorCode: Int, errorMessage: String, alertIds: [String]) {

    }
}

//Make sure routes are not empty, and choose a route from the list
let route = routes.first 

//Your can now make a route request with the route. In order to receive route status updates, you also pass a delegate to protocol RouteStatusCallback. 
CoreApi.RoutingManager.startNavigation(route: route, routingStatusCallback: self)

//Your delegate will need to be implement the methods of *RoutingStatusCallback*. 
extension YourViewController: RoutingStatusCallback {

    func onRoutingStatus(isRoutingModeOn: Bool, latestRouteResponse: MNRouteResponse) {
        // Indicates a status change for the latest route response
        // For example, if isRoutingModeOn is false, UI can adjust accordingly
    }

    func onRouteInstructionReceived(routeNode: MNRouteNode) {
        // Called when a new instruction is available for the real-time navigating user
    }

    func onRouteSegmentReached(currentRouteSegment: MNRouteSegment,
                           visitedRouteSegments: [MNRouteSegment],
                           upcomingRouteSegments: [MNRouteSegment]) {
        //Called when a route segment has been reached. 
    }

    func onUserProgressAlongRoute(routeUserProgress: MNRouteUserProgress) {
        //This function will be called as user continues progress along route
    }

    func onRouteRecalculation(newRouteResponse: MNRouteResponse) {
        // Called when the user deviates from the route provided
        // and a new route is automatically recalculated
    }

    func onDestinationReached(destinationEntityId: NSInteger) {
        // Called when the user reaches the destination (specified by the destinationEntityId)
    }
}

Understanding Route Responses

A RouteResponse contains a boolean which indicates whether or not the RouteRequest was successfully processed. An error may occur for several reasons, such as if an invalid start or data was provided, or a route could not be found.

A requested route may involve single or multiple destinations. A RouteResponse consists of a RouteVector (a list of Route objects), where each Route represents a trip to a specific destination. Each Route consists of a RouteSegmentVector (a list of RouteSegment objects), where each RouteSegment represents a separate segment of the route, such as a different floor inside a building, or an indoor-outdoor segment. Each RouteSegment consists of a RouteNodeVector (a list of RouteNode objects), where each RouteNode represents a specific location, and possibly, a corresponding instruction.

// Note: Do not forget to handle null cases
//when you receive a routingResponse from RouteingRequestCallback
void onRoutingResponseReceived(RoutingResponse routingResponse) {
    boolean success = routingResponse.isSuccessful();
    if(!success) {
        String error = routingResponse.getError();
        ErrorType errorType = routingResponse.getErrorType();
        //check or log the error or errorType
        return;
    }

    /*
        If multiple destinations are requested, there may be multiple routes.
        Each route represents the trip to a specific destination.
    */
    List<Route> routes = routingResponse.getRoutes();

    // The route index that is currently selected (e.g., in route preview mode)
    Route currentRoute =  mapApi.getCurRoute(); 

    // All route segments for the specifc route (e.g., multiple floors or indoor-outdoor)
    List<RouteSegment> routeSegments = route.getSegments();

    // Loop through all route segments
    for (RouteSegment routeSegment : routeSegments) {
        // Outside property, within property, or within building
        RouteSegmentType segmentType = routeSegment.getSegmentType();

        // get path mercators for UI plotting
        MercatorVector smoothedRoute = routeSegment.getPath();

        // Can iterate over non-smoothed (discretized) route nodes
        for (RouteNode routeNode : routeSegment.getRouteNodes()) {
            // key points will have instructions
            boolean isKeyPoint = routeNode.isKeyPoint();

            if(isKeyPoint) {
                Instruction instruction = routeNode.getInstruction(appContext);

                // instruction will take into account localization, if supplied
                // default is english
                String instructionText = instruction.text;

                // You can customize your own behaviour by using the instruction type
                // e.g., TURN_LEFT, TURN_RIGHT, ENTER_BUILDING, ...
                InstructionType instructionType = instruction.instructionType;

                //drawables for instruction type
                int drawableId = instruction.drawableResId;
            }
        }
    }
}


// ...
func routeResponseReceived(response: MNRouteResponse) {
    guard response.isSuccessful else {
            // routing failed
            return
    }

    /*
    If multiple destinations are requested, there may be multiple routes.
    Each route represents the trip to a specific destination.
    */
    let routes = response.routes
    //Index into routes to access a particular route
    let route = routes[0];

    //You can access individual segments of a route as follows
    let routeSegment = route.segments[1] //size checks are skipped for brevity
    let segmentType = routeSegment.segmentType // Outside property, within property, or within building

    // Smoothed route for UI plotting
    let smoothedRoute = routeSegment.smoothedRouteNodes

    //Get instructions
    for routeSeg in route.segments {
        for routeNode in routeSeg.routeNodes {
            if routeNode.isKeyPoint {
                // instruction will take into account localization, if supplied
                // default is english
                let instruction = routeNode.instruction

                // You can customize your own behaviour by using the instruction type
                // e.g., TURN_LEFT, TURN_RIGHT, ENTER_BUILDING, ...
                let instructionType = routeNode.instructionType

                // EntityId of the instruction's landmark (-1 if doesn't exist)
                let landmarkEntityId = routeNode.landmarkEntityId
            }
        }
    }
}

Programmatically Add Points-of-Interest

Mapsted Maps supports programmatically adding and wayfinding to your own Points of Interest (POI). You can create a Tag object which holds the necessary location data. Your custom Tag or POI can be added to the user's itinerary and/or be navigated to. You also have the ability to add multiple tags at the same time, or to delete all tags from a property.

// Create MercatorZone object
MercatorZone mercatorZone = new MercatorZone(propertyId, buildingId, floorId, xCoordinate, yCoordinate);

// Create the Tag object which holds the specific location
Tag tag = new Tag(tagName, propertyName, creationTimeStamp, propertyId, buildingId, mercatorZone);

// When using prebuilt UI/UX, 
// This will place a pin on the POI location and provide options for adding to itinerary or navigation
mapUiApi.selectCustomTags(tag);

// ...

// When not using prebuild UI/UX, 
// You can also programmatically add your tag to a RouteRequestBuilder (e.g., destination)
RouteRequest request = new RouteRequestBuilder()
                .setRouteOptions(routingOptions)
                .setStartSearchable(myStartSearchable) // only if not from 'MyLocation'
                .addDestination(tag)
                .build();
    let zone = MNZone(propertyId: thePropertyId, buildingId: theBuildingId, floorId: theFloorId)
    let mercator = MNMercator(x: xCoordinate, y: yCoordinate, z: zCoordinate)

    // When using prebuilt UI/UX, 
    // This will place a pin on the POI location and provide options for adding to itinerary or navigation
    if let mapsVC = MapstedMapUiViewController.shared as? MapstedMapUiViewController {
        let newTagName = "My Tag";
        mapsVC.addTag(tagName: newTagName, tagPos: MNPosition(zone: zone, loc: mercator))

        //Use the addTags method to add more than one tag at once. 

        //Initialize and populate your tags
        let multipleTags: [MNTag] = ... 

        //Add the batch at once.
        mapsVC.addTags(tags: multipleTags)

        /* 
        Use deleteAllTags to delete all tags added to a property 
        */
        let pId = 1234
        mapsVC.deleteAllTags(propertyId: pId)
    }

Accessibility

Accessibility Routing

Mapsted SDK supports accessibility routing. When enabled, the provided routes avoid stairs, steps, and escalators, and use ramps or elevators to help the user navigate to their destination instead. For even finer control, the SDK also allows for users to specify specific transitions which they would like to take, for example if the user would prefer to take the escalator or elevator over stairs.

RouteOptions routeOptions = new RouteOptions();

// If full accessibility is desired, it can be enabled
// Note that this will override all other customizations for stairs, escalators, elevators
routeOptions.setAccessibility(true);

// Alternatively, if for example, the user perfers escalators/elevators over stairs
// The following can be used
routeOptions.setAccessibility(false);
routeOptions.setIncludeStairs(false);
routeOptions.setIncludeEscalators(true);
routeOptions.setIncludeElevators(true);

// Create RouteRequest and process. See Wayfinding section for more information.
//Create an instance of MNRouteOptions. You can configure the first three parameters based on whether you prefer 
//stairs/escalators/elevators. The MNRouteOptions object is used to create a route request.

let useStairs = false
let useEscalators = true
let useElevators = true

let routeOptions = MNRouteOptions.init(useStairs, escalators: useEscalators, elevators: useElevators, current: true, optimized: true)

//There is an accessibility mode that overrides previous settings.
routeOptions.setAccessibility(true)

Localization

The Mapsted SDK offers multiple language support, including support for both left-to-right and right-to-left languages. Support is automatically provided for most languages, but appropriate translation and font files would be necessary to support non-mainstream languages. This is handled automatically when using the Map UI SDK or prebuilt UI/UX.

Localization Example

The figures below show the same map view in English and Arabic, respectively. As you can see, the map UI automatically handles the left-to-right and right-to-left localizations.

Programmatically Controlling Localization

The Mapsted mobile SDK will automatically capture the localization based on the device's settings, if the selected language is supported. If the language is not available, it will automatically default back to English. The localization can also be modified programmatically, as shown below.

// Sample usage for listening for language events, notifying the Mapsted Map
// Note that when using the Mapsted Map-Ui or prebuilt UI/UX this is handled automatically

 HandlerThread languageHandler = new HandlerThread("LanguageSupport");
    languageHandler.start();

    new Handler(Looper.getMainLooper()).post(() -> {
        LocaleManager.getInstance(mActivity.getApplicationContext()).getLanguageMutableLiveData().observe(mActivity, (languageLiveData) -> {
            new Handler(languageHandler.getLooper()).post(() -> {
                mapApi.setLanguageCode(languageLiveData, (language, propertyId, isAvailable) -> {
                    // TODO: Update the rest of the UI
                });
            });

        });
    });
//iOS automatically relaunches the app in use when the the user changes their 
//preferred language from settings. To change the user default language programmatically, 
//call the set method of the system provided UserDefault class by means of its shared standard object.

UserDefaults.standard.set(["en"], forKey: "AppleLanguages")
UserDefaults.standard.synchronize()

//You will need to provide the two letter language code to the method. 
//Standard language code values include "en" for English, "fr" for French, "ar" for Arabic, etc.

Mapsted SDK supports parsing deep links for various sdk features.

Deep links must be in following format

https://{your_host}/deeplink/{your_app_name}/{feature_name}/{optional_sub_feature}?{related_parameters}

  • your_host : your host/domain name
  • your_app_name : an identifier for your app
  • feature_name : feature identifier which may be feeds or map
  • optional_sub_feature : optional sub feature are available if feature_name is map. They include select (for selecting an entity) or routing (routing feature)
  • related_parameters : additional parameters like property, building, floor or entity for their respective ids. It also includes destinations which is list of buildingId:entityId items.
Description Deep link
Campaign feeds for all properties https://yourdomain.com/deeplink/appname/feeds
Campaign feeds for a particular property https://yourdomain.com/deeplink/appname/feeds?property=504
Select entity on map https://yourdomain.com/deeplink/appname/map/select?property=123&building=111&floor=222&entity=333
Open route preview https://yourdomain.com/deeplink/appname/map/routing?property=123&destinations=111:333,111:444

Note: destinations are in buildingId:entityId format.

<activity android:name=".activities.MainActivity"
    android:label="@string/app_name"
    android:launchMode="singleTop"
    android:screenOrientation="portrait">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />

        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />

        <data android:host="yourdomain.com" />
        <data android:pathPrefix="/deeplink/appname"/>
        <data android:scheme="https" />
        <data android:scheme="http" />
    </intent-filter>
</activity>
//in your activity
@Override
protected void onNewIntent(Intent intent) {
    Uri intent.getData(); 
    boolean handled = CoreApi().processDeeplink(uri, deeplinkData -> {
        Log.d("processDeeplink: deeplinkData=" + deeplinkData);
        String feature = deeplinkData.getFeaturePath();
        switch(feature){
            case DeeplinkProcessor.FEATURE_FEEDS: 
                //feeds page was requested. use deeplinkData to get propertyId.
                break;
            case DeeplinkProcessor.FEATURE_MAP_ROUTING:
                //routing feature was requested. use deeplinkData to get propertyId and destinations.
                break;
            case DeeplinkProcessor.FEATURE_MAP_SELECT:
                //an entity was requested to be selected. use deeplinkData to get propertyId, buildingId, floorId, entityId
                break;
            default:
                //could not determine the feature requested. 
        }

    });
    if(!handled) {
        //if it was not handled, deeplink is not mapsted related. 
    }
}
guard let url = URL(string: "https://mydomain.com/deeplink/myappname/map/routing?property=123&destinations=111:333,111:444") else {
    //Not valid URL
    return 
}
CoreApi.processDeepLink(uri: url, completion: { (handled, deepLinkFeature, deepLinkData)  in

    //Check whether it was handled
    guard handled else {
        //Not Mapsted deeplink. Handle via your own methods
        return
    }

    //Was handled as a Mapsted deeplink, will be processed accordingly

    switch (deepLinkFeature, deepLinkData) {

    case (.MapSelect, .MapSelect(propertyId: let p, buildingId: let b, entityId: let e)):
        //handle map selection
        //print("Select \(p)-\(b)-\(e)")
        break;

    case (.MapRouting, .MapRouting(searchEntities: let entities)):
        //handle destinations
        //print("Map Routing to \(entities.count) destinations")
        break;

    /* Handle other cases */ 

    default:
        //handle other cases
        break;

    }

})

Location Marketing

The Location Marketing module allows you to incorporate precision location-based Triggers, Events, or Feeds. Campaigns can be easily created, configured, managed, and published using the Mapsted Notify software. When using the Location Marketing module, these campaigns will be automatically synced with the Mapsted Maps - Mobile SDK.

Campaigns created via the Mapsted Notify Software can displayed as Feed items. Campaigns can also be associated with various Triggers. A Trigger is a combination of Location Criteria (e.g., geofence), Demographic Criteria (e.g., device type), and/or Behaviour Criteria (e.g., keyword searches).

When enabled, the Mapsted Maps - Mobile SDK will provide a callback to identify that a particular campaign and trigger has occurred, referred to as a Marketing Event. When using the Prebuilt UI/UX, the Marketing Event may be displayed as a Notification, a Dialog, or in a Feed Bar (a rectangular pop-up at the top of the screen). On the other hand, for Customizable UI/UX, the Marketing Event may be handled however you see fit.

Campaign Feeds

The Location Marketing module provides an API to retrieve the currently active campaigns. The campaign feeds may be retrieved for a single or multiple properties.

Feeds

LocMarketing locMarketing = ...

// ------------------------
// Option 1: Customized UI/UX 
// ------------------------
// get Feeds and show in your own UI
LocMarketing.Callback callback = new LocationMarketing.Callback() {
    @Override
    public void onFeedsResult(List<Feed> feedsList) {
        Logger.d("onFeedsResult: %d", feedsList.size());
        //show feeds to UI
    }
};

//you can retrieve propertyId from mapApi or coreApi. Alternatively, if you need
//feeds for multiple properties, you can pass in an arraylist of propertyIds as well.
locMarketing.getFeedsAsync(propertyId, callback);

// ------------------------
// Option 2: Prebuilt UI/UX
// ------------------------
// use a FeedFragment. A FeedFragment shows the feed in a list. You can provide the campaignId argument to
// pre-scroll to that campaign. Campaign ids can be retrieved from LocMarketingListener
FeedFragment feedFragment = FeedFragment.newInstance(propertyId, null);
//use fragment manager to show the above feedFragment. The parent activity should implement FeedFragmentListener

//Parent activity
public MyActivity extends AppCompatActivity implements LocationMarketApiProvider, FeedFragmentListener{
    ...
    @Override
    public LocMarketing getLocationMarketingSdk() {
        Logger.d("getLocationMarketingSdk: ");
        if (locMarketing == null) {
            locMarketing = setupLocMarketingSdk(coreApi);
        }
        return locMarketing;
    }

    @Override
    void onFeedSelected(Feed feedItem){}

    @Override
    void onFeedShareClicked(Feed feedItem){}

    @Override
    void onFeedViewed(Feed feed){}

    @Override
    void onFeedFragmentViewCreated(int propertyId){}
    ...
}
//Import the Location Marketing SDK first
import LocationMarketing 

//Instantiate Location Marketing API
let locMarketingManager = LocMarketingApi.shared

//If you have a variable for identifying the property you want to fetch feeds for 
let myPropertyId = 123 

//Define the callback method to handle feeds fetched for that property of your interest
func myCallbackMethod(fetchedCampaigns: [Campaign]) {

    //extract the feeds from campaigns
    for campaign in fetchedCampaign { 
        //do something
    }
};

//Fetch feeds and pass them to the callback method which will know how to process them.
locMarketingManager.fetchCampaignsForProperty(propertyId: myPropertyId, completion: myCallbackMethod);

Campaign Triggers/Events

Marketing Events occur whenever the necessary Trigger Criteria have occurred. The Marketing Event can be handled in a number of different ways. For example, the campaign can be displayed using a Dialog or Feeds Bar (a rectangular pop-up at the top of the screen) when the app is foreground or a Notification when app is in background. Campaigns and Triggers can be configured using the Mapsted Notify software.

//activity onCreate
//When the user interacts with the notification or dialog, LocMarketingListener will
//receive callback to open the map or open the website.
LocMarketing.LocMarketingListener locMarketingListener = new LocMarketing.LocMarketingListener() {
        @Override
        public void navigateToMap(String campaignId, List<HomeEntity> homeEntityList) {
            //use the mapApi to show the map
        }

        @Override
        public void openWebsite(String campaignId, String websiteURL) {
            //open browser or webview with the websiteUrl
        };

        @Override
        public void showInFeedsBar(Campaign campaign) {
            //add this campaign to FeedsBarFragment
        }
};

//initialize the sdk. Note, to initialize pass in a initialized coreApi instance. you can share this instance of LocMarketing through out your app.
LocMarketing locMarketing = new LocMarketing(context, coreApi, locMarketingListener);

//activity onNewIntent
public void onNewIntent(Intent intent) {
    if (LocaMarketing.canHandle(intent)){
        locMarketing.onNewIntent(intent);
    } else {
        super.onNewIntent(intent);
    }
}
//Import the Location Marketing SDK first
import LocationMarketing 

//Assuming you have already initialized, you can use its shared instance
let locMarketingManager = LocMarketingApi.shared

//To respond to notifications from the Mapsted Location Marketing API, set your delegate as listener 
locMarketingManager.setListener(listener: self)

//For this to work, you delegate needs to have implemented the LocMarketingListener protocol
extension MyViewController : LocMarketingListener {

    //Mandatory method
    //Implementation is required for this method  
    public func navigateToMap(homeEntities: [EntityInfo]) {
        //Choose from home entities and show on the map
    }   

    //Optional method
    //Provide an alternate implementation for the dismiss() method 
    //to perform additional actions after the notification popup is dismissed
    func dismiss(action: Action?) {}

    //Optional method
    //override the openWebsite() method or return false to modify the default behavior 
    //By default, this method opens the websiteURL in an embedded webview. 
    func openWebsite(websiteURL: String) -> Bool { return false }

}

Alerts

The Alerts module allows you to incorporate precision location-based Alerts. These can be easily created, configured, managed, and published using the Mapsted Notify software. When using the Alerts module, these alerts will be automatically synced with the Mapsted Maps - Mobile SDK.

Scheduled Alerts

Scheduled alerts are one time or recurring alerts that are pre scheduled. Similar to location marketing campaigns, scheduled alerts also has triggers that causes the alerts to activate when the location criteria are met. Scheduled alerts can be created and set by signing into the Mapsted Hub Web Software.

//After the coreApi is initialized, you can initialize AlertManager
AlertsManager alertsManager = AlertsManagerImpl.getInstance(getApplicationContext(), coreApi);

//setup callback for user location. When an scheduled alert is triggered at the location. 
alertsManager.setUserAtLocationCallback((scheduledAlert, autoDismiss) -> {
    ScheduledAlertNotification alertNotification = new ScheduledAlertNotification(scheduledAlert, autoDismiss);
    //If you want to show the scheduled alert in the InAppNotification, you can use the InAppNotificationApi
    if (inAppNotificationApi == null) {
        inAppNotificationApi = new MapstedInAppNotificationsApi(getSupportFragmentManager(), fragmentContainerView, new MapstedInAppNotificationsApi.Listener(){
            @Override
            public void onInAppNotificationClick(InAppNoticiation inAppNotification) {
                if(inAppNotification instanceof ScheduledAlertNotification){
                    //in app notification was clicked
                }
            }

            @Override
            public void onViewed(InAppNotification inAppNotification) {
                if(inAppNotification instanceof ScheduledAlertNotification){
                    //in app notification shown
                }
            }
        });
    }
    inAppNotificationApi.showInInAppNotificationBar(alertNotification, true);
});

//To get list of ongoing scheduled alerts
int propertyId = 504;
List<ScheduledAlert> ongoingAlertsForEntity = alertsManager.getOngoingAlertsForProperty(propertyId);
showList(ongoingAlertsForEntity); //showList being your own implementation

//to get changes in ongoing scheduled alerts, register a listener
AlertsOnChangeListener alertsOnChangeListener = new AlertsOnChangeListener(propertyId) {
    @Override
    public void onChangeInOngoingAlerts(Set<ScheduledAlert> before, Set<ScheduledAlert> after) {
        //the two sets provide the scheduled alerts before and after/now.
    }
};
alertsManager.registerListener(alertsOnChangeListener);
//unregister when not needed
alertsManager.unRegisterListener(alertsOnChangeListener);
let myPropertyId = 504;

//to get list of ongoing scheduled alerts for a property
let alertList = AlertsApi.shared.getOngoingAlertsForProperty(propertyId)
for scheduled in alertList {
    //do something with each
}

Emergency Alerts

To receive emergency alerts, first send your firebase token. Emergency alerts can be created and sent by signing into the Mapsted Hub Web software. Emergency alerts are sent via Firebase messaging service. Therefore, you would need to register your app on Firebase console.

alertsManager = AlertsManagerImpl.getInstance(getApplicationContext(), coreApi);
FirebaseMessaging.getInstance().getToken().addOnCompleteListener(task -> {
    if (!task.isSuccessful()) {
        return;
    }
    String token = task.getResult();
    alertsManager.sendFirebaseTokenForEmergencyAlerts(getApplicationContext(), token);
});
...

let identifier = "com.myCompany.myApp" //Usually the Bundle identifier from your app
AlertsApi.shared.sendFirebaseToken(token: fcmToken, appIdentifier: identifier);

...

Then, configure your app to receive the firebase messages

//create a service by extending EmergencyAlertsFirebaseMessagingService.
//Observe {@link InAppNotificationLiveDatas.DeactivatedLiveData} and 
//{@link InAppNotificationLiveDatas.ActivatedLiveData} to get 
//activated or deactivated emergency alerts when the app is in foreground. 
//When the app is in background, android notification is created. When the 
//notification is opened, the app will open with a deeplink in following format.
//`https://share.mapsted.com/deeplink/defaultapp/alerts/emergency_alert?id={id}`. 
//You can use the AlertManager to get details about the alert with id.
public class MyEmergencyFirebaseMessagingService extends EmergencyAlertsFirebaseMessagingService {
    @Override
    public void onNewToken(@NonNull String token) {
        alertsManager.sendFirebaseTokenForEmergencyAlerts(getApplicationContext(), token);
    }
}
<-- Register your service in the AndroidManifest -->
<application ..>
    <service
        android:name="com.example.myapp.MyEmergencyFirebaseMessagingService"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
</application>
..
/*
In a typical scenario, you integrate Firebase messaging AppDelegate via an extension 
to the Firebase `MessagingDelegate` protocol
*/

extension AppDelegate : MessagingDelegate {

    //implement this method
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {

        /** Other code .... */

        let appIdentifier = "com.myCompany.myApp" //Usually the Bundle identifier from your app
        AlertsApi.shared.sendFirebaseToken(token: fcmToken, appIdentifier: identifier);

        /** Other code .... */

    }

}

 /*
 If your application was launched or resumed because of the remote notification, you will get a callback 
 in the AppDelegate's `application(_:didReceiveRemoteNotification:fetchCompletionHandler:) method
 */
func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

    //Ask the Alerts API to process the notification if applicable
    AlertsApi.shared.processNotification(userInfo: userInfo, completion: { (handled, error)  in
        if handled {
            //Alert Found
            print("Alert Handled")
        }
        if let error = error {
            print("\(error.localizedDescription)")
        }

    })
}


/*
 The `application(_:didReceiveNotificationResponse:withCompletionHandler:) method will be called by 
 your app on the Application Delegate when the user responded to the notification
*/
extension AppDelegate : UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    /** ... Some code **/

  //If you are using Alerts SDK, ask it to process any alerts
  AlertsApi.shared.processNotification(response: response, window: self.window, completion:{ handled, error in
        if handled {

        }
        if let error = error {

        }
    })

    //If you are using LocMarketing SDK, ask it to process any campaign events
    LocMarketingApi.shared.processNotification(response: response, window: self.window, completion:{ success, error in
        if handled {

        }
        if let error = error {

        }

    })

    /** ... More code **/
}



//to get list of emergency alerts for a property
let myPropertyId = 504

AlertsApi.shared.fetchEmergencyAlerts(propertyId: myPropertyId, callback: { alertList in 
    for emergency in alertList {
        // do something with each
    }
})

Prebuilt UI/UX

We offer a wide variety of Prebuilt app templates and UI components which have been designed and optimized for use with the complete Mapsted Maps Mobile framework.

Depending on your desired level of customization, you can choose from several Prebuilt UI/UX options. Our Prebuilt app templates provide a fully immersive mobile application that includes basic customizations such as colour changes. For more advanced customization, you can use our Prebuilt UI components to build your own application. These components include a suite of Prebuilt UI views, such as wayfinding UI/UX, itinerary management, and category lists.


Prebuilt App Templates

Our Prebuilt app templates allow you to experience the look and feel of a full mobile application within minutes, while providing basic customization options to give the app your own look and feel. See below for examples of a few app template UI screens.

To learn how to enable a Prebuilt app template, see the sample code below.

// Extending Mapsted App Template enabled Mapsted's Prebuilt app templte
// Note that this could also be achieved using an Intent to launch
// MapstedAppTemplateMainActivity
public class MainActivity extends MapstedAppTemplateMainActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Anything you want...
    }

}
// Go to the AppDelegate.swift file in the main bundle of your application.
// Inside the application(_:didFinishLaunchingWithOptions:) method, add the relevant lines as per the example below

internal func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 
    ...

    //Compute copyright text OR use from localizable strings
    let copyRightText = NSLocalizedString("CopyrightText", comment: "Main App")

    //Set the name of the animation gif image, without the extension
    let startupAnimationGif = "Mall_mApp_GIF_Slower";

    //Instantiate the main screen
    let mainScreen = LaunchViewController.splashScreen(copyrightText: copyRightText, animationGif: startupAnimationGif);

    //Set the launch screen as the root view controller
    self.window?.rootViewController = mainScreen

    //Show the window and make it the key window
    self.window?.makeKeyAndVisible()

    ... 
    return true
}

Prebuilt UI Components

The Prebuilt app templates outlined in the previous section use a variety of Prebuilt UI components. Our UI components are specific UI/UX views which are designed and optimized to interact with the Mapsted Maps Mobile framework. This allows you to custom pick and choose which aspects of our Prebuilt UI/UX you want to use in your application.

The Prebuilt UI components are an excellent choice when you already have your own application and want to easily add some additional functionality based on the Mapsted Maps Mobile framework. For example, one common usage is to trigger a Mapsted Map view and use various Prebuilt UI components, like wayfinding UI/UX or itinerary management, when a maps button is clicked within your application. In other scenarios, various Prebuilt UI components can be reused in other areas of your application, for example category lists.


Customizable UI/UX

The customizable UI/UX allows you to use various prebuilt UI/UX components, while also allowing you the option of adding your own customizable UI. As an example, you can use your own UI views on the Mapsted Map. This section explains how you can create and display custom views on the map. The two examples below show how custom UI elements, such as the blue "Explore More" button and the horizontal floating category list, were added to the Mapsted Map component.


Programmatically Add, Update, and Remove Custom Views

Any view can be programmatically added, modified, and removd from the Mapsted Map. Each view that is passed in should be given a custom view tag (a string to uniquely identify the view), which can be used for retrieving/modify, and/or removing the view. The sample code below shows how a view can be added, modified, and removed.

// Note that if you want to add customizable UI views you need to have initialized your SDK as follows:

// the mapContainerView is the container for the MapView
// the uiContainerView is the container for your custom views that you wish to insert into the MapView
FrameLayout mapContainerView = findViewById(R.id.mapContainerView); 
FrameLayout uiContainerView = findViewById(R.id.uiContainerView);

// When you initialize your SDK, you must provide both uiContainerView and mapContainerView
mapUiApi.initializeMapstedSDK(activity, uiContainerView, mapViewContainer, new MapstedInitCallback() {
    void onCoreInitialized(){
        //Internally core api has initialized
    }

    void onMapInitialized(){
        //Internally map api has initialized
    }

    void onSuccess(){
        //MapUiApi has initialized successfully
    }

    void onFailure(SdkError sdkError){
        //MapUiApi initialization failed
    }

    void onMessage(MessageType messageType, String message){
        //sdk message during sdk initialization
    }
);

// ...

// Create Custom View (e.g., contains a button named with id custom_button)
View inflate = LayoutInflater.from(this).inflate(R.layout.custom_layout, null, false);
Button customButton = inflate.findViewById(R.id.custom_button);

// For example, set click listener
customButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // Handle click event
    }
);

// Display custom View on the map
mapUiApi.addViewToMap("my_custom_tag", myView);

// Get custom View on the map
View myView = mapUiApi.getViewOnMap("my_custom_tag");

// Can now modify view, as desired (e.g., change visibility)
// ...

// Remove custom View from the map
mapUiApi.removeViewFromMap("my_custom_tag");
//To access UI components, you need to add the following import
import appuikit

//To facilitate adding UI components to your app, a ContainerViewController class is provided 
//which allows easy swapping in/out of viewcontrollers. To use this, first drag and drop a container 
//view to your viewcontroller and set the embedded viewcontroller class in IB to ContainerViewController. 
//Assuming you have named the embedded segue "containerSegue", you can obtain a reference to the 
//ContainerViewController as follows.

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
    if segue.identifier == "containerSegue" {
        if let containerVC = segue.destination as? ContainerViewController {
            self.containerVC = containerVC
        }
    }
}

//Now to add a viewcontroller, simply call addController. Parameter isNew should be 
//set to true in case you want to maintain a stack of the viewcontrollers in memory 
//for fast swapping. If set to false, only one viewcontroller will be retained.

containerVC.addController(controller: categoryListVC, yOffset: 0, isNew: false)

//To display a floating category list, access the CategoryCarouselViewController. 
//Pass categoryUIDs which is list of category uids to display. The alignment can 
//be horizontal/vertical. Your viewcontroller needs to conform to CategorySelectionDelegate
//to get notified when user selects a category.

if let categoryListVC = CategoryCarouselViewController.instantiateViewController(propertyInfo: propertyInfo, categoryUIDs: categoryUIDs, alignmentIsVertical: false, delegate: self) as? CategoryCarouselViewController {
    categoryListVC.setCategoryTextColor(color: .white)
    containerVC.addController(controller: categoryListVC, yOffset: 0, isNew: false)
} 

extension YourViewController : CategorySelectionDelegate {
    func selectedCategory(category: MNCategory) {
        //handle the selection...
    }
}

Location Sharing (Coming Soon)

The Mapsted Maps Mobile framework offers several location-sharing options which use the power of our location positioning technology to help users interact with friends or colleagues.

Using location tags, users can easily share a specific location and/or message to one another for easy coordination. For example, a user could tag the food court on level 3 of a shopping centre, and share the location tag with several friends, along with a message that reads, "Meet me at 1 pm for lunch."

Using location sharing, users can also display their locations on each other's maps. This can be especially useful for couples or family members that wish to visit different places in a large venue, while still being aware of each other's location.

Location Tags

When location tagging is enabled in your application, users can use a long press gesture on the map to set a tag. This tag can be labelled, saved, navigated to, or shared with another user. Users also have the option to attach a message to the tag. Once the location tag is shared and received by another user, they can easily view and navigate to it. This provides a simple and easy-to-use approach for sharing information about a specific location with a friend or colleague.

To learn how to enable or disable a location tag, see the sample code below.

// For example, to enable tag UI when initializing
CustomParams.newBuilder()
                .setEnableTagUI(true) // enable tag UI
                .build();

// Note that this can also be adjusted in runtime, as shown below 

// Enable location tagging
mapUiApi.setEnableTagUI(true); // for the UI views
mapApi.setPlacePinOnSelectedLocation(true); // for pin on map

// Disable location tagging
mapUiApi.setEnableTagUI(false); // for the UI views
mapApi.setPlacePinOnSelectedLocation(false); // for pin on map
//Enable location tagging
MapstedMapApi.shared.allowTagsOnMap(enable: true)

//Disable location tagging
MapstedMapApi.shared.allowTagsOnMap(enable: false)

Programmatic Location Sharing

The location sharing framework was designed for maximum flexibility. You can use the Prebuilt option or choose to customize based on your specific needs. The location sharing framework requires a string key identifier to represent different users. It is important that the application is able to interpret this string identifier to provide meaningful UI to the user to connect and share locations with friends, colleagues, or family.

For example, our Prebuilt location sharing UI/UX offers location sharing based on the user's contact list and uses their phone number as the string key identifier. This allows for our UI to provide the user with a recognizable name or description of the friends or colleagues they can connect to, based on how they labelled them in their own contact list. Depending on your application, a phone number may not always be the best choice of string identifier. If your application maintains its own user ID system, which links to a user profile or friend list, then that option may work better for you as a string identifier. In that case, the customizable location sharing option will still allow you to take advantage of our location sharing functionality.

Prebuilt Location Sharing

Our Prebuilt location sharing UI/UX has been designed to use each user's phone number as the string key identifier. The Prebuilt UI/UX includes views for viewing the contact list, selecting which users you want to share your location with, and managing the location sharing. Once enabled, the map will automatically display the locations of other users and will also provide views for easily locating them on the map.

Customizable Location Sharing

The customizable location sharing option requires that you manage your own string key identifiers. These could still be the user's phone number, or, you can use any type of identifier that works for you. The location sharing map UI is still included in this case, but as the developer, you will be required to specify the properties for each user that will be displayed on the map, for example, string key identifier, name, or image.