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.
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
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
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
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
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
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
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

0 comments on commit fc950cc

Please sign in to comment.