I am creating a map app for an autonomous robot. I am having difficulty with annotations and polylines moving together when dragging the annotation. Currently the annotation will move and then after a brief pause the polyline will move and catch up to it.
I want both to move in unison with a “rubber band” behavior. I have not been able to find a tutorial or example of dragging an annotation that has polylines connected to it when dragging. Thanks for any help/guidance you may give.
// LAMErtkAnnotationManager.swift
// LAME-RTK
// Created: 11/19/2024
import Foundation
import MapboxMaps
import CoreData
import Turf
import UIKit
// MARK: - Notification Extension
extension Notification.Name {
/// Notification posted when an annotation's coordinate is updated.
static let didUpdateAnnotation = Notification.Name("didUpdateAnnotation")
}
// MARK: - LAMErtkAnnotationManager
/// Manages annotations (drag handles) and polylines on the Mapbox map.
/// Ensures that dragging an annotation updates the corresponding polyline in real-time.
class LAMErtkAnnotationManager: NSObject, AnnotationInteractionDelegate {
// MARK: - Properties
/// The MapView instance where annotations and polylines are managed.
var mapView: MapView
/// The Core Data context for fetching and saving GPS data points.
var viewContext: NSManagedObjectContext
/// Manager for point annotations (drag handles).
var pointAnnotationManager: PointAnnotationManager
/// Manager for polyline annotations.
var polylineAnnotationManager: PolylineAnnotationManager
/// Dictionary mapping `zoneID` to its corresponding polyline annotation.
private var polylinesByZone: [String: PolylineAnnotation] = [:]
/// Dictionary to keep track of the last known coordinates of annotations.
/// Used to detect changes during dragging.
private var annotationCoordinates: [String: CLLocationCoordinate2D] = [:]
/// Timer to periodically check for annotation coordinate changes.
/// Since MapboxMaps SDK lacks direct drag event delegates, this timer facilitates synchronization.
private var annotationCheckTimer: Timer?
// MARK: - Initialization
/// Initializes the `LAMErtkAnnotationManager` with the provided `MapView` and `NSManagedObjectContext`.
///
/// - Parameters:
/// - mapView: The `MapView` instance where annotations and polylines are managed.
/// - viewContext: The Core Data context for fetching and saving GPS data points.
init(mapView: MapView, viewContext: NSManagedObjectContext) {
self.mapView = mapView
self.viewContext = viewContext
self.pointAnnotationManager = mapView.annotations.makePointAnnotationManager()
self.polylineAnnotationManager = mapView.annotations.makePolylineAnnotationManager()
super.init()
// Assign the delegate for interaction events.
self.pointAnnotationManager.delegate = self
self.polylineAnnotationManager.delegate = self
print("***** LAMErtkAnnotationManager initialized.")
// Initialize stored annotation coordinates.
initializeAnnotationCoordinates()
// Start the timer to check for annotation updates every second.
annotationCheckTimer = Timer.scheduledTimer(timeInterval: 1.0,
target: self,
selector: #selector(checkForAnnotationUpdates),
userInfo: nil,
repeats: true)
// Observe annotation update notifications.
NotificationCenter.default.addObserver(self,
selector: #selector(handleAnnotationUpdate(notification:)),
name: .didUpdateAnnotation,
object: nil)
}
deinit {
// Invalidate the timer and remove observers to prevent memory leaks.
annotationCheckTimer?.invalidate()
NotificationCenter.default.removeObserver(self)
}
// MARK: - Annotation Management
/// Clears all point annotations and polylines from the map.
func clearAllAnnotations() {
pointAnnotationManager.annotations.removeAll()
polylineAnnotationManager.annotations.removeAll()
polylinesByZone.removeAll()
annotationCoordinates.removeAll()
print("***** All annotations cleared.")
}
/// Loads all point annotations and corresponding polylines from Core Data.
func loadAllAnnotations() {
clearAllAnnotations()
loadPointAnnotations()
loadPolylines()
print("***** All annotations loaded.")
}
/// Loads point annotations (drag handles) from Core Data.
private func loadPointAnnotations() {
let fetchRequest: NSFetchRequest = GPSDataPoint.fetchRequest()
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "zoneID", ascending: true),
NSSortDescriptor(key: "order", ascending: true)
]
do {
let gpsDataPoints = try viewContext.fetch(fetchRequest)
pointAnnotationManager.annotations = gpsDataPoints.compactMap { point -> PointAnnotation? in
guard let zoneID = point.zoneID else { return nil }
let coordinate = CLLocationCoordinate2D(latitude: point.latitude, longitude: point.longitude)
var annotation = PointAnnotation(coordinate: coordinate)
annotation.isDraggable = true
annotation.customData = ["zoneID": .string(zoneID)]
annotation.iconImage = "diamondFill" // Set the icon image to "diamondFill"
// Store the initial coordinates.
annotationCoordinates[zoneID] = coordinate
return annotation
}
print("***** Loaded \(pointAnnotationManager.annotations.count) point annotations.")
} catch {
print("***** Error fetching point annotations: \(error.localizedDescription)")
}
}
/// Loads polylines from Core Data, grouping them by `zoneID`.
private func loadPolylines() {
let fetchRequest: NSFetchRequest = GPSDataPoint.fetchRequest()
fetchRequest.sortDescriptors = [
NSSortDescriptor(key: "zoneID", ascending: true),
NSSortDescriptor(key: "order", ascending: true)
]
do {
let gpsDataPoints = try viewContext.fetch(fetchRequest)
let groupedPoints = Dictionary(grouping: gpsDataPoints, by: { $0.zoneID ?? "default" })
for (zoneID, points) in groupedPoints {
let coordinates = points.map { CLLocationCoordinate2D(latitude: $0.latitude, longitude: $0.longitude) }
if let lineString = try? LineString(coordinates) {
var polyline = PolylineAnnotation(lineString: lineString)
polyline.lineColor = StyleColor(.systemBlue)
polyline.lineWidth = 3.0
polyline.customData = ["zoneID": .string(zoneID)]
polylineAnnotationManager.annotations.append(polyline)
polylinesByZone[zoneID] = polyline
print("***** Added polyline for zoneID: \(zoneID) with \(coordinates.count) points.")
}
}
} catch {
print("***** Error fetching polylines: \(error.localizedDescription)")
}
}
/// Initializes the stored coordinates from current annotations.
private func initializeAnnotationCoordinates() {
for annotation in pointAnnotationManager.annotations {
if let zoneIDValue = annotation.customData["zoneID"],
case let .string(zoneID) = zoneIDValue {
annotationCoordinates[zoneID] = annotation.coordinate
}
}
}
// MARK: - Dynamic Polyline Updates with Turf
/// Updates the polyline associated with a given `zoneID` using Turf.
///
/// - Parameter zoneID: The `zoneID` whose polyline should be updated.
func updatePolyline(for zoneID: String) {
guard var polyline = polylinesByZone[zoneID] else { return }
let updatedCoordinates = pointAnnotationManager.annotations.compactMap { annotation -> CLLocationCoordinate2D? in
guard let annotationZoneID = annotation.customData["zoneID"],
case let .string(annotationZone) = annotationZoneID,
annotationZone == zoneID else { return nil }
return annotation.coordinate
}
guard !updatedCoordinates.isEmpty else { return }
// Use Turf to validate and create a new LineString.
if let updatedLineString = try? LineString(updatedCoordinates) {
polyline.lineString = updatedLineString
polylineAnnotationManager.annotations = polylineAnnotationManager.annotations.map { $0.id == polyline.id ? polyline : $0 }
print("***** Updated polyline for zoneID: \(zoneID) with \(updatedCoordinates.count) points.")
} else {
print("***** Error creating LineString for zoneID: \(zoneID).")
}
}
// MARK: - AnnotationInteractionDelegate
/// Handles tap interactions on annotations.
///
/// - Parameters:
/// - manager: The `AnnotationManager` instance.
/// - annotations: The list of annotations that were tapped.
func annotationManager(_ manager: AnnotationManager, didDetectTappedAnnotations annotations: [Annotation]) {
for annotation in annotations {
if let point = annotation as? PointAnnotation,
let zoneIDValue = point.customData["zoneID"],
case let .string(zoneID) = zoneIDValue {
updatePolyline(for: zoneID)
}
}
}
// MARK: - Handling Annotation Drags
/// Handles updates to annotations when they are dragged.
/// Since MapboxMaps SDK doesn't provide direct drag event delegates, this method is triggered via notifications.
///
/// - Parameter notification: The notification containing the updated annotation.
@objc private func handleAnnotationUpdate(notification: Notification) {
guard let userInfo = notification.userInfo,
let updatedAnnotation = userInfo["annotation"] as? PointAnnotation,
let zoneIDValue = updatedAnnotation.customData["zoneID"] else { return }
// Use pattern matching to extract the string from JSONValue
guard case let .string(zoneID) = zoneIDValue else { return }
let newCoordinate = updatedAnnotation.coordinate
print("***** Annotation for zoneID: \(zoneID) moved to new coordinate: \(newCoordinate)")
// Update the corresponding GPSDataPoint in Core Data.
let fetchRequest: NSFetchRequest = GPSDataPoint.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "zoneID == %@", zoneID)
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "order", ascending: true)]
fetchRequest.fetchLimit = 1
do {
if let gpsDataPoint = try viewContext.fetch(fetchRequest).first {
gpsDataPoint.latitude = newCoordinate.latitude
gpsDataPoint.longitude = newCoordinate.longitude
try viewContext.save()
print("***** Updated GPSDataPoint for zoneID: \(zoneID) with new coordinates.")
}
} catch {
print("***** Error updating GPSDataPoint for zoneID: \(zoneID): \(error.localizedDescription)")
}
// Update the corresponding polyline.
updatePolyline(for: zoneID)
}
// MARK: - Timer for Checking Annotation Updates
/// Periodically checks for changes in annotation coordinates to detect drags.
@objc private func checkForAnnotationUpdates() {
for annotation in pointAnnotationManager.annotations {
if let zoneIDValue = annotation.customData["zoneID"],
case let .string(zoneID) = zoneIDValue,
let oldCoordinate = annotationCoordinates[zoneID],
oldCoordinate.latitude != annotation.coordinate.latitude ||
oldCoordinate.longitude != annotation.coordinate.longitude {
// Update the stored coordinate.
annotationCoordinates[zoneID] = annotation.coordinate
// Post a notification with the updated annotation.
NotificationCenter.default.post(name: .didUpdateAnnotation,
object: nil,
userInfo: ["annotation": annotation])
}
}
}
}
Discover more from TrendyShopToBuy
Subscribe to get the latest posts sent to your email.