Location Data Sources
- Android v4
- iOS v3
If you are looking for documentation on Android SDK v3, please see here.
In this tutorial we will show how you can build a custom Location Source, representing locations of robot vacuums. The robots locations will be served from a mocked list and displayed on a map.
We will start by creating our implementation of a location source.
Create the class RobotVacuumLocationSource
that implements MPLocationSource
:
Implement the methods from MPLocationSource and extend the constructor of the RobotVacuumLocationSource
to accept a list of locations that will represent the Robot vacuums.
class RobotVacuumLocationSource(private val robots: ArrayList<MPLocation>): MPLocationSource {
private val mObservers = ArrayList<MPLocationsObserver>()
private var mStatus = MPLocationSourceStatus.NOT_INITIALIZED
override fun getLocations(): MutableList<MPLocation> {
return robots
}
override fun addLocationsObserver(observer: MPLocationsObserver?) {
if (observer != null) {
mObservers.add(observer)
}
}
override fun removeLocationsObserver(observer: MPLocationsObserver?) {
if (observer != null) {
mObservers.remove(observer)
}
}
private fun notifyUpdateLocations() {
for (observer in mObservers) {
observer.onLocationsUpdated(robots, this)
}
}
override fun getStatus(): MPLocationSourceStatus {
return mStatus
}
override fun getSourceId(): Int {
return 10101010
}
override fun clearCache() {
robots.clear()
mObservers.clear()
}
override fun terminate() {
robots.clear()
mObservers.clear()
}
}
Create a Fragment
or Activity
that contains a map with MapsIndoors loaded.
Add a BASE_POSITION MPLatLng
that will be used to calculate a random location for the Robot Vacuums.
private val BASE_POSITION = MPLatLng(57.0582502, 9.9504788)
Then we need to add some variables:
private var baseDisplayRule: WeakReference<MPDisplayRule?>? = null
private var robotDisplayRule: MPDisplayRule? = null
private var mLocations: ArrayList<MPLocation>? = null
private var mRobotVacuumLocationSource: RobotVacuumLocationSource? = null
Create the baseDisplayRule after MapsIndoors
has loaded:
MapsIndoors.load(requireActivity().applicationContext, "MY_API_KEY") { error ->
if (error == null) {
baseDisplayRule = WeakReference(MapsIndoors.getMainDisplayRule())
setupLocationSource()
}
}
create a method to setup the RobotVacuumLocationSource
inside your fragment:
private fun setupLocationSource() {
if (mLocations == null) {
generateLocations()
}
val locationSource = RobotVacuumLocationSource(mLocations!!)
MapsIndoors.addLocationSources(Collections.singletonList(locationSource) as List<MPLocationSource>) {
}
locationSource.setup()
startUpdatingPositions()
}
As seen in the example above we add the RobotVacuumLocationSource
through MapsIndoors.addLocationSources
and call RobotVacuumLocationSource.setup()
This method sets the status to of the source to available and notifies MapsIndoors
that locations are updated.
fun setup() {
status = MPLocationSourceStatus.AVAILABLE
notifyUpdateLocations()
}
fun setStatus(status: MPLocationSourceStatus) {
mStatus = status
for (observer in mObservers) {
observer.onStatusChanged(mStatus, this)
}
}
In the setupLocationSource
method we call generateLocations
to populate the location list with new locations:
private fun generateLocations() {
mLocations = ArrayList()
for (i in 0..15) {
val robotName = "vacuum$i"
val startPosition = getRandomPosition()
val charge = nextInt(1, 100)
val floorIndex = nextInt(4) * 10
var mpLocation = MPLocation.Builder(robotName)
.setPosition(startPosition.lat, startPosition.lng)
.setFloorIndex(floorIndex)
.setName(robotName)
.setBuilding("Stigsborgvej")
.build()
robotDisplayRule = MPDisplayRule(robotName, baseDisplayRule!!)
robotDisplayRule?.isVisible = true
if (charge >= 60) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.GREEN)
}else if (charge >= 30) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.YELLOW)
}else {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.RED)
}
MapsIndoors.addDisplayRuleForLocation(mpLocation, robotDisplayRule!!)
mLocations?.add(mpLocation)
}
}
private fun getRandomPosition(): MPLatLng {
val lat: Double = BASE_POSITION.lat + (-4 + nextInt(20)) * 0.000005
val lng: Double = BASE_POSITION.lng + (-4 + nextInt(20)) * 0.000010
return MPLatLng(lat, lng)
}
Create the startUpdatingPositions
method that calls updateLocations
every second:
private fun startUpdatingPositions() {
mUpdateTimer?.cancel()
mUpdateTimer = Timer()
mUpdateTimer?.scheduleAtFixedRate(object: TimerTask() {
override fun run() {
updateLocations();
}
}, 2000, 500)
}
Create a method that can stop the positions updates at any time:
fun stopUpdatingPositions() {
mUpdateTimer?.cancel()
mUpdateTimer?.purge()
}
Create a method called updateLocations
that will update the position of the Locations:
fun updateLocations() {
var updatedLocations = ArrayList<MPLocation>()
mLocations?.forEach {
var newPosition = getRandomPosition()
var newLocation = MPLocation.Builder(it).setPosition(MPPoint(newPosition.lat, newPosition.lng), 20)
var charge = nextInt(1, 100)
updatedLocations.add(newLocation.build())
robotDisplayRule = MPDisplayRule("robot", baseDisplayRule!!)
robotDisplayRule?.isVisible = true
if (charge >= 60) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.GREEN)
}else if (charge >= 30) {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.YELLOW)
}else {
robotDisplayRule?.setIcon(R.drawable.ic_baseline_robo_vacuum, Color.RED)
}
MapsIndoors.addDisplayRuleForLocation(it, robotDisplayRule!!)
}
mRobotVacuumLocationSource?.updateLocations(updatedLocations)
}
The Concepts
The central concepts in this feature are Location Sources and Location Observers, and they are best described in the following illustration:

Location Source
Maintains an array of locations and notifies its Location Observers about location and status updates.
Location Observer
Can observe a Location Source for status changes and location updates.
Creating Your own Location Source
In this tutorial we will show how you can build a custom Location Source, representing locations of people. The people locations will be served from a mocked list in the source and displayed on a map in a view controller.
We will start by creating our implementation of a location source.
Create a class PeopleLocationsDataSource
that inherits from NSObject and implements MPLocationSource
.
class PeopleLocationSource : NSObject, MPLocationSource {
Add some member variables to PeopleLocationsDataSource
.
observers
: The observer objects that we will notify about changeslocationUpdates
: A list of MPLocationUpdate - the MPLocation builderslocationPoints
: A list of MPPoint - the positions that we will mocklocationDirs
: A list of directions - the walking direction for each "person"queue
: A backround queuenumberOfPeople
: The number of people to mock
private var observers = [MPLocationsObserver]()
private var locationUpdates = [MPLocationUpdate]()
private var locationPoints = [MPPoint]()
private var locationDirs = [Double]()
private let queue = DispatchQueue.init(label: "UpdatePositions")
private let numberOfPeople = 100
Create a method called getRandomPoint
that simply just returns a random point (here within proximity of the demo venue)
func getRandomPoint() -> MPPoint {
let lat = 57.058037 + Double.random(in: -0.0004 ..< 0.0004)
let lng = 9.950572 + Double.random(in: -0.0004 ..< 0.0004)
return MPPoint.init(lat: lat, lon: lng, zValue: 1)
}
Create a method called createPeople
that takes a type string. Iterate numberOfPeople and for each iteration create:
- An MPLocationUpdate with an id and a source (self)
- A type - later used to style the location
- A floor
- A random point and initial direction for the person
func createPeople(_ type: String) {
for locId in 0 ..< numberOfPeople {
let locationUpdate = MPLocationUpdate.init(id: locId, from: self)
locationUpdate.type = type
locationUpdate.addPropertyValue("John Doe #\(locId)", forKey: MPLocationFieldName)
locationUpdate.floor = 1
let p = getRandomPoint()
locationPoints.append(p)
locationDirs.append(Double.random(in: 0 ..< 360))
locationUpdate.position = p.getCoordinate()
locationUpdates.append(locationUpdate)
}
}
Create a method called updatePositions
. Iterate numberOfPeople again and for each iteration:
- Get the corresponding MPLocationUpdate
- Set a new position using Google Maps' offsetting function
- Save a new heading and position
- Generate MPLocation from the MPLocationUpdate After iteration, notify each observer about the updates locations
func updatePositions() {
var updatedLocations = [MPLocation]()
for locId in 0 ..< numberOfPeople {
let locationUpdate = locationUpdates[locId]
let newPos = GMSGeometryOffset(locationPoints[locId].getCoordinate(), 0.5, locationDirs[locId])
locationUpdate.position = newPos
locationDirs[locId] = locationDirs[locId] + Double.random(in: -22 ..< 22)
locationPoints[locId] = MPPoint.init(lat: newPos.latitude, lon: newPos.longitude)
updatedLocations.append(locationUpdate.location())
}
for observer in observers {
observer.onLocationsUpdate(updatedLocations, source: self)
}
queue.asyncAfter(deadline: .now() + 0.5) {
self.updatePositions()
}
}
Create a method called startMockingPositions
that simply just calls updatePositions
in the future.
func startMockingPositions() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.updatePositions()
}
}
Create an initialiser that takes a type string. Call createPeople
and startMockingPositions
.
convenience init(type:String) {
self.init()
createPeople(type)
startMockingPositions()
}
Implement the MPLocationSource method getAllLocations
. For this demo just return an empty array as each update will also contain the full list of locations.
func getLocations() -> [MPLocation] {
return []
}
Implement the MPLocationSource method addLocationObserver
.
func add(_ observer: MPLocationsObserver) {
observers.append(observer)
}
Implement the MPLocationSource method removeLocationObserver
.
func remove(_ observer: MPLocationsObserver) {
observers = observers.filter({ (obsvr) -> Bool in
return obsvr === observer
})
}
Implement the MPLocationSource method sourceStatus
.
func status() -> MPLocationSourceStatus {
return .available
}
Implement the MPLocationSource method sourceIdentifier
.
func sourceId() -> Int32 {
return 0;
}
See the sample in PeopleLocationSource.swift
Create Another Location Source That Mocks the Availability of Meeting Rooms or Work Desks
This location source rely on MapsIndoors data, so we will consume the locations of MPMapsIndoorsLocationSource
and relay them as this source's own locations. Thus we need to observe the MPMapsIndoorsLocationSource
and act as a LocationSource at the same time.
Create a class RoomAvailabilitySource
that inherits from NSObject and implements MPLocationSource
and MPLocationsObserver
.
class RoomAvailabilitySource : NSObject, MPLocationSource, MPLocationsObserver {
Add some member variables to RoomAvailabilitySource
.
observers
: The observer objects that we will notify about changeslocationUpdates
: A dictionary of reusableMPLocationUpdate
modelsmiMapsIndoorsSource
: The MapsIndoors source to observe
private var observers = [MPLocationsObserver]()
private var locationUpdates = Dictionary<String, MPLocationUpdate>()
private let miMapsIndoorsSource:MPLocationSource = MPMapsIndoorsLocationSource()
In the initialiser, add this instance as observer for The MapsIndoors source.
override init() {
super.init()
self.miMapsIndoorsSource.add(self)
}
Create a method updateIconForLocation
that takes a location and grabs or creates a location update object. Randomly modify the icon of that location so it looks occupied.
func updateIconForLocation(location:MPLocation) -> MPLocationUpdate? {
if locationUpdates[location.locationId!] == nil {
locationUpdates[location.locationId!] = MPLocationUpdate.init(location: location)
}
let locUpdate = locationUpdates[location.locationId!]
if (Int.random(in: 0...1) == 0) {
locUpdate?.icon = UIImage(named: "closed")!
}
return locUpdate
}
Create a method updateLocations
that runs through a list of locations add creates location update objects.
func updateLocations(locations:[MPLocation]) -> [MPLocation] {
var updatedLocations = [MPLocation]()
for location in locations {
if let locUpdate = updateIconForLocation(location: location) {
updatedLocations.append(locUpdate.location())
}
}
return updatedLocations
}
Implement the MPLocationSource method getLocations
. For this demo just return the full list of MapsIndoors' locations.
func getLocations() -> [MPLocation] {
return miMapsIndoorsSource.getLocations()
}
Implement the MPLocationSource method addLocationObserver
.
func add(_ observer: MPLocationsObserver) {
observers.append(observer)
}
Implement the MPLocationSource method removeLocationObserver
.
func remove(_ observer: MPLocationsObserver) {
observers = observers.filter({ (obsvr) -> Bool in
return obsvr !== observer
})
}
Implement the MPLocationSource method sourceStatus
.
func status() -> MPLocationSourceStatus {
return .available
}
Implement the MPLocationSource method sourceIdentifier
.
func sourceId() -> Int32 {
return 2;
}
Relay onLocationsDelete
events to this sources own observers.
func onLocationsDelete(_ locations: [String], source: MPLocationSource) {
for obsvr in observers {
obsvr.onLocationsDelete(locations, source: self)
}
}
In onLocationsUpdate
create new modified locations objects and call the observers.
func onLocationsUpdate(_ locationUpdates: [MPLocation], source: MPLocationSource) {
let locations = updateLocations(locations: locationUpdates)
for obsvr in observers {
obsvr.onLocationsUpdate(locations, source: self)
}
}
Relay onStatusChange
events to this sources own observers.
func onStatusChange(_ status: MPLocationSourceStatus, source: MPLocationSource) {
for obsvr in observers {
obsvr.onStatusChange(status, source: self)
}
}
See the sample in RoomAvailabilitySource.swift
Create a View Controller Displaying a Map That Shows the Mocked People Locations and the Mocked Room Availability on top of a MapsIndoors Map
Create a class LocationSourcesController
that inherits from UIViewController
.
class LocationSourcesController: UIViewController {
Add a GMSMapView
and a MPMapControl
to the class
var map: GMSMapView? = nil
var mapControl: MPMapControl? = nil
override func viewDidLoad() {
super.viewDidLoad()
Inside viewDidLoad
, register the sources PeopleLocationSource
and RoomAvailabilitySource
MapsIndoors.register([
PeopleLocationSource.init(type: "People"),
RoomAvailabilitySource.init()
])
Inside viewDidLoad
, setup the map so that it shows the demo venue and initialise mapControl
self.map = GMSMapView.init(frame: CGRect.zero)
self.view = self.map
self.map?.camera = .camera(withLatitude: 57.057964, longitude: 9.9504112, zoom: 20)
self.mapControl = MPMapControl.init(map: self.map!)
Inside viewDidLoad
, setup a display setting that refers to the type of locations that your people location source operates with.
let dr = MPLocationDisplayRule.init(name: "People", andIcon: UIImage.init(named: "user.png"), andZoomLevelOn: 17)!
self.mapControl?.add(dr)
Optionally, when you leave this controller. Remove the custom Location Source by adding back the MPMapsIndoorsLocationSource
as the only one.
override func viewDidDisappear(_ animated: Bool) {
MapsIndoors.register([MPMapsIndoorsLocationSource()])
}