Skip to content

Commit

Permalink
Add initial solution to moving target problem (#1245)
Browse files Browse the repository at this point in the history
  • Loading branch information
macdrevx committed Apr 5, 2022
1 parent db27bd5 commit c459b65
Show file tree
Hide file tree
Showing 25 changed files with 1,182 additions and 216 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Mapbox welcomes participation and contributions from everyone.
* Add map view example with `debugOptions`. ([#1225](https://github.com/mapbox/mapbox-maps-ios/pull/1225))
* Introduce `line-trim-offset` property for LineLayer. ([#1231](https://github.com/mapbox/mapbox-maps-ios/pull/1231))
* Add `MapboxMap.coordinateBoundsUnwrapped`. ([#1241](https://github.com/mapbox/mapbox-maps-ios/pull/1241))
* Update `DefaultViewportTransition` to solve the moving target problem. ([#1245](https://github.com/mapbox/mapbox-maps-ios/pull/1245))

## 10.4.1 - March 28, 2022

Expand Down
2 changes: 2 additions & 0 deletions Sources/MapboxMaps/Camera/AnimationOwner.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ public struct AnimationOwner: RawRepresentable, Equatable {
public static let unspecified = AnimationOwner(rawValue: "com.mapbox.maps.unspecified")

internal static let cameraAnimationsManager = AnimationOwner(rawValue: "com.mapbox.maps.cameraAnimationsManager")

internal static let defaultViewportTransition = AnimationOwner(rawValue: "com.mapbox.maps.viewport.defaultTransition")
}
25 changes: 20 additions & 5 deletions Sources/MapboxMaps/Foundation/MapViewDependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ internal protocol MapViewDependencyProviderProtocol: AnyObject {
doubleTouchGestureRecognizer: UIGestureRecognizer) -> ViewportImplProtocol
}

// swiftlint:disable:next type_body_length
internal final class MapViewDependencyProvider: MapViewDependencyProviderProtocol {
internal let notificationCenter: NotificationCenterProtocol = NotificationCenter.default

Expand Down Expand Up @@ -259,14 +260,28 @@ internal final class MapViewDependencyProvider: MapViewDependencyProviderProtoco
anyTouchGestureRecognizer: UIGestureRecognizer,
doubleTapGestureRecognizer: UIGestureRecognizer,
doubleTouchGestureRecognizer: UIGestureRecognizer) -> ViewportImplProtocol {
let lowZoomToHighZoomAnimationSpecProvider = LowZoomToHighZoomAnimationSpecProvider(
mapboxMap: mapboxMap)
let highZoomToLowZoomAnimationSpecProvider = HighZoomToLowZoomAnimationSpecProvider(
mapboxMap: mapboxMap)
let animationSpecProvider = DefaultViewportTransitionAnimationSpecProvider(
mapboxMap: mapboxMap,
lowZoomToHighZoomAnimationSpecProvider: lowZoomToHighZoomAnimationSpecProvider,
highZoomToLowZoomAnimationSpecProvider: highZoomToLowZoomAnimationSpecProvider)
let animationFactory = DefaultViewportTransitionAnimationFactory(
mapboxMap: mapboxMap)
let animationHelper = DefaultViewportTransitionAnimationHelper(
mapboxMap: mapboxMap,
animationSpecProvider: animationSpecProvider,
cameraAnimationsManager: cameraAnimationsManager,
animationFactory: animationFactory)
let defaultViewportTransition = DefaultViewportTransition(
options: .init(),
animationHelper: animationHelper)
return ViewportImpl(
options: .init(),
mainQueue: MainQueue(),
defaultTransition: DefaultViewportTransition(
options: .init(),
animationHelper: DefaultViewportTransitionAnimationHelper(
mapboxMap: mapboxMap,
cameraAnimationsManager: cameraAnimationsManager)),
defaultTransition: defaultViewportTransition,
anyTouchGestureRecognizer: anyTouchGestureRecognizer,
doubleTapGestureRecognizer: doubleTapGestureRecognizer,
doubleTouchGestureRecognizer: doubleTouchGestureRecognizer)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
internal protocol CameraOptionsComponentProtocol {
var cameraOptions: CameraOptions { get }
func updated(with cameraOptions: CameraOptions) -> CameraOptionsComponentProtocol?
}

internal struct CameraOptionsComponent<T>: CameraOptionsComponentProtocol {
internal let keyPath: WritableKeyPath<CameraOptions, T?>
internal let value: T

internal var cameraOptions: CameraOptions {
var cameraOptions = CameraOptions()
cameraOptions[keyPath: keyPath] = value
return cameraOptions
}

internal func updated(with cameraOptions: CameraOptions) -> CameraOptionsComponentProtocol? {
cameraOptions[keyPath: keyPath].map {
CameraOptionsComponent(keyPath: keyPath, value: $0)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,21 @@ extension DefaultViewportTransition: ViewportTransition {
public func run(to toState: ViewportState,
completion: @escaping (Bool) -> Void) -> Cancelable {
let resultCancelable = CompositeCancelable()
var animation: DefaultViewportTransitionAnimationProtocol!
resultCancelable.add(toState.observeDataSource { [options, animationHelper] cameraOptions in
resultCancelable.add(animationHelper.animate(
to: cameraOptions,
maxDuration: options.maxDuration,
completion: completion))
// stop receiving updates (ignore moving targets)
return false
if let animation = animation {
animation.updateTargetCamera(with: cameraOptions)
} else {
animation = animationHelper.makeAnimation(
cameraOptions: cameraOptions,
maxDuration: options.maxDuration)
animation.start { isFinished in
resultCancelable.cancel()
completion(isFinished)
}
resultCancelable.add(animation)
}
return true
})
return resultCancelable
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import Foundation

internal protocol DefaultViewportTransitionAnimationProtocol: Cancelable {
func updateTargetCamera(with cameraOptions: CameraOptions)
func start(with completion: @escaping (Bool) -> Void)
}

internal final class DefaultViewportTransitionAnimation: DefaultViewportTransitionAnimationProtocol {
private let components: [DefaultViewportTransitionAnimationProtocol]

internal init(components: [DefaultViewportTransitionAnimationProtocol]) {
self.components = components
}

func start(with completion: @escaping (Bool) -> Void) {
let group = DispatchGroup()
var allFinished = true
for component in components {
group.enter()
component.start { isFinished in
allFinished = allFinished && isFinished
group.leave()
}
}
group.notify(queue: .main) {
completion(allFinished)
}
}

internal func updateTargetCamera(with cameraOptions: CameraOptions) {
for component in components {
component.updateTargetCamera(with: cameraOptions)
}
}

internal func cancel() {
for component in components {
component.cancel()
}
}
}

internal final class DefaultViewportTransitionAnimationComponent: DefaultViewportTransitionAnimationProtocol {
private let animator: SimpleCameraAnimatorProtocol
private let delay: TimeInterval
private let cameraOptionsComponent: CameraOptionsComponentProtocol
private let mapboxMap: MapboxMapProtocol

internal init(animator: SimpleCameraAnimatorProtocol,
delay: TimeInterval,
cameraOptionsComponent: CameraOptionsComponentProtocol,
mapboxMap: MapboxMapProtocol) {
self.animator = animator
self.delay = delay
self.cameraOptionsComponent = cameraOptionsComponent
self.mapboxMap = mapboxMap
}

func start(with completion: @escaping (Bool) -> Void) {
animator.addCompletion { position in
completion(position != .current)
}
animator.startAnimation(afterDelay: delay)
}

internal func updateTargetCamera(with cameraOptions: CameraOptions) {
guard let updatedComponent = cameraOptionsComponent.updated(with: cameraOptions) else {
return
}
let isComplete = animator.state == .inactive
if isComplete {
mapboxMap.setCamera(to: updatedComponent.cameraOptions)
} else {
animator.to = updatedComponent.cameraOptions
}
}

internal func cancel() {
animator.cancel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Foundation

internal protocol DefaultViewportTransitionAnimationFactoryProtocol: AnyObject {
func makeAnimationComponent(animator: SimpleCameraAnimatorProtocol,
delay: TimeInterval,
cameraOptionsComponent: CameraOptionsComponentProtocol) -> DefaultViewportTransitionAnimationProtocol

func makeAnimation(components: [DefaultViewportTransitionAnimationProtocol]) -> DefaultViewportTransitionAnimationProtocol
}

internal final class DefaultViewportTransitionAnimationFactory: DefaultViewportTransitionAnimationFactoryProtocol {
private let mapboxMap: MapboxMapProtocol

internal init(mapboxMap: MapboxMapProtocol) {
self.mapboxMap = mapboxMap
}

internal func makeAnimation(components: [DefaultViewportTransitionAnimationProtocol]) -> DefaultViewportTransitionAnimationProtocol {
return DefaultViewportTransitionAnimation(components: components)
}

internal func makeAnimationComponent(animator: SimpleCameraAnimatorProtocol,
delay: TimeInterval,
cameraOptionsComponent: CameraOptionsComponentProtocol) -> DefaultViewportTransitionAnimationProtocol {
return DefaultViewportTransitionAnimationComponent(
animator: animator,
delay: delay,
cameraOptionsComponent: cameraOptionsComponent,
mapboxMap: mapboxMap)
}
}
Loading

0 comments on commit c459b65

Please sign in to comment.