Skip to content

Commit

Permalink
MAPSIOS-1193: Simplify annotations cluster expansion API (#2011)
Browse files Browse the repository at this point in the history
* MAPSIOS-1193: Simplify annotations cluster expansion API
  • Loading branch information
aleksproger committed Feb 6, 2024
1 parent 1439880 commit fc950cc
Show file tree
Hide file tree
Showing 29 changed files with 454 additions and 231 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,6 @@ final class PointAnnotationClusteringExample: UIViewController, ExampleProtocol

view.addSubview(mapView)

// Add a tap gesture handler for clustering layer.
let circleClusterLayerId = "mapbox-iOS-cluster-circle-layer-manager-" + clusterLayerID
mapView.gestures.onLayerTap(circleClusterLayerId) { [weak self] queriedFeature, _ in
self?.handleClusterTap(queriedFeature: queriedFeature)
return true
}.store(in: &cancelables)

// Add the source and style layers once the map has loaded.
mapView.mapboxMap.onMapLoaded.observeNext { _ in
self.addPointAnnotations()
Expand Down Expand Up @@ -135,6 +128,9 @@ final class PointAnnotationClusteringExample: UIViewController, ExampleProtocol
clusterProperties: clusterProperties)
let pointAnnotationManager = mapView.annotations.makePointAnnotationManager(id: clusterLayerID, clusterOptions: clusterOptions)
pointAnnotationManager.annotations = annotations
pointAnnotationManager.onClusterTap = { [weak self] context in
self?.mapView.camera.ease(to: CameraOptions(center: context.coordinate, zoom: context.expansionZoom), duration: 1)
}

// Additional properties on the text and circle layers can be modified like this below
// To modify the text layer use: "mapbox-iOS-cluster-text-layer-manager-" and SymbolLayer.self
Expand All @@ -150,24 +146,6 @@ final class PointAnnotationClusteringExample: UIViewController, ExampleProtocol
finish()
}

// If the tapped feature it is a cluster get the center and zoom level it expands at
// then move the camera there.
func handleClusterTap(queriedFeature: QueriedFeature) {
let cluster = queriedFeature.feature
let sourceID = clusterLayerID
if case let .point(clusterCenter) = cluster.geometry {
mapView.mapboxMap.getGeoJsonClusterExpansionZoom(forSourceId: sourceID, feature: cluster) { [weak self] result in
switch result {
case .success(let zoomLevel):
let cameraOptions = CameraOptions(center: clusterCenter.coordinates, zoom: zoomLevel.value as? CGFloat)
self?.mapView.camera.ease(to: cameraOptions, duration: 1)
case .failure(let error):
print("An error occurred: \(error.localizedDescription). Please try another cluster.")
}
}
}
}

// Load GeoJSON file from local bundle and decode into a `FeatureCollection`.
func decodeGeoJSON(from fileName: String) throws -> FeatureCollection? {
guard let path = Bundle.main.path(forResource: fileName, ofType: "geojson") else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ struct AnnotationsExample: View {

@State private var taps = [Tap]()
@State private var alert: String?
@State private var viewport = Viewport.camera(center: .init(latitude: 27.2, longitude: -26.9), zoom: 1.53, bearing: 0, pitch: 0)
var body: some View {
MapReader { proxy in
Map(initialViewport: .camera(center: .init(latitude: 27.2, longitude: -26.9), zoom: 1.53, bearing: 0, pitch: 0)) {
Map(viewport: $viewport) {
ForEvery(Self.flights, id: \.name) { flight in
CircleAnnotationGroup(flight.airports, id: \.name) { airport in
CircleAnnotation(centerCoordinate: airport.coordinate, isDraggable: true)
Expand Down Expand Up @@ -97,6 +98,11 @@ struct AnnotationsExample: View {
}
}
.clusterOptions(clusterOptions)
.onClusterTapGesture { context in
withViewportAnimation(.easeIn(duration: 1)) {
viewport = .camera(center: context.coordinate, zoom: context.expansionZoom)
}
}
}
.onMapTapGesture { context in
taps.append(Tap(coordinate: context.coordinate))
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ Mapbox welcomes participation and contributions from everyone.

## main

* Add `onClusterTap` and `onClusterLongPress` to AnnotationManagers(UIKit) and AnnotationGroups(SwiftUI) which support clustering

## 11.2.0-beta.1 - 1 February, 2024

### Features ✨ and improvements 🏁

* vision OS support. 🚀
* Vision OS support. 🚀
* Add easing curve parameter to `CameraAnimationsManager.fly(to:duration:curve:completion)`, make `TimingCurve` public with few more options.
* Expose `MapboxMap.centerAltitudeMode` and ensure correct `centerAltitudeMode` on gesture ending.
* Expose extra configuration methods for `MapboxMap`: `setNorthOrientation(_:)`, `setConstrainMode(_:)` and `setViewportMode(_:)`.
Expand Down
16 changes: 11 additions & 5 deletions Sources/MapboxMaps/Annotations/AnnotationManagerFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,19 +29,24 @@ internal final class AnnotationManagerFactory: AnnotationManagerFactoryProtocol
private let offsetPointCalculator: OffsetPointCalculator
private let offsetPolygonCalculator: OffsetPolygonCalculator
private let offsetLineStringCalculator: OffsetLineStringCalculator
private let mapFeatureQueryable: MapFeatureQueryable

private lazy var imagesManager = AnnotationImagesManager(style: style)

internal init(style: StyleProtocol,
displayLink: Signal<Void>,
offsetPointCalculator: OffsetPointCalculator,
offsetPolygonCalculator: OffsetPolygonCalculator,
offsetLineStringCalculator: OffsetLineStringCalculator) {
internal init(
style: StyleProtocol,
displayLink: Signal<Void>,
offsetPointCalculator: OffsetPointCalculator,
offsetPolygonCalculator: OffsetPolygonCalculator,
offsetLineStringCalculator: OffsetLineStringCalculator,
mapFeatureQueryable: MapFeatureQueryable
) {
self.style = style
self.displayLink = displayLink
self.offsetPointCalculator = offsetPointCalculator
self.offsetPolygonCalculator = offsetPolygonCalculator
self.offsetLineStringCalculator = offsetLineStringCalculator
self.mapFeatureQueryable = mapFeatureQueryable
}

internal func makePointAnnotationManager(
Expand All @@ -54,6 +59,7 @@ internal final class AnnotationManagerFactory: AnnotationManagerFactoryProtocol
layerPosition: layerPosition,
displayLink: displayLink,
clusterOptions: clusterOptions,
mapFeatureQueryable: mapFeatureQueryable,
imagesManager: imagesManager,
offsetCalculator: offsetPointCalculator)
}
Expand Down
18 changes: 11 additions & 7 deletions Sources/MapboxMaps/Annotations/AnnotationOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ public protocol AnnotationManager: AnyObject {
var slot: String? { get set }
}

internal protocol AnnotationManagerInternal: AnnotationManager {
protocol AnnotationManagerInternal: AnnotationManager {
var allLayerIds: [String] { get }

func destroy()

func handleTap(with featureId: String, context: MapContentGestureContext) -> Bool
func handleTap(layerId: String, feature: Feature, context: MapContentGestureContext) -> Bool

func handleLongPress(with featureId: String, context: MapContentGestureContext) -> Bool
func handleLongPress(layerId: String, feature: Feature, context: MapContentGestureContext) -> Bool

func handleDragBegin(with featureId: String, context: MapContentGestureContext) -> Bool

Expand Down Expand Up @@ -57,7 +57,7 @@ public protocol AnnotationInteractionDelegate: AnyObject {
public final class AnnotationOrchestrator {
private let impl: AnnotationOrchestratorImplProtocol

internal init(impl: AnnotationOrchestratorImplProtocol) {
init(impl: AnnotationOrchestratorImplProtocol) {
self.impl = impl
}

Expand All @@ -74,9 +74,13 @@ public final class AnnotationOrchestrator {
/// - layerPosition: Optionally set the `LayerPosition` of the layer managed.
/// - clusterOptions: Optionally set the `ClusterOptions` to cluster the Point Annotations
/// - Returns: An instance of `PointAnnotationManager`
public func makePointAnnotationManager(id: String = String(UUID().uuidString.prefix(5)),
layerPosition: LayerPosition? = nil,
clusterOptions: ClusterOptions? = nil) -> PointAnnotationManager {
public func makePointAnnotationManager(
id: String = String(UUID().uuidString.prefix(5)),
layerPosition: LayerPosition? = nil,
clusterOptions: ClusterOptions? = nil,
onClusterTap: ((AnnotationClusterGestureContext) -> Void)? = nil,
onClusterLongPress: ((AnnotationClusterGestureContext) -> Void)? = nil
) -> PointAnnotationManager {
// swiftlint:disable:next force_cast
return impl.makePointAnnotationManager(id: id, layerPosition: layerPosition, clusterOptions: clusterOptions) as! PointAnnotationManager
}
Expand Down
27 changes: 11 additions & 16 deletions Sources/MapboxMaps/Annotations/AnnotationOrchestratorImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,18 @@ internal protocol AnnotationOrchestratorImplProtocol: AnyObject {
func removeAnnotationManager(withId id: String)
}

internal final class AnnotationOrchestratorImpl: NSObject, AnnotationOrchestratorImplProtocol {

final class AnnotationOrchestratorImpl: NSObject, AnnotationOrchestratorImplProtocol {
private(set) var managersByLayerId: [String: AnnotationManagerInternal] = [:]

private let mapFeatureQueryable: MapFeatureQueryable

private let factory: AnnotationManagerFactoryProtocol

internal init(mapFeatureQueryable: MapFeatureQueryable,
factory: AnnotationManagerFactoryProtocol) {
self.mapFeatureQueryable = mapFeatureQueryable
init(factory: AnnotationManagerFactoryProtocol) {
self.factory = factory
super.init()
}

/// Dictionary of annotation managers keyed by their identifiers.
internal var annotationManagersById: [String: AnnotationManager] {
annotationManagersByIdInternal
}
var annotationManagersById: [String: AnnotationManager] { annotationManagersByIdInternal }

private var annotationManagersByIdInternal = [String: AnnotationManagerInternal]() {
didSet {
Expand All @@ -54,9 +47,11 @@ internal final class AnnotationOrchestratorImpl: NSObject, AnnotationOrchestrato
/// - layerPosition: Optionally set the `LayerPosition` of the layer managed.
/// - clusterOptions: Optionally set the `ClusterOptions` to cluster the Point Annotations
/// - Returns: An instance of `PointAnnotationManager`
internal func makePointAnnotationManager(id: String,
layerPosition: LayerPosition?,
clusterOptions: ClusterOptions?) -> AnnotationManagerInternal {
func makePointAnnotationManager(
id: String,
layerPosition: LayerPosition?,
clusterOptions: ClusterOptions?
) -> AnnotationManagerInternal {
removeAnnotationManager(withId: id, warnIfRemoved: true, function: #function)
let annotationManager = factory.makePointAnnotationManager(
id: id,
Expand All @@ -75,7 +70,7 @@ internal final class AnnotationOrchestratorImpl: NSObject, AnnotationOrchestrato
/// - id: Optional string identifier for this manager..
/// - layerPosition: Optionally set the `LayerPosition` of the layer managed.
/// - Returns: An instance of `PolygonAnnotationManager`
internal func makePolygonAnnotationManager(id: String, layerPosition: LayerPosition?) -> AnnotationManagerInternal {
func makePolygonAnnotationManager(id: String, layerPosition: LayerPosition?) -> AnnotationManagerInternal {
removeAnnotationManager(withId: id, warnIfRemoved: true, function: #function)
let annotationManager = factory.makePolygonAnnotationManager(
id: id,
Expand All @@ -94,7 +89,7 @@ internal final class AnnotationOrchestratorImpl: NSObject, AnnotationOrchestrato
/// - id: Optional string identifier for this manager.
/// - layerPosition: Optionally set the `LayerPosition` of the layer managed.
/// - Returns: An instance of `PolylineAnnotationManager`
internal func makePolylineAnnotationManager(id: String, layerPosition: LayerPosition?) -> AnnotationManagerInternal {
func makePolylineAnnotationManager(id: String, layerPosition: LayerPosition?) -> AnnotationManagerInternal {
removeAnnotationManager(withId: id, warnIfRemoved: true, function: #function)
let annotationManager = factory.makePolylineAnnotationManager(
id: id,
Expand All @@ -112,7 +107,7 @@ internal final class AnnotationOrchestratorImpl: NSObject, AnnotationOrchestrato
/// - id: Optional string identifier for this manager.
/// - layerPosition: Optionally set the `LayerPosition` of the layer managed.
/// - Returns: An instance of `CircleAnnotationManager`
internal func makeCircleAnnotationManager(id: String, layerPosition: LayerPosition?) -> AnnotationManagerInternal {
func makeCircleAnnotationManager(id: String, layerPosition: LayerPosition?) -> AnnotationManagerInternal {
removeAnnotationManager(withId: id, warnIfRemoved: true, function: #function)
let annotationManager = factory.makeCircleAnnotationManager(
id: id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
public var sourceId: String { id }

public var layerId: String { id }

private var dragId: String { "\(id)_drag" }

public let id: String

Expand Down Expand Up @@ -60,7 +62,7 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
}

/// Storage for common layer properties
internal var layerProperties: [String: Any] = [:] {
var layerProperties: [String: Any] = [:] {
didSet {
syncLayerOnce.reset()
}
Expand All @@ -77,19 +79,19 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
private var syncDragSourceOnce = Once(happened: true)
private var syncLayerOnce = Once(happened: true)
private var insertDraggedLayerAndSourceOnce = Once()
private var dragId: String { id + "_drag" }
private var displayLinkToken: AnyCancelable?

var allLayerIds: [String] { [layerId, dragId] }

/// In SwiftUI isDraggable and isSelected are disabled.
var isSwiftUI = false

internal init(id: String,
style: StyleProtocol,
layerPosition: LayerPosition?,
displayLink: Signal<Void>,
offsetCalculator: OffsetCalculatorType) {
init(id: String,
style: StyleProtocol,
layerPosition: LayerPosition?,
displayLink: Signal<Void>,
offsetCalculator: OffsetCalculatorType
) {
self.id = id
self.style = style
self.offsetCalculator = offsetCalculator
Expand Down Expand Up @@ -280,10 +282,15 @@ public class CircleAnnotationManager: AnnotationManagerInternal {

// MARK: - User interaction handling

internal func handleTap(with featureId: String, context: MapContentGestureContext) -> Bool {

func handleTap(layerId: String, feature: Feature, context: MapContentGestureContext) -> Bool {

guard let featureId = feature.identifier?.string else { return false }

let tappedIndex = annotations.firstIndex { $0.id == featureId }
guard let tappedIndex else { return false }
var tappedAnnotation = annotations[tappedIndex]

tappedAnnotation.isSelected.toggle()

if !isSwiftUI {
Expand All @@ -299,17 +306,17 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
return tappedAnnotation.tapHandler?(context) ?? false
}

func handleLongPress(with featureId: String, context: MapContentGestureContext) -> Bool {
annotations.first {
$0.id == featureId
}?.longPressHandler?(context) ?? false
func handleLongPress(layerId: String, feature: Feature, context: MapContentGestureContext) -> Bool {
guard let featureId = feature.identifier?.string else { return false }

return annotations.first { $0.id == featureId }?.longPressHandler?(context) ?? false
}

internal func handleDragBegin(with featureIdentifier: String, context: MapContentGestureContext) -> Bool {
func handleDragBegin(with featureId: String, context: MapContentGestureContext) -> Bool {
guard !isSwiftUI else { return false }

let predicate = { (annotation: CircleAnnotation) -> Bool in
annotation.id == featureIdentifier && annotation.isDraggable
annotation.id == featureId && annotation.isDraggable
}

if let idx = draggedAnnotations.firstIndex(where: predicate) {
Expand Down Expand Up @@ -337,7 +344,7 @@ public class CircleAnnotationManager: AnnotationManagerInternal {
return false
}

internal func handleDragChanged(with translation: CGPoint) {
func handleDragChanged(with translation: CGPoint) {
guard !isSwiftUI,
let draggedAnnotationIndex,
draggedAnnotationIndex < draggedAnnotations.endIndex,
Expand Down
Loading

0 comments on commit fc950cc

Please sign in to comment.