Nick Rossik
Nick Rossik

Reputation: 1144

Move map (MapCameraPosition) via ARFaceTracking

I am trying to move the map by rotating the phone near my face. Below I have created a wrapper for ARView and a coordinator class.

This is more or less a working option with bounds, but if you remove bounds from the map - moving with a face doesn't work. As you can understand, I don't need bounds on the map, but if you remove them - moving doesn't work. I can't think of anything for 3 days now. Can you give me an idea what I can do?

Inside the method I assign a value for MKCoordinateRegion:

struct ARWrapper: UIViewRepresentable {
  @Binding
  var faceBounds: MKCoordinateRegion

  func updateUIView(_ uiView: ARView, context: Context) {}

  func makeUIView(context: Context) -> ARView {
    let arView = ARView(frame: .zero)
    let faceConfig = ARFaceTrackingConfiguration()

    // Set the coordinator as the session delegate
    arView.session.delegate = context.coordinator

    let session = arView.session
    session.run(faceConfig)

    return arView
  }

  func makeCoordinator() -> Coordinator {
    Coordinator(self)
  }

  class Coordinator: NSObject, ARSessionDelegate {
    var parent: ARWrapper

    init(_ parent: ARWrapper) {
      self.parent = parent
    }

    // Implement ARSessionDelegate methods here
    func session(_ session: ARSession, didUpdate frame: ARFrame) {}

    func spanFrom(centerZ: Double, distanceZ: Double) -> MKCoordinateSpan {
      let initial = parent.faceBounds.span
      let delta = distanceZ / centerZ

      let span = MKCoordinateSpan(
        latitudeDelta: initial.latitudeDelta * delta,
        longitudeDelta: initial.longitudeDelta * delta
      )

      return span
    }

    func session(_ session: ARSession, didUpdate anchors: [ARAnchor]) {
      guard let faceAnchor = anchors.first as? ARFaceAnchor else {
        print("Can't find Face anchor. Exit")
        return
      }

      let centerX: Float = 0
      let centerY: Float = 0
      let centerZ: Float = 0  // zoom span

      // Here's main calculation

      let mapLocation = CLLocationCoordinate2D(latitude: 25.19745, longitude: 55.27417)
      let translation = simd_make_float3(faceAnchor.transform.columns.3)

      let distanceX = translation.x * 100
      let distanceY = translation.y * 100
      let distanceZ = (translation.z * 100) * -1  // zoom span

      // Horizontal
      let deltaX = (centerX - distanceX) * 300
      let calcCenterX = mapLocation.shift(byDistance: Double(deltaX), azimuth: Double.pi / 2)

      // Vertical
      let deltaY = (centerY - distanceY) * 500
      parent.faceBounds.center = calcCenterX.shift(byDistance: Double(deltaY), azimuth: .zero)

      let region = MKCoordinateRegion(
        center: parent.faceBounds.center,
        span: .init()
      )

      parent.faceBounds = region
    }
  }
}

Here's a .shift(byDistance:) method:

extension CLLocationCoordinate2D {
  /// Get coordinate moved from current to `distanceMeters` meters with azimuth `azimuth` [0, Double.pi)
  ///
  /// - Parameters:
  ///   - distanceMeters: the distance in meters
  ///   - azimuth: the azimuth (bearing)
  /// - Returns: new coordinate
  func shift(byDistance distanceMeters: Double, azimuth: Double) -> CLLocationCoordinate2D {
    let bearing = azimuth
    let origin = self
    let distRadians = distanceMeters / 6_372_797.6  // earth radius in meters

    let lat1 = origin.latitude * Double.pi / 180
    let lon1 = origin.longitude * Double.pi / 180

    let lat2 = asin(sin(lat1) * cos(distRadians) + cos(lat1) * sin(distRadians) * cos(bearing))
    let lon2 =
      lon1
      + atan2(sin(bearing) * sin(distRadians) * cos(lat1), cos(distRadians) - sin(lat1) * sin(lat2))
    return CLLocationCoordinate2D(
      latitude: lat2 * 180 / Double.pi, longitude: lon2 * 180 / Double.pi)
  }
}

Create map via MapCameraPosition:

@State
private var defaultCameraPosition: MapCameraPosition = .region(
  .init(center: .unitedEmirates, span: .init(latitudeDelta: 3000, longitudeDelta: 3000)))

@State
private
  var faceTrackingBounds: MKCoordinateRegion = .init(
    center: .unitedEmirates, latitudinalMeters: 20000, longitudinalMeters: 2000)

var body: some View {
  ZStack {
    ARWrapper(faceBounds: $mapData.faceTrackingBounds)
    Map(
      position: $mapData.defaultCameraPosition,
      // if we remove bounds — nothing work
      bounds: .init(
        centerCoordinateBounds: mapData.faceTrackingBounds,
        minimumDistance: 50,
        maximumDistance: 100000
     )
    )
  }
  .onAppear {
    mapData.defaultCameraPosition = .region(
      .init(center: .unitedEmirates, span: mapData.faceTrackingBounds.span))
  }
}

Upvotes: 0

Views: 40

Answers (0)

Related Questions