Skip to main content
Version: 6.0

Building & Floor Selection

Using Mappedin SDK for iOS with your own map requires a Pro license. Try a demo map for free or refer to the Pricing page for more information.

Floor Selection

When mapping a building with multiple floors, each floor has its own unique map. These are represented by the Floor class, which are accessed using MapData.getByType(). The currently displayed Floor can be accessed using MapView.currentFloor.

tip

A complete example demonstrating Building and Floor Selection can be found in the Mappedin iOS Github repo: BuildingFloorSelectionDemoViewController.kt

Changing Floors

When initialized, MapView displays the Floor with the elevation that's closest to 0. This can be overridden by setting Show3DMapOptions.initialFloor to a Floor.id and passing it to MapView.show3dMap().

// Setting the initial floor to Floor.id 'm_123456789'.
self.mapView.show3dMap(options: Show3DMapOptions(initialFloor: "m_123456789")) { r2 in
if case .success = r2 {
// Successfully initialized the map view.
} else if case .failure(let error) = r2 {
// Failed to initialize the map view.
}
}

The Floor displayed in a MapView can also be changed during runtime by calling MapView.setFloor() and passing in a Floor.id.

// Set the floor to Floor.id 'm_987654321'.
mapView.setFloor(floorId: "m_987654321") { result in
switch result {
case .success:
print("Floor changed successfully")
case .failure(let error):
print("Error: \(error)")
}
}

Listening for Floor Changes

Mappedin SDK for iOS provides the ability to listen for floor changes on a MapView. The code below listens for the floor-change event and logs the new floor and building name to the console.

mapView.on(Events.floorChange) { [weak self] payload in
guard let self = self, let payload = payload else { return }
print("Floor changed to: \(payload.floor.name) in building: \(payload.floor.floorStack?.name ?? "unknown")")
}

Building Selection

Mappedin Maps can contain one to many buildings with each building having one to many floors. Each building has its floors organized into a FloorStack object. When multiple buildings are present, there are multiple FloorStack objects, one for each building. A FloorStack contains one to many Floor objects, each representing the map of each level of the building.

The FloorStack object is accessed by using MapData.getByType().

// Get all floor stacks
mapView.mapData.getByType(.floorStack) { [weak self] (result: Result<[FloorStack], Error>) in
guard let self = self else { return }
if case .success(let stacks) = result {
self.floorStacks = stacks.sorted { $0.name.localizedCompare($1.name) == .orderedAscending }

// Get all floors in the floor stack
self.mapView.mapData.getByType(.floor) { [weak self] (floorsResult: Result<[Floor], Error>) in
guard let self = self else { return }
if case .success(let floors) = floorsResult {
print("Floors: \(floors)")
}
}
}
}

Determine If a Coordinate Is Inside a Building

The FloorStack object has a geoJSON property that can be used to determine if a coordinate is inside a building.

The code below listens for a click event on the map and determines if the clicked coordinate is inside a building. If it is, the building name is logged to the console. If it is not, a message is logged to the console.

// Filter to get only buildings (type == .building)
self.buildings = self.floorStacks.filter { $0.type == .building }

// Act on the click event to check if the coordinate is within any building.
mapView.on(Events.click) { [weak self] payload in
guard let self = self, let payload = payload else { return }
let coordinate = payload.coordinate
var matchingBuildings: [String] = []

// This demonstrates how to detect if a coordinate is within a building.
// For click events, this can be detected by checking the ClickEvent.floors.
// Checking the coordinate is for demonstration purposes and useful for non click events.
for building in self.buildings {
if self.isCoordinateWithinFeature(coordinate: coordinate, feature: building.geoJSON) {
matchingBuildings.append(building.name)
}
}

DispatchQueue.main.async {
let message: String
if !matchingBuildings.isEmpty {
message = "Coordinate is within building: \(matchingBuildings.joined(separator: ", "))"
} else {
message = "Coordinate is not within any building"
}
self.showToast(message: message)
}
}

/// Check if a coordinate is within a GeoJSON Feature.
/// This implements point-in-polygon checking similar to @turf/boolean-contains.
private func isCoordinateWithinFeature(coordinate: Coordinate, feature: Feature?) -> Bool {
guard let geometry = feature?.geometry else { return false }
let point = [coordinate.longitude, coordinate.latitude]

switch geometry {
case .polygon(let coordinates):
return isPointInPolygon(point: point, polygon: coordinates)
case .multiPolygon(let coordinates):
return coordinates.contains { polygon in
isPointInPolygon(point: point, polygon: polygon)
}
default:
return false
}
}

/// Check if a point is inside a polygon using the ray-casting algorithm.
/// The polygon is represented as a list of linear rings (first is outer, rest are holes).
private func isPointInPolygon(point: [Double], polygon: [[[Double]]]) -> Bool {
guard !polygon.isEmpty else { return false }

// Check if point is inside the outer ring
let outerRing = polygon[0]
guard isPointInRing(point: point, ring: outerRing) else {
return false
}

// Check if point is inside any hole (if so, it's not in the polygon)
for i in 1..<polygon.count {
if isPointInRing(point: point, ring: polygon[i]) {
return false
}
}

return true
}

/// Check if a point is inside a linear ring using the ray-casting algorithm.
/// This counts how many times a ray from the point crosses the polygon boundary.
private func isPointInRing(point: [Double], ring: [[Double]]) -> Bool {
guard ring.count >= 4 else { return false } // A valid ring needs at least 4 points (3 + closing point)

let x = point[0]
let y = point[1]
var inside = false

var j = ring.count - 1
for i in 0..<ring.count {
let xi = ring[i][0]
let yi = ring[i][1]
let xj = ring[j][0]
let yj = ring[j][1]

let intersect = ((yi > y) != (yj > y)) &&
(x < (xj - xi) * (y - yi) / (yj - yi) + xi)

if intersect {
inside = !inside
}
j = i
}

return inside
}
/// Shows a toast-like message at the bottom of the screen.
private func showToast(message: String) {
let toastLabel = UILabel()
toastLabel.backgroundColor = UIColor.black.withAlphaComponent(0.7)
toastLabel.textColor = .white
toastLabel.textAlignment = .center
toastLabel.font = UIFont.systemFont(ofSize: 14)
toastLabel.text = message
toastLabel.alpha = 1.0
toastLabel.layer.cornerRadius = 10
toastLabel.clipsToBounds = true
toastLabel.numberOfLines = 0

let maxWidth = view.frame.size.width - 40
let expectedSize = toastLabel.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.greatestFiniteMagnitude))
let labelWidth = min(expectedSize.width + 20, maxWidth)
let labelHeight = expectedSize.height + 16

toastLabel.frame = CGRect(
x: (view.frame.size.width - labelWidth) / 2,
y: view.frame.size.height - 150,
width: labelWidth,
height: labelHeight
)

view.addSubview(toastLabel)

UIView.animate(withDuration: 2.0, delay: 1.0, options: .curveEaseOut, animations: {
toastLabel.alpha = 0.0
}, completion: { _ in
toastLabel.removeFromSuperview()
})
}
tip

A complete example demonstrating Building and Floor Selection can be found in the Mappedin iOS Github repo: BuildingFloorSelectionDemoViewController.kt