Location Tutorial

With the Flare client libraries, you can develop a native application for iOS or Android that uses various sensors in a mobile device to determine its position inside an environment and its proximity to things in the environment.

This tutorial will show you how to:

  • get the current location using GPS
  • find the Flare environment containing the current location
  • register the current device on the server
  • find the current location using beacons
  • send the current location to the server
  • get the current zone
  • be notified when the current zone changes
  • be notified when the device is near to a thing

A complete solution can be found in the sample Trilateral app (for both iOS and Android), and you may want to follow along inside the app as you read through the tutorial.

This tutorial assumes that you have the Flare server running, you have set up your environment, and that you have some beacons deployed.

Get the current location using GPS

Each environment in the database has a geofence that defines a circle on the surface of the earth, defined as all points within a certain radius of a given point specified by its latitude and longitude.

When the application launches, it can get the current location using the device's GPS hardware.

var flareManager: FlareManager
var beaconManager = BeaconManager()

required init(coder aDecoder: NSCoder) {
    let host = "1.2.3.4"
    let port = 1234
    flareManager = FlareManager(host: host, port: port)
    super.init(coder: aDecoder)
}

override func viewDidLoad() {
    super.viewDidLoad()

    flareManager.delegate = self
    beaconManager.delegate = self

    flareManager.connect()
}

override func viewDidAppear(animated: Bool) {
    beaconManager.start()
    beaconManager.startMonitoringLocation()
    flareManager.connect()
}

override func viewDidDisappear(animated: Bool) {
    beaconManager.stop()
    beaconManager.stopMonitoringLocation()
    flareManager.disconnect()
}

func deviceLocationDidChange(location: CLLocation) {
    NSLog("Location: \(location.coordinate.latitude),\(location.coordinate.longitude)")
}
  • On iOS, the BeaconManager class in the Flare.framework uses CoreLocation for both determining the GPS location and ranging the distance to nearby beacons. See Requesting Permission to Use Location Services for instructions on getting the user's permission. In the app's ViewController, create FlareManager and BeaconManager objects and set their delegates to self. Call BeaconManager's startMonitoringLocation() and stopMonitoringLocation() to start or stop monitoring the GPS location. The deviceLocationDidChange() callback will be called once at application launch, and whenever the location changes significantly.
  • On Android, add the ACCESS_FINE_LOCATION and ACCESS_COARSE_LOCATION permissions to the app's manifest. In the default activity's onCreate() method, get the default LocationManager system service, register a location listener class to receive updates, and request updates when the location changes significantly. Call getLastKnownLocation() to get the current location.

Get the current environment

GET /environments?latitude=40.751267&longitude=-73.99229

The /environments API normally returns a list of all environments in the database. If you specify latitude and longitude parameters, the results will be filtered to environments containing the given coordinates. Normally environments do not overlap, so if the user is inside an environment then this call should return an array with one result. (If you do not receive the expected results, try increasing the radius to 1000 meters to allow for a margin of error, especially when inside a building.)

The loadEnvironments() method will load not only the environment(s) matching the given location, but will recursively cache all the zones and things in the environment(s) as well. This allows some of the callback notifications to return device objects from memory.

var currentEnvironment: Environment?

func deviceLocationDidChange(location: CLLocation) {
    NSLog("Location: \(location.coordinate.latitude),\(location.coordinate.longitude)")
    let params = ["latitude":location.coordinate.latitude, "longitude":location.coordinate.longitude]

    flareManager.loadEnvironments(params, loadDevices: false) { (environments) -> () in
        if environments.count > 0 {
            self.currentEnvironment = environments[0]
            NSLog("Current environment: \(self.currentEnvironment!.name)")
            ...
        } else {
            NSLog("No environments found nearby.")
        }
    }
}
  • On iOS, the location is passed as a JSONDictionary with "latitude" and "longitude" strings as keys and coordinates in degrees as values. See the sample code
  • On Android, the location is passed as a android.location.Location object.

Get the current device object

var device: Device?

self.flareManager.getCurrentDevice(self.currentEnvironment!.id, template: self.deviceTemplate()) { (device) -> () in
    self.device = device
}

func deviceTemplate() -> JSONDictionary {
    let uidevice = UIDevice.currentDevice()
    let name = uidevice.name
    let description = "\(uidevice.model), iOS \(uidevice.systemVersion)"
    return ["name":name, "description":description, "data":JSONDictionary(), "position":["x":0, "y":0]]
}

// From FlareManager.swift:

// tries to find an existing device object in the current environment
// if one is not found, creates a new device object
public func getCurrentDevice(environmentId: String, template: JSONDictionary, handler: (Device?) -> ()) {
    self.savedDevice(environmentId) { (device) -> () in
        if device != nil {
            handler(device)
        } else {
            self.newDeviceObject(environmentId, template: template) { (device) -> () in
                if device != nil {
                    handler(device)
                }
            }
        }
    }
}

// looks for an existing device object in the current environment, and if found calls the handler with it
public func savedDevice(environmentId: String, handler: (Device?) -> ()) {
    if let deviceId = NSUserDefaults.standardUserDefaults().stringForKey("deviceId") {
        self.getDevice(deviceId, environmentId: environmentId) { (json) -> () in
            if let validId = json["_id"] as? String {
                if let deviceEnvironment = json["environment"] as? String {
                    if deviceEnvironment == environmentId {
                        var device = Device(json: json)
                        self.addToIndex(device)
                        handler(device)
                    }
                }
        }
    }
}

// creates a new device object using the default values in the template
public func newDeviceObject(environmentId: String, template: JSONDictionary, handler: (Device?) -> ()) {
    newDevice(environmentId, device: template) { (json) -> () in
        var device = Device(json: json)
        self.addToIndex(device)
        NSUserDefaults.standardUserDefaults().setObject(device.id, forKey: "deviceId")
        handler(device)
    }
}

The server needs to know about the user's device in order to track its location. Device objects on the server are specific to each environment. The process of loading a device is as follows:

  1. If the client application has saved its device ID, check to see if there is a valid device with that ID in the current environment.
  2. If the client application has not saved its device ID, or there is not a valid device with that ID in the current environment, then create a new device.
  • On iOS, the FlareManager getCurrentDevice() method handles all of this for you. The device ID is saved in the standard user defaults under the key deviceId.
  • On Android, you can copy the sample code to your app. The sample code saves the device ID in the shared preferences under the key deviceId.

When creating a new device, you must supply a device template that contains the default values for the device, such as the name, description, data object, position, etc.

  • On iOS, the sample code uses the name of the iOS device from the Settings, and the description contains the type of device (e.g. iPhone) and the operating system version (e.g. iOS 9.0).
  • On Android, the sample code uses the user's first name from the address book, and the description contains the brand, model and operating system version.

Get the current position using beacons

var beaconManager = BeaconManager()

override func viewDidLoad() {
    ...
    beaconManager.delegate = self
    ...
}

// called at startup, and when the GPS location changes significantly
func deviceLocationDidChange(location: CLLocation) {
    ...
    flareManager.loadEnvironments(params, loadDevices: false) { (environments) -> () in
        if environments.count > 0 {
            ...
            self.beaconManager.loadEnvironment(self.currentEnvironment!)
            self.beaconManager.start()
            ...
        }
    }
}

func devicePositionDidChange(position: CGPoint) {
    if device != nil {
        device!.position = position
        // update interface
        ...
    }
}

The beacon manager class models the current environment, and all the zones and things that it contains. If some of the things are beacons, and three or more beacons are within range, then the beacon manager can determine the postion of the device by calcualting a weighted average of the beacon position using the relative signal strengths.

  • On iOS, create a BeaconManager object, and set the delegate to self. When the environment has been loaded, call beaconManager.loadEnvironment() to initalize the environment, zones, and beacons. Call beaconManager.start() to start looking for beacons. When three beacons are in range and the device position can be calculated, the BeaconManager will call the devicePositionDidChange() delegate method.
  • On Android, the FlareBeaconManager class is a singleton object, and you can interact with it using class methods. You must call setDeviceTypeAndConsumer() and bind() as shown in the examples. Set the environment to initialize the environment, zones, and beacons, and restartRangingBeacons() to start looking for beacons. The lambda passed to setCallback() will be called once per second when the device position is calcualted.

Send the position to the server

flareManager.setPosition(device!, position: position, sender: nil)

When the device calculates its own position, it can tell the server that it has moved. Simply call setPosition() with the device object and its position. (It is not necessary to specify a sender for the message when the device is updating its own position.)

Get the current zone

var currentZone: Zone?

flareManager.listZones(currentEnvironment._id, point: position) { jsonArray in
    for json in jsonArray {
        if let zoneId = json["_id"] as String, zone = flareManager.flareIndex[zoneId] as Zone {
            ...
        }
    }
}

Once the device knows its position, it can call the REST API to ask the server what zone it is currently in.

Receive zone notifications

var currentZone: Zone?

flareManager.subscribe(device)

func enter(zone: Zone, device: Device) {
    NSLog("\(zone.name) enter: \(device.name)")
    self.currentZone = zone
}

func exit(zone: Zone, device: Device) {
    NSLog("\(zone.name) exit: \(device.name)")
    self.currentZone = nil
}

When the device tells the server that the position has changed, this may trigger zone notifications if the device has entered or exited a zone. You can subscribe to any Flare object to receive notifications about it. If you have subscribed to either the device or the zone, the delegate will receive an enter message when the device enters the zone and an exit message when it exits the zone.

Receive thing notifications

var nearbyThing: Thing?

flareManager.subscribe(device)

func near(thing: Thing, device: Device, distance: Double) {
    NSLog("near: \(thing.name)")
    if device == self.device && thing != self.nearbyThing {
        nearbyThing = thing
        flareManager.subscribe(thing)
        flareManager.getData(thing)
        flareManager.getPosition(thing)

    }
}

func far(thing: Thing, device: Device) {
    NSLog("far: \(thing.name)")
    if device == self.device && thing == self.nearbyThing {
        flareManager.unsubscribe(thing)
        nearbyThing = nil
    }
}

When the device tells the server that the position has changed, this may trigger proximity notifications if the device becomes near to a thing, or moves far from a thing. You can subscribe to any Flare object to receive notifications about it. If you have subscribed to either the device or the thing, the delegate will receive a near message when the device becomes near to the thing, and a far message when it is no longer near to the thing.

When a device is near to a thing, the application may show more information about it. It can get the most recent data and position for the thing, and subscribe to be notified if anything changes. When the device moves away, the app can unsubscribe from the thing.

Finding relative locations

for thing in things {
    let distance = device.distanceTo(thing)
    let angle = device.angleTo(thing)
    NSLog("\(device.name) to \(thing.name): \(distance) meters, \(angle) degrees")
}

In the user interface of your application, you may want to indicate the distance and angle towards nearby things in the environment. The Device class has some helper methods that perform basic trigonometry for you. The distanceTo() method returns the distance to a thing (in meters), and the angleTo() method returns the angle towards a thing (in degrees couterclockwise from the X axis).