Using Cisco DNA Spaces

Last updated:

To get started with Cisco DNA positioning, the MapsIndoors SDK offers all the required building blocks without any external dependencies.

The Position Provider implementation exists at the customer application level, and needs to use the PositionProvider interface from the MapsIndoors SDK. The MapsIndoors SDK can then use the positioning results given by the Position Provider when setting the Position Provider with MapControl.setPositionProvider(PositionProvider).

Floor Mapping for Android

The Position Provider should align with the MapsIndoors Floor index convention (floors are indexed as e.g 0, 10, 20, 30 corresponding to ground floor, 1st floor, 2nd floor, 3rd floor, with negative floors indices allowed as well to indicate Floors below ground, e.g. -10). It is therefore up to the Position Provider class to convert any given Floor indexing from the positioning source to that of MapsIndoors.

For a typical Position Provider, the mapping from the positioning's index needs to be mapped to the MapsIndoors Floor format.

The MapsIndoors backend is closely integrated with the CiscoDNA platform, so the MapsIndoors backend handles the floor mapping conversion for that integration. From an application perspective no Floor mapping implementation is required when integrating CiscoDNA positioning through the MapsIndoors platform.

Fetch Attributes from Solution

You can choose to fetch the Position Provider information (CMS > Solution Details > App Settings > Position Provider) from the CMS as follows:

Map<String, Map<String, Object>> providerConfig = MapsIndoors.getSolution().getPositionProviderConfig();

The outer keyset (Map<String, Map<String, Object>>) contains the name of the positioning provider, for example, indooratlas3 for IndoorAtlas, or ciscodna when using Cisco DNA Spaces.

The inner keyset (Map<String, Object>) consist of various attribute fields for a given positioning provider, such as keys, floor mapping etc. These attribute fields will vary across different positioning providers, so refer to their own documentation for details.

Implementing Cisco DNA for Android

This Guide requires you to already have an activity that shows a MapsIndoors Map as well as a Cisco DNA network with positioning active.

We start by implementing a Positioning Provider service. This service is needed so you can have multiple positioning providers running in the same application, and have the code stored in one location.

To begin, create a class with a constructor that receives an Activity and a MapControl object.

PositionProviderService.java

public class PositionProviderService {
private MapControl mMapControl;
private Activity mActivity;

public PositionProviderService(Activity activity, MapControl mapControl) {
mMapControl = mapControl;
mActivity = activity;
}
}

Now we will start implementing the CiscoDNA position provider. Create a class called CiscoDNAPositionProvider that implements the PositionProvider interface from the MapsIndoors SDK. Then create a constructor that takes a Context, as well as a String named tenantId.

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
private Context mContext;
private String mTenantId;

//Used for the PositionProvider Interface
private boolean mIsRunning;
private boolean mIsIPSEnabled;

public CiscoDNAPositionProvider(@NonNull Context context, @NonNull String tenantId){
mContext = context;
mIsRunning = false;
mTenantId = tenantId;
}
}

We will start by implementing logic to each of the implemented methods from the PositionProvider interface. Some of these will be generic, while others will be specific to CiscoDNA.

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
//Implement the necessary variables for the interface logic
private boolean mIsIPSEnabled;
private boolean mIsRunning;
private String mProviderId;
private final List<OnStateChangedListener> mOnStateChangedListenersList = new ArrayList<>();
private final List<OnPositionUpdateListener> mOnPositionUpdateListeners = new ArrayList<>();
private PositionResult mLatestPosition;

@NonNull
@Override
public String[] getRequiredPermissions() {
//Specific to CiscoDNA as it only uses Wifi it does not require any permissions
return new String[0];
}

@Override
public boolean isPSEnabled() {
return mIsIPSEnabled;
}

@Override
public void startPositioning(@Nullable String s) {
//This will be implemented when we have setup the required CISCO dna logic.
}

@Override
public void stopPositioning(@Nullable String s) {
//This will be implemented when we have setup the required CISCO dna logic.
}

@Override
public boolean isRunning() {
return mIsRunning;
}

@Override
public void addOnPositionUpdateListener(@Nullable OnPositionUpdateListener onPositionUpdateListener) {
if(onPositionUpdateListener != null && !mOnPositionUpdateListeners.contains(onPositionUpdateListener)){
mOnPositionUpdateListeners.add(onPositionUpdateListener);
}
}

@Override
public void removeOnPositionUpdateListener(@Nullable OnPositionUpdateListener onPositionUpdateListener) {
if(onPositionUpdateListener != null){
mOnPositionUpdateListeners.remove(onPositionUpdateListener);
}
}

@Override
public void setProviderId(@Nullable String s) {
mProviderId = s;
}

@Override
public void addOnStateChangedListener(@Nullable OnStateChangedListener onStateChangedListener) {
if(onStateChangedListener != null && !mOnStateChangedListenersList.contains(onStateChangedListener)){
mOnStateChangedListenersList.add(onStateChangedListener);
}
}

@Override
public void removeOnStateChangedListener(@Nullable OnStateChangedListener onStateChangedListener) {
if(onStateChangedListener != null){
mOnStateChangedListenersList.remove(onStateChangedListener);
}
}

@Override
public void checkPermissionsAndPSEnabled(@Nullable PermissionsAndPSListener permissionsAndPSListener) {
// Do locations permissions check here... this is not necessary for CiscoDNA, as it is
// a wifi based positioning system, meaning the positioning is completely external to the
// device itself.
}

@Nullable
@Override
public String getProviderId() {
return mProviderId;
}

@Nullable
@Override
public PositionResult getLatestPosition() {
return mLatestPosition;
}

@Override
public void startPositioningAfter(int i, @Nullable String s) { }

@Override
public void terminate() { }
}

The CiscoDNA positioning requires three parameters:

  1. The device’s IPv4 address (LAN)
  2. The external IP address of the network in question (WAN)
  3. The Tenant ID

Start by creating a method to retrieve the LAN address:

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
...
@Nullable
private String getLocalAddress(){
try {
for (Enumeration<NetworkInterface> en = NetworkInterface.getNetworkInterfaces(); en.hasMoreElements();) {
NetworkInterface intf = en.nextElement();
for (Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses(); enumIpAddr.hasMoreElements();) {
InetAddress inetAddress = enumIpAddr.nextElement();
if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
String ipv4 = inetAddress.getHostAddress().toString();
Log.i(TAG, "LAN: " + ipv4);
return ipv4;
}
}
}
} catch (SocketException ex) {
Log.e(this.getClass().getSimpleName(), "Failed to resolve LAN address");
}

return null;
}
...
}

Then create a method to retrieve the WAN address (we recommend using a 3rd party service for this):

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
...
private void fetchExternalAddress(@NonNull ReadyListener listener){
OkHttpClient httpClient = new OkHttpClient();
Request request = new Request.Builder().url("https://ipinfo.io/ip").build();

httpClient.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NotNull Call call, @NotNull IOException e) {
listener.onResult();
}

@Override
public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException {
if(response.isSuccessful()){
String str = response.body().string();
mWan = str;
Log.i(TAG, "WAN: " + mWan);
}
listener.onResult();
}
});
}
...
}

Lastly, we need the Tenant ID. This is an ID you might have hardcoded inside your app, or saved as a string resource. Otherwise you can use the PositionProviderConfig fields from MapsIndoors to get this ID like this:

PositionProviderService.java

Map<String, Object> ciscoDnaConfig = MapsIndoors.getSolution().getPositionProviderConfig().get("ciscodna");
String tenantId = (String) ciscoDnaConfig.get("ciscoDnaSpaceTenantId");

If all of the three above mentioned strings can be acquired, you can ask our endpoint for a CiscoDNA Device ID string. A device ID is only available if there has been a recorded positioning for the device, in the past 24 hours. We will implement this as a new method into our CiscoDNAPositionProvider.

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
...
private void updateAddressesAndId(ReadyListener onComplete) {
mLan = getLocalAddress();
fetchExternalAddress(() -> {
if(mTenantId != null && mLan != null && mWan != null){
String url = MAPSINDOORS_CISCO_ENDPOINT + mTenantId + "/api/ciscodna/devicelookup?clientIp=" + mLan + "&wanIp=" + mWan;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url(url).build();
try {
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
Gson gson = new Gson();
String json = response.body().string();
JsonObject jsonObject = gson.fromJson(json, JsonObject.class);
mCiscoDeviceId = jsonObject.get("deviceId").getAsString();
} else {
Log.i(TAG, "Could not obtain deviceId from backend deviceID request! Code: " + response.code());
}
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(onComplete != null){
onComplete.onResult();
}
});
}
...
}

Now you can make a method to start a subscription that we use when starting positioning to receive position updates. You use the SDKs LiveDataManager to create a subscription.

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
private CiscoDNATopic mTopic;
private boolean mIsSubscribed;
...
private void startSubscription(){
mTopic = new CiscoDNATopic(mTenantId, mCiscoDeviceId);

if(!mIsSubscribed){
LiveDataManager.getInstance().setOnTopicSubscribedListener(topic -> {
if(topic.equals(mTopic)){
mIsIPSEnabled = true;
mIsSubscribed = true;
}
});
LiveDataManager.getInstance().subscribeTopic(mTopic);
}
}

private void unsubscribe(){
LiveDataManager.getInstance().unsubscribeTopic(mTopic);
mIsSubscribed = false;
}
...
}

To handle the subscription we just created, we need to create some callbacks in the constructor of the PositionProvider to handle the position results and lifecycle of the subscription:

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
public CiscoDNAPositionProvider(@NonNull Context context, @NonNull String tenantId){
mContext = context;
mIsRunning = false;
mTenantId = tenantId;

LiveDataManager.getInstance().setOnTopicUnsubscribedListener(topic -> {
if(topic.matchesCriteria(mTopic)){
mIsSubscribed = false;
mCiscoDeviceId = null;
mIsIPSEnabled = false;
}
});

LiveDataManager.getInstance().setOnReceivedLiveUpdateListener((topic, message) -> {
if(message.getId().equals(mCiscoDeviceId)){
mLatestPosition = message.getPositionResult();

// Report to listeners
for(OnPositionUpdateListener listener : mOnPositionUpdateListeners){
listener.onPositionUpdate(mLatestPosition);
}
}
});
}
...
}

Implement the startPositoning and stopPositioning method:

CiscoDNAPositionProvider.java

public class CiscoDNAPositionProvider implements PositionProvider {
@Override
public void startPositioning(@Nullable String s) {
if(!mIsRunning){
mIsRunning = true;
updateAddressesAndId(() -> {
if(mCiscoDeviceId != null && !mCiscoDeviceId.isEmpty()){
startSubscription();
}
});
}
}

@Override
public void stopPositioning(@Nullable String s) {
if(mIsRunning){
mIsRunning = false;
if(mIsSubscribed){
unsubscribe();
}
}
}
}

Now we need to start up our PositionProvider to get positioning onto our map. This we will do through our PositionProviderService. We start by creating a method to setup the Cisco DNA positionProvider from the PositionProviderService.

PositionProviderService.java

public class PositionProviderService {
private PositionProvider mCiscoDNAPositionProvider;
...
public void setupCiscoPositioning(){
Map<String, Object> ciscoDnaConfig = MapsIndoors.getSolution().getPositionProviderConfig().get("ciscodna");
String tenantId = (String) ciscoDnaConfig.get("ciscoDnaSpaceTenantId");

if(tenantId == null || tenantId.isEmpty()){
// Cannot setup CiscoDNA positioning in this case
return;
}

mCiscoDNAPositionProvider = new CiscoDNAPositionProvider(mActivity, tenantId);
MapsIndoors.setPositionProvider(mCiscoDNAPositionProvider);
MapsIndoors.startPositioning();
mMapControl.showUserPosition(true);

mCiscoDNAPositionProvider.addOnPositionUpdateListener(new OnPositionUpdateListener() {
@Override
public void onPositioningStarted(@NonNull PositionProvider positionProvider) { }

@Override
public void onPositionFailed(@NonNull PositionProvider positionProvider) { }

@Override
public void onPositionUpdate(@NonNull PositionResult positionResult) {
mActivity.runOnUiThread(() -> {
mMapControl.getPositionIndicator().setIconFromDisplayRule( new LocationDisplayRule.Builder( "BlueDotRule" )
.setVectorDrawableIcon(android.R.drawable.presence_invisible, 23, 23 )
.setTint(Color.BLUE)
.setShowLabel(true)
.setLabel("You")
.setLabel(null)
.build());
});
}
});
}
...
}

Lastly, we need to start this up after initializing our MapControl.

MapsActivity.java

mMapControl.init(miError -> {
mPositionProviderService = new PositionProviderService(yourActivity, mMapControl);
mPositionProviderService.setupCiscoPositioning();
}

A full example of the Cisco DNA position provider together with PositionProviderService can be found here: PositionProviders

MapsIndoors and CiscoDNA for iOS

MapsIndoors is a dynamic mapping platform from MapsPeople that can provide maps of your indoor and outdoor localities and helps you create search and navigation experiences for your local users. CiscoDNA is Cisco’s newest digital and cloud-based IT infrastructure management platform. Among many other things, CiscoDNA can pinpoint the physical and geographic position of devices connected wirelessly to the local IT network.

How User Positioning Works in MapsIndoors for iOS

In order to show a user's position in an indoor map with MapsIndoors, a Position Provider must be implemented. MapsIndoors does not implement a Position Provider itself, but rely on 3rd party positioning software to create this experience. In an outdoor environment this Position Provider can be a wrapper of the Core Location Services in iOS.

A Position Provider in MapsIndoors must adhere to the MPPositionProvider protocol. Once you have an instance of an MPPositionProvider you can register it by assigning it to MapsIndoors.positionProvider. See the overview of the interface dependencies below.

cisco-dna-ios

Wrapping the CiscoDNA Positioning in a Position Provider for iOS

Just like in the mock Position Provider example, we need to implement a Position Provider that wraps the MapsIndoors CiscoDNA services to inject the CiscoDNA indoor positioning into MapsIndoors. If you only require this to work for indoor positioning, we would be good with just wrapping the CiscoDNA part. MapsIndoors however has the capability to support wayfinding both outdoors and indoors, so the shown solution implements two Position Providers, one for CoreLocation (GPSPositionProvider) and one for CiscoDNA (CiscoDNAPositionProvider2). The CiscoDNAPositionProvider2 subclasses the GPSPositionProvider so it can determine whether the CiscoDNA position or the Core Location position should be used in your application. Both Position Providers are written in Objective C, but can of course be used in Swift as well.

The CiscoDNAPositionProvider2 communicates with some MapsIndoors services to get the Cisco device id, and uses a message subscription service (MQTT) to subscribe for position updates. Each time a Cisco position is received, its age is determined. If the age of the latest Cisco position is above 120 seconds or the application is not connected to the wifi, the CoreLocation position is used instead.

Integration Guide for iOS

  1. Make sure you have integrated MapsIndoors succesfully.

  2. Download and unzip this zip file containing the CiscoDNA integration source.

  3. Create a group in your Xcode project, e.g. called CiscoDNA.

  4. Drag and drop the files in the downloaded folder to your new group. Choose "Copy items if needed".

  5. If this is the first Objective C code in your project, Xcode will suggest that you create an Objective C Bridging Header file. Select "Yes" or "Create Bridging Header".

  6. Drag and drop the rest of the files into the CiscoDNA group. Choose "Copy items if needed".

  7. In your Objective C Bridging Header, add #import "CiscoDNAPositionProvider2.h".

  8. In AppDelegate.swift-didFinishLaunchingWithOptions, add the following code:

    let pp = MPCiscoDnaPositionProvider2.init()
    pp.tenantId = "my-cisco-dna-spaces-tenant-id"
    MapsIndoors.positionProvider = pp
    MapsIndoors.positionProvider?.startPositioning(nil)
  9. Replace my-cisco-dna-spaces-tenant-id with your own Cisco tenant ID.

  10. In your view controller displaying the Google Map using MPMapControl, call mapControl.showUserPosition(true).

  11. Build and run the application. You should now be able to show a blue dot for the user's position.

If you need a working project example with MapsIndoors and CiscoDNA (excluding API keys), you can download it here.

Fetch Attributes from Solution

You can choose to fetch the Position Provider information (CMS > Solution Details > App Settings > Position Provider) from the CMS as follows:

MPSolutionProvider().getSolutionWithCompletion { solution, error in
let providerConfig: Dictionary<String, Dictionary>? = solution?.positionProviderConfigs
}

The keys of the outer Dictionary are the names of the positioning provider, for example, indooratlas3 for IndoorAtlas, or ciscodna when using Cisco DNA Spaces.

The inner Dictionary consists of various attribute fields for a given positioning provider, such as keys, floor mapping etc. These attribute fields will vary across different positioning providers, so refer to their own documentation for details.

MapsIndoors & CiscoDNA for Web

MapsIndoors is a dynamic mapping platform from MapsPeople that can provide maps of your indoor and outdoor localities and helps you create search and navigation experiences for your local users. CiscoDNA is Cisco’s newest digital and cloud-based IT infrastructure management platform. Among many other things, CiscoDNA can pinpoint the physical and geographic position of devices connected wirelessly to the local IT network.

User Positioning in MapsIndoors for Web

In order to show a user's position on an indoor map with MapsIndoors, a Position Provider must be implemented. The MapsIndoors JavaScript SDK does not provide a default Position Provider but relies on 3rd party positioning software to create this experience. In an outdoor environment, this Position Provider can be a wrapper of the browser's native Geolocation API.

Code Sample

Please note that the following code sample assumes that you have already succesfully implemented MapsIndoors into your application.

The JavaScript SDK doesn't have a built-in interface like the Android and iOS SDKs. However, by following these steps, you should be able to achieve the same functionality.

The first step is to create the class CiscoPositioningService, and the constructor for it.

class CiscoPositioningService {
/**
* @param {string} args.clientIp - The local IP address of the device
* @param {string} args.tenantId - The Cisco tenant id.
* @param {number} [args.pollingInterval=1000] - The interval that the position will be polled from the backend.
* @param {string} [args.region="eu"] - The Cisco app region.
*/

constructor(args = {}) {
if (!args.clientIp)
throw new TypeError('Invalid argument: "clientIp"');

if (!args.tenantId)
throw new TypeError('Invalid argument: "tenantId"');

this._pollingInterval = 1000;
this._tenantId = args.tenantId;
this._successCallbacks = new Map();
this._errorCallbacks = new Map();
this._deviceId = '';

args.region = args.region || 'eu';

this.pollingInterval = args.pollInterval;

fetch(`https://ciscodna.mapsindoors.com/${this._tenantId}/api/ciscodna/devicelookup?clientIp=${args.clientIp}&region=${args.region}`)
.then(this._errorHandler)
.then(res => res.json())
.then(({ deviceId }) => {
this._deviceId = deviceId;
this._startPolling();
}).catch(err => {
console.error(err.message);
});
}

Next step is to create watchPosition and clearWatch, to watch for the positioning updates the system recieves.

    watchPosition(successCallback, errorCallback) {
const watchId = Symbol();
if (!(successCallback instanceof Function))
throw new TypeError('Invalid argument: "successCallback"');

if (errorCallback instanceof Function) {
this._errorCallbacks.set(watchId, errorCallback);
}

this._successCallbacks.set(watchId, successCallback);

if (!this._interval) {
this._startPolling();
}

return watchId;
}

clearWatch(watchId) {
this._successCallbacks.delete(watchId);
this._errorCallbacks.delete(watchId);

if (this._successCallbacks.size === 0) {
this._stopPolling();
}
}

getCurrentPosition(successCallback, errorCallback) {
fetch(`https://ciscodna.mapsindoors.com/${this._tenantId}/api/ciscodna/${this._deviceId}`)
.then(this._errorHandler)
.then(res => res.json())
.then(data => {
this._successCallbacks.forEach(cb => cb.call(null, data));
}).catch(err => {
this._errorCallbacks.forEach(cb => cb.call(null, err));
});
}

The next step is to create some functions that manage how often the system retrieves an update, or polls, from the Cisco DNA setup.

    set pollingInterval(value) {
if (!isNaN(value) && this._pollingInterval !== value) {
this._pollingInterval = value;
this._stopPolling();
this._startPolling();
}
}

get pollingInterval() {
return this._pollingInterval;
}


/**
* @private
*/

_startPolling() {
if (!this._interval && this._deviceId > '' && this._successCallbacks.size > 0) {
this._interval = window.setInterval(() => {
this.getCurrentPosition(response => {
this._successCallbacks.forEach(callback => callback(response));
},
error => {
this._errorCallbacks.forEach(callback => callback(err));
});

}, this._pollingInterval);
}
}

/**
* @private
*/

_stopPolling() {
if (this._interval) {
window.clearInterval(this._interval);
this._interval = null;
}
}

Lastly, an error handler is implemented.

    /**
* @private
*/


_errorHandler(response) {
if (!response.ok) {
const contentType = response.headers.get('content-type');
if (contentType && contentType.indexOf('application/json') !== -1) {
return response.json().then(({ message }) => {
throw new Error(message);
});
} else {
let statusText;
switch (response.status) {
case 400:
statusText = 'The client IP is invalid.';
break;
case 404:
statusText = 'Device not found.';
break;
case 403:
statusText = 'The TenantId supplied is not authorized to access the device at the location.'
break;
default:
statusText = 'Unknown error.';
}

throw new Error(statusText);
}
}

return response;
}
} // end class

Once the class is created, it can then be used, for example, in the following way - Keep in mind that you cannot fetch the client/device IP address from the browser, an option to get around this could be a seperate service that returns the IP address:

const map = mapView.getMap();
let watchId;

mapsindoors.services.SolutionsService.getSolution('57e4e4992e74800ef8b69718').then(solution => {
if (solution.positionProviderConfigs && solution.positionProviderConfigs.ciscodna) {
const tenantId = solution.positionProviderConfigs.ciscodna.ciscoDnaSpaceTenantId;
const region = solution.positionProviderConfigs.ciscodna.ciscoDnaSpaceTenantRegion || 'usa';
const clientIp = '10.0.0.134';
const cps = new CiscoPositioningService({ clientIp, tenantId, region });

watchId = cps.watchPosition(function (data) {
console.log(data);
map.setCenter({ lat: data.latitude, lng: data.longitude });
}, function (err) {
console.log(err);
})
}
});

const floorSelector = document.createElement('div');
new mapsindoors.FloorSelector(floorSelector, mi);
map.controls[google.maps.ControlPosition.RIGHT_TOP].push(floorSelector);